优化结构

This commit is contained in:
Yvan 2026-08-28 10:21:59 +08:00
parent a16d362bb5
commit fec3820464
19 changed files with 138 additions and 136 deletions

View File

@ -11,12 +11,12 @@
| # | 问题 | 位置 | 说明 | | # | 问题 | 位置 | 说明 |
|---|------|------|------| |---|------|------|------|
| W-1 | **payment 空注册表导致应用必然启动失败(高危,阻塞)**wire 装配链创建空注册表(`NewPaymentOrderSourceRegistry()`/`NewPaymentFulfillmentRegistry()` 返回空 map后仅流向 `NewConfiguredPaymentUsecase` 的 fail-fast 检查;`RegisterBusinessModule` 全库零调用、`PaymentBusinessModule` 无任何生产实现 → sources/fulfillments 恒为空 → `NewConfiguredPaymentUsecase` 必报"支付业务订单来源未注册"→ wireApp 失败应用无法启动。biz.go 无条件包含 payment.ProviderSet无跳过路径。task 域有 `taskRegistry()` 自定义 provider 做装配前注册payment 域缺等价机制 | cmd/wire_gen.go:114-120internal/biz/payment/payment.go:390-398,400-412biz/payment/payment_order.go:144-152接口无实现 | R-9 的 fail-fast 机制本身正确落地,但与"仓库内零业务模块"叠加产生阻塞。修法:装配前注册机制(仿 taskRegistry或默认业务模块或允许显式禁用 | | ~~W-1~~ | ~~payment 空注册表导致应用启动失败。~~ **已修复:模板允许无业务模块启动,调用未配置能力时返回明确错误。** | ~~原位置~~ | ~~已完成~~ |
| W-2 | **CompleteUpload 吞掉 ListChunks 底层错误并误置会话失败**`err != nil || len(chunks) != ChunkTotal` 合并判定DB 瞬断被误报为"分片不全",且 fail() 将会话置 failed → 用户必须重建会话全量重传。直接违背 D-16 修复意图(仓储层已正确透传,仅此处用例层吞掉) | internal/biz/system/media_upload.go:208-211 | 修法:拆开 err 与数量不足两个分支分别返回 | | ~~W-2~~ | ~~CompleteUpload 吞掉 ListChunks 底层错误。~~ **已修复:底层错误与分片数量不足已拆分处理。** | ~~internal/biz/system/media_upload.go~~ | ~~已完成~~ |
| W-3 | MergeRuntimeConfig 嵌套子节合并缺口:仅当 `next.Data == nil` 才补 current.Datanext.Data 非 nil 但子节Database 等)为 nil 时静默丢失watchLoop 无 reloadConfig 那样的后置校验Admin 子节同理 | internal/config/clone.go:29-40config/runtime.go:271 | 建议补"next.Data 非 nil 但子节 nil"回归测试并做嵌套合并 | | ~~W-3~~ | ~~MergeRuntimeConfig 嵌套子节合并缺口。~~ **已修复并增加回归测试。** | ~~internal/config/clone.goruntime_test.go~~ | ~~已完成~~ |
| W-4 | 退款状态词表两处语义边角:① `NormalizeStatus` 将裸词 "REFUND" 归 failed——微信 v2 已退款订单在 vendor 查询回退路径会被归为失败,与 `PaymentStatusRefunded` 语义冲突;② `NormalizeRefundStatus` 成功词表缺 "TRADE_SUCCESS"(支付宝风格),会触发保守失败关闭 | internal/paymentkit/status.go:12,16,25 | 低危但需知晓/修正词表 | | ~~W-4~~ | ~~退款状态词表语义边角。~~ **已修复并补充测试。** | ~~internal/paymentkit/status.go~~ | ~~已完成~~ |
| W-5 | 公告/参数/版本列表无 ORDER BY统一接入 listRows 后这三个列表无排序LIMIT/OFFSET 翻页顺序不稳定MySQL/PG 均不保证) | data/system/announcement.go:74、parameter.go:73、version.go:67 | 收敛时遗留;补 `id desc``created_at desc` | | ~~W-5~~ | ~~公告/参数/版本列表无 ORDER BY。~~ **已修复:统一按 `id desc` 排序。** | ~~data/system/*.go~~ | ~~已完成~~ |
| W-6 | 外部退款分支存在不可达死分支(复制未裁剪):`accepted := providerErr == nil` 后 `else if !accepted` 永不触发 | internal/biz/payment/payment.go:923-928 | 删除死分支 | | ~~W-6~~ | ~~外部退款分支存在不可达死分支。~~ **已修复。** | ~~internal/biz/payment/payment.go~~ | ~~已完成~~ |
## 二、死代码与零消费者机制 ## 二、死代码与零消费者机制
@ -24,10 +24,10 @@
| 层 | 死代码 | 位置 | | 层 | 死代码 | 位置 |
|----|--------|------| |----|--------|------|
| biz 接口+data 实现 | `RecordDataAccess` 整链(接口+实现,查询侧 ListDataAccess/DeleteDataAccess 是活的,仅写入侧死) | biz/system/audit.go:103data/system/data_access_log.go:24 | | ~~biz 接口+data 实现~~ | ~~`RecordDataAccess` 写入链已删除;查询侧仍保留。~~ | ~~相关文件~~ |
| service 包装 | security_session.go 14 个方法中 8 个死ActiveToken/LoginLocked/IncrementLoginFailure/LockLogin/ClearLoginState/IncrementLoginIP/UseMultipoint/RotateActiveTokenbiz 内部直调 usecase这些包装无人调用存活 6 个被 public/rate_limit 消费) | service/system/security_session.go:17-67 | | ~~service 包装~~ | ~~security_session.go 中 8 个无消费者透传方法已删除。~~ | ~~相关文件~~ |
| biz 注入面 | `RegisterBusinessModule`W-1 的成因之一,两阶段注册+回滚补偿零调用);`PaymentBusinessModule`/`PayInternal`/`RefundInternal`/`AuthorizeRefund` 接口面仍无生产实现biz 调用链真实存在,仅实现者缺——与 W-1 一并处理) | biz/payment/payment.go:400-412payment_order.go:139-152 | | biz 注入面 | `RegisterBusinessModule`W-1 的成因之一,两阶段注册+回滚补偿零调用);`PaymentBusinessModule`/`PayInternal`/`RefundInternal`/`AuthorizeRefund` 接口面仍无生产实现biz 调用链真实存在,仅实现者缺——与 W-1 一并处理) | biz/payment/payment.go:400-412payment_order.go:139-152 |
| dto 死字段 | GetAuthorityButtonsRequest.Selected输入被丢弃MenuResponse.Authorities 恒 nullDynamicMenuResponse.MenuButtons/Authorities 恒 nilSysBaseMenuID 输入侧两处被丢弃version 导出结构体零值噪声字段群ID:0/CreatedAt 零时间/authoritys:null | dto/permission.go:6、menu.go:19,24,109,126,128service/system/version.go:22-96 | | ~~dto 死字段(已确认项)~~ | ~~GetAuthorityButtonsRequest.Selected 已删除;其余字段因仍参与响应或兼容契约暂保留。~~ | ~~dto/permission.go~~ |
| 死分支 | export_excel.go 的 `case []byte` 在 data 层按列类型转换R-2 修复)后成为死分支 | service/system/export_excel.go:85-86 | | 死分支 | export_excel.go 的 `case []byte` 在 data 层按列类型转换R-2 修复)后成为死分支 | service/system/export_excel.go:85-86 |
### 2.2 mq / websocket 零消费者基础设施【既定排除范围,历轮明确不处理,现状保持】 ### 2.2 mq / websocket 零消费者基础设施【既定排除范围,历轮明确不处理,现状保持】
@ -44,7 +44,7 @@
| `paymentkit.XMLValues`/`XMLEncode` | internal/paymentkit/xml.go:16,41 | 仅测试调用,生产 XML 走 gopay 库 | | `paymentkit.XMLValues`/`XMLEncode` | internal/paymentkit/xml.go:16,41 | 仅测试调用,生产 XML 走 gopay 库 |
| `paymentkit.NestedString` | internal/paymentkit/json.go:18 | 生产+测试均零调用(旧 shim 删除后的孤儿) | | `paymentkit.NestedString` | internal/paymentkit/json.go:18 | 生产+测试均零调用(旧 shim 删除后的孤儿) |
| `logging.NewZapLogger` | internal/logging/zap.go:542 | 仅测试调用,生产用 NewReloadableZapLogger | | `logging.NewZapLogger` | internal/logging/zap.go:542 | 仅测试调用,生产用 NewReloadableZapLogger |
| `data/payment.contains` | data/payment/payment.go:520-527 | 与 paymentkit.ContainsFold 功能重复 | | ~~`data/payment.contains`~~ | ~~已改用 `paymentkit.ContainsFold`,本地实现已删除。~~ | ~~data/payment/payment.go~~ |
| `paymentkit status.go 的 ConfiguredInt64/ConfiguredValues/Text/FirstText/FirstString` 定位漂移 | internal/paymentkit/status.go:36-90 | 属"供应商配置解析"超出 README 声称范围(文档漂移,非死代码) | | `paymentkit status.go 的 ConfiguredInt64/ConfiguredValues/Text/FirstText/FirstString` 定位漂移 | internal/paymentkit/status.go:36-90 | 属"供应商配置解析"超出 README 声称范围(文档漂移,非死代码) |
## 三、重复实现 / 双份维护 ## 三、重复实现 / 双份维护
@ -53,7 +53,7 @@
| # | 问题 | 位置 | 轮次 | | # | 问题 | 位置 | 轮次 |
|---|------|------|------| |---|------|------|------|
| D-2 | payment 渠道适配器剩余重复(第一非空字符串/JSON 编码/双状态归一化已收敛):下单方式归一化骨架仍 9 份Replacer 归一化行逐字出现 9 次);退款身份校验 4 份同构;状态归一化 SDK 专属词表 7 个 normalize*State 与 paymentkit 通用归一化双轨维护(同一状态词需两族词表同步) | integration/payment/alipay.go:434-456、douyin.go:108-126、qq.go:124-144 等gopay_helpers.go:145-225 | 二轮(四轮部分收敛) | | ~~D-2部分完成~~ | ~~支付方式归一化骨架已统一到 `paymentkit.NormalizePaymentMethod`;各渠道状态词表与退款身份校验因语义不同保留。~~ | ~~integration/paymentinternal/paymentkit~~ | ~~部分完成~~ |
| D-4 | handler 四段式样板约 70 处ShouldBindJSON→Fail→service→Write | server/handler/* | 二轮 | | D-4 | handler 四段式样板约 70 处ShouldBindJSON→Fail→service→Write | server/handler/* | 二轮 |
| D-6 | mq/websocket 两包各写一套 map 解码 helper 且逐字符相同TestConfig 探测骨架三处同构【属 2.2 排除范围交叉项】 | emqx.go:226-260 vs websocket/server.go:189-240 | 二轮 | | D-6 | mq/websocket 两包各写一套 map 解码 helper 且逐字符相同TestConfig 探测骨架三处同构【属 2.2 排除范围交叉项】 | emqx.go:226-260 vs websocket/server.go:189-240 | 二轮 |
@ -69,11 +69,11 @@
| # | 问题 | 位置 | 轮次 | | # | 问题 | 位置 | 轮次 |
|---|------|------|------| |---|------|------|------|
| D-11 | authority 树构建算法两份【既定不采用,保留】 | biz authority.go:48-78 vs menu.go:80-99 | 一轮 | | D-11 | authority 树构建算法两份【既定不采用,保留】 | biz authority.go:48-78 vs menu.go:80-99 | 一轮 |
| D-15 | defaults 合并逻辑三层三份 | service/integration / biz/integration / data/integration/migrations.go:24-29 | 二轮 | | ~~D-15~~ | ~~defaults 合并逻辑三层三份。~~ **已修复:统一使用 `integrationbiz.MergeIntegrationDefaults`。** | ~~相关文件~~ | ~~已完成~~ |
| D-17 剩余 | CallbackFields 已复用 paymentkit见文末剩余 values() 与 testRow() 近重复 | data/payment/payment.go:31-50,273-292 | 二轮(四轮部分收敛) | | ~~D-17 剩余~~ | ~~`values()` 与 `testRow()` 已收敛为共享读取逻辑,并保留启用状态差异。~~ | ~~data/payment/payment.go~~ | ~~已完成~~ |
| D-19 | payment 金额守恒校验四处重复biz+vendor+wechat_v2+douyin | biz/payment/payment.go:1198-1211 等 | 二轮 | | ~~D-19评估后保留~~ | ~~payment 金额守恒校验四处重复。~~ **经复核保留四处输入字段与供应商容错语义不同强行合并会破坏分层biz 最终守恒校验作为跨边界不变式。** | ~~biz/payment 与各供应商适配器~~ | ~~不改动~~ |
| D-21 | service/payment 同文件两份 30 字段映射Order 方法内联映射与 paymentOrderResponse 重复同一张字段表 | service/payment/payment.go:14-34,65-82 | 四轮 | | ~~D-21~~ | ~~支付订单响应映射已统一复用 `paymentOrderResponse`。~~ | ~~service/payment/payment.go~~ | ~~已完成~~ |
| D-22 | data/payment 本地 contains 与 paymentkit.ContainsFold 重复(同 2.3 | data/payment/payment.go:520-527 | 四轮 | | ~~D-22~~ | ~~同 2.3,已改用 `paymentkit.ContainsFold`。~~ | ~~data/payment/payment.go~~ | ~~已完成~~ |
## 四、过度分层:转发门面 / 透传壳 / 回调穿透 ## 四、过度分层:转发门面 / 透传壳 / 回调穿透
@ -95,10 +95,10 @@
| # | 问题 | 位置 | 轮次 | | # | 问题 | 位置 | 轮次 |
|---|------|------|------| |---|------|------|------|
| S-1 | biz/system 33 个非测试文件17 个 <60 行微文件群仍在errors 14/cache 15/maintenance 19/actor 19/access_control 20/storage 26/data_scope 27 合计约 575 可归并 8-10 个文件 | biz/system/* | 三轮四轮复核未动 | | ~~S-1第一批~~ | ~~已合并 actor/data-scope 上下文载荷文件;其余微文件仍待按职责归并。~~ | ~~internal/biz/system/context.go~~ | ~~部分完成~~ |
| S-2 | SystemConfigService 一型仍四文件system.go(19)+system_config.go(26)+system_init.go(38)+system_info.go(23) | service/system/* | 三轮(四轮复核未动) | | ~~S-2~~ | ~~SystemConfigService 已合并为单文件。~~ | ~~internal/service/system/system.go~~ | ~~已完成~~ |
| S-3 | router 22 文件 464 行平均 21 行/文件routes.go 手工 21 连调(与 X-1 一并解决) | server/router/* | 三轮 | | ~~S-3第一批~~ | ~~routes.go 已改为有序注册表;各领域路由文件仍保留。~~ | ~~internal/server/router/routes.go~~ | ~~部分完成~~ |
| S-4 | provider.go+providers.go 双小文件模式 ×4 子包8 文件可并 4modules/surface 单函数包data/provider 单接口包data_scope_record.go 单行别名文件 | 各处 | 三轮 | | ~~S-4~~ | ~~四个 data 子包的 provider 文件已合并。~~ | ~~internal/data/*/provider.go~~ | ~~已完成~~ |
| S-5 | 巨微两极biz/payment/payment.go 1150 行 vs 同域微文件dto 超小文件 vs settings.go 170 行跨四域 | biz/payment、service/dto | 三轮 | | S-5 | 巨微两极biz/payment/payment.go 1150 行 vs 同域微文件dto 超小文件 vs settings.go 170 行跨四域 | biz/payment、service/dto | 三轮 |
| S-6 | dto 包组织混乱system.go 混装三域settings.go 横跨四域 | service/dto/system.go、settings.go | 一轮 | | S-6 | dto 包组织混乱system.go 混装三域settings.go 横跨四域 | service/dto/system.go、settings.go | 一轮 |
| S-7 | data 层组织纪律转换函数命名四种风格PO 分布无规则audit.go 名不副实runtime.go 拼盘 | data/system/* | 二轮 | | S-7 | data 层组织纪律转换函数命名四种风格PO 分布无规则audit.go 名不副实runtime.go 拼盘 | data/system/* | 二轮 |
@ -119,15 +119,15 @@
| P-11 | pkg/mq 去项目化kra- 前缀)【属 2.2 排除范围交叉项】 | pkg/mq | 一轮 | | P-11 | pkg/mq 去项目化kra- 前缀)【属 2.2 排除范围交叉项】 | pkg/mq | 一轮 |
| P-13 | pkg/module、pkg/task、pkg/database/migration 同层契约组维持现状 | pkg/* | 一轮 | | P-13 | pkg/module、pkg/task、pkg/database/migration 同层契约组维持现状 | pkg/* | 一轮 |
| P-14 | httpx 移动后业务语义未剥离CodePasswordChangeRequired=10001 与 x-token cookie 仍留在 internal/server/httpxSetTokenCookie 注释自称 "no KRA business dependency" 与语义不符 | internal/server/httpx/response.go:17,58-64 | 四轮P-1 移动残留) | | P-14 | httpx 移动后业务语义未剥离CodePasswordChangeRequired=10001 与 x-token cookie 仍留在 internal/server/httpxSetTokenCookie 注释自称 "no KRA business dependency" 与语义不符 | internal/server/httpx/response.go:17,58-64 | 四轮P-1 移动残留) |
| P-15 | 包移动注释漂移http.go:2、response.go:59 仍写 "pkg/httpx"zap.go:29 写 "pkg/logging"source.go:73 skipStackFile 仍是 "/pkg/logging/" 且缺 "/internal/logging/"logging 自身栈帧跳过标记失效——功能性缺口);:74-81 残留 4 个永不匹配的死标记transport 旧路径等) | server/handler/http.go:2、server/httpx/response.go:59、internal/logging/zap.go:29、source.go:73-81 | 四轮P-2 移动残留) | | ~~P-15~~ | ~~包移动注释与 logging 栈跳过标记已修正。~~ | ~~相关文件~~ | ~~已完成~~ |
| P-16 | CLAUDE.md 结构描述整体过时(描述 api/、internal/global/ 等不存在目录),与 AGENTS.md 不同步 | CLAUDE.md:9-17 | 四轮 | | P-16 | CLAUDE.md 结构描述整体过时(描述 api/、internal/global/ 等不存在目录),与 AGENTS.md 不同步 | CLAUDE.md:9-17 | 四轮 |
## 七、分层 / 职责违规 ## 七、分层 / 职责违规
| # | 问题 | 位置 | 轮次 | | # | 问题 | 位置 | 轮次 |
|---|------|------|------| |---|------|------|------|
| L-3 | biz DO 带 json 标签PaymentResult/PaymentTestResult死标签PaymentRequest指纹编码格式锚死 DObiz/integration Definition 家族充当前端契约 | biz/payment/payment.go:59-204biz/integration | 二轮 | | ~~L-3部分完成~~ | ~~PaymentResult/PaymentTestResult 的死 JSON 标签已移除PaymentRequest 字段与指纹语义保留Definition 家族因仍被 service 消费暂不迁移。~~ | ~~biz/payment/payment.gobiz/integration~~ | ~~部分完成~~ |
| L-4 | DO 兼过滤器API.OrderKey/Desc/StrictAll、SystemParameter/ExportTemplate 时间区间混入实体 | biz/system/api.go、parameter.go、export.go | 二轮 | | ~~L-4部分完成~~ | ~~`SystemParameter` 的查询字段与时间区间已拆为 `SystemParameterFilter`API/Export 过滤字段仍待独立迁移。~~ | ~~biz/service/data system parameter~~ | ~~部分完成~~ |
| L-5 | middleware 硬编码业务语义:中文消息黑名单判断审计(改文案即改审计行为);业务路径硬编码;支付回调专用逻辑内嵌通用中间件;限流策略内联 | server/middleware/* | 二轮 | | L-5 | middleware 硬编码业务语义:中文消息黑名单判断审计(改文案即改审计行为);业务路径硬编码;支付回调专用逻辑内嵌通用中间件;限流策略内联 | server/middleware/* | 二轮 |
| L-6 | biz 契约泄漏存储/表现原语QueryExport 返回 []map[string]anyexport DO 携带 SQL 片段UserOptions UI 形状AuthenticationResult 携带密码哈希 | biz/system/* | 二轮 | | L-6 | biz 契约泄漏存储/表现原语QueryExport 返回 []map[string]anyexport DO 携带 SQL 片段UserOptions UI 形状AuthenticationResult 携带密码哈希 | biz/system/* | 二轮 |
| L-7 | data 层纪律Table("字符串") 绕过 POsaveRelations 回写入参 DOOriginSetting 裸转换 | data/system/* | 二轮 | | L-7 | data 层纪律Table("字符串") 绕过 POsaveRelations 回写入参 DOOriginSetting 裸转换 | data/system/* | 二轮 |
@ -181,7 +181,6 @@
1. **先修 W-1 payment 启动阻塞**(应用当前无法启动,最高优先级)+ W-2/W-5用户可感知的正确性缺陷 1. **先修 W-1 payment 启动阻塞**(应用当前无法启动,最高优先级)+ W-2/W-5用户可感知的正确性缺陷
2. **清 2.1 死代码残留 + 2.3 新死代码**RecordDataAccess 链、security_session 8 方法、dto 死字段群、RegisterBusinessModule随 W-1 一并决策、CloneData/XML 系列等) 2. **清 2.1 死代码残留 + 2.3 新死代码**RecordDataAccess 链、security_session 8 方法、dto 死字段群、RegisterBusinessModule随 W-1 一并决策、CloneData/XML 系列等)
3. **补 W-3 嵌套合并回归 + W-4 词表修正 + P-15 移动残留清理**(小而具体) 3. **补 W-3 嵌套合并回归 + W-4 词表修正 + P-15 移动残留清理**(小而具体)
4. **继续 S 系列文件级减法**S-1/S-2/S-4 未动,零行为变更 4. **继续 S 系列文件级减法**S-1/S-3 仍有剩余微文件与路由文件可按职责整理
5. **D-2/D-19 payment 剩余重复 + L 系列归位**(独立批次) 5. **D-2/D-19 payment 剩余重复 + L 系列归位**(独立批次)
6. **X-1 路由单源化等大动作**(最后) 6. **X-1 路由单源化等大动作**(最后)

View File

@ -213,6 +213,11 @@ func DefaultIntegrationConfig(kind, provider string) map[string]any {
} }
func mergeIntegrationDefaults(defaults, values map[string]any) map[string]any { func mergeIntegrationDefaults(defaults, values map[string]any) map[string]any {
return MergeIntegrationDefaults(defaults, values)
}
// MergeIntegrationDefaults returns a new map with stored values overriding defaults.
func MergeIntegrationDefaults(defaults, values map[string]any) map[string]any {
out := make(map[string]any, len(defaults)+len(values)) out := make(map[string]any, len(defaults)+len(values))
for key, value := range defaults { for key, value := range defaults {
out[key] = value out[key] = value

View File

@ -158,51 +158,51 @@ type PaymentConfirmation struct {
} }
type PaymentResult struct { type PaymentResult struct {
Provider string `json:"provider"` Provider string
Status string `json:"status"` Status string
TradeNo string `json:"tradeNo"` TradeNo string
ProviderTradeNo string `json:"providerTradeNo"` ProviderTradeNo string
Amount int64 `json:"amount"` Amount int64
PayerPaidAmount int64 `json:"payerPaidAmount,omitempty"` PayerPaidAmount int64
CashPaidAmount int64 `json:"cashPaidAmount,omitempty"` CashPaidAmount int64
PointPaidAmount int64 `json:"pointPaidAmount,omitempty"` PointPaidAmount int64
DiscountAmount int64 `json:"discountAmount,omitempty"` DiscountAmount int64
ProviderDiscountAmount int64 `json:"providerDiscountAmount,omitempty"` ProviderDiscountAmount int64
MerchantDiscountAmount int64 `json:"merchantDiscountAmount,omitempty"` MerchantDiscountAmount int64
SettlementAmount int64 `json:"settlementAmount,omitempty"` SettlementAmount int64
Currency string `json:"currency"` Currency string
PayerCurrency string `json:"payerCurrency,omitempty"` PayerCurrency string
AmountBreakdownKnown bool `json:"amountBreakdownKnown"` AmountBreakdownKnown bool
Duplicate bool `json:"duplicate"` Duplicate bool
Payload json.RawMessage `json:"payload"` Payload json.RawMessage
OrderStatus string `json:"orderStatus,omitempty"` OrderStatus string
FulfillmentStatus string `json:"fulfillmentStatus,omitempty"` FulfillmentStatus string
RefundStatus string `json:"refundStatus,omitempty"` RefundStatus string
EventID string `json:"-"` EventID string
QueryID string `json:"-"` QueryID string
SuccessAck PaymentCallbackAck `json:"-"` SuccessAck PaymentCallbackAck
FailureAck PaymentCallbackAck `json:"-"` FailureAck PaymentCallbackAck
} }
// PaymentTestStage is one step of a provider connectivity test. A skipped // PaymentTestStage is one step of a provider connectivity test. A skipped
// query/refund is reported explicitly because a prepay flow cannot be marked // query/refund is reported explicitly because a prepay flow cannot be marked
// paid without a sandbox payer interaction. // paid without a sandbox payer interaction.
type PaymentTestStage struct { type PaymentTestStage struct {
Name string `json:"name"` Name string
Status string `json:"status"` Status string
Message string `json:"message,omitempty"` Message string
TradeNo string `json:"tradeNo,omitempty"` TradeNo string
Duration int64 `json:"durationMs,omitempty"` Duration int64
} }
type PaymentTestResult struct { type PaymentTestResult struct {
Provider string `json:"provider"` Provider string
TradeNo string `json:"tradeNo"` TradeNo string
Passed bool `json:"passed"` Passed bool
FullFlow bool `json:"fullFlow"` FullFlow bool
Mode string `json:"mode,omitempty"` Mode string
Stages []PaymentTestStage `json:"stages"` Stages []PaymentTestStage
Result *PaymentResult `json:"result,omitempty"` Result *PaymentResult
} }
type PaymentCallbackAck struct { type PaymentCallbackAck struct {

View File

@ -1,20 +0,0 @@
package system
import "context"
type AccessControlUsecase struct {
authorities AuthorityAccessRepo
apis APIRepo
}
func NewAccessControlUsecase(authorities AuthorityAccessRepo, apis APIRepo) *AccessControlUsecase {
return &AccessControlUsecase{authorities: authorities, apis: apis}
}
func (uc *AccessControlUsecase) Authorize(ctx context.Context, authorityID uint, path, method string) (bool, error) {
return uc.apis.Authorize(ctx, authorityID, path, method)
}
func (uc *AccessControlUsecase) ResolveDataScope(ctx context.Context, authorityID, userID uint) (DataScope, error) {
return uc.authorities.ResolveDataScope(ctx, authorityID, userID)
}

View File

@ -45,6 +45,23 @@ func NewAuthorityUsecase(repo AuthorityAccessRepo) *AuthorityUsecase {
return &AuthorityUsecase{AuthorityAccessRepo: repo} return &AuthorityUsecase{AuthorityAccessRepo: repo}
} }
type AccessControlUsecase struct {
authorities AuthorityAccessRepo
apis APIRepo
}
func NewAccessControlUsecase(authorities AuthorityAccessRepo, apis APIRepo) *AccessControlUsecase {
return &AccessControlUsecase{authorities: authorities, apis: apis}
}
func (uc *AccessControlUsecase) Authorize(ctx context.Context, authorityID uint, path, method string) (bool, error) {
return uc.apis.Authorize(ctx, authorityID, path, method)
}
func (uc *AccessControlUsecase) ResolveDataScope(ctx context.Context, authorityID, userID uint) (DataScope, error) {
return uc.authorities.ResolveDataScope(ctx, authorityID, userID)
}
func (uc *AuthorityUsecase) AuthorityTree(ctx context.Context) ([]*Authority, error) { func (uc *AuthorityUsecase) AuthorityTree(ctx context.Context) ([]*Authority, error) {
items, err := uc.ListAuthorities(ctx) items, err := uc.ListAuthorities(ctx)
if err != nil { if err != nil {

View File

@ -21,6 +21,25 @@ type MediaRepo interface {
UploadRepo UploadRepo
} }
type StoredFile struct {
Name string
Path string
URL string
Size int64
LastModified time.Time
ContentType string
}
// FileStorage owns the persistence boundary for uploaded files.
type FileStorage interface {
Put(context.Context, string, io.Reader) (*StoredFile, error)
Open(context.Context, string) (io.ReadCloser, error)
Delete(context.Context, string) error
Compose(context.Context, []string, string) (*StoredFile, string, error)
DeletePrefix(context.Context, string) error
List(context.Context, string, string, int) ([]*StoredFile, string, bool, error)
}
type MediaUsecase struct { type MediaUsecase struct {
MediaRepo MediaRepo
files FileStorage files FileStorage

View File

@ -6,13 +6,18 @@ import (
) )
type SystemParameter struct { type SystemParameter struct {
ID uint ID uint
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
Name string
Key string
Value string
Desc string
}
type SystemParameterFilter struct {
Name string Name string
Key string Key string
Value string
Desc string
StartCreatedAt *time.Time StartCreatedAt *time.Time
EndCreatedAt *time.Time EndCreatedAt *time.Time
} }
@ -23,7 +28,7 @@ type ParameterRepo interface {
DeleteParameters(context.Context, []string) error DeleteParameters(context.Context, []string) error
FindParameterByID(context.Context, string) (*SystemParameter, error) FindParameterByID(context.Context, string) (*SystemParameter, error)
FindParameterByKey(context.Context, string) (*SystemParameter, error) FindParameterByKey(context.Context, string) (*SystemParameter, error)
ListParameters(context.Context, int, int, *SystemParameter) ([]*SystemParameter, int64, error) ListParameters(context.Context, int, int, *SystemParameterFilter) ([]*SystemParameter, int64, error)
} }
type ParameterUsecase struct{ ParameterRepo } type ParameterUsecase struct{ ParameterRepo }

View File

@ -1,26 +0,0 @@
package system
import (
"context"
"io"
"time"
)
type StoredFile struct {
Name string
Path string
URL string
Size int64
LastModified time.Time
ContentType string
}
// FileStorage owns the persistence boundary for uploaded files.
type FileStorage interface {
Put(context.Context, string, io.Reader) (*StoredFile, error)
Open(context.Context, string) (io.ReadCloser, error)
Delete(context.Context, string) error
Compose(context.Context, []string, string) (*StoredFile, string, error)
DeletePrefix(context.Context, string) error
List(context.Context, string, string, int) ([]*StoredFile, string, bool, error)
}

View File

@ -3,6 +3,7 @@ package integration
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"reflect"
integrationbiz "kra/internal/biz/integration" integrationbiz "kra/internal/biz/integration"
"kra/pkg/database/migration" "kra/pkg/database/migration"
@ -66,13 +67,9 @@ func ensurePaymentIntegrationConfigs(db *gorm.DB) error {
return err return err
} }
values := integrationObject(json.RawMessage(row.Config)) values := integrationObject(json.RawMessage(row.Config))
changed := false merged := integrationbiz.MergeIntegrationDefaults(defaults, values)
for key, value := range defaults { changed := !reflect.DeepEqual(values, merged)
if _, exists := values[key]; !exists { values = merged
values[key] = value
changed = true
}
}
if changed { if changed {
encoded, marshalErr := json.Marshal(values) encoded, marshalErr := json.Marshal(values)
if marshalErr != nil { if marshalErr != nil {

View File

@ -57,7 +57,7 @@ func (r *parameterRepo) FindParameterByKey(ctx context.Context, key string) (*sy
} }
return parameterFromPO(po), nil return parameterFromPO(po), nil
} }
func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *system.SystemParameter) ([]*system.SystemParameter, int64, error) { func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *system.SystemParameterFilter) ([]*system.SystemParameter, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&parameterPO{}) db := r.data.DB().WithContext(ctx).Model(&parameterPO{})
if q != nil { if q != nil {
if q.StartCreatedAt != nil && q.EndCreatedAt != nil { if q.StartCreatedAt != nil && q.EndCreatedAt != nil {

View File

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
bizpayment "kra/internal/biz/payment" bizpayment "kra/internal/biz/payment"
"kra/internal/paymentkit"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -435,7 +436,7 @@ func alipayCreateMethod(extra, config map[string]any) (string, error) {
if value == "" { if value == "" {
value = firstAny(config, "method", "pay_method", "trade_type", "channel") value = firstAny(config, "method", "pay_method", "trade_type", "channel")
} }
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) normalized := paymentkit.NormalizePaymentMethod(value)
switch normalized { switch normalized {
case "", "create", "trade_create", "alipay_trade_create", "jsapi", "miniapp", "mini_program": case "", "create", "trade_create", "alipay_trade_create", "jsapi", "miniapp", "mini_program":
return "alipay.trade.create", nil return "alipay.trade.create", nil

View File

@ -12,6 +12,7 @@ import (
"fmt" "fmt"
"io" "io"
bizpayment "kra/internal/biz/payment" bizpayment "kra/internal/biz/payment"
"kra/internal/paymentkit"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
@ -207,7 +208,7 @@ func allinpayCreateMethod(extra, config map[string]any) (string, error) {
if value == "" { if value == "" {
value = firstAny(config, "method", "pay_method", "trade_type") value = firstAny(config, "method", "pay_method", "trade_type")
} }
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) normalized := paymentkit.NormalizePaymentMethod(value)
switch normalized { switch normalized {
case "", "pay", "unified", "unified_pay": case "", "pay", "unified", "unified_pay":
return "pay", nil return "pay", nil

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"
@ -110,7 +111,7 @@ func douyinCreateMethod(extra, config map[string]any) (string, error) {
if value == "" { if value == "" {
value = firstAny(config, "method", "pay_method", "trade_type", "pay_type", "channel") value = firstAny(config, "method", "pay_method", "trade_type", "pay_type", "channel")
} }
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) normalized := paymentkit.NormalizePaymentMethod(value)
switch normalized { switch normalized {
case "", "jsapi", "js_api", "mini", "mini_program", "miniprogram", "applet": case "", "jsapi", "js_api", "mini", "mini_program", "miniprogram", "applet":
return "jsapi", nil return "jsapi", nil

View File

@ -5,6 +5,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"
@ -71,7 +72,7 @@ func lakalaCreateMethod(extra, config map[string]any) string {
if method == "" { if method == "" {
method = firstAny(config, "method", "pay_method", "trade_type") method = firstAny(config, "method", "pay_method", "trade_type")
} }
return strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(method))) return paymentkit.NormalizePaymentMethod(method)
} }
func lakalaCreateResult(tradeNo string, rsp *lakala.PaymentRsp) (*bizpayment.PaymentResult, error) { func lakalaCreateResult(tradeNo string, rsp *lakala.PaymentRsp) (*bizpayment.PaymentResult, error) {

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
bizpayment "kra/internal/biz/payment" bizpayment "kra/internal/biz/payment"
"kra/internal/paymentkit"
"strings" "strings"
"time" "time"
@ -89,7 +90,7 @@ func saobeiCreateMethod(extra, config map[string]any) (string, error) {
if value == "" { if value == "" {
value = firstAny(config, "method", "pay_method", "trade_type") value = firstAny(config, "method", "pay_method", "trade_type")
} }
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) normalized := paymentkit.NormalizePaymentMethod(value)
switch normalized { switch normalized {
case "", "mini", "mini_pay", "miniapp", "mini_app", "mini_program", "miniprogram", "jsapi", "js_api": case "", "mini", "mini_pay", "miniapp", "mini_app", "mini_program", "miniprogram", "jsapi", "js_api":
return "mini", nil return "mini", nil

View File

@ -11,6 +11,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"
@ -180,7 +181,7 @@ func wechatV2CreateMethod(extra, config map[string]any) (string, error) {
if value == "" { if value == "" {
value = firstAny(config, "trade_type", "pay_type", "method", "pay_method", "channel") value = firstAny(config, "trade_type", "pay_type", "method", "pay_method", "channel")
} }
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) normalized := paymentkit.NormalizePaymentMethod(value)
switch normalized { switch normalized {
case "", "jsapi", "js_api", "mini", "miniapp", "mini_program", "miniprogram", "applet": case "", "jsapi", "js_api", "mini", "miniapp", "mini_program", "miniprogram", "applet":
return gopayWechat.TradeType_JsApi, nil return gopayWechat.TradeType_JsApi, nil

View File

@ -33,6 +33,12 @@ func NormalizeRefundStatus(value, fallback string) string {
} }
} }
// NormalizePaymentMethod canonicalizes provider method identifiers while
// leaving provider-specific classification to the integration adapter.
func NormalizePaymentMethod(value string) string {
return strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
}
func ConfiguredInt64(values map[string]any, key string, fallback int64) int64 { func ConfiguredInt64(values map[string]any, key string, fallback int64) int64 {
value, ok := values[key] value, ok := values[key]
if !ok { if !ok {

View File

@ -84,15 +84,9 @@ func (s *IntegrationConfigService) Delete(ctx context.Context, kind, provider st
} }
func mergeConfigJSON(defaults map[string]any, raw json.RawMessage) json.RawMessage { func mergeConfigJSON(defaults map[string]any, raw json.RawMessage) json.RawMessage {
values := make(map[string]any, len(defaults))
for key, value := range defaults {
values[key] = value
}
stored := map[string]any{} stored := map[string]any{}
_ = json.Unmarshal(raw, &stored) _ = json.Unmarshal(raw, &stored)
for key, value := range stored { values := integrationbiz.MergeIntegrationDefaults(defaults, stored)
values[key] = value
}
encoded, _ := json.Marshal(values) encoded, _ := json.Marshal(values)
return encoded return encoded
} }

View File

@ -24,12 +24,12 @@ func (s *ParameterService) UpdateParameterRequest(ctx context.Context, req *dto.
return s.UpdateParameter(ctx, parameterDomain(req)) return s.UpdateParameter(ctx, parameterDomain(req))
} }
func (s *ParameterService) ParametersFilter(ctx context.Context, page, size int, name, key string, start, end *time.Time) ([]*dto.SystemParameterResponse, int64, error) { func (s *ParameterService) ParametersFilter(ctx context.Context, page, size int, name, key string, start, end *time.Time) ([]*dto.SystemParameterResponse, int64, error) {
return s.Parameters(ctx, page, size, &system.SystemParameter{Name: name, Key: key, StartCreatedAt: start, EndCreatedAt: end}) return s.Parameters(ctx, page, size, &system.SystemParameterFilter{Name: name, Key: key, StartCreatedAt: start, EndCreatedAt: end})
} }
func parameterDTO(v *system.SystemParameter) *dto.SystemParameterResponse { func parameterDTO(v *system.SystemParameter) *dto.SystemParameterResponse {
return &dto.SystemParameterResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc} return &dto.SystemParameterResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}
} }
func (s *ParameterService) Parameters(ctx context.Context, page, size int, q *system.SystemParameter) ([]*dto.SystemParameterResponse, int64, error) { func (s *ParameterService) Parameters(ctx context.Context, page, size int, q *system.SystemParameterFilter) ([]*dto.SystemParameterResponse, int64, error) {
items, total, err := s.uc.ListParameters(ctx, page, size, q) items, total, err := s.uc.ListParameters(ctx, page, size, q)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err