feat: 分级拦截日志 + 两类日志文件;成绩录入防呆/解析/宽度修复
## 日志(用户要求:分级 + 每个拦截都要有指出代码行的调试日志 + 两类文件)
- 新增 internal/logging:拦截点捕获(file:line + 函数名,相对路径)、两类文件轮转、
按级别/类别分流。httpapi 与 middleware 各自薄封装,无循环依赖。
- 两类目录(数据目录下):
operations/ 放行类(INFO「操作已放行」+ 常规运行日志,按 log.level)
interceptions/ 拦截类(WARN 中文原因 + 拦截点 + method/path/actor/role/requestId、
DEBUG 上下文、ERROR 服务端异常)—— **固定 debug 采集**,
否则默认 info 下"每次拦截的调试日志"根本看不见
- 命名 operations-YYYYMMDD[.N].log,按天+按大小轮转,每类保留 keepFiles 份;写盘失败
降级只写控制台且只 Warn 一次。
- 所有业务错误统一走 newAPIError 捕获拦截点(11 处内联 &apiError{} 已收进构造函数,
防止绕过);中间件层拦截(401/403/CSRF/未改初始密码/429)也留痕,响应体与状态码一字未变。
- config.yaml 的 log 段新增 dir / maxSizeMB / keepFiles(留空即默认)。
- 服务端包数 12 → 13(新增 internal/logging)。
## 成绩录入(用户实机踩坑)
- 真因一:成绩格 placeholder 是光秃秃的 `1:06.33`,而全仓库没有任何 ::placeholder 样式,
示例文本与真实输入长得一样 → 用户把灰字当成已填内容,那 13 格其实是空的。
- 真因二:2 条报错是秒位 ≥ 60(1:70)或全角冒号(1:06);1:06 与 1:06.00 在解析器里
本来就等价(都 = 66),格式不是问题。
- 真因三:成绩格没有宽度定义(旁边名次 72px、状态 110px 都有),被挤到第 4 位数字看不见。
修复:parseSecondsDetail 区分 empty/seconds-overflow/unrecognized;新增 entryGuards.ts
纯函数(录入页与审核页共用),报错**打出那一格的实际内容**并说明原因;解析器新认全角
冒号/全角数字/1分06秒 等写法;成绩格 min-width 112px(按 fontTools 实测文本宽度定);
示例文本改 `例 1:06.33` 等并给录入表格内的 ::placeholder 加淡色斜体;新增
onBeforeRouteLeave 离开守卫(原来切菜单会静默丢未保存成绩)。
测试:服务端 13 包全绿(372 个测试函数);前端 vue-tsc 通过、205 例全过;Console 18 文件。
docs/ 与 devdocs/ 同步(FAQ 到 Q76,部署指南新增第 10 节「服务端日志与拦截复盘」)。
This commit is contained in:
1 parent
8e707e1e9b
commit
e5e0944d26
32 files changed
+2017
-138
No files matched your search
@@ -126,9 +126,9 @@ func New(opts Options) http.Handler {
|
||||
var h http.Handler = mux
|
||||
h = s.broadcastChanges(h)
|
||||
h = middleware.NewAuditor(opts.Event, logger).Middleware(h)
|
||||
h = middleware.RateLimitWrite(svc)(h)
|
||||
h = middleware.CSRF(h)
|
||||
h = middleware.MustChangePassword(h)
|
||||
h = middleware.RateLimitWrite(svc, logger)(h)
|
||||
h = middleware.CSRF(logger, h)
|
||||
h = middleware.MustChangePassword(logger, h)
|
||||
h = middleware.Authenticate(svc, logger)(h)
|
||||
h = middleware.Log(logger)(h)
|
||||
h = middleware.Recover(logger)(h)
|
||||
@@ -149,17 +149,17 @@ func (s *server) public(fn handler) http.HandlerFunc { return s.handle(fn) }
|
||||
|
||||
// read 注册「guest 也不可读」的接口,权限不足一律 403。
|
||||
func (s *server) read(role auth.Role, fn handler) http.HandlerFunc {
|
||||
return middleware.RequireRole(role)(s.handle(fn)).ServeHTTP
|
||||
return middleware.RequireRole(s.logger, role)(s.handle(fn)).ServeHTTP
|
||||
}
|
||||
|
||||
// write 注册写接口:未登录 401,角色不足 403;成功后按 X-Request-Id 支持幂等回放。
|
||||
func (s *server) write(role auth.Role, fn handler) http.HandlerFunc {
|
||||
return middleware.RequireWrite(role)(s.cached(s.handle(fn))).ServeHTTP
|
||||
return middleware.RequireWrite(s.logger, role)(s.cached(s.handle(fn))).ServeHTTP
|
||||
}
|
||||
|
||||
// writeVoid 注册无返回体的写接口。
|
||||
func (s *server) writeVoid(role auth.Role, fn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
|
||||
return middleware.RequireWrite(role)(s.cached(s.handleVoid(fn))).ServeHTTP
|
||||
return middleware.RequireWrite(s.logger, role)(s.cached(s.handleVoid(fn))).ServeHTTP
|
||||
}
|
||||
|
||||
func (s *server) routes(mux *http.ServeMux) {
|
||||
@@ -269,14 +269,14 @@ func (s *server) routes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/export/", s.notWired("导出"))
|
||||
mux.HandleFunc("/api/rules/", s.notWired("规则导入导出"))
|
||||
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeAPIError(w, s.logger, notFound("接口不存在"))
|
||||
s.writeAPIError(w, r, notFound("接口不存在"))
|
||||
})
|
||||
}
|
||||
|
||||
// notWired 为待接线的接口返回 501,避免请求被静态资源兜底成 HTML。
|
||||
func (s *server) notWired(name string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
writeAPIError(w, s.logger, notImplemented(name+"接口尚未接入"))
|
||||
s.writeAPIError(w, r, notImplemented(name+"接口尚未接入"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user