diff --git a/AGENTS.md b/AGENTS.md index 1441e25..c40f3e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ internal/worker/ Timed-task runtime and scheduler. internal/utils/ Stateless, internal-only helper packages. pkg/ Reusable infrastructure packages. web/ Vue administration frontend. +docs/ Review notes, migration plans, and operational documentation. ``` ## Layering & dependency rules diff --git a/CLAUDE.md b/CLAUDE.md index e21cd16..212462e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,15 +62,10 @@ design rather than add the import. **service (DTO ↔ DO)** -- `convert` parses an incoming proto into a DO. The reverse - direction is built inline at the return site; the reply type is - whatever the proto declares — usually the resource itself - (`return &v1.{...}, nil`), sometimes a list wrapper - (`*v1.Set`), or `&emptypb.Empty{}` for deletes. Inlining - keeps each handler self-contained. -- Embed `UnimplementedServiceServer`. -- Parse AIP list requests via `filtering` / `ordering` / `pagination`; - apply `fieldmask.Update` for partial updates. +- Convert hand-written HTTP DTOs into domain objects at the service boundary, + and build response DTOs from returned domain objects. +- Keep handlers focused on binding, authentication context, response envelopes, + and transport-specific limits. - Validate request inputs at the service boundary before delegating to the usecase. - Return `biz` errors. No business rules, no storage access, no PO. @@ -109,18 +104,16 @@ design rather than add the import. ### Add-a-resource checklist -1. **DTO**: define `Create` / `Get` / - `List` / `Update` / `Delete` in - `api///`, then `make api`. +1. **DTO**: define request and response types in `internal/service/dto/`. 2. **DO + repo interface**: declare both in `biz`; build the usecase on top of the interface. 3. **Repo impl**: implement in `data` returning `biz.Repo`; add a PO and the matching conversion helpers when storage shape diverges from DO. -4. **Wiring**: register the repo constructor in `data.ProviderSet`, the - usecase in `biz.ProviderSet`, the service in `service.ProviderSet`; - register HTTP/gRPC services in `internal/server`. -5. **Regenerate**: `make all` to refresh Wire and `go.mod`. +4. **Transport**: add the service adapter, handler, router registration, and + routecatalog metadata. +5. **Wiring**: register providers in the relevant sets. +6. **Regenerate**: `make generate` or `make all` to refresh Wire and `go.mod`. ### Testing seam @@ -130,8 +123,7 @@ tests exercise repo implementations at the storage boundary. ## Generation & generated files -Regenerate via `make api` or `make all`; never hand-edit -`*.pb.go`, `*_grpc.pb.go`, `*_http.pb.go`, or `wire_gen.go`. +Regenerate via `make generate` or `make all`; never hand-edit `wire_gen.go`. ## Naming & error reasons @@ -141,8 +133,8 @@ Regenerate via `make api` or `make all`; never hand-edit `Service`. PO types live inside `internal/data/`; pick a name that fits the storage driver and convert with `new(do)` / `toBiz(po)` free functions. -- Error reasons: declared in `api///error_reason.proto`, - surfaced as `Err` in `biz`. +- Error reasons use stable strings and are surfaced as `Err` in + `biz`. ## Commits & security diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index b179d6f..4de9642 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -113,11 +113,7 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg paymentHooks := payment3.NewPaymentHooks() paymentOrderSourceRegistry := payment3.NewPaymentOrderSourceRegistry() paymentFulfillmentRegistry := payment3.NewPaymentFulfillmentRegistry() - paymentUsecase, err := payment3.NewConfiguredPaymentUsecase(paymentRepo, paymentOrderRepo, paymentHooks, paymentOrderSourceRegistry, paymentFulfillmentRegistry, logger) - if err != nil { - cleanup() - return nil, nil, err - } + paymentUsecase := payment3.NewPaymentUsecase(paymentRepo, paymentOrderRepo, paymentHooks, paymentOrderSourceRegistry, paymentFulfillmentRegistry, logger) paymentService := payment4.NewPaymentService(paymentUsecase) handlerPayment := handler.NewPayment(paymentService) taskRepo := task.NewTaskRepo(dataData) diff --git a/docs/code-review-issues.md b/docs/code-review-issues.md index dd488fb..ba8e37a 100644 --- a/docs/code-review-issues.md +++ b/docs/code-review-issues.md @@ -1,183 +1,90 @@ # 代码审查问题清单(internal + pkg) -- 审查日期:2026-08-27 ~ 2026-08-28,共四轮全量审查。第四轮(2026-08-28):核查第三至六轮处置声称是否真实落地 + 全量回归审查(重点:重构引入的新问题),`go build ./...` 编译验证通过 -- 文档结构:只保留待修复问题,按**类型**归类(不按轮次);每条标注发现轮次;已修复并经复查确认的直接删除 +- 审查日期:2026-08-27 ~ 2026-08-28,共五轮全量审查。第五轮(2026-08-28):核查第四至七轮全部处置声称(W/S/P/F/C/L/D 系列已修复与评估保留项)+ 全新视角回归审查;`go build ./...` 编译验证通过 +- 文档结构:只保留待修复问题,按**类型**归类(不按轮次);每条标注发现轮次;已修复并经复查确认的、以及经评估决定保留的直接删除 - gva/ 目录是遗留参考库(独立 module 不参与 kra 编译),不在审查范围 -- 依赖方向合规确认:pkg 无 import internal;integration 不 import data/service;data 不再 import integration/payment(第四轮重构后复验单向);无循环依赖 +- 依赖方向合规确认:pkg 无 import internal;integration 不 import data/service;data 不 import integration/payment;无循环依赖 +- 第五轮总评:核心业务路径(支付/调度/数据权限/上传)逻辑严谨、分层契约执行到位;历轮修复全部属实;当前债务已收敛为**敏感数据落地防护不对称、文档漂移、中转微文件**三类 --- -## 一、正确性缺陷(第四轮回归审查新发现,最高优先级) +## 一、安全与正确性缺陷(第五轮新发现,最优先) | # | 问题 | 位置 | 说明 | |---|------|------|------| -| ~~W-1~~ | ~~payment 空注册表导致应用启动失败。~~ **已修复:模板允许无业务模块启动,调用未配置能力时返回明确错误。** | ~~原位置~~ | ~~已完成~~ | -| ~~W-2~~ | ~~CompleteUpload 吞掉 ListChunks 底层错误。~~ **已修复:底层错误与分片数量不足已拆分处理。** | ~~internal/biz/system/media_upload.go~~ | ~~已完成~~ | -| ~~W-3~~ | ~~MergeRuntimeConfig 嵌套子节合并缺口。~~ **已修复并增加回归测试。** | ~~internal/config/clone.go;runtime_test.go~~ | ~~已完成~~ | -| ~~W-4~~ | ~~退款状态词表语义边角。~~ **已修复并补充测试。** | ~~internal/paymentkit/status.go~~ | ~~已完成~~ | -| ~~W-5~~ | ~~公告/参数/版本列表无 ORDER BY。~~ **已修复:统一按 `id desc` 排序。** | ~~data/system/*.go~~ | ~~已完成~~ | -| ~~W-6~~ | ~~外部退款分支存在不可达死分支。~~ **已修复。** | ~~internal/biz/payment/payment.go~~ | ~~已完成~~ | +| ~~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.go;capture.go~~ | ~~已完成(2026-08-28;针对性测试通过)~~ | +| ~~V-3~~ | ~~已补齐 `key`、`public_cert`、`signing_key`、`secret_key` 等敏感键,并将 integration payment test 路由标记为 `BodyPolicyPaymentConfig`,请求体统一使用摘要策略。~~ | ~~server/middleware/redact.go;routecatalog/catalog.go~~ | ~~已完成(2026-08-28;针对性测试通过)~~ | +| ~~V-4~~ | ~~已在 config watcher 与 data reload 入口补充运行时契约说明:watch 只发布不可变快照,`reloadSystem` 才重建数据库/缓存/存储/集成客户端;保留双路径以维护回滚与连接退休安全。~~ | ~~internal/config/runtime.go;internal/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)~~ | -## 二、死代码与零消费者机制 - -### 2.1 三层死方法残留(第三至五轮处置后仍未删,均经全仓 Grep 反查确认零调用)【二轮发现,四轮复核仍在】 - -| 层 | 死代码 | 位置 | -|----|--------|------| -| ~~biz 接口+data 实现~~ | ~~`RecordDataAccess` 写入链已删除;查询侧仍保留。~~ | ~~相关文件~~ | -| ~~service 包装~~ | ~~security_session.go 中 8 个无消费者透传方法已删除。~~ | ~~相关文件~~ | -| ~~biz 注入面~~ | ~~经复核保留:`RegisterBusinessModule` 与 `PaymentBusinessModule` 是 `docs/PAYMENT.md` 明确要求的业务接入契约;`PayInternal`/`RefundInternal`/`AuthorizeRefund` 已由 biz 调用链消费,模板无具体业务实现属于预期扩展点,不是死代码。评估完成,保留(2026-08-28)。~~ | ~~biz/payment/payment.go:400-412;payment_order.go:139-152~~ | -| ~~dto 死字段(已确认项)~~ | ~~GetAuthorityButtonsRequest.Selected 已删除;其余字段因仍参与响应或兼容契约暂保留。~~ | ~~dto/permission.go~~ | -| ~~死分支~~ | ~~经复核保留:service 层接收通用 `map[string]any`,`[]byte` 仍是合法 usecase/fake 输入;该分支有针对性测试,不属于可证明死代码。评估完成,保留(2026-08-28)。~~ | ~~service/system/export_excel.go:85-86~~ | - -### 2.2 mq / websocket 零消费者基础设施【既定排除范围,历轮明确不处理,现状保持】 - -- mq 全链(声明式订阅+legacy API+簿记/dispatcher/reconcile)业务消费者为零;`_ mq.Client` 幻影参数(cmd/main.go:65);integration/provider.go:33-38 三重死绑定 + Hub 死绑定 -- websocket:Hub 接口零消费者;integration 层 On* 四注册方法零调用,双层 handler 登记机制两层都为空 -- namedClient/Client() 整型死代码(integration/mq/emqx.go:632-650) - -### 2.3 零散死代码(第四轮新扫描)【四轮】 +## 二、死代码残留(第五轮新扫描) | 死代码 | 位置 | 证据 | |--------|------|------| -| ~~`config.CloneData`~~ | ~~已删除:全仓零调用,且 `Clone`/`MergeRuntimeConfig` 已覆盖实际快照复制入口。已完成(2026-08-28)。~~ | ~~internal/config/clone.go~~ | -| ~~`paymentkit.XMLValues`/`XMLEncode`~~ | ~~已删除:仅测试调用;XML 编解码辅助已移入 payment 集成测试文件,生产 XML 继续走 gopay。已完成(2026-08-28)。~~ | ~~internal/paymentkit/xml.go~~ | -| ~~`paymentkit.NestedString`~~ | ~~已删除:生产零调用;旧测试已改为覆盖现存 `JSONObject`/`StringAtPath`。已完成(2026-08-28)。~~ | ~~internal/paymentkit/json.go~~ | -| ~~`logging.NewZapLogger`~~ | ~~已删除:唯一测试调用改用生产构造器 `NewReloadableZapLogger`,避免维护双入口。~~ | ~~internal/logging/zap.go;data/gorm_logger_test.go~~ | -| ~~`data/payment.contains`~~ | ~~已改用 `paymentkit.ContainsFold`,本地实现已删除。~~ | ~~data/payment/payment.go~~ | -| ~~`paymentkit status.go 的 ConfiguredInt64/ConfiguredValues/Text/FirstText/FirstString` 定位漂移~~ | ~~经复核保留:这些函数仍被 payment data/integration 生产路径消费,属于配置解析边界;问题是 README 描述过窄而非死代码。评估完成,保留(2026-08-28)。~~ | ~~internal/paymentkit/status.go:36-90~~ | +| ~~PaymentOrderSourceRegistry.Len~~ | ~~已删除:全仓(含测试)零调用。~~ | ~~biz/payment/payment_order.go~~ | +| ~~PaymentFulfillmentRegistry.Len~~ | ~~已删除:全仓(含测试)零调用。~~ | ~~biz/payment/payment.go~~ | +| ~~NewConfiguredPaymentUsecase 恒 nil 错误透传壳~~ | ~~已删除并通过 Wire 重新生成,ProviderSet 直接绑定 `NewPaymentUsecase`。~~ | ~~biz/payment/provider.go;cmd/wire_gen.go~~ | +| ~~biz/integration mergeIntegrationDefaults 一行转发壳~~ | ~~已删除,调用点统一使用 `MergeIntegrationDefaults`。~~ | ~~biz/integration/integration_config.go~~ | +| ~~P-15 残留~~ | ~~已修正 handler/http.go 注释与 source.go 旧路径标记,并同步移除过时测试样本。~~ | ~~server/handler/http.go;internal/logging/source.go~~ | +| ~~`convertAuthority` 的 DeletedAt 值传递被 json:"-" 丢弃~~ | ~~已删除无效赋值,避免向已隐藏字段传递无效数据。~~ | ~~service/system/user_conversion.go~~ | -## 三、重复实现 / 双份维护 - -### 3.1 大块可消除 +## 三、重复实现残留(历轮收敛后的漏网项) | # | 问题 | 位置 | 轮次 | |---|------|------|------| -| ~~D-2(部分完成)~~ | ~~支付方式归一化骨架已统一到 `paymentkit.NormalizePaymentMethod`;各渠道状态词表与退款身份校验因语义不同保留。~~ | ~~integration/payment;internal/paymentkit~~ | ~~部分完成~~ | -| ~~D-4~~ | ~~经复核样板虽多,但各 handler 的绑定方式、错误文案、响应 envelope 和鉴权上下文差异明显;抽统一门面会隐藏 transport 语义并扩大回归面,暂不改动。~~ | ~~server/handler/*~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~D-6~~ | ~~经复核暂不改动:重复 helper 与 MQ/WebSocket 零消费者基础设施同属既定排除范围;上收 `internal/utils` 会扩大无消费者包的公共表面积。~~ | ~~emqx.go:226-260 vs websocket/server.go:189-240~~ | ~~评估完成,保留(2026-08-28)~~ | +| ~~D-2 遗漏 1-3~~ | ~~已统一 qq、wechat v3 与 payment data 的支付方式归一化到 `paymentkit.NormalizePaymentMethod`,消除点号/短横线/空格处理漂移。~~ | ~~integration/payment/qq.go;wechat_v3.go;data/payment/payment.go~~ | ~~已完成(2026-08-28;针对性测试通过)~~ | +| ~~D-31~~ | ~~已复用 `paymentOrderFingerprint` 完成 BeforeCreate 前后不可变校验,保留同等字段覆盖范围并删除 10 个 canonical 局部变量。~~ | ~~biz/payment/payment.go~~ | ~~已完成(2026-08-28;针对性测试通过)~~ | +| ~~D-32~~ | ~~已新增 `BodyPolicyUpload` 并让 AccessLog 按 routecatalog 决定上传请求体限额,删除硬编码 URL 后缀判断。~~ | ~~routecatalog/catalog.go;server/middleware/access_log.go~~ | ~~已完成(2026-08-28;针对性测试通过)~~ | -### 3.2 配置/数据不变式双份维护 +## 四、文档漂移(第五轮新发现,低成本高收益) -| # | 问题 | 位置 | 轮次 | -|---|------|------|------| -| ~~D-9~~ | ~~`config.Store` 与 `runtimeconfig.Store` 的同构 listener/通知/克隆机制经复核不合并:前者负责文件快照与 fsnotify 全量替换,后者负责数据库集成配置按 provider key 通知;已补决策注释固化边界。~~ | ~~config/runtime.go;integration/runtimeconfig/store.go~~ | ~~评估完成,保留分离(2026-08-28)~~ | -| ~~D-10~~ | ~~经复核保留三处清理:`persistConfigValues` 覆盖完整运行时保存,`persistDatabaseConfig` 覆盖初始化页的局部写入,`removeIntegrationConfigFromFile` 覆盖启动时对旧模板的兼容清理;入口不同且各自可独立触发,合并会削弱不落盘不变式。~~ | ~~internal/data/config_store.go;data.go~~ | ~~评估完成,保留(2026-08-28)~~ | +| # | 问题 | 位置 | +|---|------|------| +| ~~F-21~~ | ~~已将 CLAUDE.md 正文同步为 Gin+手写 DTO+make generate 的现行仓库约定,移除 proto/AIP/fieldmask 时代描述。~~ | ~~CLAUDE.md~~ | ~~已完成(2026-08-28)~~ | +| ~~F-22~~ | ~~已修正 pkg/README.md,移除不存在的 logging/httpx/paymentkit 公共包描述。~~ | ~~pkg/README.md~~ | ~~已完成(2026-08-28)~~ | +| ~~F-23~~ | ~~已修正 internal/README.md 与 internal/server/README.md 的 httpx 路径。~~ | ~~internal/README.md;internal/server/README.md~~ | ~~已完成(2026-08-28)~~ | +| ~~F-24~~ | ~~已将 integration README 的 paymentkit 路径改为 `internal/paymentkit`。~~ | ~~internal/integration/README.md~~ | ~~已完成(2026-08-28)~~ | +| ~~F-25~~ | ~~已修正 service README,根包仅聚合 Wire ProviderSet,不再声称 re-export 门面。~~ | ~~internal/service/README.md~~ | ~~已完成(2026-08-28)~~ | +| ~~F-26~~ | ~~已在 AGENTS.md 项目结构中补充 `docs/`。~~ | ~~AGENTS.md~~ | ~~已完成(2026-08-28)~~ | -### 3.3 中小重复 +## 五、过分拆分残留(微文件清单,行数实测) -| # | 问题 | 位置 | 轮次 | -|---|------|------|------| -| ~~D-11~~ | ~~经复核保留:authority 树与 menu 树输入模型、过滤规则和输出结构不同,共享算法会引入泛型/回调抽象而降低可读性。~~ | ~~biz authority.go vs menu.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~D-15~~ | ~~defaults 合并逻辑三层三份。~~ **已修复:统一使用 `integrationbiz.MergeIntegrationDefaults`。** | ~~相关文件~~ | ~~已完成~~ | -| ~~D-17 剩余~~ | ~~`values()` 与 `testRow()` 已收敛为共享读取逻辑,并保留启用状态差异。~~ | ~~data/payment/payment.go~~ | ~~已完成~~ | -| ~~D-19(评估后保留)~~ | ~~payment 金额守恒校验四处重复。~~ **经复核保留:四处输入字段与供应商容错语义不同,强行合并会破坏分层;biz 最终守恒校验作为跨边界不变式。** | ~~biz/payment 与各供应商适配器~~ | ~~不改动~~ | -| ~~D-21~~ | ~~支付订单响应映射已统一复用 `paymentOrderResponse`。~~ | ~~service/payment/payment.go~~ | ~~已完成~~ | -| ~~D-22~~ | ~~同 2.3,已改用 `paymentkit.ContainsFold`。~~ | ~~data/payment/payment.go~~ | ~~已完成~~ | +| 文件 | 行数 | 说明 | +|---|---|---| +| ~~internal/modules/surface/surface.go~~ | ~~19~~ | ~~评估保留:隔离 routecatalog 到 pkg/module 的适配依赖。~~ | +| ~~internal/data/provider/provider.go~~ | ~~8~~ | ~~评估保留:`Database` seam 被多个 data 子包独立依赖。~~ | +| ~~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-2~~ | ~~经复核保留:文件除 paymentkit 转发外还承载 `callbackFields`、`firstNonEmpty` 与 `parseConfiguredAmount` 等包内协议语义;高频短别名被 100+ 处渠道代码消费,整体展开只会放大改动而不减少规则源。~~ | ~~integration/payment/result.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~F-4~~ | ~~经复核保留:handler/http.go 只提供响应类型与函数别名,避免各 handler 重复导入 httpx,不承载业务规则。~~ | ~~server/handler/http.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~F-6~~ | ~~经复核保留:TaskUsecase 面向 worker 的 repo/校验能力,TaskApplicationUsecase 负责运行时同步与恢复;两者生命周期和依赖不同。~~ | ~~biz/task/task.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~F-7~~ | ~~经复核保留:InitializationRepo → initialize.Repo → data.Data 是启动编排的接口倒置链,避免 initialize 直接依赖 data 实现。~~ | ~~initialize/initialize.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~F-8~~ | ~~已删除 `newReloadableDB` 的匿名变参垫片;审计回调仍只在数据库激活/替换时由 `registerDataScopeCallbacks` 绑定,职责链闭环。~~ | ~~data/runtime_clients.go;data_scope.go~~ | ~~已完成(2026-08-28)~~ | -| ~~F-9~~ | ~~经复核保留:system usecase 透传壳是 biz 对外契约与 Wire 注入面的稳定边界,删除会把 service/worker 直接绑定到 repo。~~ | ~~biz/system/*~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~F-10~~ | ~~经复核保留剩余活跃方法:security session 负责 token/cache 语义,不能与 service handler 合并;已确认的 8 个无消费者方法按死代码处理。~~ | ~~service/system/security_session.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~F-11~~ | ~~经复核保留:biz 接口嵌入透传用于组合窄 repo 能力,调用方依赖稳定 usecase 契约,机械拆分会扩大 Wire 与测试替身改动。~~ | ~~biz/system/*~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~F-13~~ | ~~已删除 `Adapter` 类型别名和包级 `New` 双入口;`Factory.New` 直接实现 biz 的 `PaymentAdapterFactory`,工厂表使用 biz 接口类型。~~ | ~~integration/payment/adapter.go;vendor.go~~ | ~~已完成(2026-08-28)~~ | +## 审查后认为合理、不建议改动的部分(历轮评估保留决策汇总) -## 五、过分拆分 / 文件组织 - -**根因模式三条**【三轮】:①零逻辑 usecase 壳(wire 强制每域一个构造器放大);②"每资源 N 文件"机械切分;③为 import 美观引入的中间缝合包/门面。 - -| # | 问题 | 位置 | 轮次 | -|---|------|------|------| -| ~~S-1(第一批)~~ | ~~已合并 actor/data-scope 上下文载荷文件;复核剩余微文件均对应独立职责,不再机械归并。~~ | ~~internal/biz/system/context.go~~ | ~~部分完成(2026-08-28)~~ | -| ~~S-2~~ | ~~SystemConfigService 已合并为单文件。~~ | ~~internal/service/system/system.go~~ | ~~已完成~~ | -| ~~S-3(第一批)~~ | ~~routes.go 已改为有序注册表;各领域路由文件承担独立注册边界,复核后保留。~~ | ~~internal/server/router/routes.go~~ | ~~部分完成(2026-08-28)~~ | -| ~~S-4~~ | ~~四个 data 子包的 provider 文件已合并。~~ | ~~internal/data/*/provider.go~~ | ~~已完成~~ | -| ~~S-5~~ | ~~经复核保留:payment 大文件承载跨供应商编排与契约,微文件分别对应独立边界;dto 合并会重新混装领域,收益不足。~~ | ~~biz/payment、service/dto~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~S-6~~ | ~~经复核保留:`system.go`/`settings.go` 虽跨域,但移动类型会放大 service DTO 导入与生成契约变化;本批不做机械拆分。~~ | ~~service/dto/system.go、settings.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~S-7~~ | ~~经复核保留:data 层转换与 PO 命名差异来自不同存储关系和历史兼容,统一命名需全域迁移,当前无安全局部收益。~~ | ~~data/system/*~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~S-8~~ | ~~经复核保留:media 四文件分别覆盖资源、元数据、上传会话与上传流程,职责边界清晰,不为减少文件合并。~~ | ~~biz/system/media*~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~S-9~~ | ~~经复核保留:单方法 handler 结构体由 Wire/路由注入约束形成,合并会改变构造与注册契约,暂不改动。~~ | ~~server/handler/session.go、navigation.go、set.go~~ | ~~评估完成,保留(2026-08-28)~~ | - -## 六、包归属问题 - -| # | 问题 | 位置 | 轮次 | -|---|------|------|------| -| ~~P-3~~ | ~~经复核保留:pkg/module 是跨 internal/modules、routecatalog 和启动编排的基础契约,移动会反向扩大依赖面。~~ | ~~pkg/module~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~P-5~~ | ~~按 2.2 既定排除范围保留:MQ/WebSocket 零消费者基础设施暂无迁移收益。~~ | ~~pkg/mq、pkg/websocket~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~P-6~~ | ~~按 2.2 既定排除范围保留:重复 helper 只服务于零消费者适配器,上收 utils 会扩大公共 API。~~ | ~~integration/mq、integration/websocket~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~P-7~~ | ~~经复核保留:aws-sdk-v2 与 minio-go 分别覆盖 S3 兼容和 MinIO 专属能力,统一 SDK 会损失 provider 行为或引入迁移风险。~~ | ~~integration/storage~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~P-8~~ | ~~经复核保留:query/page/trace 解析函数依赖 Gin transport 类型;移入 pkg/utils 会把 HTTP 语义泄漏到通用包,收益不足。~~ | ~~server/handler/query.go 等~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~P-9~~ | ~~经复核保留:configuration.go 的 DTO 塑形、JSON 规范化和掩码共享同一初始化事务边界,拆分会增加中间状态与回滚路径。~~ | ~~initialize/configuration.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~P-10~~ | ~~经复核保留:pagination/gormkit 直接表达当前 GORM 存储契约,继续下沉不会减少依赖。~~ | ~~pkg/database~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~P-11~~ | ~~按 2.2 既定排除范围保留:pkg/mq 的 kra 前缀与项目级协议契约一致,暂无独立复用边界。~~ | ~~pkg/mq~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~P-13~~ | ~~经复核保留:pkg/module、pkg/task、pkg/database/migration 组成同层基础设施契约组,拆包只增加导入跳转。~~ | ~~pkg/*~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~P-14~~ | ~~已将 `httpx` 收敛为通用 `SetCookie`,`x-token` 命名保留在 handler/middleware 业务边界;密码修改冲突码移至 middleware,并由 handler 继续提供兼容常量。~~ | ~~internal/server/httpx/response.go;server/handler/http.go;server/middleware/auth.go~~ | ~~已完成(2026-08-28;针对性测试通过)~~ | -| ~~P-15~~ | ~~包移动注释与 logging 栈跳过标记已修正。~~ | ~~相关文件~~ | ~~已完成~~ | -| ~~P-16~~ | ~~已同步 CLAUDE.md 的目录结构与分层说明,移除不存在的 `api/`、`internal/global/` 等描述。~~ | ~~CLAUDE.md~~ | ~~已完成(2026-08-28)~~ | - -## 七、分层 / 职责违规 - -| # | 问题 | 位置 | 轮次 | -|---|------|------|------| -| ~~L-3(部分完成)~~ | ~~PaymentResult/PaymentTestResult 的死 JSON 标签已移除;PaymentRequest 字段与指纹语义保留,Definition 家族因仍被 service 消费暂不迁移。~~ | ~~biz/payment/payment.go;biz/integration~~ | ~~部分完成~~ | -| ~~L-4~~ | ~~`SystemParameter` 的查询字段与时间区间已拆为 `SystemParameterFilter`;API/Export 过滤字段已分别迁移至独立 `APIFilter`、`ExportTemplateFilter`,实体 DO 不再承载列表过滤/排序字段。~~ | ~~biz/service/data system parameter、api、export~~ | ~~已完成(2026-08-28;针对性测试通过)~~ | -| ~~L-5~~ | ~~经复核保留:错误审计的中文消息白名单、业务路径例外和支付回调摘要已由现有测试固定为安全策略;支付回调/配置正文脱敏依赖 routecatalog body policy,抽成无业务中间件会削弱防泄漏边界。限流配置来自 SecurityService,未发现可安全下沉的独立策略对象。~~ | ~~server/middleware/*~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~L-6(部分完成)~~ | ~~`AuthenticationResult` 已在返回 service 前清空密码哈希并补回归测试。~~ `QueryExport` 动态行、export DO 的兼容 SQL 字段、`UserOptions` 选项形状仍保留:前两项涉及公开导入导出契约与存量数据兼容,后者虽命名偏 UI,但实际是稳定的 label/value 投影;当前直接迁移收益不足以覆盖契约风险。 | biz/system/* | ~~部分完成(2026-08-28;针对性测试通过)~~ | -| ~~L-7~~ | ~~已将 system data 的静态表查询改为 PO `Model(...)`;动态导出表仍按已校验模板访问。复核确认 `saveRelations` 只做 DO→PO 转换、`OriginSetting` 通过显式 JSON 解析且已有损坏数据测试,原“裸转换/回写”描述已不成立。~~ | ~~data/system/*~~ | ~~已完成(2026-08-28;针对性测试通过)~~ | -| ~~L-8~~ | ~~经复核保留:seedSystem、authorityAccessRepo、BuildVersionBundle 都位于单一事务/查询边界;拆分会增加状态传递和事务上下文穿透,当前无安全局部收益。~~ | ~~data/system/seed.go、authority.go、version.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~L-9(部分完成)~~ | ~~`AuthorityResponse.DeletedAt` 已改为 `json:"-"`,且 integration 配置字段已迁移为 DTO 自有类型并在 service 边界映射。~~ ID 类型分叉涉及现有 handler/usecase/数据库键类型的兼容迁移;`ErrorRecordMutationRequest` 的指针字段用于区分省略与显式空值且已有测试,原建议不成立,保留。 | ~~service/dto/*~~ | ~~部分完成(2026-08-28;针对性测试通过)~~ | -| ~~L-10~~ | ~~经复核保留:Kratos errors 仅用于需要稳定 HTTP reason/status 的跨层错误;其余 stdlib sentinel/包装错误服务于内部状态机和 `errors.Is` 判定。一次性统一会改变现有响应映射与错误文本,当前无安全收益。~~ | ~~biz/system/errors.go 等~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~L-12~~ | ~~经复核保留:DTO binding 标签覆盖结构性必填约束,handler 手工校验覆盖认证上下文、跨字段关系和 transport 特例;职责不同,强行统一会把业务规则推入 DTO 或遗漏上下文校验。~~ | ~~server/handler/* + service/dto/*~~ | ~~评估完成,保留(2026-08-28)~~ | - -## 八、简单实现复杂化 - -| # | 问题 | 位置 | 轮次 | -|---|------|------|------| -| ~~C-1~~ | ~~经复核保留:Create 的字段回比保护业务模块返回的权威订单快照,指纹校验保护持久化幂等键;两道校验处于不同边界,删除任一都会重新开放金额/业务对象漂移。~~ | ~~biz/payment/payment.go:385-402~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~C-2~~ | ~~经复核保留:`loadUser` 面向单用户完整关系加载,`loadUsers` 批量预取关联以避免 N+1,且基础 PO→DO 已复用 `baseBizUser`;继续合并会损害查询策略。~~ | ~~data/system/user.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~C-4~~ | ~~经复核保留:`*Data` nil 防御覆盖初始化前、热重载失败与测试替身边界;`NewIntegrationRuntime(nil)` 返回空 Store 保持 Wire/独立测试可用性,删除会改变失败模式。~~ | ~~data/data.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~C-6~~ | ~~经复核保留:请求体上限、multipart 文件声明大小和分片会话参数分别保护 transport、解析器与业务边界,不是同一层重复校验。~~ | ~~server/handler/media.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~C-7~~ | ~~按 2.2 既定排除范围保留:双层登记机制暂无业务消费者,移除会改变未来模块接入 seam。~~ | ~~integration/websocket~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~C-8~~ | ~~经复核保留:PaymentLogger 是支付审计的稳定替换 seam,即使当前只有一个实现也便于测试隔离和未来多 sink 扩展。~~ | ~~biz/payment/payment_log.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~C-11~~ | ~~已改用具名 `rowQueryOptions` 传递分页与 required 语义,消除 data 层相邻布尔参数歧义;查询行为不变。~~ | ~~data/system/list.go~~ | ~~已完成(2026-08-28;针对性测试通过)~~ | -| ~~C-12~~ | ~~已修复:DailyWriter 在跨日轮转时再次执行过期目录清理,并补充长期运行场景回归测试。~~ | ~~internal/logging/daily.go;daily_test.go~~ | ~~已完成(2026-08-28)~~ | - -## 九、结构性设计(大动作需决策) - -| # | 问题 | 位置 | 轮次 | -|---|------|------|------| -| ~~X-1~~ | ~~已完成影响分析,暂不实施单一声明源迁移:`routecatalog` 承载审计/Swagger/模块同步元数据,`router` 负责 Gin handler 绑定;当前 catalog 还无法表达 handler 注入与注册顺序,强行合并会扩大启动与路由回归面。~~ | ~~server/router/*;routecatalog/catalog.go~~ | ~~评估完成,保留分离(2026-08-28)~~ | -| ~~X-2~~ | ~~经复核保留:新增资源触碰 DTO、service、biz、repo、route 和 Wire 是当前分层契约的必要显式步骤;自动注册会隐藏依赖。~~ | ~~—~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~X-3~~ | ~~已完成影响分析,暂不合并三种配置形状:storage/email 需要强类型 `config.Store` 快照与文件兼容,mq/websocket 需要按 provider 的 `runtimeconfig.Store` 热通知;统一形状会牺牲强类型校验或通知粒度。~~ | ~~data/integration_config.go 等~~ | ~~评估完成,保留分离(2026-08-28)~~ | -| ~~X-4~~ | ~~经复核保留:swagger 运行时文档由 routecatalog 元数据生成并需补充运行时路径参数/安全声明,server 根包是合理的装配位置。~~ | ~~server/swagger.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~X-6~~ | ~~经复核保留:staticfiles 是 HTTP 静态路由入口,storage/local.go 是持久化 provider;两者生命周期和接口不同。~~ | ~~server/staticfiles、integration/storage/local.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~X-7~~ | ~~经复核保留:gopay_helpers.go 同时承载 GoPay 请求装配和跨渠道响应谓词;拆到各 provider 会复制通用装配代码,当前文件仍是单一 SDK 边界。~~ | ~~integration/payment/gopay_helpers.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~X-8~~ | ~~经复核保留:vendor DSL 的 Required 字段用于严格拒绝未完成商户配置,14 键映射保持协议扩展点;放宽会增加运行时失败。~~ | ~~integration/payment/vendor.go~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~X-9~~ | ~~已完成影响分析,现状并非“只换快照”:`watchLoop` 负责发布合并快照,`Data.reloadConfig` 显式重建数据库、Redis、Mongo、storage 与 integration runtime;两者分工避免文件 watcher 直接持有基础设施生命周期。合并为单通道需重做锁、回滚与连接退休策略,暂不改动。~~ | ~~config/runtime.go:263-287;data/config_store.go:124-252~~ | ~~评估完成,保留分离(2026-08-28)~~ | -| ~~X-10~~ | ~~经复核保留:新增渠道需同时声明 biz provider、adapter 工厂和配置元数据,三处分别属于领域常量、I/O 实现和管理面契约,自动化注册会牺牲显式校验。~~ | ~~biz/payment;integration/payment;biz/integration~~ | ~~评估完成,保留(2026-08-28)~~ | -| ~~X-11~~ | ~~经复核保留:TaskScheduler 的多锁分别保护任务表、运行时注册表和订阅广播,属于真实并发需求,不合并。~~ | ~~worker/task_scheduler.go~~ | ~~评估完成,保留(2026-08-28)~~ | - -## 审查后认为合理、不建议改动的部分 - -- **modules 与 routecatalog 分离**:启动期装配 vs 请求期热路径,消费方零重叠 -- **Provider 接口缝模式** + provider.Database 中性接口:正当(system 包内 Provider 与 DatabaseProvider 两个近义缝命名易混淆,建议注释互指) -- **config.Store 与 runtimeconfig.Store 分离**:正确(D-9 词表同构为已知保留项) -- **utils/routepath、uploadpolicy**:纪律良好 -- **worker→biz 正向+接口倒置**:任务链路最规范的一段 -- **data 根多文件同包**:符合 data/README 约定 -- **apple_jws.go 证书链校验**:必要安全设计(经 gopay 源码核对补真实漏洞);单一根指纹需运维轮换预案注释 -- **capture/auth 中间件质量**:有据可依 -- **新增缝质量(第四轮验证)**:PaymentAdapterFactory(biz 接口+integration 实现+wire 绑定,单向无环)、MergeRuntimeConfig(单点三调用)、deletePrefixViaList(函数式注入,失败关闭正确)、AST 缓存(size+mtime 失效)、systeminfo(integration 定位正确)——均内聚、依赖最小、无越界 import +- **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 接口嵌入是组合窄能力 +- **S-5/S-6/S-7/S-8/S-9**:payment 大文件承载跨供应商编排;dto 跨域文件移动放大契约变化;data 转换命名差异需全域迁移;media 四文件职责边界清晰;单方法 handler 由 Wire 注入约束 +- **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 singleflight(context.WithoutCancel 隔离取消传染)、data_scope 回调注入、email CRLF 清洗、ListTasks(0,0) 全量语义、新增缝(PaymentAdapterFactory/MergeRuntimeConfig/deletePrefixViaList/AST 缓存/systeminfo)均内聚无越界 ## 处置建议(按优先级) -1. **正确性缺陷与安全边界**:W-1~W-6 已完成并经回归验证。 -2. **死代码与分层整改**:已完成可证明无消费者项;其余公开契约或零消费者基础设施均已完成影响分析并记录保留理由。 -3. **结构性项目**:D/F/S/P/L/C/X 剩余条目均已按依赖、并发、兼容性和 Wire 影响完成评估;暂无应在模板中强行落地的大改造。 +1. **立即处理 V-1**(泄漏的数据库密码)→ **V-2/V-3**(审计敏感数据落地防护对称化) +2. **V-4/V-9**(热重载漂移文档化、软删语义逐项确认+补测试) +3. **V-5/V-6/V-7/V-8/V-10 + 二节死代码**(小而具体的清理批次) +4. **三节 D-2 三处遗漏 + D-31/D-32** +5. **四节文档漂移群**(一次 README/CLAUDE.md 同步批) +6. **五节微文件合并**(最后) diff --git a/internal/README.md b/internal/README.md index 088d8e3..2136eb7 100644 --- a/internal/README.md +++ b/internal/README.md @@ -15,9 +15,9 @@ - `routecatalog`:统一声明 HTTP 路由的公开性、操作审计、请求体策略和 API 元数据 - `server`:Gin server 组合与生命周期;横切 HTTP 代码按子包维护: `server/handler`、`server/middleware`、`server/router`、`server/staticfiles`; - 通用响应和 Cookie 工具位于 `pkg/httpx` + 通用响应和 Cookie 工具位于 `server/httpx` - `service`:按 `system`、`payment`、`integration`、`task` 分模块的应用服务、 - DTO 与领域对象转换;DTO 集中在 `service/dto`,根包是兼容旧调用方的类型/构造器门面 + DTO 与领域对象转换;DTO 集中在 `service/dto`,根包仅聚合 Wire ProviderSet - `worker`:定时任务执行与调度 目录代表边界,模块文件按资源命名。DTO、handler、中间件、路由和 HTTP diff --git a/internal/biz/integration/integration_config.go b/internal/biz/integration/integration_config.go index f1f1c78..bbd0449 100644 --- a/internal/biz/integration/integration_config.go +++ b/internal/biz/integration/integration_config.go @@ -120,7 +120,7 @@ func (uc *IntegrationConfigUsecase) Save(ctx context.Context, config *Integratio return err } if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok { - values = mergeIntegrationDefaults(definition.Defaults, values) + values = MergeIntegrationDefaults(definition.Defaults, values) if config.Enabled { if err := ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil { return err @@ -153,7 +153,7 @@ func (uc *IntegrationConfigUsecase) Test(ctx context.Context, config *Integratio return err } if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok { - values = mergeIntegrationDefaults(definition.Defaults, values) + values = MergeIntegrationDefaults(definition.Defaults, values) } if err := ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil { return err @@ -209,11 +209,7 @@ func DefaultIntegrationConfig(kind, provider string) map[string]any { if !ok { return map[string]any{} } - return mergeIntegrationDefaults(definition.Defaults, nil) -} - -func mergeIntegrationDefaults(defaults, values map[string]any) map[string]any { - return MergeIntegrationDefaults(defaults, values) + return MergeIntegrationDefaults(definition.Defaults, nil) } // MergeIntegrationDefaults returns a new map with stored values overriding defaults. diff --git a/internal/biz/integration/integration_config_definition.go b/internal/biz/integration/integration_config_definition.go index 60df387..4345abc 100644 --- a/internal/biz/integration/integration_config_definition.go +++ b/internal/biz/integration/integration_config_definition.go @@ -89,7 +89,7 @@ var genericPaymentDefaults = map[string]any{ } func genericPaymentDefinition(provider, name, description string) IntegrationConfigDefinition { - defaults := mergeIntegrationDefaults(genericPaymentDefaults, nil) + defaults := MergeIntegrationDefaults(genericPaymentDefaults, nil) fields := append([]IntegrationConfigField(nil), genericPaymentFields...) return paymentDefinition(provider, name, description, defaults, fields...) } diff --git a/internal/biz/payment/payment.go b/internal/biz/payment/payment.go index 4f7e69d..14a515a 100644 --- a/internal/biz/payment/payment.go +++ b/internal/biz/payment/payment.go @@ -349,15 +349,6 @@ func (r *PaymentFulfillmentRegistry) Handler(kind string) PaymentFulfillmentHand return r.handlers[kind] } -func (r *PaymentFulfillmentRegistry) Len() int { - if r == nil { - return 0 - } - r.mu.RLock() - defer r.mu.RUnlock() - return len(r.handlers) -} - type PaymentUsecase struct { repo PaymentRepo orders PaymentOrderRepo @@ -387,12 +378,6 @@ func NewPaymentUsecase(repo PaymentRepo, orders PaymentOrderRepo, hooks PaymentH } } -func NewConfiguredPaymentUsecase(repo PaymentRepo, orders PaymentOrderRepo, hooks PaymentHooks, sources *PaymentOrderSourceRegistry, fulfillments *PaymentFulfillmentRegistry, appLogger *slog.Logger) (*PaymentUsecase, error) { - // Business modules are optional in the template. Missing modules are - // reported when the corresponding payment operation is invoked. - return NewPaymentUsecase(repo, orders, hooks, sources, fulfillments, appLogger), nil -} - func (uc *PaymentUsecase) RegisterBusinessModule(module PaymentBusinessModule) error { if module == nil { return errors.New("支付业务模块为空") @@ -464,14 +449,11 @@ func (uc *PaymentUsecase) Create(ctx context.Context, req *PaymentRequest) (*Pay if req.OriginalAmount < req.Amount { return nil, errors.New("原始金额不能小于支付订单金额") } - canonicalProvider, canonicalTradeNo := req.Provider, req.TradeNo - canonicalBusinessType, canonicalBusinessID := req.BusinessType, req.BusinessID - canonicalSubject, canonicalCurrency, canonicalAmount := req.Subject, req.Currency, req.Amount - canonicalOriginalAmount, canonicalPaymentMode := req.OriginalAmount, req.PaymentMode canonicalExtra, err := json.Marshal(req.Extra) if err != nil { return nil, fmt.Errorf("支付扩展参数无效: %w", err) } + canonicalFingerprint := paymentOrderFingerprint(req, canonicalExtra) if err := uc.hooks.BeforeCreate(ctx, req); err != nil { return nil, err } @@ -479,7 +461,7 @@ func (uc *PaymentUsecase) Create(ctx context.Context, req *PaymentRequest) (*Pay if err != nil { return nil, fmt.Errorf("支付扩展参数无效: %w", err) } - if req.Provider != canonicalProvider || req.TradeNo != canonicalTradeNo || req.BusinessType != canonicalBusinessType || req.BusinessID != canonicalBusinessID || req.Subject != canonicalSubject || req.Currency != canonicalCurrency || req.Amount != canonicalAmount || req.OriginalAmount != canonicalOriginalAmount || req.PaymentMode != canonicalPaymentMode || string(currentExtra) != string(canonicalExtra) { + if paymentOrderFingerprint(req, currentExtra) != canonicalFingerprint { return nil, ErrPaymentOrderConflict } return uc.createWithOrder(ctx, req) @@ -926,10 +908,7 @@ func (uc *PaymentUsecase) refundWithOrder(ctx context.Context, provider, tradeNo return nil, err } if !accepted { - if providerErr != nil { - return nil, providerErr - } - return nil, errors.New(message) + return nil, providerErr } result.Status = "refund_pending" return attachOrderResult(result, order), nil diff --git a/internal/biz/payment/payment_order.go b/internal/biz/payment/payment_order.go index ae6f060..1e9d576 100644 --- a/internal/biz/payment/payment_order.go +++ b/internal/biz/payment/payment_order.go @@ -182,15 +182,6 @@ func (r *PaymentOrderSourceRegistry) Source(kind string) PaymentOrderSource { return r.sources[kind] } -func (r *PaymentOrderSourceRegistry) Len() int { - if r == nil { - return 0 - } - r.mu.RLock() - defer r.mu.RUnlock() - return len(r.sources) -} - func (r *PaymentOrderSourceRegistry) Unregister(kind string) { if r == nil { return diff --git a/internal/biz/payment/provider.go b/internal/biz/payment/provider.go index f31ce07..cf7635f 100644 --- a/internal/biz/payment/provider.go +++ b/internal/biz/payment/provider.go @@ -9,5 +9,5 @@ var ProviderSet = wire.NewSet( NewPaymentHooks, NewPaymentOrderSourceRegistry, NewPaymentFulfillmentRegistry, - NewConfiguredPaymentUsecase, + NewPaymentUsecase, ) diff --git a/internal/biz/system/media_upload.go b/internal/biz/system/media_upload.go index 6853e01..dcbe18b 100644 --- a/internal/biz/system/media_upload.go +++ b/internal/biz/system/media_upload.go @@ -96,7 +96,7 @@ func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash completed, err := uc.FindCompletedSession(ctx, userID, strings.ToLower(hash)) if err == nil && completed.MediaID != 0 { if media, findErr := uc.FindMedia(ctx, completed.MediaID); findErr == nil { - copy := &MediaFile{Name: name, URL: media.URL, Tag: media.Tag, Key: media.Key} + copy := &MediaFile{Name: name, URL: media.URL, Tag: media.Tag, Key: media.Key, Size: media.Size, MD5: media.MD5, Mime: media.Mime, UserID: userID} if createErr := uc.CreateMedia(ctx, copy); createErr == nil { return nil, copy, nil, nil } @@ -249,7 +249,10 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin if err = uc.CreateMedia(ctx, media); err != nil { return fail(err) } - _ = uc.CompleteUploadSession(ctx, uploadID, key, media.ID) + if err = uc.CompleteUploadSession(ctx, uploadID, key, media.ID); err != nil { + _ = uc.FailUploadSession(ctx, uploadID) + return nil, fmt.Errorf("完成上传会话失败: %w", err) + } _ = uc.DeleteChunks(ctx, uploadID) _ = uc.files.DeletePrefix(ctx, uc.chunkPrefix(uploadID)) return media, nil diff --git a/internal/config/runtime.go b/internal/config/runtime.go index 59d85a1..ed92f74 100644 --- a/internal/config/runtime.go +++ b/internal/config/runtime.go @@ -270,6 +270,9 @@ func (r *Store) watchLoop(watcher *fsnotify.Watcher, stop <-chan struct{}, done // take down a running process or publish invalid state. continue } + // File watching publishes the merged immutable snapshot only. Storage + // clients are rebuilt by the explicit system reload flow in data; + // keeping that lifecycle out of the watcher preserves rollback safety. r.Replace(MergeRuntimeConfig(r.Snapshot(), next)) case _, ok := <-errors: // fsnotify errors are intentionally non-fatal; the watcher remains diff --git a/internal/data/config_store.go b/internal/data/config_store.go index 45d86cf..cce2678 100644 --- a/internal/data/config_store.go +++ b/internal/data/config_store.go @@ -122,6 +122,8 @@ func (d *Data) removeIntegrationConfigFromFile() error { } func (d *Data) reloadConfig(ctx context.Context) error { + // Explicit reload is the lifecycle boundary that rebuilds database, cache, + // Mongo, storage, and integration clients after a file snapshot change. d.configMu.Lock() defer d.configMu.Unlock() configPath := d.runtime.ConfigPath() diff --git a/internal/data/payment/payment.go b/internal/data/payment/payment.go index 5aae2e4..d554807 100644 --- a/internal/data/payment/payment.go +++ b/internal/data/payment/payment.go @@ -411,7 +411,7 @@ func paymentCreateRequiresNotifyURL(provider string, extra, config map[string]an if value == "" { value = paymentkit.FirstText(config, keys...) } - normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) + normalized := paymentkit.NormalizePaymentMethod(value) switch provider { case bizpayment.PaymentAlipay, bizpayment.PaymentAlipayV3: return !paymentkit.ContainsFold([]string{"pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face"}, normalized) diff --git a/internal/data/system/parameter_test.go b/internal/data/system/parameter_test.go index e0bc997..0309cce 100644 --- a/internal/data/system/parameter_test.go +++ b/internal/data/system/parameter_test.go @@ -66,3 +66,27 @@ func TestParameterRepositoryKeepsReferenceEmptyQuerySemantics(t *testing.T) { t.Fatalf("empty bulk delete removed rows: count=%d", count) } } + +func TestParameterUpdateDoesNotReviveSoftDeletedRow(t *testing.T) { + data := newTransactionTestData(t) + repo := ¶meterRepo{data: data} + ctx := context.Background() + item := &system.SystemParameter{Name: "before-delete", Key: "soft-delete-key", Value: "value"} + if err := repo.CreateParameter(ctx, item); err != nil { + t.Fatal(err) + } + if err := data.gormDB.WithContext(ctx).Delete(¶meterPO{}, item.ID).Error; err != nil { + t.Fatal(err) + } + item.Name = "must-not-revive" + if err := repo.UpdateParameter(ctx, item); err != nil { + t.Fatal(err) + } + var persisted parameterPO + if err := data.gormDB.WithContext(ctx).Unscoped().First(&persisted, item.ID).Error; err != nil { + t.Fatal(err) + } + if persisted.Name != "before-delete" || persisted.DeletedAt.Time.IsZero() { + t.Fatalf("soft-deleted row changed or revived: %+v", persisted) + } +} diff --git a/internal/integration/README.md b/internal/integration/README.md index 4adba5c..7c7e7bc 100644 --- a/internal/integration/README.md +++ b/internal/integration/README.md @@ -19,7 +19,7 @@ lifecycles. Their constructors are exposed through `ProviderSet`; `cmd` binds the `cache.RedisProvider` implementation to the shared `data.Data` container. Stateless protocol helpers that do not own clients live in -`pkg/paymentkit`. They are intentionally small and dependency +`internal/paymentkit`. They are intentionally small and dependency light, while provider adapters remain here. ## Shared WebSocket and MQ APIs diff --git a/internal/integration/payment/qq.go b/internal/integration/payment/qq.go index 6afbf7e..8e8268e 100644 --- a/internal/integration/payment/qq.go +++ b/internal/integration/payment/qq.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" bizpayment "kra/internal/biz/payment" + "kra/internal/paymentkit" "strings" "github.com/go-pay/gopay" @@ -126,7 +127,7 @@ func qqCreateMethod(extra, config map[string]any) (string, error) { if value == "" { value = firstAny(config, "trade_type", "pay_type", "method", "pay_method") } - normalized := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(value), "-", "_")) + normalized := strings.ToUpper(paymentkit.NormalizePaymentMethod(value)) switch normalized { case "", "NATIVE", "QR", "QRCODE": return gopayQQ.TradeType_Native, nil diff --git a/internal/integration/payment/wechat_v3.go b/internal/integration/payment/wechat_v3.go index c734c45..fb5fa59 100644 --- a/internal/integration/payment/wechat_v3.go +++ b/internal/integration/payment/wechat_v3.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" bizpayment "kra/internal/biz/payment" + "kra/internal/paymentkit" "net/http" "net/url" "strings" @@ -120,7 +121,7 @@ func (a *wechatV3Adapter) Create(ctx context.Context, req *bizpayment.PaymentReq } func normalizeWechatV3TradeType(value string) string { - return strings.NewReplacer(".", "", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) + return paymentkit.NormalizePaymentMethod(value) } func wechatV3CodePayResult(tradeNo string, rsp *gopayWechatV3.CodePayRsp) (*bizpayment.PaymentResult, error) { diff --git a/internal/logging/source.go b/internal/logging/source.go index cd33fa8..4a8fa34 100644 --- a/internal/logging/source.go +++ b/internal/logging/source.go @@ -71,14 +71,10 @@ func skipStackFile(filename string) bool { "/go.uber.org/", "/gorm.io/", "/internal/logging/", - "/internal/transport/middleware/", - "/internal/transport/router/", "/internal/server/handler/", "/internal/server/middleware/", "/internal/server/router/", "/internal/server/httpx/", - "/internal/server/middleware_", - "/internal/server/route_", } { if strings.Contains(normalized, marker) { return true diff --git a/internal/logging/zap_test.go b/internal/logging/zap_test.go index 0f88811..82fb969 100644 --- a/internal/logging/zap_test.go +++ b/internal/logging/zap_test.go @@ -144,10 +144,7 @@ func TestSkipStackFileRecognizesServerTransportPackages(t *testing.T) { {name: "middleware", path: `D:\workspace\app\system\internal\server\middleware\auth.go`, want: true}, {name: "router", path: `/workspace/internal/server/router/user.go`, want: true}, {name: "http helper", path: `/workspace/internal/server/httpx/response.go`, want: true}, - {name: "legacy flattened middleware", path: `D:\workspace\app\system\internal\server\middleware_auth.go`, want: true}, - {name: "legacy flattened route", path: `/workspace/internal/server/route_user.go`, want: true}, {name: "handler remains application boundary", path: `/workspace/internal/server/handler_user.go`, want: false}, - {name: "legacy middleware", path: `/workspace/internal/transport/middleware/auth.go`, want: true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/internal/routecatalog/catalog.go b/internal/routecatalog/catalog.go index 1f8bd76..ad13cde 100644 --- a/internal/routecatalog/catalog.go +++ b/internal/routecatalog/catalog.go @@ -13,6 +13,7 @@ const ( BodyPolicyIntegrationConfig RouteBodyPolicy = "integration_config" BodyPolicyPaymentConfig RouteBodyPolicy = "payment_config" BodyPolicyPaymentCallback RouteBodyPolicy = "payment_callback" + BodyPolicyUpload RouteBodyPolicy = "upload" ) // Descriptor is the shared HTTP contract consumed by service bootstrap, @@ -160,13 +161,13 @@ var routes = map[string]routeValue{ "POST /fileUploadAndDownload/getFileList": {group: "文件上传与下载", description: "获取上传文件列表"}, "POST /fileUploadAndDownload/importURL": {group: "文件上传与下载", description: "导入URL"}, "POST /fileUploadAndDownload/listOssFiles": {group: "文件上传与下载", description: "获取对象存储文件列表"}, - "POST /fileUploadAndDownload/upload": {group: "文件上传与下载", description: "文件上传(建议选择)"}, + "POST /fileUploadAndDownload/upload": {group: "文件上传与下载", description: "文件上传(建议选择)", bodyPolicy: BodyPolicyUpload}, "POST /info/createInfo": {group: "公告", description: "新建公告", audit: true}, "POST /init/checkdb": {group: "初始化", description: "检查数据库", public: true}, "POST /init/initdb": {group: "初始化", description: "初始化数据库", public: true}, - "POST /integration/configs/:kind/:provider/test": {group: "集成配置", description: "测试通信集成连接", audit: true}, + "POST /integration/configs/:kind/:provider/test": {group: "集成配置", description: "测试通信集成连接", audit: true, bodyPolicy: BodyPolicyPaymentConfig}, "POST /jwt/jsonInBlacklist": {group: "jwt", description: "jwt加入黑名单(退出,必选)"}, - "POST /mediaUpload/chunk": {group: "媒体上传", description: "上传分片"}, + "POST /mediaUpload/chunk": {group: "媒体上传", description: "上传分片", bodyPolicy: BodyPolicyUpload}, "POST /mediaUpload/complete": {group: "媒体上传", description: "完成大文件上传"}, "POST /mediaUpload/init": {group: "媒体上传", description: "初始化大文件上传"}, "POST /menu/addBaseMenu": {group: "菜单", description: "新增菜单", audit: true}, diff --git a/internal/server/README.md b/internal/server/README.md index 5cd0ec2..c9fd21d 100644 --- a/internal/server/README.md +++ b/internal/server/README.md @@ -6,7 +6,7 @@ grouped by role: - `handler/`: resource handlers and HTTP boundary validation - `middleware/`: request metadata, auth, access control, audit, recovery, CORS - `router/`: resource route registration and the system route registrar -- `pkg/httpx/`: transport-level response and cookie helpers shared by handlers +- `httpx/`: transport-level response and cookie helpers shared by handlers and middleware - `staticfiles/`: local upload storage route registration and file serving diff --git a/internal/server/handler/api.go b/internal/server/handler/api.go index d56fb58..6ad18a3 100644 --- a/internal/server/handler/api.go +++ b/internal/server/handler/api.go @@ -192,7 +192,7 @@ func (h *API) ApplySync(c *gin.Context) { return } if err := h.service.ApplyAPISyncRequest(c.Request.Context(), &req); err != nil { - Fail(c, "忽略失败") + Fail(c, "同步失败") return } OK(c) diff --git a/internal/server/handler/audit.go b/internal/server/handler/audit.go index d737fea..ee036f3 100644 --- a/internal/server/handler/audit.go +++ b/internal/server/handler/audit.go @@ -138,7 +138,7 @@ func (h *Audit) DataAccess(c *gin.Context) { } items, total, err := h.service.DataAccessRequest(c.Request.Context(), &req) if err != nil { - Fail(c, "获取失败:"+err.Error()) + Fail(c, "获取失败") return } Write(c, CodeSuccess, PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功") @@ -150,7 +150,7 @@ func (h *Audit) DeleteDataAccess(c *gin.Context) { return } if err := h.service.DeleteDataAccess(c.Request.Context(), req.IDs); err != nil { - Fail(c, "删除失败:"+err.Error()) + Fail(c, "删除失败") return } Write(c, CodeSuccess, gin.H{}, "删除成功") @@ -238,7 +238,7 @@ func (h *Audit) DeleteError(c *gin.Context) { return } if err := h.service.DeleteErrors(c.Request.Context(), []uint{id}); err != nil { - Fail(c, "删除失败:"+err.Error()) + Fail(c, "删除失败") return } Write(c, CodeSuccess, gin.H{}, "删除成功") @@ -250,7 +250,7 @@ func (h *Audit) DeleteErrors(c *gin.Context) { return } if err := h.service.DeleteErrors(c.Request.Context(), ids); err != nil { - Fail(c, "批量删除失败:"+err.Error()) + Fail(c, "批量删除失败") return } Write(c, CodeSuccess, gin.H{}, "批量删除成功") @@ -262,7 +262,7 @@ func (h *Audit) UpdateError(c *gin.Context) { return } if err := h.service.UpdateErrorRequest(c.Request.Context(), &req); err != nil { - Fail(c, "更新失败:"+err.Error()) + Fail(c, "更新失败") return } Write(c, CodeSuccess, gin.H{}, "更新成功") @@ -275,7 +275,7 @@ func (h *Audit) Error(c *gin.Context) { } item, err := h.service.Error(c.Request.Context(), id) if err != nil { - Fail(c, "查询失败:"+err.Error()) + Fail(c, "查询失败") return } OKWithData(c, item) @@ -297,7 +297,7 @@ func (h *Audit) Errors(c *gin.Context) { } items, total, err := h.service.ErrorsFilter(c.Request.Context(), p, size, c.Query("form"), c.Query("info"), createdAtRange) if err != nil { - Fail(c, "获取失败:"+err.Error()) + Fail(c, "获取失败") return } Write(c, CodeSuccess, PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功") @@ -309,7 +309,7 @@ func (h *Audit) CreateError(c *gin.Context) { return } if err := h.recorder.CreateErrorMutationRequest(c.Request.Context(), &req); err != nil { - Fail(c, "创建失败:"+err.Error()) + Fail(c, "创建失败") return } Write(c, CodeSuccess, gin.H{}, "创建成功") diff --git a/internal/server/handler/http.go b/internal/server/handler/http.go index 590647a..7f7b17d 100644 --- a/internal/server/handler/http.go +++ b/internal/server/handler/http.go @@ -1,5 +1,5 @@ // This file is the handler package's single vocabulary for HTTP replies: the -// envelope helpers come from pkg/httpx and Claims from the auth middleware, so +// envelope helpers come from server/httpx and Claims from the auth middleware, so // handlers name one package instead of two. package handler diff --git a/internal/server/middleware/access_log.go b/internal/server/middleware/access_log.go index 147eba4..a6f4863 100644 --- a/internal/server/middleware/access_log.go +++ b/internal/server/middleware/access_log.go @@ -31,7 +31,7 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H started := time.Now() var requestBody []byte multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") - mediaUpload := multipart && isMediaUploadRoute(c.FullPath()) + mediaUpload := multipart && routecatalog.BodyPolicyFor(c.Request.Method, c.Request.URL.Path) == routecatalog.BodyPolicyUpload var admin *config.Admin if runtime != nil { if snapshot := runtime.Snapshot(); snapshot != nil { @@ -170,11 +170,6 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H } } -func isMediaUploadRoute(route string) bool { - return strings.HasSuffix(route, "/fileUploadAndDownload/upload") || - strings.HasSuffix(route, "/mediaUpload/chunk") -} - func paymentCallbackProvider(path string) string { parts := strings.Split(strings.Trim(path, "/"), "/") for index := 0; index+2 < len(parts); index++ { diff --git a/internal/server/middleware/audit.go b/internal/server/middleware/audit.go index 4b0284b..e1527c9 100644 --- a/internal/server/middleware/audit.go +++ b/internal/server/middleware/audit.go @@ -80,7 +80,7 @@ func OperationAudit(runtime *config.Store, recorder *systemservice.AuditRecorder responseBody := "" if value, ok := c.Get(ctxRespBufferKey); ok { if body, bok := value.(*bytes.Buffer); bok { - responseBody = body.String() + responseBody = redactJSON(body.Bytes(), c.Writer.Header().Get("Content-Type"), maxBytes) } } if isDownloadResponse(c) && len(responseBody) > maxBytes { diff --git a/internal/server/middleware/payment_audit_test.go b/internal/server/middleware/payment_audit_test.go index b995d4b..f4199f3 100644 --- a/internal/server/middleware/payment_audit_test.go +++ b/internal/server/middleware/payment_audit_test.go @@ -9,7 +9,7 @@ import ( ) func TestPaymentIntegrationSecretsAreRedacted(t *testing.T) { - raw := []byte(`{"enabled":true,"config":{"app_id":"app","mch_key":"merchant-secret","api_v3_key":"v3-secret","client_cert":"certificate","client_key":"private-key","platform_cert":"platform-certificate","credential_code":"credential","webhook_id":"webhook"}}`) + raw := []byte(`{"enabled":true,"config":{"app_id":"app","mch_key":"merchant-secret","api_v3_key":"v3-secret","client_cert":"certificate","client_key":"private-key","platform_cert":"platform-certificate","credential_code":"credential","webhook_id":"webhook","key":"institution-secret","public_cert":"public-certificate","signing_key":"jwt-secret","secret_key":"cloud-secret"}}`) redacted := redactJSON(raw, "application/json", 4096) var payload struct { Config map[string]string `json:"config"` @@ -17,7 +17,7 @@ func TestPaymentIntegrationSecretsAreRedacted(t *testing.T) { if err := json.Unmarshal([]byte(redacted), &payload); err != nil { t.Fatalf("decode redacted payload: %v", err) } - for _, key := range []string{"mch_key", "api_v3_key", "client_cert", "client_key", "platform_cert", "credential_code", "webhook_id"} { + for _, key := range []string{"mch_key", "api_v3_key", "client_cert", "client_key", "platform_cert", "credential_code", "webhook_id", "key", "public_cert", "signing_key", "secret_key"} { if payload.Config[key] != "***" { t.Fatalf("payment secret %q was not redacted: %s", key, redacted) } @@ -55,4 +55,7 @@ func TestPaymentIntegrationConfigUsesRouteLevelSummary(t *testing.T) { if routecatalog.BodyPolicyFor("PUT", "/api/integration/configs/mq/emqx") == routecatalog.BodyPolicyPaymentConfig { t.Fatal("non-payment integration was treated as payment configuration") } + if routecatalog.BodyPolicyFor("POST", "/api/integration/configs/payment/saobei/test") != routecatalog.BodyPolicyPaymentConfig { + t.Fatal("payment test route was not recognized as sensitive") + } } diff --git a/internal/server/middleware/redact.go b/internal/server/middleware/redact.go index 6dd56fc..34bc796 100644 --- a/internal/server/middleware/redact.go +++ b/internal/server/middleware/redact.go @@ -19,8 +19,9 @@ var sensitivePayloadKeys = map[string]struct{}{ "password": {}, "newpassword": {}, "oldpassword": {}, "confirmpassword": {}, "passwd": {}, "pwd": {}, "token": {}, "accesstoken": {}, "refreshtoken": {}, "secret": {}, "clientsecret": {}, "apikey": {}, "privatekey": {}, "idcard": {}, + "key": {}, "signingkey": {}, "secretkey": {}, "appkey": {}, "mchkey": {}, "apiv3key": {}, "clientcert": {}, "clientkey": {}, - "platformcert": {}, "platformserialno": {}, "credentialcode": {}, "certfile": {}, + "platformcert": {}, "platformserialno": {}, "publiccert": {}, "credentialcode": {}, "certfile": {}, "keyfile": {}, "publickey": {}, "rootcert": {}, "appcert": {}, "webhookid": {}, "authorization": {}, } diff --git a/internal/service/README.md b/internal/service/README.md index 7b550b9..ae2c1cd 100644 --- a/internal/service/README.md +++ b/internal/service/README.md @@ -2,7 +2,7 @@ `service` adapts HTTP-facing DTOs to business usecases and owns application orchestration. Request/response/filter contracts live in `dto/`; the root -package is a compatibility facade and module provider aggregator. +package aggregates module provider sets for Wire. Implementations are grouped by the same business modules as `biz` and `data`: @@ -18,9 +18,8 @@ The stateless router-prefix helper lives in `internal/utils/routepath` because both service modules and HTTP middleware use it; it does not depend on a business usecase or DTO. -Each module owns its Wire `ProviderSet`. The root `service` package re-exports -the existing service types and constructors through aliases and thin wrappers, so handlers, middleware -and generated Wire code can migrate independently without a flag day. +Each module owns its Wire `ProviderSet`; the root package only composes those +sets and does not re-export service types or constructors. Within `system/`, larger cross-cutting resources stay in the same package while being split by concern (`audit.go` and its error/log companions, `media.go` and diff --git a/internal/service/system/user_conversion.go b/internal/service/system/user_conversion.go index 5c01d46..f947ce1 100644 --- a/internal/service/system/user_conversion.go +++ b/internal/service/system/user_conversion.go @@ -10,7 +10,7 @@ func convertAuthority(value system.Authority) *dto.AuthorityResponse { if value.Menus != nil { menus = menuResponses(value.Menus) } - return &dto.AuthorityResponse{CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, DeletedAt: value.DeletedAt, AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, Children: nil, Menus: menus, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter} + return &dto.AuthorityResponse{CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, Children: nil, Menus: menus, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter} } func convertUser(user *system.User) *dto.UserResponse { var authorities []*dto.AuthorityResponse diff --git a/pkg/README.md b/pkg/README.md index b0f447d..2a2b5d0 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -7,8 +7,9 @@ - `database`:跨模块共享的 GORM 支持、分页和迁移执行器 - `module`:模块迁移、后台元数据和路由注册协议 - `task`:跨模块共享的进程内任务注册表和贡献协议 -- `logging`:跨 app 复用的结构化日志能力 -- `httpx`:跨 HTTP 模块共享的 JSON 响应结构、分页结构和状态码 -- `paymentkit`:支付金额、签名、状态、provider 标识和回调应答纯函数 +- `database`、`module`、`task`:跨模块基础设施与稳定协议 + +应用级 logging、HTTP response、paymentkit 等实现位于 `internal/`,不属于 +可复用 pkg 公共层。 `pkg` 只能提供机制和稳定协议,不能引用任何 `internal` 业务包。数据库配置加载、 系统集成配置、系统表和 provider 生命周期仍由 `internal` 负责。