优化结构

This commit is contained in:
Yvan 2026-08-28 17:42:53 +08:00
parent 9148c10c0c
commit 8d290f2954
9 changed files with 27 additions and 17 deletions

View File

@ -14,7 +14,7 @@
| # | 问题 | 位置 | 状态 |
|---|------|------|------|
| V-39 | **(新发现,高)公开无限流写接口 `/sysError/createSysError`**:挂在 public.Group 无认证router/audit.go:32SecurityRateLimit 仅覆盖 /base/login 与 /base/captcharate_limit.go:16handler 直接绑定落库无内容限制handler/audit.go:305-315dto Info *string 无长度 bindingdto/audit.go:67-73——未登录者可无限速 POST 超大 Info 入库(该路径无 MaxBytesReader→ sys_error 表膨胀 DoS + 伪造错误记录污染排障视图 | server/router/audit.go:32middleware/rate_limit.go:16handler/audit.go:305-315dto/audit.go:67-73 | 十一轮 |
| ~~V-39~~ | ~~公开无限流写接口 `/sysError/createSysError`~~:已移入鉴权路由组、纳入安全限流,并限制 `Info` 最大长度 | ~~server/router/audit.gomiddleware/rate_limit.goservice/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-143media_upload.go:100-107,262 | 八轮发现,专项 |
| V-13b | swagger空 env fail-opengin.go:68 仍注册gin 白名单 vs main.go 黑名单双清单不一致qa/uat 时日志谎报env 门禁零测试——需统一单一判定函数+环境矩阵测试 | server/gin.go:64-70cmd/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-59data/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-167integration_config.go:348-351 | 九轮发现,声称不实 |
| ~~V-30~~ | ~~douyin `platform_serial_no` required 标记与别名校验不一致~~:字段已标记 required支付 required 校验同时识别 `platform_cert_serial` 别名 | ~~biz/integration/integration_config_definition.gointegration_config.go~~ | ~~十一轮复核~~ |
| V-38 | 媒体域裸错误ErrMediaTooLarge/ErrUploadSessionNotFoundvs 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/writeTimeoutconfig.yaml:9,15——DB 故障时 cleanup 链data.go:191-198 先 auditLog.Close 后关 DB被挂起 flush 永久阻塞,进程无法优雅退出 | data/data_scope_audit.go:67-73,88configs/config.yaml:9,15 | 十一轮 |
| V-43 | **(新发现,中)前端 token 存 localStorage7 天长期凭证 XSS 可窃)+ 401 登出回调可被并发弹窗吞掉**application/index.vue:16 已有弹窗时 show-error 被忽略、handleClose 置 null cb 不执行 → 401 后 token 残留循环) | web/src/pinia/modules/user.js:23components/application/index.vue:16utils/request.js:231-241 | 十一轮 |
| V-44 | 新发现V-36 词表收窄的噪音回归:"导出token无效或已过期"export.go:222不再命中词表——该路由 publiccatalog.go:107,109匿名坏 token 请求以 200+CodeError 进 ErrorAudit 落 sys_error 表,可被匿名刷量 | handler/export.go:222middleware/error_audit.go:26-45,73 | 十一轮 |
| V-45 | 新发现sysError 写路径自身失败无日志isSelfAudit 豁免error_audit.go:25-27声称"handler 已记录",但 CreateError 失败仅 Failhandler/audit.go:305-315无任何日志——静默失败盲区 | middleware/error_audit.go:25-27handler/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 errtask_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 块+未使用 newImgSizeglobal.js:51,54 未使用 allKeys+悬空比较表达式;另 3 处已注释 console 代码cropper.vue:198、upload.vue:363、menus.vue:302 | web/src/utils/image.js:32-35core/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 | 十一轮 |
## 三、重复实现 / 双轨残留

View File

@ -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)
}
}

View File

@ -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"},

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"`