优化结构

This commit is contained in:
Yvan 2026-08-28 14:54:06 +08:00
parent 56f9440a6b
commit ae5345de14
15 changed files with 188 additions and 93 deletions

View File

@ -9,14 +9,16 @@ agents must follow when changing the template.
cmd/ Entrypoint, Wire injector, main.go. cmd/ Entrypoint, Wire injector, main.go.
configs/ Runtime config (config.yaml). No secrets. configs/ Runtime config (config.yaml). No secrets.
internal/config/ Viper config models, loading, snapshots, and reloads. internal/config/ Viper config models, loading, snapshots, and reloads.
internal/logging/ Structured logging sinks and source metadata.
internal/server/ Gin handlers, middleware, routers, static files, HTTP wiring. internal/server/ Gin handlers, middleware, routers, static files, HTTP wiring.
internal/service/dto/ Hand-written request and response DTOs. internal/service/dto/ Hand-written request and response DTOs.
internal/service/ DTO/DO transport adapters; organized by domain. internal/service/ DTO/DO transport adapters; organized by domain.
internal/biz/ Domain models, usecases, repo interfaces, errors. internal/biz/ Domain models, usecases, repo interfaces, errors.
internal/data/ Repo implementations, database clients, migrations. internal/data/ Repo implementations, database clients, migrations.
internal/initialize/ First-install and configuration orchestration. internal/initialize/ First-install and configuration orchestration.
internal/integration/ External I/O adapters: cache, email, payment, storage. internal/integration/ External I/O adapters: cache, email, payment, storage (mq, websocket, runtimeconfig, systeminfo).
internal/modules/ Built-in module schema, seed, menu, API, and task contributions. internal/modules/ Built-in module schema, seed, menu, API, and task contributions.
internal/paymentkit/ Shared payment normalization, signing, and JSON helpers.
internal/routecatalog/ HTTP route metadata and runtime policies. internal/routecatalog/ HTTP route metadata and runtime policies.
internal/worker/ Timed-task runtime and scheduler. internal/worker/ Timed-task runtime and scheduler.
internal/utils/ Stateless, internal-only helper packages. internal/utils/ Stateless, internal-only helper packages.

View File

@ -1,90 +1,102 @@
# 代码审查问题清单internal + pkg # 代码审查问题清单internal + pkg
- 审查日期2026-08-27 ~ 2026-08-28五轮全量审查。第五轮2026-08-28核查第四至七轮全部处置声称W/S/P/F/C/L/D 系列已修复与评估保留项)+ 全新视角回归审查`go build ./...` 编译验证通过 - 审查日期2026-08-27 ~ 2026-08-28七轮全量审查。第七轮2026-08-28核查第六轮待办项 + **过分拆专项审查**(用户指定重点:少文件文件夹/少行数文件是否过度拆解)`go build ./...` 编译验证通过
- 文档结构:只保留待修复问题,按**类型**归类(不按轮次);每条标注发现轮次;已修复并经复查确认的、以及经评估决定保留的直接删除 - 文档结构:只保留待修复问题,按**类型**归类;每条标注发现轮次;已修复并经复查确认的、经评估决定保留的、用户决策不修的直接删除
- gva/ 目录是遗留参考库(独立 module 不参与 kra 编译),不在审查范围 - gva/ 目录是遗留参考库(独立 module 不参与 kra 编译),不在审查范围
- 依赖方向合规确认pkg 无 import internalintegration 不 import data/servicedata 不 import integration/payment无循环依赖 - 依赖方向合规pkg 无 import internalintegration 不 import data/servicedata 不 import integration/payment无循环依赖
- 第五轮总评:核心业务路径(支付/调度/数据权限/上传)逻辑严谨、分层契约执行到位;历轮修复全部属实;当前债务已收敛为**敏感数据落地防护不对称、文档漂移、中转微文件**三类
--- ---
## 一、安全与正确性缺陷(第五轮新发现,最优先) ## 一、安全与正确性缺陷(最高优先级)
| # | 问题 | 位置 | 说明 |
|---|------|------|------|
| ~~V-1~~ | ~~已清空 `configs/config.yaml` 中的 MySQL DSN、数据库密码和 JWT signing key模板仅保留 `KRA_*` 环境变量覆盖说明。泄漏值仍存在于历史提交,需在部署侧轮换数据库密码与 JWT 密钥。~~ | ~~configs/config.yaml:9,13,91~~ | ~~已完成2026-08-28~~ |
| ~~V-2~~ | ~~已让操作审计 Response 复用 `redactJSON`,与访问日志保持同一脱敏和长度上限策略,避免 token/密钥字段明文入库。~~ | ~~server/middleware/audit.gocapture.go~~ | ~~已完成2026-08-28针对性测试通过~~ |
| ~~V-3~~ | ~~已补齐 `key`、`public_cert`、`signing_key`、`secret_key` 等敏感键,并将 integration payment test 路由标记为 `BodyPolicyPaymentConfig`,请求体统一使用摘要策略。~~ | ~~server/middleware/redact.goroutecatalog/catalog.go~~ | ~~已完成2026-08-28针对性测试通过~~ |
| ~~V-4~~ | ~~已在 config watcher 与 data reload 入口补充运行时契约说明watch 只发布不可变快照,`reloadSystem` 才重建数据库/缓存/存储/集成客户端;保留双路径以维护回滚与连接退休安全。~~ | ~~internal/config/runtime.gointernal/data/config_store.go~~ | ~~已完成2026-08-28~~ |
| ~~V-5~~ | ~~已修正 ApplySync 失败文案为「同步失败」。~~ | ~~server/handler/api.go:195~~ | ~~已完成2026-08-28~~ |
| ~~V-6~~ | ~~已移除 audit handler 八处 `err.Error()` 拼接;客户端只收到稳定分类提示,内部错误仍由审计/日志链路保留。~~ | ~~server/handler/audit.go~~ | ~~已完成2026-08-28~~ |
| ~~V-7~~ | ~~已补齐秒传复制的 `Size`、`MD5`、`Mime`、`UserID` 字段,保持与完整上传记录一致。~~ | ~~biz/system/media_upload.go~~ | ~~已完成2026-08-28~~ |
| ~~V-8~~ | ~~`CompleteUploadSession` 失败时现在回退会话状态并返回错误,避免 media 已创建而 session 长期停留 `merging`;非关键分片清理仍保持 best-effort。~~ | ~~biz/system/media_upload.go~~ | ~~已完成2026-08-28~~ |
| ~~V-9~~ | ~~已逐项确认用户重名检查排除软删记录是有意的重新注册语义export 关系替换使用软删以保留审计历史;对软删记录更新匹配 0 行符合不可恢复约束;关联查询显式 `deleted_at IS NULL` 与 Model scope 叠加属于迁移期防御,并新增参数软删除更新回归测试。~~ | ~~data/system/*~~ | ~~评估完成保留2026-08-28~~ |
| ~~V-10~~ | ~~已删除外部退款分支中 `!accepted` 的不可达兜底,统一返回已确认的 `providerErr`。~~ | ~~biz/payment/payment.go~~ | ~~已完成2026-08-28~~ |
## 二、死代码残留(第五轮新扫描)
| 死代码 | 位置 | 证据 |
|--------|------|------|
| ~~PaymentOrderSourceRegistry.Len~~ | ~~已删除:全仓(含测试)零调用。~~ | ~~biz/payment/payment_order.go~~ |
| ~~PaymentFulfillmentRegistry.Len~~ | ~~已删除:全仓(含测试)零调用。~~ | ~~biz/payment/payment.go~~ |
| ~~NewConfiguredPaymentUsecase 恒 nil 错误透传壳~~ | ~~已删除并通过 Wire 重新生成ProviderSet 直接绑定 `NewPaymentUsecase`。~~ | ~~biz/payment/provider.gocmd/wire_gen.go~~ |
| ~~biz/integration mergeIntegrationDefaults 一行转发壳~~ | ~~已删除,调用点统一使用 `MergeIntegrationDefaults`。~~ | ~~biz/integration/integration_config.go~~ |
| ~~P-15 残留~~ | ~~已修正 handler/http.go 注释与 source.go 旧路径标记,并同步移除过时测试样本。~~ | ~~server/handler/http.gointernal/logging/source.go~~ |
| ~~`convertAuthority` 的 DeletedAt 值传递被 json:"-" 丢弃~~ | ~~已删除无效赋值,避免向已隐藏字段传递无效数据。~~ | ~~service/system/user_conversion.go~~ |
## 三、重复实现残留(历轮收敛后的漏网项)
| # | 问题 | 位置 | 轮次 | | # | 问题 | 位置 | 轮次 |
|---|------|------|------| |---|------|------|------|
| ~~D-2 遗漏 1-3~~ | ~~已统一 qq、wechat v3 与 payment data 的支付方式归一化到 `paymentkit.NormalizePaymentMethod`,消除点号/短横线/空格处理漂移。~~ | ~~integration/payment/qq.gowechat_v3.godata/payment/payment.go~~ | ~~已完成2026-08-28针对性测试通过~~ | | V-11 | **脱敏退化洞(部分修复后残留核心)**maxBytes 已改为 `max(1MiB, logLimit)` 跟随配置,但 captureWriter **无 truncated 标志**redactJSON 原文分支用 `>` 而非 `>=`——当 `access_log_max_bytes ≥ 1MiB` 且响应超该值时,截断体长度恰等于 limit未脱敏明文仍进入 resp_data 与操作审计 Response`zap.access_resp_data` 默认 true。修法captureWriter 加 truncated 标志,截断体直接返回占位符 | server/middleware/capture.go:19-39,56-60access_log.go:53-57audit.go:79 | 六轮发现,七轮复核部分修复 |
| ~~D-31~~ | ~~已复用 `paymentOrderFingerprint` 完成 BeforeCreate 前后不可变校验,保留同等字段覆盖范围并删除 10 个 canonical 局部变量。~~ | ~~biz/payment/payment.go~~ | ~~已完成2026-08-28针对性测试通过~~ | | V-19 | **integration 掩码/恢复键集不对称(高危破坏链)**读路径掩码service 层 contains cert/p12 启发式宽于写路径恢复data 层 definition Secret + 后缀表)——① **`key_pem` 两层均不命中明文泄漏**biz 校验 :335 接受的微信 V2 退款私钥别名);② **回显保存会把 `******` 落库破坏支付配置**被读掩码但不被写恢复的键cert_pem/pkcs12_file/pkcs12_content/cert_path/cert_content/app_cert_path/root_cert_content/public_cert_content 及 QQ cert_file——definition 标 Secret=false 却被 contains "cert" 掩码)经前端回显提交后原样落库并 publish 到 runtime支付退款能力静默损坏且非空校验仍通过③ 空值也被掩码 + 新建分支无恢复,未配置渠道保存即落库 `******`;④ data 层掩码/恢复不递归数组service 层递归);⑤ service 层掩码值硬编码 `"******"` 未引用 config.MaskedSecret。修法统一两侧判定为共享键表 | service/integration/integration_config.go:69-89,79掩码data/integration/integration_config.go:224-281,258恢复biz/integration/integration_config.go:335biz/integration/integration_config_definition.go:169 | 六轮发现,七轮复核部分修复+发现破坏链 |
| ~~D-32~~ | ~~已新增 `BodyPolicyUpload` 并让 AccessLog 按 routecatalog 决定上传请求体限额,删除硬编码 URL 后缀判断。~~ | ~~routecatalog/catalog.goserver/middleware/access_log.go~~ | ~~已完成2026-08-28针对性测试通过~~ | | V-15 | **handler 层 service 错误泄露 86 处(零收敛)**organization.go 16 处、version.go 15 处stage 分类骨架已建但所有分支仍拼 err.Error()形同虚设、parameter.go 7、authority.go 9、payment.go 11含 TestProvider 的 `Write(c, CodeError, result, err.Error())`、integration_config.go 5provider 连接细节、media.go 5**分片上传四端点为新泄露**222/263/281/298、task.go 4、menu.go 5、api.go:137,156、user.go:150,238、dictionary.go:103、export.go:201、api_token.go:26、system_config.go:77、permission.go:48。audit 域V-6与各读路径固定文案保持良好failLogViewer 范式未推广 | server/handler/organization.go:24-248、version.go:107-120,158-167、media.go:222-298 等 | 六轮发现,七轮复核未修复 |
| V-16b | **支付方式词表残余不对齐**V-16 三处主表已修其他渠道漏网douyin.go:116 缺 `miniapp`/`mini_app`其他渠道均接受lakala.go:41 缺 `mini_app`alipay.go:440 缺裸词 `mini`/`miniprogram`。另有测试缺口alipayCreateMethod/saobei/lakala/allinpay 无单测wechat_v2/v3 未将新增同义词作为用例字面量 | integration/payment/douyin.go:116、lakala.go:41、alipay.go:440 | 七轮 |
| V-17 | 秒传一致性两处残留:① 秒传 copy 直接沿用 `media.Mime` 不走 CompleteUpload 的 TypeByExtension 兜底(源记录 Mime 空则落空串);② 秒传 Tag 沿用源文件扩展而 Name 用新名,扩展与 Tag 不一致(普通路径 Tag 取 session.FileName 扩展,行为分叉) | biz/system/media_upload.go:100 vs :250-255 | 六轮发现,七轮复核部分修复 |
| V-18 | 上传会话回收缺口(孤儿 media 补偿已加 :259-263 best-effort① failed 会话分片无人回收StaleUploadSessionIDs:121-125 只查 uploadingfail() 只置状态不删分片,唯一回收入口是用户手动 Cancel**merging 僵死同样不回收**Claim 置 merging 后进程崩溃则会话永停 mergingSaveChunk/Complete 均拒);③ 补偿三步错误均 `_ =` 吞掉无重试/审计 | data/system/media_upload.go:121-125,85-88biz/system/media_upload.go:205-208,259-263 | 六轮发现,七轮复核部分修复 |
| V-20 | 前端 token 拼入 URL 未修复:完整长期登录 token 仍拼入二维码 URL`console.log(codeUrl.value)` 把含 token 的 URL 打进控制台 | web/src/components/upload/QR-code.vue:55,57scanUpload.vue:111-115 | 六轮发现,七轮复核未修复 |
| V-13b | Swagger 开关三处质量缺陷:① 无 env=production 下"swagger 不注册"的测试用例(现有测试全用 `config.Admin{}`);② env 仅精确匹配 "production"(配 prod/live 等同义值仍暴露Env 为自由字符串无枚举校验);③ fail-open——配置缺失时选择暴露而非隐藏与最小暴露原则相反 | server/gin.go:64-66config/types.go:186-190 | 七轮V-13 修复的保留项) |
## 四、文档漂移(第五轮新发现,低成本高收益) ## 二、死代码与碎屑
| # | 问题 | 位置 | | # | 问题 | 位置 | 轮次 |
|---|------|------| |---|------|------|------|
| ~~F-21~~ | ~~已将 CLAUDE.md 正文同步为 Gin+手写 DTO+make generate 的现行仓库约定,移除 proto/AIP/fieldmask 时代描述。~~ | ~~CLAUDE.md~~ | ~~已完成2026-08-28~~ | | Z-1 | audit.go isDownloadResponse 不可达分支仍在redactJSON 返回值恒 ≤limit`len>maxBytes` 数学恒假):连带死代码群 operationDownloadHeaders 表(:168-178+ isDownloadResponse:180-188整体不可达——上轮只删了注释未删分支留下比注释更大的死块:82-84 下载截断判断与 redactJSON 内部逻辑冗余 | server/middleware/audit.go:82-84,166-188 | 六轮发现,七轮复核部分修复 |
| ~~F-22~~ | ~~已修正 pkg/README.md移除不存在的 logging/httpx/paymentkit 公共包描述。~~ | ~~pkg/README.md~~ | ~~已完成2026-08-28~~ | | Z-2 | 两个 migration step 重复播种同一条 test APIensureCommunicationSurface:88 已含+已授权ensureCommunicationTestSurface:145-153 再种一遍)——新装环境纯冗余、存量环境一次性补种后永久空转 | data/system/migrations.go:88,145-153,31-32 | 六轮发现,七轮复核未修复 |
| ~~F-23~~ | ~~已修正 internal/README.md 与 internal/server/README.md 的 httpx 路径。~~ | ~~internal/README.mdinternal/server/README.md~~ | ~~已完成2026-08-28~~ | | Z-3 | 支付指纹两套算法并存paymentTestFingerprint整结构 marshal与 paymentOrderFingerprint白名单字段+extra——测试单号随机不参与幂等无害但属碎屑 | data/payment/payment.go:227-231 vs biz/payment/payment.go:957-966 | 六轮发现,七轮复核未修复 |
| ~~F-24~~ | ~~已将 integration README 的 paymentkit 路径改为 `internal/paymentkit`。~~ | ~~internal/integration/README.md~~ | ~~已完成2026-08-28~~ | | Z-5 | error_audit.go:25 单行复合布尔未修反而加剧(新增 `auditPersistFailed != true &&` 前缀使该行更长;新功能本身正确且有测试) | server/middleware/error_audit.go:25 | 六轮发现,七轮复核恶化 |
| ~~F-25~~ | ~~已修正 service README根包仅聚合 Wire ProviderSet不再声称 re-export 门面。~~ | ~~internal/service/README.md~~ | ~~已完成2026-08-28~~ | | Z-6 | 测试缺口五处OperationAudit 端到端测试现有仅函数级paymentCreateRequiresNotifyURL 测试NormalizePaymentMethod 直接单元测试;支付词表新增同义无用例(见 V-16bCORS allow-all 无回归测试credentials=false 一旦回退不会被捕获) | 各处 | 六轮发现,七轮扩展 |
| ~~F-26~~ | ~~已在 AGENTS.md 项目结构中补充 `docs/`。~~ | ~~AGENTS.md~~ | ~~已完成2026-08-28~~ | | Z-7 | audit.go capturedBody 改造引入的新死赋值::46-47 `requestBody = []byte(stringValue(value))` 在 ctxReqBodyKey 存在分支执行后必被 :89 再次命中置 capturedBody=true赋的值永不被读:88-100 if/else 两终分支重复调用逐字相同的 operationRequestBody 表达式,可折叠 | server/middleware/audit.go:46-47,88-100 | 七轮 |
| Z-8 | routecatalog 描述含糊getSysParam 与 getSysParamsList 描述均为「获取参数列表」(前者语义是按 key 取单值),管理端菜单两条同名接口 | routecatalog/catalog.go:118-119 | 七轮 |
## 五、过分拆分残留(微文件清单,行数实测) ## 三、重复实现 / 双轨残留
| 文件 | 行数 | 说明 | | # | 问题 | 位置 | 轮次 |
|---|---|---| |---|------|------|------|
| ~~internal/modules/surface/surface.go~~ | ~~19~~ | ~~评估保留:隔离 routecatalog 到 pkg/module 的适配依赖。~~ | | Y-1 | 上传限额 fallback 复制access_log.go:42-46 手写 `MaxFileSize>0?…:Default`+1MB 边际biz `EffectiveMaxFileSize` 已有同源 fallbackhandler/media.go 已复用——middleware 已 import biz/system 可收敛;调整即静默漂移 | server/middleware/access_log.go:42-46 vs biz/system/settings.go:27-32 | 六轮发现,七轮复核未修复 |
| ~~internal/data/provider/provider.go~~ | ~~8~~ | ~~评估保留:`Database` seam 被多个 data 子包独立依赖。~~ | | Y-2 | 响应体双重 JSON 处理请求体方向已修audit.go 优先复用 AccessLog 已脱敏值access_log.go:112 与 audit.go:79 仍各自对同一响应体做 解析+脱敏+重序列化 两次;且 capture.go:55-67 先 marshal 后查超限,超限时 marshal 工作白做——建议超限先判长度 | server/middleware/access_log.go:112audit.go:79capture.go:55-67 | 六轮发现,七轮复核部分修复 |
| ~~internal/data/data_scope_record.go~~ | ~~9~~ | ~~评估保留:隔离 data 回调写模型与 system 查询模型。~~ |
| ~~internal/biz/task/task_registry.go~~ | ~~11~~ | ~~评估保留biz 只暴露窄注册接口,避免 worker/pkg/task 类型穿透。~~ |
| ~~internal/service/dto/{email,authentication,permission,system_init}.go~~ | ~~7/8/12/13~~ | ~~评估保留:按 transport 契约分文件,合并只减少文件数。~~ |
| ~~internal/server/handler/{session,navigation,set,http}.go~~ | ~~25/26/26/31~~ | ~~评估保留:单方法 handler 与响应词汇表由 Wire/路由注入约束形成。~~ |
| ~~internal/server/router 8 个单域注册微文件~~ | ~~12-19~~ | ~~评估保留:领域路由注册边界清晰,统一合并会扩大单文件变更面。~~ |
Wire ProviderSet 微文件(约 10 个 ≤9 行)属 Wire 惯例不计债务biz/system 微文件群、SystemConfigService、provider/providers 双文件等已在历轮合并完成。 ## 四、文档漂移(残余项)
| # | 问题 | 位置 | 轮次 |
|---|------|------|------|
| F-33 | CLAUDE.md 残余矛盾四处(原样未动)::77-78 "API error reason enum"proto 残留vs AGENTS.md "stable reason strings":102 "HTTP/gRPC servers"(项目无 gRPC:8-25 目录树缺 docs/、internal/logging/、internal/paymentkit/AGENTS.md 均有);:115 Wiring 简化版与 AGENTS.md 详细版不同步 | CLAUDE.md:8-25,77-78,102,115 | 六轮发现,七轮复核未修复 |
| F-34 | pkg/README.md「当前包含」漏列 mq/、websocket/ 实际包;且 :7-10 与 :10 重复罗列 database/module/task | pkg/README.md:5-13 | 六轮发现,七轮复核未修复 |
## 五、过分拆清单(第七轮专项,用户指定重点)
全库 291 个非测试 .go 文件中 <40 行者 81 15 wire ProviderSet 微文件为项目惯例非债务 50 个为分层契约/域对称模式自然产物合理**16 个为真实过拆候选**
### 5.1 硬过拆建议合并4 项)
| 文件 | 行数 | 问题 | 合并目标 |
|---|---|---|---|
| internal/data/data_scope_record.go | 9 | 单行类型别名,包内 13 处使用、主要消费者就是 data_scope.go文件零独立职责 | 并入 data/data_scope.go |
| internal/service/dto/authentication.go | 8 | 仅 LoginResponse 1 个类型同域分裂实锤LoginRequest/Route 在 system.go一个登录流程 DTO 横跨两文件 | 并入 dto/system.go |
| internal/data/task/provider.go 别名层 | 11 | `type Provider = dataprovider.Database` 纯转手别名 | 删别名直接用 dataprovider.Database |
| internal/biz/task/task_registry.go 别名行 | 11 | TaskMethodFunc/TaskMethod 两行纯别名零增值(窄接口本身有三处消费,保留) | 删 :5-6 别名行 |
### 5.2 轻度过拆建议合并5 项)
| 文件 | 行数 | 问题 | 建议 |
|---|---|---|---|
| handler/navigation.go | 26 | 仅 Menu 一个端点,依赖的 UserService 与 user.go 相同,无架构理由 | 并入 user.go |
| handler/session.go | 25 | 仅 Logout 一个端点单方法 | 并入 user.go 或 public.go |
| handler/http.go | 31 | 转发不一致:转发 httpx 常量但 session.go:4 又直接 import middleware"单一词汇表"目标未达成,包内双风格) | 删转发改直用 httpx或补齐统一风格 |
| data/system/time.go | 15 | deletedAtPointer 单函数无独立文件必要 | 并入 models.go |
| data/system/audit.go | 17 | 3 个 repo 构造器与实现跨文件分离(方法散布 5 个文件,阅读跳 2 次) | 构造器移回各自首个实现文件 |
### 5.3 可选合并模式性过拆2 组)
- **router 21 个微文件**12-33 行/个)→ 并入 routes.go 约 420 行(与 routecatalog 449 行体量一致,项目已证明可接受);无 wire/测试/依赖方向约束,仅剩"每域一文件"对称风格
- **modules 4 个 definition 包**(各 1 文件 17-24 行、单一消费者 catalog.go→ 可合并为 catalog.go 单文件 ~110 行;保留理由是模块插件式对称,若不打算支持外部模块注册属过度形式化
### 5.4 接口碎片化(结构性)
- **同一 DB seam 三套名字、同包双 seam**data/provider.Database2 方法)→ data/task/provider.go 别名 → data/system.Provider4 方法超集)三名字并存;且 data/system/security.go import 外部 2 方法版而不用同包超集——同包两个 seam 混用。建议task 删别名system 包内统一用一个 seam
### 5.5 判定为合理的(复检确认,防误报)
- **modules/surface保留理由升级为编译级硬约束**——catalog.go:7-10 import 四个子包 definition子包 definition.go:7 又 import surface若并入根包即循环导入Go 禁止),独立成包是唯一解
- 单文件但职责完整的包cache/email/runtimeconfig/systeminfo/routecatalog/httpx/staticfiles/utils 两包/gormkit/pagination/module/task 等 24 个):均有真实多消费者或分层必需
- biz 域微文件cache/email/errors/maintenance/permission 等 8 个):分层契约自然形态
- dto email/permission/system_init7-13 行):轻度过拆但按域对称可容忍
- service 层 access_control/email/permission/security_session 微文件DTO↔DO 对称模式
## 审查后认为合理、不建议改动的部分(历轮评估保留决策汇总) ## 审查后认为合理、不建议改动的部分(历轮评估保留决策汇总)
- **biz 注入面**RegisterBusinessModule/PaymentBusinessModule 是 docs/PAYMENT.md:121-129 明文声明的业务接入契约模板无生产实现属预期PayInternal/RefundInternal/AuthorizeRefund 被 biz 调用链消费 - **biz 注入面**RegisterBusinessModule/PaymentBusinessModule 是 docs/PAYMENT.md:121-129 声明的业务接入契约PayInternal/RefundInternal/AuthorizeRefund 被 biz 调用链消费
- **F-2/F-4/F-6/F-7/F-9/F-10/F-11**result.go 自有协议语义非纯转发handler/http.go 别名层task 双 usecase 生命周期不同initialize 三层是启动编排倒置链system usecase 壳是 Wire 契约边界security_session 剩余 6 方法跨三层消费biz 接口嵌入是组合窄能力 - **F-2/F-4http.go 见 5.2 重判)/F-6/F-7/F-9/F-10/F-11、S-5~S-9、D-4/D-6/D-9/D-10/D-11/D-19、L-5/L-10/L-12、P-3~P-13、X-1~X-11、C-2/C-4/C-6/C-8**:历轮影响分析后的保留决策,理由均已复核与代码相符
- **S-5/S-6/S-7/S-8/S-9**payment 大文件承载跨供应商编排dto 跨域文件移动放大契约变化data 转换命名差异需全域迁移media 四文件职责边界清晰;单方法 handler 由 Wire 注入约束 - **质量标杆**第七轮正面确认payment 幂等指纹+回调强制平台查单+hook 前后指纹校验、退款 lease 语义、task_scheduler 锁序、SSRF 拨号防护、auth singleflightWithoutCancel、log_file.go 的 os.Root+symlink+SameFile 防 TOCTOU、staticfiles 安全、media_upload 分片校验链、system 配置掩码+preserve 闭环、BodyPolicyUpload 闭环、V-12/13/14/21/22 修复质量test policy 运行时判定+测试、Swagger env 开关、CORS credentials=false+Vary、GetAndDelete 原子化、V-16 三处词表补齐+paymentkit 统一归一化方向正确、recovery panic dump 不含 body、traceparent 完整校验、支付回调空 buffer 隔离
- **D-4/D-6/D-9/D-10/D-11/D-19**handler 样板各域差异明显mq helper 属排除范围config/runtimeconfig 分离已补决策注释;三处清理入口不同各自独立触发;树算法输入模型不同;金额守恒四处输入形态互异(完整结构体/配置化 JSON/XML 值映射/SDK 结构体)
- **L-5/L-10/L-12**错误审计白名单被测试锁定Kratos errors 仅跨层/stdlib 管内部的双体系分层binding 覆盖结构必填、手工覆盖上下文(覆盖不均但新代码倾向 binding未恶化
- **P-3/P-5/P-6/P-7/P-8/P-9/P-10/P-11/P-13**pkg 各包定位经复核成立mq/websocket 属既定排除
- **X-1~X-4/X-6~X-8/X-10/X-11**:路由双声明/集成配置双通道/热重载分工watchLoop 发布快照 vs reloadConfig 重建基础设施,合并需重做锁与退休策略)等均完成影响分析保留
- **C-2/C-4/C-6/C-8**loadUser/loadUsers 查询策略不同;*Data nil 防御覆盖测试替身边界媒体三重限制各守一层PaymentLogger 是审计替换 seam
- **L-7 复核**data 层 Table() 已全 PO 化(仅 2 处导出动态表名合理保留saveRelations 只做 DO→PO 转换
- **质量标杆**第五轮正面确认payment 幂等指纹+回调强制平台查单、退款 lease 语义、task_scheduler 锁序、task_executor SSRF 拨号防护+orphan 跟踪、auth singleflightcontext.WithoutCancel 隔离取消传染、data_scope 回调注入、email CRLF 清洗、ListTasks(0,0) 全量语义、新增缝PaymentAdapterFactory/MergeRuntimeConfig/deletePrefixViaList/AST 缓存/systeminfo均内聚无越界
## 处置建议(按优先级) ## 处置建议(按优先级)
1. **立即处理 V-1**(泄漏的数据库密码)→ **V-2/V-3**(审计敏感数据落地防护对称化) 1. **V-19 掩码/恢复键集不对称**(回显保存会把 `******` 落库破坏支付配置——统一共享键表后连带解决 key_pem 泄漏)
2. **V-4/V-9**(热重载漂移文档化、软删语义逐项确认+补测试) 2. **V-11 截断标记保护**captureWriter truncated 标志+占位符)
3. **V-5/V-6/V-7/V-8/V-10 + 二节死代码**(小而具体的清理批次) 3. **V-15 错误泄露全域收敛**(推广 failLogViewer 范式86 处)+ **V-16b 词表对齐**douyin/lakala/alipay 三处补齐+测试)
4. **三节 D-2 三处遗漏 + D-31/D-32** 4. **V-17/V-18/V-20/V-13b**(秒传一致性、会话回收扩 failed+merging、前端 token、Swagger 开关质量)
5. **四节文档漂移群**(一次 README/CLAUDE.md 同步批) 5. **二节死代码 + 三节双轨 + 四节文档**(一批小清理
6. **五节微文件合并**(最后) 6. **五节过分拆**4 硬+5 轻+2 组可选+seam 统一——纯文件级减法,零行为变更

View File

@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
stdmime "mime"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -246,10 +247,18 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
return fail(errors.New("整文件校验失败")) return fail(errors.New("整文件校验失败"))
} }
media := &MediaFile{Name: session.FileName, URL: stored.URL, Tag: strings.TrimPrefix(filepath.Ext(session.FileName), "."), Key: key, Size: stored.Size, Mime: mime, MD5: hash, UserID: userID} media := &MediaFile{Name: session.FileName, URL: stored.URL, Tag: strings.TrimPrefix(filepath.Ext(session.FileName), "."), Key: key, Size: stored.Size, Mime: mime, MD5: hash, UserID: userID}
if strings.TrimSpace(media.Mime) == "" {
media.Mime = stdmime.TypeByExtension(ext)
if media.Mime == "" {
media.Mime = "application/octet-stream"
}
}
if err = uc.CreateMedia(ctx, media); err != nil { if err = uc.CreateMedia(ctx, media); err != nil {
return fail(err) return fail(err)
} }
if err = uc.CompleteUploadSession(ctx, uploadID, key, media.ID); err != nil { if err = uc.CompleteUploadSession(ctx, uploadID, key, media.ID); err != nil {
_ = uc.DeleteMedia(ctx, media.ID)
_ = uc.files.Delete(ctx, key)
_ = uc.FailUploadSession(ctx, uploadID) _ = uc.FailUploadSession(ctx, uploadID)
return nil, fmt.Errorf("完成上传会话失败: %w", err) return nil, fmt.Errorf("完成上传会话失败: %w", err)
} }

View File

@ -87,6 +87,38 @@ func (s *Store) Delete(ctx context.Context, key string) error {
return nil return nil
} }
func (s *Store) GetAndDelete(ctx context.Context, key string) (string, bool, error) {
if client := s.client(); client != nil {
value, err := client.Eval(ctx, "local v=redis.call('GET',KEYS[1]); if v then redis.call('DEL',KEYS[1]); end; return v", []string{key}).Result()
if err == redis.Nil {
return "", false, nil
}
if err != nil {
return "", false, err
}
if value == nil {
return "", false, nil
}
switch typed := value.(type) {
case string:
return typed, true, nil
case []byte:
return string(typed), true, nil
default:
return "", false, nil
}
}
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.memory[key]
if !ok || (!entry.expiresAt.IsZero() && time.Now().After(entry.expiresAt)) {
delete(s.memory, key)
return "", false, nil
}
delete(s.memory, key)
return entry.value, true, nil
}
func (s *Store) Increment(ctx context.Context, key string, expiration time.Duration) (int64, error) { func (s *Store) Increment(ctx context.Context, key string, expiration time.Duration) (int64, error) {
if client := s.client(); client != nil { if client := s.client(); client != nil {
if expiration > 0 { if expiration > 0 {

View File

@ -438,7 +438,7 @@ func alipayCreateMethod(extra, config map[string]any) (string, error) {
} }
normalized := paymentkit.NormalizePaymentMethod(value) normalized := paymentkit.NormalizePaymentMethod(value)
switch normalized { switch normalized {
case "", "create", "trade_create", "alipay_trade_create", "jsapi", "miniapp", "mini_program": case "", "create", "trade_create", "alipay_trade_create", "jsapi", "js_api", "miniapp", "mini_app", "mini_program":
return "alipay.trade.create", nil return "alipay.trade.create", nil
case "pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face": case "pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face":
return "alipay.trade.pay", nil return "alipay.trade.pay", nil

View File

@ -183,7 +183,7 @@ func wechatV2CreateMethod(extra, config map[string]any) (string, error) {
} }
normalized := paymentkit.NormalizePaymentMethod(value) normalized := paymentkit.NormalizePaymentMethod(value)
switch normalized { switch normalized {
case "", "jsapi", "js_api", "mini", "miniapp", "mini_program", "miniprogram", "applet": case "", "jsapi", "js_api", "mini", "miniapp", "mini_app", "mini_program", "miniprogram", "applet":
return gopayWechat.TradeType_JsApi, nil return gopayWechat.TradeType_JsApi, nil
case "app", "app_pay": case "app", "app_pay":
return gopayWechat.TradeType_App, nil return gopayWechat.TradeType_App, nil

View File

@ -49,7 +49,7 @@ func (a *wechatV3Adapter) Create(ctx context.Context, req *bizpayment.PaymentReq
tradeType = normalizeWechatV3TradeType(firstAny(c, "trade_type", "pay_type", "method")) tradeType = normalizeWechatV3TradeType(firstAny(c, "trade_type", "pay_type", "method"))
} }
switch tradeType { switch tradeType {
case "", "jsapi", "mini", "miniprogram", "mini_program", "applet": case "", "jsapi", "js_api", "mini", "miniapp", "mini_app", "miniprogram", "mini_program", "applet":
if openID := firstAny(req.Extra, "openid", "open_id"); openID != "" { if openID := firstAny(req.Extra, "openid", "open_id"); openID != "" {
bm.SetBodyMap("payer", func(payer gopay.BodyMap) { payer.Set("openid", openID) }) bm.SetBodyMap("payer", func(payer gopay.BodyMap) { payer.Set("openid", openID) })
} }

View File

@ -165,7 +165,7 @@ var routes = map[string]routeValue{
"POST /info/createInfo": {group: "公告", description: "新建公告", audit: true}, "POST /info/createInfo": {group: "公告", description: "新建公告", audit: true},
"POST /init/checkdb": {group: "初始化", description: "检查数据库", public: true}, "POST /init/checkdb": {group: "初始化", description: "检查数据库", public: true},
"POST /init/initdb": {group: "初始化", description: "初始化数据库", public: true}, "POST /init/initdb": {group: "初始化", description: "初始化数据库", public: true},
"POST /integration/configs/:kind/:provider/test": {group: "集成配置", description: "测试通信集成连接", audit: true, bodyPolicy: BodyPolicyPaymentConfig}, "POST /integration/configs/:kind/:provider/test": {group: "集成配置", description: "测试通信集成连接", audit: true, bodyPolicy: BodyPolicyIntegrationConfig},
"POST /jwt/jsonInBlacklist": {group: "jwt", description: "jwt加入黑名单(退出,必选)"}, "POST /jwt/jsonInBlacklist": {group: "jwt", description: "jwt加入黑名单(退出,必选)"},
"POST /mediaUpload/chunk": {group: "媒体上传", description: "上传分片", bodyPolicy: BodyPolicyUpload}, "POST /mediaUpload/chunk": {group: "媒体上传", description: "上传分片", bodyPolicy: BodyPolicyUpload},
"POST /mediaUpload/complete": {group: "媒体上传", description: "完成大文件上传"}, "POST /mediaUpload/complete": {group: "媒体上传", description: "完成大文件上传"},

View File

@ -61,7 +61,9 @@ func NewGinEngineWithRuntime(runtime *config.Store, access *systemservice.Access
if ws != nil && ws.Enabled() { if ws != nil && ws.Enabled() {
engine.GET(ws.Path(), handleWebSocket) engine.GET(ws.Path(), handleWebSocket)
} }
registerSwagger(engine, prefix, version, logger) if snapshot == nil || snapshot.Admin == nil || snapshot.Admin.App == nil || strings.ToLower(strings.TrimSpace(snapshot.Admin.App.Env)) != "production" {
registerSwagger(engine, prefix, version, logger)
}
staticfiles.Register(engine, runtime) staticfiles.Register(engine, runtime)
if logger != nil { if logger != nil {
for _, route := range engine.Routes() { for _, route := range engine.Routes() {

View File

@ -31,7 +31,7 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
started := time.Now() started := time.Now()
var requestBody []byte var requestBody []byte
multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
mediaUpload := multipart && routecatalog.BodyPolicyFor(c.Request.Method, c.Request.URL.Path) == routecatalog.BodyPolicyUpload mediaUpload := routecatalog.BodyPolicyFor(c.Request.Method, c.Request.URL.Path) == routecatalog.BodyPolicyUpload
var admin *config.Admin var admin *config.Admin
if runtime != nil { if runtime != nil {
if snapshot := runtime.Snapshot(); snapshot != nil { if snapshot := runtime.Snapshot(); snapshot != nil {
@ -46,7 +46,14 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
} }
} }
bytesIn := c.Request.ContentLength bytesIn := c.Request.ContentLength
logLimit := 1024
if admin != nil && admin.Zap != nil && admin.Zap.AccessLogMaxBytes > 0 {
logLimit = admin.Zap.AccessLogMaxBytes
}
maxBytes := 1 << 20 maxBytes := 1 << 20
if logLimit > maxBytes {
maxBytes = logLimit
}
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes} writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
c.Writer = writer c.Writer = writer
c.Header("X-Kra-Version", version) c.Header("X-Kra-Version", version)
@ -75,10 +82,6 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
if bytesIn < 0 { if bytesIn < 0 {
bytesIn = 0 bytesIn = 0
} }
logLimit := 1024
if admin != nil && admin.Zap != nil && admin.Zap.AccessLogMaxBytes > 0 {
logLimit = admin.Zap.AccessLogMaxBytes
}
bodyPolicy := routecatalog.BodyPolicyFor(c.Request.Method, c.Request.URL.Path) bodyPolicy := routecatalog.BodyPolicyFor(c.Request.Method, c.Request.URL.Path)
paymentCallback := bodyPolicy == routecatalog.BodyPolicyPaymentCallback paymentCallback := bodyPolicy == routecatalog.BodyPolicyPaymentCallback
paymentConfigWrite := bodyPolicy == routecatalog.BodyPolicyPaymentConfig paymentConfigWrite := bodyPolicy == routecatalog.BodyPolicyPaymentConfig

View File

@ -65,10 +65,6 @@ func OperationAudit(runtime *config.Store, recorder *systemservice.AuditRecorder
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody)) c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
} }
} }
// The reference middleware captures up to the global 1 MiB response
// safety limit and only applies the configured operation-log limit when a
// download response is recorded. Using maxBytes here would silently
// truncate ordinary JSON responses before that decision is possible.
started := time.Now() started := time.Now()
c.Next() c.Next()
userID := uint(0) userID := uint(0)

View File

@ -34,7 +34,7 @@ func CORS(runtime *config.Store) gin.HandlerFunc {
corsHandled := false corsHandled := false
if mode == "allow-all" { if mode == "allow-all" {
if origin != "" { if origin != "" {
setCORSHeaders(c, origin, defaultCORSHeaders, defaultCORSMethods, defaultCORSExpose, true) setCORSHeaders(c, origin, defaultCORSHeaders, defaultCORSMethods, defaultCORSExpose, false)
corsHandled = true corsHandled = true
} }
} else if rule := matchingCORSRule(cors.Whitelist, origin); rule != nil { } else if rule := matchingCORSRule(cors.Whitelist, origin); rule != nil {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
integrationbiz "kra/internal/biz/integration" integrationbiz "kra/internal/biz/integration"
"strings"
"kra/internal/service/dto" "kra/internal/service/dto"
) )
@ -38,13 +39,13 @@ func (s *IntegrationConfigService) List(ctx context.Context, kind string) ([]*dt
enabled = config.Enabled enabled = config.Enabled
delete(byProvider, definition.Provider) delete(byProvider, definition.Provider)
} }
result = append(result, &dto.IntegrationConfigResponse{Kind: definition.Kind, Provider: definition.Provider, Name: definition.Name, Description: definition.Description, Enabled: enabled, Configured: configured, Config: values, Fields: integrationFieldsDTO(definition.Fields)}) result = append(result, &dto.IntegrationConfigResponse{Kind: definition.Kind, Provider: definition.Provider, Name: definition.Name, Description: definition.Description, Enabled: enabled, Configured: configured, Config: redactIntegrationJSON(values), Fields: integrationFieldsDTO(definition.Fields)})
} }
for _, config := range configs { for _, config := range configs {
if _, exists := byProvider[config.Provider]; !exists { if _, exists := byProvider[config.Provider]; !exists {
continue continue
} }
result = append(result, &dto.IntegrationConfigResponse{Kind: config.Kind, Provider: config.Provider, Name: config.Provider, Enabled: config.Enabled, Configured: true, Config: config.Values}) result = append(result, &dto.IntegrationConfigResponse{Kind: config.Kind, Provider: config.Provider, Name: config.Provider, Enabled: config.Enabled, Configured: true, Config: redactIntegrationJSON(config.Values)})
} }
return result, nil return result, nil
} }
@ -55,16 +56,44 @@ func (s *IntegrationConfigService) Find(ctx context.Context, kind, provider stri
return nil, err return nil, err
} }
definition, found := integrationbiz.IntegrationDefinition(config.Kind, config.Provider) definition, found := integrationbiz.IntegrationDefinition(config.Kind, config.Provider)
response := &dto.IntegrationConfigResponse{Kind: config.Kind, Provider: config.Provider, Name: config.Provider, Enabled: config.Enabled, Configured: true, Config: config.Values} response := &dto.IntegrationConfigResponse{Kind: config.Kind, Provider: config.Provider, Name: config.Provider, Enabled: config.Enabled, Configured: true, Config: redactIntegrationJSON(config.Values)}
if found { if found {
response.Name = definition.Name response.Name = definition.Name
response.Description = definition.Description response.Description = definition.Description
response.Fields = integrationFieldsDTO(definition.Fields) response.Fields = integrationFieldsDTO(definition.Fields)
response.Config = mergeConfigJSON(definition.Defaults, config.Values) response.Config = redactIntegrationJSON(mergeConfigJSON(definition.Defaults, config.Values))
} }
return response, nil return response, nil
} }
func redactIntegrationJSON(raw json.RawMessage) json.RawMessage {
var value any
if json.Unmarshal(raw, &value) != nil {
return raw
}
var walk func(any)
walk = func(v any) {
switch x := v.(type) {
case map[string]any:
for k, item := range x {
n := strings.ToLower(strings.ReplaceAll(k, "-", "_"))
if strings.Contains(n, "secret") || strings.Contains(n, "password") || strings.Contains(n, "private") || strings.Contains(n, "credential") || strings.Contains(n, "token") || strings.HasSuffix(n, "_key") || strings.Contains(n, "cert") || strings.Contains(n, "p12") {
x[k] = "******"
} else {
walk(item)
}
}
case []any:
for _, item := range x {
walk(item)
}
}
}
walk(value)
out, _ := json.Marshal(value)
return out
}
func (s *IntegrationConfigService) Save(ctx context.Context, kind, provider string, req *dto.IntegrationConfigRequest) error { func (s *IntegrationConfigService) Save(ctx context.Context, kind, provider string, req *dto.IntegrationConfigRequest) error {
if req == nil { if req == nil {
return s.uc.Save(ctx, nil) return s.uc.Save(ctx, nil)

View File

@ -53,7 +53,13 @@ func (s *ExportService) IssueToken(ctx context.Context, templateID string, param
} }
func (s *ExportService) ConsumeToken(ctx context.Context, token string, expectBlank bool) (*ExportToken, error) { func (s *ExportService) ConsumeToken(ctx context.Context, token string, expectBlank bool) (*ExportToken, error) {
raw, ok, err := s.cache.Get(ctx, "export:"+token) key := "export:" + token
raw, ok, err := s.cache.Get(ctx, key)
if atomic, supported := s.cache.(interface {
GetAndDelete(context.Context, string) (string, bool, error)
}); supported {
raw, ok, err = atomic.GetAndDelete(ctx, key)
}
if err != nil || !ok { if err != nil || !ok {
return nil, ErrExportTokenInvalid return nil, ErrExportTokenInvalid
} }
@ -64,7 +70,11 @@ func (s *ExportService) ConsumeToken(ctx context.Context, token string, expectBl
if value.Blank != expectBlank { if value.Blank != expectBlank {
return nil, ErrExportTokenType return nil, ErrExportTokenType
} }
_ = s.cache.Delete(ctx, "export:"+token) if _, supported := s.cache.(interface {
GetAndDelete(context.Context, string) (string, bool, error)
}); !supported {
_ = s.cache.Delete(ctx, key)
}
return &ExportToken{TemplateID: value.TemplateID, Params: value.Params, Blank: value.Blank}, nil return &ExportToken{TemplateID: value.TemplateID, Params: value.Params, Blank: value.Blank}, nil
} }
func exportTemplateDomain(value *dto.ExportTemplateRequest) *system.ExportTemplate { func exportTemplateDomain(value *dto.ExportTemplateRequest) *system.ExportTemplate {

View File

@ -40,5 +40,5 @@ func convertUser(user *system.User) *dto.UserResponse {
if user.Department != nil { if user.Department != nil {
department = departmentResponse(user.Department) department = departmentResponse(user.Department)
} }
return &dto.UserResponse{ID: user.ID, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, DeletedAt: nil, UUID: user.UUID, Username: user.Username, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Authority: convertAuthority(user.Authority), Authorities: authorities, DeptID: user.DeptID, Department: department, Departments: departments, Positions: positions, Phone: user.Phone, Email: user.Email, Enable: user.Enable, OriginSetting: user.OriginSetting, PasswordUpdatedAt: user.PasswordUpdatedAt} return &dto.UserResponse{ID: user.ID, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, UUID: user.UUID, Username: user.Username, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Authority: convertAuthority(user.Authority), Authorities: authorities, DeptID: user.DeptID, Department: department, Departments: departments, Positions: positions, Phone: user.Phone, Email: user.Email, Enable: user.Enable, OriginSetting: user.OriginSetting, PasswordUpdatedAt: user.PasswordUpdatedAt}
} }