diff --git a/docs/code-review-issues.md b/docs/code-review-issues.md index ae1da01..48e8b33 100644 --- a/docs/code-review-issues.md +++ b/docs/code-review-issues.md @@ -14,7 +14,7 @@ | # | 问题 | 位置 | 状态 | |---|------|------|------| -| V-39 | **(新发现,高)公开无限流写接口 `/sysError/createSysError`**:挂在 public.Group 无认证(router/audit.go:32);SecurityRateLimit 仅覆盖 /base/login 与 /base/captcha(rate_limit.go:16);handler 直接绑定落库无内容限制(handler/audit.go:305-315);dto Info *string 无长度 binding(dto/audit.go:67-73)——未登录者可无限速 POST 超大 Info 入库(该路径无 MaxBytesReader)→ sys_error 表膨胀 DoS + 伪造错误记录污染排障视图 | server/router/audit.go:32;middleware/rate_limit.go:16;handler/audit.go:305-315;dto/audit.go:67-73 | 十一轮 | +| ~~V-39~~ | ~~公开无限流写接口 `/sysError/createSysError`~~:已移入鉴权路由组、纳入安全限流,并限制 `Info` 最大长度 | ~~server/router/audit.go;middleware/rate_limit.go;service/dto/audit.go~~ | ~~十一轮~~ | | V-33 | 支付指纹严格相等校验对存量订单幂等重放的冲突风险(initialized/pending 存量订单重放报参数冲突而非幂等短路)——修复需兼容旧指纹、区分订单状态、覆盖并发重放,直接放宽会削弱防篡改 | biz/payment/payment.go:645-657 | 十轮发现,专项 | | V-24 | Delete↔Create 竞态窗口仍开(锁仅覆盖 Delete,秒传/上传/Complete 的 CreateMedia 路径不持锁);多副本需数据库/分布式锁 | biz/system/media.go:50,111-143;media_upload.go:100-107,262 | 八轮发现,专项 | | V-13b | swagger:空 env fail-open(gin.go:68 仍注册);gin 白名单 vs main.go 黑名单双清单不一致(qa/uat 时日志谎报);env 门禁零测试——需统一单一判定函数+环境矩阵测试 | server/gin.go:64-70;cmd/main.go:105-126,171 | 七轮发现,专项 | @@ -22,19 +22,19 @@ | V-20 | 前端 token 拼入 URL(完整 JWT 拼二维码 URL)——需后端一次性短时效票据+扫码协议迁移 | web/src/components/upload/QR-code.vue:55 | 六轮发现,专项 | | V-15 | **envelope 净化剩余三洞**(全角冒号已修✓——response.go:53 分隔符已扩 ["失败:","失败:"]):① 17 处无冒号/紧贴拼接调用截断不可命中(api.go:137,156、authority.go 9 处、menu.go:170,178,198、user.go:150),非黑名单错误原样透出;② **CodeError data 通道完全无净化**——payment TestProvider Stages[].Message 原始 err 随 data 到前端;③ 净化静默无日志("不泄露也不可排障")。另有误伤:宽泛词 marker+约 45 处截断吞 biz 可读提示——需统一错误模型+结构化 Data 脱敏+日志策略 | server/httpx/response.go:32-59;data/payment/payment.go:79-139 | 六轮发现,专项 | | V-19 | **证书别名缺口仍在(三层同源✓但键表未补别名——"已纳入键表"声称不实)**:app_cert_content/root_cert_content/pkcs12_content/cert_content/key_content/cert_path 等仍不命中 IsIntegrationSecretKey(后缀表无 *_content/*_path 项,integration_config.go:402-413)→ API 直连提交明文落库并回显,日志 resp_data 同泄;修法:剥后缀匹配词干或显式枚举(勿用裸 content/path 后缀防误伤) | biz/integration/integration_config.go:402-413,329-332,427-435 | 六轮发现,十轮部分+十一轮复核未闭合 | -| V-30 | **douyin platform_serial_no 仍 required=false("已改 required"声称不实)**——definition.go:167 第三参 false,校验器 :348-351 仍强制;正确修法需通用 required 循环感知别名(platform_cert_serial)再置 true,否则别名用例(payment_config_test.go:25-33)会被拒 | biz/integration/integration_config_definition.go:166-167;integration_config.go:348-351 | 九轮发现,声称不实 | +| ~~V-30~~ | ~~douyin `platform_serial_no` required 标记与别名校验不一致~~:字段已标记 required,支付 required 校验同时识别 `platform_cert_serial` 别名 | ~~biz/integration/integration_config_definition.go;integration_config.go~~ | ~~十一轮复核~~ | | V-38 | 媒体域裸错误(ErrMediaTooLarge/ErrUploadSessionNotFound)vs AGENTS typed errors 契约——需连同错误码表与 HTTP 映射一起迁移 | biz/system/media.go:52、media_upload.go:21 | 十轮发现,专项 | ### 待修(正确性缺陷) | # | 问题 | 位置 | 状态 | |---|------|------|------| -| V-40 | **(新发现,中)上传会话 TTL 按"创建时间"而非"最后活动时间"**:StaleUploadSessionIDs 用 session.updated_at 判陈旧(data:127-131),但 UpsertChunk 只写 chunk 表不 touch 会话行(data:106-112)——大文件跨天续传(创建超 24h 但持续活动)被判 stale 整体清除,续传报"会话不存在" | data/system/media_upload.go:106-112,127-131 | 十一轮 | +| ~~V-40~~ | ~~上传会话 TTL 未按最后活动时间刷新~~:每次分片写入后同步更新上传会话 `updated_at` | ~~data/system/media_upload.go~~ | ~~十一轮~~ | | V-41 | (新发现,中低)merging 仅 1h 宽限(data:129),远程存储 Compose 超 1h 的合法合并被清理并整体失败(有回滚无泄漏,功能误杀) | data/system/media_upload.go:129 | 十一轮 | | V-42 | **(新发现,中)dataScopeAuditWriter.Close 无超时且 MySQL DSN 无网络超时**:Close 无限期 `<-w.done`(:67-73)、flush 用 context.Background 无 deadline(:88)、DSN 无 readTimeout/writeTimeout(config.yaml:9,15)——DB 故障时 cleanup 链(data.go:191-198 先 auditLog.Close 后关 DB)被挂起 flush 永久阻塞,进程无法优雅退出 | data/data_scope_audit.go:67-73,88;configs/config.yaml:9,15 | 十一轮 | | V-43 | **(新发现,中)前端 token 存 localStorage(7 天长期凭证 XSS 可窃)+ 401 登出回调可被并发弹窗吞掉**(application/index.vue:16 已有弹窗时 show-error 被忽略、handleClose 置 null cb 不执行 → 401 后 token 残留循环) | web/src/pinia/modules/user.js:23;components/application/index.vue:16;utils/request.js:231-241 | 十一轮 | -| V-44 | (新发现,低)V-36 词表收窄的噪音回归:"导出token无效或已过期"(export.go:222)不再命中词表——该路由 public(catalog.go:107,109),匿名坏 token 请求以 200+CodeError 进 ErrorAudit 落 sys_error 表,可被匿名刷量 | handler/export.go:222;middleware/error_audit.go:26-45,73 | 十一轮 | -| V-45 | (新发现,低)sysError 写路径自身失败无日志:isSelfAudit 豁免(error_audit.go:25-27)声称"handler 已记录",但 CreateError 失败仅 Fail(handler/audit.go:305-315)无任何日志——静默失败盲区 | middleware/error_audit.go:25-27;handler/audit.go:305-315 | 十一轮 | +| ~~V-44~~ | ~~V-36 词表收窄导致“导出token无效或已过期”不再命中~~:已补充 `token无效` 匹配 | ~~middleware/error_audit.go~~ | ~~十一轮~~ | +| ~~V-45~~ | ~~sysError 写路径失败无日志~~:创建失败时已记录结构化错误日志 | ~~server/handler/audit.go~~ | ~~十一轮~~ | | V-46 | (新发现,低)调度器启动重试无上限无退避:永久性错误(cron 解析失败整批 return err,task_scheduler.go:204-207)会每 5s 重试永不成功+刷 Warn 日志(约 720 条/小时)——需区分永久/暂时性错误 | worker/task_scheduler.go:93-112,204-207 | 十一轮(V-35 修复残留) | | V-47 | (新发现,低)PersistRuntimeConfig 三段写非原子(先 storage 再 email 再 config,任一失败返回)——前面已写的集成配置与未替换的 runtime 出现不一致窗口 | data/initialization_backend.go:74-83 | 十一轮 | | V-31 残留 | UpdateMediaName 仍透传 gorm 原文(404 语义丢失) | data/system/media.go:125-127 | 九轮,部分修复 | @@ -48,7 +48,7 @@ | Z-13 残留 | cors.go:13-14 逗号空格、audit.go:104 单行 13 字段、audit.go:46-47 空 else-if——纯格式问题(用户评估:格式化工具统一处理) | server/middleware/ | 八轮,评估暂缓 | | Z-17 | scanUpload.vue 空 catch 吞路由就绪异常——需统一前端错误上报 | web/src/view/media/scanUpload.vue:115-116 | 九轮,专项 | | Z-22 | (新发现)Z-19 清理后前端死代码残留:image.js:32-35 空 if 块+未使用 newImgSize;global.js:51,54 未使用 allKeys+悬空比较表达式;另 3 处已注释 console 代码(cropper.vue:198、upload.vue:363、menus.vue:302) | web/src/utils/image.js:32-35;core/global.js:51,54 | 十一轮 | -| Z-23 | (新发现,V-30 关联)douyin 别名分支放错校验器:integration_config.go:247-249 在 validateCommunicationIntegrationConfig(仅 MQ/WS 走入)里写 douyin 支付的别名回退——该分支不可达,死代码 | biz/integration/integration_config.go:247-249 | 十一轮 | +| ~~Z-23~~ | ~~douyin 别名分支放错校验器~~:已移除通信校验器中的不可达分支,别名逻辑归入支付校验器 | ~~biz/integration/integration_config.go~~ | ~~十一轮~~ | | Z-24 | (新发现)秒传分支变量名 `copy` 遮蔽内置函数(可读性) | biz/system/media_upload.go:100 | 十一轮 | ## 三、重复实现 / 双轨残留 diff --git a/internal/biz/integration/integration_config.go b/internal/biz/integration/integration_config.go index 6165fbb..3e6d6ec 100644 --- a/internal/biz/integration/integration_config.go +++ b/internal/biz/integration/integration_config.go @@ -244,9 +244,6 @@ func validateCommunicationIntegrationConfig(kind, provider string, values map[st } for _, field := range definition.Fields { value := integrationText(values, field.Key) - if provider == bizpayment.PaymentDouyin && field.Key == "platform_serial_no" { - value = integrationFirst(values, "platform_serial_no", "platform_cert_serial") - } if field.Required && value == "" { return fmt.Errorf("%s 缺少配置字段 %s", provider, field.Key) } @@ -316,7 +313,11 @@ func validatePaymentIntegrationConfig(provider string, values map[string]any) er return errors.New("不支持的支付渠道") } for _, field := range definition.Fields { - if field.Required && integrationText(values, field.Key) == "" { + value := integrationText(values, field.Key) + if provider == bizpayment.PaymentDouyin && field.Key == "platform_serial_no" { + value = integrationFirst(values, "platform_serial_no", "platform_cert_serial") + } + if field.Required && value == "" { return fmt.Errorf("%s 缺少配置字段 %s", provider, field.Key) } } diff --git a/internal/biz/integration/integration_config_definition.go b/internal/biz/integration/integration_config_definition.go index 7eb32e8..b93d453 100644 --- a/internal/biz/integration/integration_config_definition.go +++ b/internal/biz/integration/integration_config_definition.go @@ -164,7 +164,7 @@ var integrationDefinitions = map[string][]IntegrationConfigDefinition{ paymentDefinition(bizpayment.PaymentApple, "Apple IAP", "Apple App Store Server API", map[string]any{"issuer_id": "", "key_id": "", "bundle_id": "", "private_key": "", "price_divisor": 10, "environment": "production", "test_product_id": "", "test_transaction_id": ""}, integrationField("issuer_id", "Issuer ID", true, false, "text"), integrationField("key_id", "Key ID", true, false, "text"), integrationField("bundle_id", "Bundle ID", true, false, "text"), integrationField("private_key", "P8 私钥", true, true, "textarea"), integrationField("price_divisor", "价格除数", false, false, "number"), integrationSelect("environment", "环境", false, "production", "sandbox"), integrationField("test_product_id", "测试商品 ID", false, false, "text"), integrationField("test_transaction_id", "沙箱交易 ID", false, false, "text")), paymentDefinition(bizpayment.PaymentDouyin, "抖音支付", "抖音开放平台支付", map[string]any{"app_id": "", "merchant_id": "", "serial_no": "", "api_key": "", "private_key": "", "platform_cert": "", "platform_serial_no": "", "trade_type": "jsapi", "environment": "production"}, - integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("serial_no", "商户证书序列号", true, false, "text"), integrationField("api_key", "API 密钥", true, true, "password"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("platform_cert", "平台证书", true, true, "textarea"), integrationField("platform_serial_no", "平台证书序列号", false, false, "text"), integrationSelect("trade_type", "默认交易类型", false, "app", "jsapi", "h5", "native"), integrationSelect("environment", "环境", false, "production")), + integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("serial_no", "商户证书序列号", true, false, "text"), integrationField("api_key", "API 密钥", true, true, "password"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("platform_cert", "平台证书", true, true, "textarea"), integrationField("platform_serial_no", "平台证书序列号", true, false, "text"), integrationSelect("trade_type", "默认交易类型", false, "app", "jsapi", "h5", "native"), integrationSelect("environment", "环境", false, "production")), paymentDefinition(bizpayment.PaymentQQ, "QQ 钱包", "QQ 钱包支付", map[string]any{"mch_id": "", "api_key": "", "sign_type": "MD5", "trade_type": "NATIVE", "cert_file": "", "key_file": "", "environment": "production"}, integrationField("mch_id", "商户号", true, false, "text"), integrationField("api_key", "API 密钥", true, true, "password"), integrationSelect("sign_type", "签名算法", false, "MD5", "HMAC-SHA256"), integrationSelect("trade_type", "默认交易类型", false, "JSAPI", "NATIVE", "APP", "MICROPAY"), integrationField("cert_file", "退款证书路径", false, false, "text"), integrationField("key_file", "退款私钥路径", false, true, "text"), integrationSelect("environment", "环境", false, "production")), paymentDefinition(bizpayment.PaymentAllinPay, "通联支付", "通联收银宝支付", map[string]any{"cus_id": "", "app_id": "", "private_key": "", "public_key": "", "org_id": "", "pay_type": "W02", "query_order_type": "reqsn", "currency": "CNY", "environment": "production"}, diff --git a/internal/data/system/media_upload.go b/internal/data/system/media_upload.go index 740ec38..34bf3ae 100644 --- a/internal/data/system/media_upload.go +++ b/internal/data/system/media_upload.go @@ -105,10 +105,16 @@ func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error { } func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *system.UploadChunk) error { po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size} - return r.data.DB().WithContext(ctx).Clauses(clause.OnConflict{ + if err := r.data.DB().WithContext(ctx).Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "upload_id"}, {Name: "chunk_index"}}, DoUpdates: clause.AssignmentColumns([]string{"chunk_hash", "size", "updated_at", "deleted_at"}), - }).Create(&po).Error + }).Create(&po).Error; err != nil { + return err + } + if err := r.data.DB().WithContext(ctx).Model(&uploadSessionPO{}).Where("id = ? AND status = ?", uploadID, "uploading").Update("updated_at", time.Now()).Error; err != nil { + return err + } + return nil } func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*system.UploadChunk, error) { var pos []uploadChunkPO diff --git a/internal/server/handler/audit.go b/internal/server/handler/audit.go index ee036f3..241f754 100644 --- a/internal/server/handler/audit.go +++ b/internal/server/handler/audit.go @@ -309,6 +309,9 @@ func (h *Audit) CreateError(c *gin.Context) { return } if err := h.recorder.CreateErrorMutationRequest(c.Request.Context(), &req); err != nil { + if h.logger != nil { + h.logger.ErrorContext(c.Request.Context(), "创建错误记录失败", "mod", "audit", "error", err) + } Fail(c, "创建失败") return } diff --git a/internal/server/middleware/error_audit.go b/internal/server/middleware/error_audit.go index c64ee74..ca7ce7c 100644 --- a/internal/server/middleware/error_audit.go +++ b/internal/server/middleware/error_audit.go @@ -70,7 +70,7 @@ func failureLogModule(path string) string { } func expectedClientFailure(message string) bool { - for _, value := range []string{"参数错误", "请输入用户名和密码", "验证码错误", "用户名不存在或者密码错误", "用户被禁止登录", "账号已锁定", "请求太过频繁", "权限不足", "密码已过期", "未登录", "token 失效", "令牌失效"} { + for _, value := range []string{"参数错误", "请输入用户名和密码", "验证码错误", "用户名不存在或者密码错误", "用户被禁止登录", "账号已锁定", "请求太过频繁", "权限不足", "密码已过期", "未登录", "token 失效", "token无效", "令牌失效"} { if strings.Contains(message, value) { return true } diff --git a/internal/server/middleware/rate_limit.go b/internal/server/middleware/rate_limit.go index 0915e06..f9fd599 100644 --- a/internal/server/middleware/rate_limit.go +++ b/internal/server/middleware/rate_limit.go @@ -13,7 +13,7 @@ import ( func SecurityRateLimit(settings *systemservice.SecurityService) gin.HandlerFunc { return func(c *gin.Context) { path := strings.TrimSuffix(c.Request.URL.Path, "/") - if !strings.HasSuffix(path, "/base/login") && !strings.HasSuffix(path, "/base/captcha") { + if !strings.HasSuffix(path, "/base/login") && !strings.HasSuffix(path, "/base/captcha") && !strings.HasSuffix(path, "/sysError/createSysError") { c.Next() return } diff --git a/internal/server/router/audit.go b/internal/server/router/audit.go index ccd9636..7e853ba 100644 --- a/internal/server/router/audit.go +++ b/internal/server/router/audit.go @@ -29,5 +29,5 @@ func RegisterAudit(group, public *gin.RouterGroup, h *handler.Audit) { errors.PUT("/updateSysError", h.UpdateError) errors.GET("/findSysError", h.Error) errors.GET("/getSysErrorList", h.Errors) - public.Group("/sysError").POST("/createSysError", h.CreateError) + group.Group("/sysError").POST("/createSysError", h.CreateError) } diff --git a/internal/service/dto/audit.go b/internal/service/dto/audit.go index 0235d4a..2d43e8e 100644 --- a/internal/service/dto/audit.go +++ b/internal/service/dto/audit.go @@ -67,7 +67,7 @@ type ErrorRecordRequest struct { type ErrorRecordMutationRequest struct { ID uint `json:"ID"` Form *string `json:"form" binding:"required"` - Info *string `json:"info"` + Info *string `json:"info" binding:"max=16384"` Level string `json:"level"` RequestID string `json:"request_id"` TraceID string `json:"trace_id"`