优化结构
This commit is contained in:
parent
9148c10c0c
commit
8d290f2954
|
|
@ -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 | 十一轮 |
|
||||
|
||||
## 三、重复实现 / 双轨残留
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
Loading…
Reference in New Issue