优化结构

This commit is contained in:
Yvan 2026-08-28 13:56:12 +08:00
parent 33ba777137
commit 56f9440a6b
32 changed files with 159 additions and 270 deletions

View File

@ -22,6 +22,7 @@ internal/worker/ Timed-task runtime and scheduler.
internal/utils/ Stateless, internal-only helper packages. internal/utils/ Stateless, internal-only helper packages.
pkg/ Reusable infrastructure packages. pkg/ Reusable infrastructure packages.
web/ Vue administration frontend. web/ Vue administration frontend.
docs/ Review notes, migration plans, and operational documentation.
``` ```
## Layering & dependency rules ## Layering & dependency rules

View File

@ -62,15 +62,10 @@ design rather than add the import.
**service (DTO ↔ DO)** **service (DTO ↔ DO)**
- `convert<Resource>` parses an incoming proto into a DO. The reverse - Convert hand-written HTTP DTOs into domain objects at the service boundary,
direction is built inline at the return site; the reply type is and build response DTOs from returned domain objects.
whatever the proto declares — usually the resource itself - Keep handlers focused on binding, authentication context, response envelopes,
(`return &v1.<Resource>{...}, nil`), sometimes a list wrapper and transport-specific limits.
(`*v1.<Resources>Set`), or `&emptypb.Empty{}` for deletes. Inlining
keeps each handler self-contained.
- Embed `Unimplemented<Resource>ServiceServer`.
- Parse AIP list requests via `filtering` / `ordering` / `pagination`;
apply `fieldmask.Update` for partial updates.
- Validate request inputs at the service boundary before delegating to the - Validate request inputs at the service boundary before delegating to the
usecase. usecase.
- Return `biz` errors. No business rules, no storage access, no PO. - 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 ### Add-a-resource checklist
1. **DTO**: define `Create<Resource>` / `Get<Resource>` / 1. **DTO**: define request and response types in `internal/service/dto/`.
`List<Resources>` / `Update<Resource>` / `Delete<Resource>` in
`api/<domain>/<version>/`, then `make api`.
2. **DO + repo interface**: declare both in `biz`; build the usecase on 2. **DO + repo interface**: declare both in `biz`; build the usecase on
top of the interface. top of the interface.
3. **Repo impl**: implement in `data` returning `biz.<Resource>Repo`; 3. **Repo impl**: implement in `data` returning `biz.<Resource>Repo`;
add a PO and the matching conversion helpers when storage shape add a PO and the matching conversion helpers when storage shape
diverges from DO. diverges from DO.
4. **Wiring**: register the repo constructor in `data.ProviderSet`, the 4. **Transport**: add the service adapter, handler, router registration, and
usecase in `biz.ProviderSet`, the service in `service.ProviderSet`; routecatalog metadata.
register HTTP/gRPC services in `internal/server`. 5. **Wiring**: register providers in the relevant sets.
5. **Regenerate**: `make all` to refresh Wire and `go.mod`. 6. **Regenerate**: `make generate` or `make all` to refresh Wire and `go.mod`.
### Testing seam ### Testing seam
@ -130,8 +123,7 @@ tests exercise repo implementations at the storage boundary.
## Generation & generated files ## Generation & generated files
Regenerate via `make api` or `make all`; never hand-edit Regenerate via `make generate` or `make all`; never hand-edit `wire_gen.go`.
`*.pb.go`, `*_grpc.pb.go`, `*_http.pb.go`, or `wire_gen.go`.
## Naming & error reasons ## Naming & error reasons
@ -141,8 +133,8 @@ Regenerate via `make api` or `make all`; never hand-edit
`<Resource>Service`. PO types live inside `internal/data/`; pick a `<Resource>Service`. PO types live inside `internal/data/`; pick a
name that fits the storage driver and convert with name that fits the storage driver and convert with
`new<Resource>(do)` / `toBiz(po)` free functions. `new<Resource>(do)` / `toBiz(po)` free functions.
- Error reasons: declared in `api/<domain>/<version>/error_reason.proto`, - Error reasons use stable strings and are surfaced as `Err<Resource><Cause>` in
surfaced as `Err<Resource><Cause>` in `biz`. `biz`.
## Commits & security ## Commits & security

6
cmd/wire_gen.go generated
View File

@ -113,11 +113,7 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg
paymentHooks := payment3.NewPaymentHooks() paymentHooks := payment3.NewPaymentHooks()
paymentOrderSourceRegistry := payment3.NewPaymentOrderSourceRegistry() paymentOrderSourceRegistry := payment3.NewPaymentOrderSourceRegistry()
paymentFulfillmentRegistry := payment3.NewPaymentFulfillmentRegistry() paymentFulfillmentRegistry := payment3.NewPaymentFulfillmentRegistry()
paymentUsecase, err := payment3.NewConfiguredPaymentUsecase(paymentRepo, paymentOrderRepo, paymentHooks, paymentOrderSourceRegistry, paymentFulfillmentRegistry, logger) paymentUsecase := payment3.NewPaymentUsecase(paymentRepo, paymentOrderRepo, paymentHooks, paymentOrderSourceRegistry, paymentFulfillmentRegistry, logger)
if err != nil {
cleanup()
return nil, nil, err
}
paymentService := payment4.NewPaymentService(paymentUsecase) paymentService := payment4.NewPaymentService(paymentUsecase)
handlerPayment := handler.NewPayment(paymentService) handlerPayment := handler.NewPayment(paymentService)
taskRepo := task.NewTaskRepo(dataData) taskRepo := task.NewTaskRepo(dataData)

View File

@ -1,183 +1,90 @@
# 代码审查问题清单internal + pkg # 代码审查问题清单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 编译),不在审查范围 - gva/ 目录是遗留参考库(独立 module 不参与 kra 编译),不在审查范围
- 依赖方向合规确认pkg 无 import internalintegration 不 import data/servicedata 不再 import integration/payment第四轮重构后复验单向无循环依赖 - 依赖方向合规确认pkg 无 import internalintegration 不 import data/servicedata 不 import integration/payment无循环依赖
- 第五轮总评:核心业务路径(支付/调度/数据权限/上传)逻辑严谨、分层契约执行到位;历轮修复全部属实;当前债务已收敛为**敏感数据落地防护不对称、文档漂移、中转微文件**三类
--- ---
## 一、正确性缺陷(第四轮回归审查新发现,最高优先级 ## 一、安全与正确性缺陷(第五轮新发现,最优先
| # | 问题 | 位置 | 说明 | | # | 问题 | 位置 | 说明 |
|---|------|------|------| |---|------|------|------|
| ~~W-1~~ | ~~payment 空注册表导致应用启动失败。~~ **已修复:模板允许无业务模块启动,调用未配置能力时返回明确错误。** | ~~原位置~~ | ~~已完成~~ | | ~~V-1~~ | ~~已清空 `configs/config.yaml` 中的 MySQL DSN、数据库密码和 JWT signing key模板仅保留 `KRA_*` 环境变量覆盖说明。泄漏值仍存在于历史提交,需在部署侧轮换数据库密码与 JWT 密钥。~~ | ~~configs/config.yaml:9,13,91~~ | ~~已完成2026-08-28~~ |
| ~~W-2~~ | ~~CompleteUpload 吞掉 ListChunks 底层错误。~~ **已修复:底层错误与分片数量不足已拆分处理。** | ~~internal/biz/system/media_upload.go~~ | ~~已完成~~ | | ~~V-2~~ | ~~已让操作审计 Response 复用 `redactJSON`,与访问日志保持同一脱敏和长度上限策略,避免 token/密钥字段明文入库。~~ | ~~server/middleware/audit.gocapture.go~~ | ~~已完成2026-08-28针对性测试通过~~ |
| ~~W-3~~ | ~~MergeRuntimeConfig 嵌套子节合并缺口。~~ **已修复并增加回归测试。** | ~~internal/config/clone.goruntime_test.go~~ | ~~已完成~~ | | ~~V-3~~ | ~~已补齐 `key`、`public_cert`、`signing_key`、`secret_key` 等敏感键,并将 integration payment test 路由标记为 `BodyPolicyPaymentConfig`,请求体统一使用摘要策略。~~ | ~~server/middleware/redact.goroutecatalog/catalog.go~~ | ~~已完成2026-08-28针对性测试通过~~ |
| ~~W-4~~ | ~~退款状态词表语义边角。~~ **已修复并补充测试。** | ~~internal/paymentkit/status.go~~ | ~~已完成~~ | | ~~V-4~~ | ~~已在 config watcher 与 data reload 入口补充运行时契约说明watch 只发布不可变快照,`reloadSystem` 才重建数据库/缓存/存储/集成客户端;保留双路径以维护回滚与连接退休安全。~~ | ~~internal/config/runtime.gointernal/data/config_store.go~~ | ~~已完成2026-08-28~~ |
| ~~W-5~~ | ~~公告/参数/版本列表无 ORDER BY。~~ **已修复:统一按 `id desc` 排序。** | ~~data/system/*.go~~ | ~~已完成~~ | | ~~V-5~~ | ~~已修正 ApplySync 失败文案为「同步失败」。~~ | ~~server/handler/api.go:195~~ | ~~已完成2026-08-28~~ |
| ~~W-6~~ | ~~外部退款分支存在不可达死分支。~~ **已修复。** | ~~internal/biz/payment/payment.go~~ | ~~已完成~~ | | ~~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-412payment_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:65integration/provider.go:33-38 三重死绑定 + Hub 死绑定
- websocketHub 接口零消费者integration 层 On* 四注册方法零调用,双层 handler 登记机制两层都为空
- namedClient/Client() 整型死代码integration/mq/emqx.go:632-650
### 2.3 零散死代码(第四轮新扫描)【四轮】
| 死代码 | 位置 | 证据 | | 死代码 | 位置 | 证据 |
|--------|------|------| |--------|------|------|
| ~~`config.CloneData`~~ | ~~已删除:全仓零调用,且 `Clone`/`MergeRuntimeConfig` 已覆盖实际快照复制入口。已完成2026-08-28。~~ | ~~internal/config/clone.go~~ | | ~~PaymentOrderSourceRegistry.Len~~ | ~~已删除:全仓(含测试)零调用。~~ | ~~biz/payment/payment_order.go~~ |
| ~~`paymentkit.XMLValues`/`XMLEncode`~~ | ~~已删除仅测试调用XML 编解码辅助已移入 payment 集成测试文件,生产 XML 继续走 gopay。已完成2026-08-28。~~ | ~~internal/paymentkit/xml.go~~ | | ~~PaymentFulfillmentRegistry.Len~~ | ~~已删除:全仓(含测试)零调用。~~ | ~~biz/payment/payment.go~~ |
| ~~`paymentkit.NestedString`~~ | ~~已删除:生产零调用;旧测试已改为覆盖现存 `JSONObject`/`StringAtPath`。已完成2026-08-28。~~ | ~~internal/paymentkit/json.go~~ | | ~~NewConfiguredPaymentUsecase 恒 nil 错误透传壳~~ | ~~已删除并通过 Wire 重新生成ProviderSet 直接绑定 `NewPaymentUsecase`。~~ | ~~biz/payment/provider.gocmd/wire_gen.go~~ |
| ~~`logging.NewZapLogger`~~ | ~~已删除:唯一测试调用改用生产构造器 `NewReloadableZapLogger`,避免维护双入口。~~ | ~~internal/logging/zap.godata/gorm_logger_test.go~~ | | ~~biz/integration mergeIntegrationDefaults 一行转发壳~~ | ~~已删除,调用点统一使用 `MergeIntegrationDefaults`。~~ | ~~biz/integration/integration_config.go~~ |
| ~~`data/payment.contains`~~ | ~~已改用 `paymentkit.ContainsFold`,本地实现已删除。~~ | ~~data/payment/payment.go~~ | | ~~P-15 残留~~ | ~~已修正 handler/http.go 注释与 source.go 旧路径标记,并同步移除过时测试样本。~~ | ~~server/handler/http.gointernal/logging/source.go~~ |
| ~~`paymentkit status.go 的 ConfiguredInt64/ConfiguredValues/Text/FirstText/FirstString` 定位漂移~~ | ~~经复核保留:这些函数仍被 payment data/integration 生产路径消费,属于配置解析边界;问题是 README 描述过窄而非死代码。评估完成保留2026-08-28。~~ | ~~internal/paymentkit/status.go:36-90~~ | | ~~`convertAuthority` 的 DeletedAt 值传递被 json:"-" 丢弃~~ | ~~已删除无效赋值,避免向已隐藏字段传递无效数据。~~ | ~~service/system/user_conversion.go~~ |
## 三、重复实现 / 双份维护 ## 三、重复实现残留(历轮收敛后的漏网项)
### 3.1 大块可消除
| # | 问题 | 位置 | 轮次 | | # | 问题 | 位置 | 轮次 |
|---|------|------|------| |---|------|------|------|
| ~~D-2(部分完成)~~ | ~~支付方式归一化骨架已统一到 `paymentkit.NormalizePaymentMethod`;各渠道状态词表与退款身份校验因语义不同保留。~~ | ~~integration/paymentinternal/paymentkit~~ | ~~部分完成~~ | | ~~D-2 遗漏 1-3~~ | ~~已统一 qq、wechat v3 与 payment data 的支付方式归一化到 `paymentkit.NormalizePaymentMethod`,消除点号/短横线/空格处理漂移。~~ | ~~integration/payment/qq.gowechat_v3.godata/payment/payment.go~~ | ~~已完成2026-08-28针对性测试通过~~ |
| ~~D-4~~ | ~~经复核样板虽多,但各 handler 的绑定方式、错误文案、响应 envelope 和鉴权上下文差异明显;抽统一门面会隐藏 transport 语义并扩大回归面,暂不改动。~~ | ~~server/handler/*~~ | ~~评估完成保留2026-08-28~~ | | ~~D-31~~ | ~~已复用 `paymentOrderFingerprint` 完成 BeforeCreate 前后不可变校验,保留同等字段覆盖范围并删除 10 个 canonical 局部变量。~~ | ~~biz/payment/payment.go~~ | ~~已完成2026-08-28针对性测试通过~~ |
| ~~D-6~~ | ~~经复核暂不改动:重复 helper 与 MQ/WebSocket 零消费者基础设施同属既定排除范围;上收 `internal/utils` 会扩大无消费者包的公共表面积。~~ | ~~emqx.go:226-260 vs websocket/server.go:189-240~~ | ~~评估完成保留2026-08-28~~ | | ~~D-32~~ | ~~已新增 `BodyPolicyUpload` 并让 AccessLog 按 routecatalog 决定上传请求体限额,删除硬编码 URL 后缀判断。~~ | ~~routecatalog/catalog.goserver/middleware/access_log.go~~ | ~~已完成2026-08-28针对性测试通过~~ |
### 3.2 配置/数据不变式双份维护 ## 四、文档漂移(第五轮新发现,低成本高收益)
| # | 问题 | 位置 | 轮次 | | # | 问题 | 位置 |
|---|------|------|------| |---|------|------|
| ~~D-9~~ | ~~`config.Store` 与 `runtimeconfig.Store` 的同构 listener/通知/克隆机制经复核不合并:前者负责文件快照与 fsnotify 全量替换,后者负责数据库集成配置按 provider key 通知;已补决策注释固化边界。~~ | ~~config/runtime.gointegration/runtimeconfig/store.go~~ | ~~评估完成保留分离2026-08-28~~ | | ~~F-21~~ | ~~已将 CLAUDE.md 正文同步为 Gin+手写 DTO+make generate 的现行仓库约定,移除 proto/AIP/fieldmask 时代描述。~~ | ~~CLAUDE.md~~ | ~~已完成2026-08-28~~ |
| ~~D-10~~ | ~~经复核保留三处清理:`persistConfigValues` 覆盖完整运行时保存,`persistDatabaseConfig` 覆盖初始化页的局部写入,`removeIntegrationConfigFromFile` 覆盖启动时对旧模板的兼容清理;入口不同且各自可独立触发,合并会削弱不落盘不变式。~~ | ~~internal/data/config_store.godata.go~~ | ~~评估完成保留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.mdinternal/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~~ | | ~~internal/modules/surface/surface.go~~ | ~~19~~ | ~~评估保留:隔离 routecatalog 到 pkg/module 的适配依赖。~~ |
| ~~D-15~~ | ~~defaults 合并逻辑三层三份。~~ **已修复:统一使用 `integrationbiz.MergeIntegrationDefaults`。** | ~~相关文件~~ | ~~已完成~~ | | ~~internal/data/provider/provider.go~~ | ~~8~~ | ~~评估保留:`Database` seam 被多个 data 子包独立依赖。~~ |
| ~~D-17 剩余~~ | ~~`values()` 与 `testRow()` 已收敛为共享读取逻辑,并保留启用状态差异。~~ | ~~data/payment/payment.go~~ | ~~已完成~~ | | ~~internal/data/data_scope_record.go~~ | ~~9~~ | ~~评估保留:隔离 data 回调写模型与 system 查询模型。~~ |
| ~~D-19评估后保留~~ | ~~payment 金额守恒校验四处重复。~~ **经复核保留四处输入字段与供应商容错语义不同强行合并会破坏分层biz 最终守恒校验作为跨边界不变式。** | ~~biz/payment 与各供应商适配器~~ | ~~不改动~~ | | ~~internal/biz/task/task_registry.go~~ | ~~11~~ | ~~评估保留biz 只暴露窄注册接口,避免 worker/pkg/task 类型穿透。~~ |
| ~~D-21~~ | ~~支付订单响应映射已统一复用 `paymentOrderResponse`。~~ | ~~service/payment/payment.go~~ | ~~已完成~~ | | ~~internal/service/dto/{email,authentication,permission,system_init}.go~~ | ~~7/8/12/13~~ | ~~评估保留:按 transport 契约分文件,合并只减少文件数。~~ |
| ~~D-22~~ | ~~同 2.3,已改用 `paymentkit.ContainsFold`。~~ | ~~data/payment/payment.go~~ | ~~已完成~~ | | ~~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.godata_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.govendor.go~~ | ~~已完成2026-08-28~~ |
## 五、过分拆分 / 文件组织 - **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 接口嵌入是组合窄能力
**根因模式三条**【三轮】:①零逻辑 usecase 壳wire 强制每域一个构造器放大);②"每资源 N 文件"机械切分;③为 import 美观引入的中间缝合包/门面。 - **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 属既定排除
| ~~S-1第一批~~ | ~~已合并 actor/data-scope 上下文载荷文件;复核剩余微文件均对应独立职责,不再机械归并。~~ | ~~internal/biz/system/context.go~~ | ~~部分完成2026-08-28~~ | - **X-1~X-4/X-6~X-8/X-10/X-11**:路由双声明/集成配置双通道/热重载分工watchLoop 发布快照 vs reloadConfig 重建基础设施,合并需重做锁与退休策略)等均完成影响分析保留
| ~~S-2~~ | ~~SystemConfigService 已合并为单文件。~~ | ~~internal/service/system/system.go~~ | ~~已完成~~ | - **C-2/C-4/C-6/C-8**loadUser/loadUsers 查询策略不同;*Data nil 防御覆盖测试替身边界媒体三重限制各守一层PaymentLogger 是审计替换 seam
| ~~S-3第一批~~ | ~~routes.go 已改为有序注册表;各领域路由文件承担独立注册边界,复核后保留。~~ | ~~internal/server/router/routes.go~~ | ~~部分完成2026-08-28~~ | - **L-7 复核**data 层 Table() 已全 PO 化(仅 2 处导出动态表名合理保留saveRelations 只做 DO→PO 转换
| ~~S-4~~ | ~~四个 data 子包的 provider 文件已合并。~~ | ~~internal/data/*/provider.go~~ | ~~已完成~~ | - **质量标杆**第五轮正面确认payment 幂等指纹+回调强制平台查单、退款 lease 语义、task_scheduler 锁序、task_executor SSRF 拨号防护+orphan 跟踪、auth singleflightcontext.WithoutCancel 隔离取消传染、data_scope 回调注入、email CRLF 清洗、ListTasks(0,0) 全量语义、新增缝PaymentAdapterFactory/MergeRuntimeConfig/deletePrefixViaList/AST 缓存/systeminfo均内聚无越界
| ~~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.goserver/handler/http.goserver/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.gobiz/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.godaily_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-287data/config_store.go:124-252~~ | ~~评估完成保留分离2026-08-28~~ |
| ~~X-10~~ | ~~经复核保留:新增渠道需同时声明 biz provider、adapter 工厂和配置元数据三处分别属于领域常量、I/O 实现和管理面契约,自动化注册会牺牲显式校验。~~ | ~~biz/paymentintegration/paymentbiz/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 中间件质量**:有据可依
- **新增缝质量(第四轮验证)**PaymentAdapterFactorybiz 接口+integration 实现+wire 绑定单向无环、MergeRuntimeConfig单点三调用、deletePrefixViaList函数式注入失败关闭正确、AST 缓存size+mtime 失效、systeminfointegration 定位正确)——均内聚、依赖最小、无越界 import
## 处置建议(按优先级) ## 处置建议(按优先级)
1. **正确性缺陷与安全边界**W-1W-6 已完成并经回归验证。 1. **立即处理 V-1**(泄漏的数据库密码)→ **V-2/V-3**(审计敏感数据落地防护对称化)
2. **死代码与分层整改**:已完成可证明无消费者项;其余公开契约或零消费者基础设施均已完成影响分析并记录保留理由。 2. **V-4/V-9**(热重载漂移文档化、软删语义逐项确认+补测试)
3. **结构性项目**D/F/S/P/L/C/X 剩余条目均已按依赖、并发、兼容性和 Wire 影响完成评估;暂无应在模板中强行落地的大改造。 3. **V-5/V-6/V-7/V-8/V-10 + 二节死代码**(小而具体的清理批次)
4. **三节 D-2 三处遗漏 + D-31/D-32**
5. **四节文档漂移群**(一次 README/CLAUDE.md 同步批)
6. **五节微文件合并**(最后)

View File

@ -15,9 +15,9 @@
- `routecatalog`:统一声明 HTTP 路由的公开性、操作审计、请求体策略和 API 元数据 - `routecatalog`:统一声明 HTTP 路由的公开性、操作审计、请求体策略和 API 元数据
- `server`Gin server 组合与生命周期;横切 HTTP 代码按子包维护: - `server`Gin server 组合与生命周期;横切 HTTP 代码按子包维护:
`server/handler`、`server/middleware`、`server/router`、`server/staticfiles` `server/handler`、`server/middleware`、`server/router`、`server/staticfiles`
通用响应和 Cookie 工具位于 `pkg/httpx` 通用响应和 Cookie 工具位于 `server/httpx`
- `service`:按 `system`、`payment`、`integration`、`task` 分模块的应用服务、 - `service`:按 `system`、`payment`、`integration`、`task` 分模块的应用服务、
DTO 与领域对象转换DTO 集中在 `service/dto`,根包是兼容旧调用方的类型/构造器门面 DTO 与领域对象转换DTO 集中在 `service/dto`,根包仅聚合 Wire ProviderSet
- `worker`:定时任务执行与调度 - `worker`:定时任务执行与调度
目录代表边界模块文件按资源命名。DTO、handler、中间件、路由和 HTTP 目录代表边界模块文件按资源命名。DTO、handler、中间件、路由和 HTTP

View File

@ -120,7 +120,7 @@ func (uc *IntegrationConfigUsecase) Save(ctx context.Context, config *Integratio
return err return err
} }
if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok { if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok {
values = mergeIntegrationDefaults(definition.Defaults, values) values = MergeIntegrationDefaults(definition.Defaults, values)
if config.Enabled { if config.Enabled {
if err := ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil { if err := ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
return err return err
@ -153,7 +153,7 @@ func (uc *IntegrationConfigUsecase) Test(ctx context.Context, config *Integratio
return err return err
} }
if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok { 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 { if err := ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
return err return err
@ -209,11 +209,7 @@ func DefaultIntegrationConfig(kind, provider string) map[string]any {
if !ok { if !ok {
return map[string]any{} return map[string]any{}
} }
return mergeIntegrationDefaults(definition.Defaults, nil) return MergeIntegrationDefaults(definition.Defaults, nil)
}
func mergeIntegrationDefaults(defaults, values map[string]any) map[string]any {
return MergeIntegrationDefaults(defaults, values)
} }
// MergeIntegrationDefaults returns a new map with stored values overriding defaults. // MergeIntegrationDefaults returns a new map with stored values overriding defaults.

View File

@ -89,7 +89,7 @@ var genericPaymentDefaults = map[string]any{
} }
func genericPaymentDefinition(provider, name, description string) IntegrationConfigDefinition { func genericPaymentDefinition(provider, name, description string) IntegrationConfigDefinition {
defaults := mergeIntegrationDefaults(genericPaymentDefaults, nil) defaults := MergeIntegrationDefaults(genericPaymentDefaults, nil)
fields := append([]IntegrationConfigField(nil), genericPaymentFields...) fields := append([]IntegrationConfigField(nil), genericPaymentFields...)
return paymentDefinition(provider, name, description, defaults, fields...) return paymentDefinition(provider, name, description, defaults, fields...)
} }

View File

@ -349,15 +349,6 @@ func (r *PaymentFulfillmentRegistry) Handler(kind string) PaymentFulfillmentHand
return r.handlers[kind] 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 { type PaymentUsecase struct {
repo PaymentRepo repo PaymentRepo
orders PaymentOrderRepo 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 { func (uc *PaymentUsecase) RegisterBusinessModule(module PaymentBusinessModule) error {
if module == nil { if module == nil {
return errors.New("支付业务模块为空") return errors.New("支付业务模块为空")
@ -464,14 +449,11 @@ func (uc *PaymentUsecase) Create(ctx context.Context, req *PaymentRequest) (*Pay
if req.OriginalAmount < req.Amount { if req.OriginalAmount < req.Amount {
return nil, errors.New("原始金额不能小于支付订单金额") 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) canonicalExtra, err := json.Marshal(req.Extra)
if err != nil { if err != nil {
return nil, fmt.Errorf("支付扩展参数无效: %w", err) return nil, fmt.Errorf("支付扩展参数无效: %w", err)
} }
canonicalFingerprint := paymentOrderFingerprint(req, canonicalExtra)
if err := uc.hooks.BeforeCreate(ctx, req); err != nil { if err := uc.hooks.BeforeCreate(ctx, req); err != nil {
return nil, err return nil, err
} }
@ -479,7 +461,7 @@ func (uc *PaymentUsecase) Create(ctx context.Context, req *PaymentRequest) (*Pay
if err != nil { if err != nil {
return nil, fmt.Errorf("支付扩展参数无效: %w", err) 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 nil, ErrPaymentOrderConflict
} }
return uc.createWithOrder(ctx, req) return uc.createWithOrder(ctx, req)
@ -926,10 +908,7 @@ func (uc *PaymentUsecase) refundWithOrder(ctx context.Context, provider, tradeNo
return nil, err return nil, err
} }
if !accepted { if !accepted {
if providerErr != nil { return nil, providerErr
return nil, providerErr
}
return nil, errors.New(message)
} }
result.Status = "refund_pending" result.Status = "refund_pending"
return attachOrderResult(result, order), nil return attachOrderResult(result, order), nil

View File

@ -182,15 +182,6 @@ func (r *PaymentOrderSourceRegistry) Source(kind string) PaymentOrderSource {
return r.sources[kind] 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) { func (r *PaymentOrderSourceRegistry) Unregister(kind string) {
if r == nil { if r == nil {
return return

View File

@ -9,5 +9,5 @@ var ProviderSet = wire.NewSet(
NewPaymentHooks, NewPaymentHooks,
NewPaymentOrderSourceRegistry, NewPaymentOrderSourceRegistry,
NewPaymentFulfillmentRegistry, NewPaymentFulfillmentRegistry,
NewConfiguredPaymentUsecase, NewPaymentUsecase,
) )

View File

@ -96,7 +96,7 @@ func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash
completed, err := uc.FindCompletedSession(ctx, userID, strings.ToLower(hash)) completed, err := uc.FindCompletedSession(ctx, userID, strings.ToLower(hash))
if err == nil && completed.MediaID != 0 { if err == nil && completed.MediaID != 0 {
if media, findErr := uc.FindMedia(ctx, completed.MediaID); findErr == nil { 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 { if createErr := uc.CreateMedia(ctx, copy); createErr == nil {
return nil, copy, nil, 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 { if err = uc.CreateMedia(ctx, media); err != nil {
return fail(err) 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.DeleteChunks(ctx, uploadID)
_ = uc.files.DeletePrefix(ctx, uc.chunkPrefix(uploadID)) _ = uc.files.DeletePrefix(ctx, uc.chunkPrefix(uploadID))
return media, nil return media, nil

View File

@ -270,6 +270,9 @@ func (r *Store) watchLoop(watcher *fsnotify.Watcher, stop <-chan struct{}, done
// take down a running process or publish invalid state. // take down a running process or publish invalid state.
continue 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)) r.Replace(MergeRuntimeConfig(r.Snapshot(), next))
case _, ok := <-errors: case _, ok := <-errors:
// fsnotify errors are intentionally non-fatal; the watcher remains // fsnotify errors are intentionally non-fatal; the watcher remains

View File

@ -122,6 +122,8 @@ func (d *Data) removeIntegrationConfigFromFile() error {
} }
func (d *Data) reloadConfig(ctx context.Context) 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() d.configMu.Lock()
defer d.configMu.Unlock() defer d.configMu.Unlock()
configPath := d.runtime.ConfigPath() configPath := d.runtime.ConfigPath()

View File

@ -411,7 +411,7 @@ func paymentCreateRequiresNotifyURL(provider string, extra, config map[string]an
if value == "" { if value == "" {
value = paymentkit.FirstText(config, keys...) value = paymentkit.FirstText(config, keys...)
} }
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) normalized := paymentkit.NormalizePaymentMethod(value)
switch provider { switch provider {
case bizpayment.PaymentAlipay, bizpayment.PaymentAlipayV3: case bizpayment.PaymentAlipay, bizpayment.PaymentAlipayV3:
return !paymentkit.ContainsFold([]string{"pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face"}, normalized) return !paymentkit.ContainsFold([]string{"pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face"}, normalized)

View File

@ -66,3 +66,27 @@ func TestParameterRepositoryKeepsReferenceEmptyQuerySemantics(t *testing.T) {
t.Fatalf("empty bulk delete removed rows: count=%d", count) t.Fatalf("empty bulk delete removed rows: count=%d", count)
} }
} }
func TestParameterUpdateDoesNotReviveSoftDeletedRow(t *testing.T) {
data := newTransactionTestData(t)
repo := &parameterRepo{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(&parameterPO{}, 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)
}
}

View File

@ -19,7 +19,7 @@ lifecycles. Their constructors are exposed through `ProviderSet`; `cmd` binds
the `cache.RedisProvider` implementation to the shared `data.Data` container. the `cache.RedisProvider` implementation to the shared `data.Data` container.
Stateless protocol helpers that do not own clients live in 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. light, while provider adapters remain here.
## Shared WebSocket and MQ APIs ## Shared WebSocket and MQ APIs

View File

@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
bizpayment "kra/internal/biz/payment" bizpayment "kra/internal/biz/payment"
"kra/internal/paymentkit"
"strings" "strings"
"github.com/go-pay/gopay" "github.com/go-pay/gopay"
@ -126,7 +127,7 @@ func qqCreateMethod(extra, config map[string]any) (string, error) {
if value == "" { if value == "" {
value = firstAny(config, "trade_type", "pay_type", "method", "pay_method") 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 { switch normalized {
case "", "NATIVE", "QR", "QRCODE": case "", "NATIVE", "QR", "QRCODE":
return gopayQQ.TradeType_Native, nil return gopayQQ.TradeType_Native, nil

View File

@ -9,6 +9,7 @@ import (
"errors" "errors"
"fmt" "fmt"
bizpayment "kra/internal/biz/payment" bizpayment "kra/internal/biz/payment"
"kra/internal/paymentkit"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -120,7 +121,7 @@ func (a *wechatV3Adapter) Create(ctx context.Context, req *bizpayment.PaymentReq
} }
func normalizeWechatV3TradeType(value string) string { 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) { func wechatV3CodePayResult(tradeNo string, rsp *gopayWechatV3.CodePayRsp) (*bizpayment.PaymentResult, error) {

View File

@ -71,14 +71,10 @@ func skipStackFile(filename string) bool {
"/go.uber.org/", "/go.uber.org/",
"/gorm.io/", "/gorm.io/",
"/internal/logging/", "/internal/logging/",
"/internal/transport/middleware/",
"/internal/transport/router/",
"/internal/server/handler/", "/internal/server/handler/",
"/internal/server/middleware/", "/internal/server/middleware/",
"/internal/server/router/", "/internal/server/router/",
"/internal/server/httpx/", "/internal/server/httpx/",
"/internal/server/middleware_",
"/internal/server/route_",
} { } {
if strings.Contains(normalized, marker) { if strings.Contains(normalized, marker) {
return true return true

View File

@ -144,10 +144,7 @@ func TestSkipStackFileRecognizesServerTransportPackages(t *testing.T) {
{name: "middleware", path: `D:\workspace\app\system\internal\server\middleware\auth.go`, want: true}, {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: "router", path: `/workspace/internal/server/router/user.go`, want: true},
{name: "http helper", path: `/workspace/internal/server/httpx/response.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: "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 { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {

View File

@ -13,6 +13,7 @@ const (
BodyPolicyIntegrationConfig RouteBodyPolicy = "integration_config" BodyPolicyIntegrationConfig RouteBodyPolicy = "integration_config"
BodyPolicyPaymentConfig RouteBodyPolicy = "payment_config" BodyPolicyPaymentConfig RouteBodyPolicy = "payment_config"
BodyPolicyPaymentCallback RouteBodyPolicy = "payment_callback" BodyPolicyPaymentCallback RouteBodyPolicy = "payment_callback"
BodyPolicyUpload RouteBodyPolicy = "upload"
) )
// Descriptor is the shared HTTP contract consumed by service bootstrap, // 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/getFileList": {group: "文件上传与下载", description: "获取上传文件列表"},
"POST /fileUploadAndDownload/importURL": {group: "文件上传与下载", description: "导入URL"}, "POST /fileUploadAndDownload/importURL": {group: "文件上传与下载", description: "导入URL"},
"POST /fileUploadAndDownload/listOssFiles": {group: "文件上传与下载", description: "获取对象存储文件列表"}, "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 /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}, "POST /integration/configs/:kind/:provider/test": {group: "集成配置", description: "测试通信集成连接", audit: true, bodyPolicy: BodyPolicyPaymentConfig},
"POST /jwt/jsonInBlacklist": {group: "jwt", description: "jwt加入黑名单(退出,必选)"}, "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/complete": {group: "媒体上传", description: "完成大文件上传"},
"POST /mediaUpload/init": {group: "媒体上传", description: "初始化大文件上传"}, "POST /mediaUpload/init": {group: "媒体上传", description: "初始化大文件上传"},
"POST /menu/addBaseMenu": {group: "菜单", description: "新增菜单", audit: true}, "POST /menu/addBaseMenu": {group: "菜单", description: "新增菜单", audit: true},

View File

@ -6,7 +6,7 @@ grouped by role:
- `handler/`: resource handlers and HTTP boundary validation - `handler/`: resource handlers and HTTP boundary validation
- `middleware/`: request metadata, auth, access control, audit, recovery, CORS - `middleware/`: request metadata, auth, access control, audit, recovery, CORS
- `router/`: resource route registration and the system route registrar - `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 and middleware
- `staticfiles/`: local upload storage route registration and file serving - `staticfiles/`: local upload storage route registration and file serving

View File

@ -192,7 +192,7 @@ func (h *API) ApplySync(c *gin.Context) {
return return
} }
if err := h.service.ApplyAPISyncRequest(c.Request.Context(), &req); err != nil { if err := h.service.ApplyAPISyncRequest(c.Request.Context(), &req); err != nil {
Fail(c, "忽略失败") Fail(c, "同步失败")
return return
} }
OK(c) OK(c)

View File

@ -138,7 +138,7 @@ func (h *Audit) DataAccess(c *gin.Context) {
} }
items, total, err := h.service.DataAccessRequest(c.Request.Context(), &req) items, total, err := h.service.DataAccessRequest(c.Request.Context(), &req)
if err != nil { if err != nil {
Fail(c, "获取失败:"+err.Error()) Fail(c, "获取失败")
return return
} }
Write(c, CodeSuccess, PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功") 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 return
} }
if err := h.service.DeleteDataAccess(c.Request.Context(), req.IDs); err != nil { if err := h.service.DeleteDataAccess(c.Request.Context(), req.IDs); err != nil {
Fail(c, "删除失败:"+err.Error()) Fail(c, "删除失败")
return return
} }
Write(c, CodeSuccess, gin.H{}, "删除成功") Write(c, CodeSuccess, gin.H{}, "删除成功")
@ -238,7 +238,7 @@ func (h *Audit) DeleteError(c *gin.Context) {
return return
} }
if err := h.service.DeleteErrors(c.Request.Context(), []uint{id}); err != nil { if err := h.service.DeleteErrors(c.Request.Context(), []uint{id}); err != nil {
Fail(c, "删除失败:"+err.Error()) Fail(c, "删除失败")
return return
} }
Write(c, CodeSuccess, gin.H{}, "删除成功") Write(c, CodeSuccess, gin.H{}, "删除成功")
@ -250,7 +250,7 @@ func (h *Audit) DeleteErrors(c *gin.Context) {
return return
} }
if err := h.service.DeleteErrors(c.Request.Context(), ids); err != nil { if err := h.service.DeleteErrors(c.Request.Context(), ids); err != nil {
Fail(c, "批量删除失败:"+err.Error()) Fail(c, "批量删除失败")
return return
} }
Write(c, CodeSuccess, gin.H{}, "批量删除成功") Write(c, CodeSuccess, gin.H{}, "批量删除成功")
@ -262,7 +262,7 @@ func (h *Audit) UpdateError(c *gin.Context) {
return return
} }
if err := h.service.UpdateErrorRequest(c.Request.Context(), &req); err != nil { if err := h.service.UpdateErrorRequest(c.Request.Context(), &req); err != nil {
Fail(c, "更新失败:"+err.Error()) Fail(c, "更新失败")
return return
} }
Write(c, CodeSuccess, gin.H{}, "更新成功") 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) item, err := h.service.Error(c.Request.Context(), id)
if err != nil { if err != nil {
Fail(c, "查询失败:"+err.Error()) Fail(c, "查询失败")
return return
} }
OKWithData(c, item) 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) items, total, err := h.service.ErrorsFilter(c.Request.Context(), p, size, c.Query("form"), c.Query("info"), createdAtRange)
if err != nil { if err != nil {
Fail(c, "获取失败:"+err.Error()) Fail(c, "获取失败")
return return
} }
Write(c, CodeSuccess, PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功") Write(c, CodeSuccess, PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
@ -309,7 +309,7 @@ func (h *Audit) CreateError(c *gin.Context) {
return return
} }
if err := h.recorder.CreateErrorMutationRequest(c.Request.Context(), &req); err != nil { if err := h.recorder.CreateErrorMutationRequest(c.Request.Context(), &req); err != nil {
Fail(c, "创建失败:"+err.Error()) Fail(c, "创建失败")
return return
} }
Write(c, CodeSuccess, gin.H{}, "创建成功") Write(c, CodeSuccess, gin.H{}, "创建成功")

View File

@ -1,5 +1,5 @@
// This file is the handler package's single vocabulary for HTTP replies: the // 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. // handlers name one package instead of two.
package handler package handler

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 && isMediaUploadRoute(c.FullPath()) mediaUpload := multipart && 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 {
@ -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 { func paymentCallbackProvider(path string) string {
parts := strings.Split(strings.Trim(path, "/"), "/") parts := strings.Split(strings.Trim(path, "/"), "/")
for index := 0; index+2 < len(parts); index++ { for index := 0; index+2 < len(parts); index++ {

View File

@ -80,7 +80,7 @@ func OperationAudit(runtime *config.Store, recorder *systemservice.AuditRecorder
responseBody := "" responseBody := ""
if value, ok := c.Get(ctxRespBufferKey); ok { if value, ok := c.Get(ctxRespBufferKey); ok {
if body, bok := value.(*bytes.Buffer); bok { 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 { if isDownloadResponse(c) && len(responseBody) > maxBytes {

View File

@ -9,7 +9,7 @@ import (
) )
func TestPaymentIntegrationSecretsAreRedacted(t *testing.T) { 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) redacted := redactJSON(raw, "application/json", 4096)
var payload struct { var payload struct {
Config map[string]string `json:"config"` Config map[string]string `json:"config"`
@ -17,7 +17,7 @@ func TestPaymentIntegrationSecretsAreRedacted(t *testing.T) {
if err := json.Unmarshal([]byte(redacted), &payload); err != nil { if err := json.Unmarshal([]byte(redacted), &payload); err != nil {
t.Fatalf("decode redacted payload: %v", err) 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] != "***" { if payload.Config[key] != "***" {
t.Fatalf("payment secret %q was not redacted: %s", key, redacted) 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 { if routecatalog.BodyPolicyFor("PUT", "/api/integration/configs/mq/emqx") == routecatalog.BodyPolicyPaymentConfig {
t.Fatal("non-payment integration was treated as payment configuration") 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")
}
} }

View File

@ -19,8 +19,9 @@ var sensitivePayloadKeys = map[string]struct{}{
"password": {}, "newpassword": {}, "oldpassword": {}, "confirmpassword": {}, "password": {}, "newpassword": {}, "oldpassword": {}, "confirmpassword": {},
"passwd": {}, "pwd": {}, "token": {}, "accesstoken": {}, "refreshtoken": {}, "passwd": {}, "pwd": {}, "token": {}, "accesstoken": {}, "refreshtoken": {},
"secret": {}, "clientsecret": {}, "apikey": {}, "privatekey": {}, "idcard": {}, "secret": {}, "clientsecret": {}, "apikey": {}, "privatekey": {}, "idcard": {},
"key": {}, "signingkey": {}, "secretkey": {},
"appkey": {}, "mchkey": {}, "apiv3key": {}, "clientcert": {}, "clientkey": {}, "appkey": {}, "mchkey": {}, "apiv3key": {}, "clientcert": {}, "clientkey": {},
"platformcert": {}, "platformserialno": {}, "credentialcode": {}, "certfile": {}, "platformcert": {}, "platformserialno": {}, "publiccert": {}, "credentialcode": {}, "certfile": {},
"keyfile": {}, "publickey": {}, "rootcert": {}, "appcert": {}, "webhookid": {}, "keyfile": {}, "publickey": {}, "rootcert": {}, "appcert": {}, "webhookid": {},
"authorization": {}, "authorization": {},
} }

View File

@ -2,7 +2,7 @@
`service` adapts HTTP-facing DTOs to business usecases and owns application `service` adapts HTTP-facing DTOs to business usecases and owns application
orchestration. Request/response/filter contracts live in `dto/`; the root 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`: 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 both service modules and HTTP middleware use it; it does not depend on a
business usecase or DTO. business usecase or DTO.
Each module owns its Wire `ProviderSet`. The root `service` package re-exports Each module owns its Wire `ProviderSet`; the root package only composes those
the existing service types and constructors through aliases and thin wrappers, so handlers, middleware sets and does not re-export service types or constructors.
and generated Wire code can migrate independently without a flag day.
Within `system/`, larger cross-cutting resources stay in the same package while 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 being split by concern (`audit.go` and its error/log companions, `media.go` and

View File

@ -10,7 +10,7 @@ func convertAuthority(value system.Authority) *dto.AuthorityResponse {
if value.Menus != nil { if value.Menus != nil {
menus = menuResponses(value.Menus) 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 { func convertUser(user *system.User) *dto.UserResponse {
var authorities []*dto.AuthorityResponse var authorities []*dto.AuthorityResponse

View File

@ -7,8 +7,9 @@
- `database`:跨模块共享的 GORM 支持、分页和迁移执行器 - `database`:跨模块共享的 GORM 支持、分页和迁移执行器
- `module`:模块迁移、后台元数据和路由注册协议 - `module`:模块迁移、后台元数据和路由注册协议
- `task`:跨模块共享的进程内任务注册表和贡献协议 - `task`:跨模块共享的进程内任务注册表和贡献协议
- `logging`:跨 app 复用的结构化日志能力 - `database`、`module`、`task`:跨模块基础设施与稳定协议
- `httpx`:跨 HTTP 模块共享的 JSON 响应结构、分页结构和状态码
- `paymentkit`支付金额、签名、状态、provider 标识和回调应答纯函数 应用级 logging、HTTP response、paymentkit 等实现位于 `internal/`,不属于
可复用 pkg 公共层。
`pkg` 只能提供机制和稳定协议,不能引用任何 `internal` 业务包。数据库配置加载、 `pkg` 只能提供机制和稳定协议,不能引用任何 `internal` 业务包。数据库配置加载、
系统集成配置、系统表和 provider 生命周期仍由 `internal` 负责。 系统集成配置、系统表和 provider 生命周期仍由 `internal` 负责。