diff --git a/docs/code-review-issues.md b/docs/code-review-issues.md index be8c7f6..7110345 100644 --- a/docs/code-review-issues.md +++ b/docs/code-review-issues.md @@ -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-120;internal/biz/payment/payment.go:390-398,400-412;biz/payment/payment_order.go:144-152(接口无实现) | R-9 的 fail-fast 机制本身正确落地,但与"仓库内零业务模块"叠加产生阻塞。修法:装配前注册机制(仿 taskRegistry)或默认业务模块或允许显式禁用 | -| W-2 | **CompleteUpload 吞掉 ListChunks 底层错误并误置会话失败**:`err != nil || len(chunks) != ChunkTotal` 合并判定,DB 瞬断被误报为"分片不全",且 fail() 将会话置 failed → 用户必须重建会话全量重传。直接违背 D-16 修复意图(仓储层已正确透传,仅此处用例层吞掉) | internal/biz/system/media_upload.go:208-211 | 修法:拆开 err 与数量不足两个分支分别返回 | -| W-3 | MergeRuntimeConfig 嵌套子节合并缺口:仅当 `next.Data == nil` 才补 current.Data;next.Data 非 nil 但子节(Database 等)为 nil 时静默丢失,watchLoop 无 reloadConfig 那样的后置校验;Admin 子节同理 | internal/config/clone.go:29-40;config/runtime.go:271 | 建议补"next.Data 非 nil 但子节 nil"回归测试并做嵌套合并 | -| W-4 | 退款状态词表两处语义边角:① `NormalizeStatus` 将裸词 "REFUND" 归 failed——微信 v2 已退款订单在 vendor 查询回退路径会被归为失败,与 `PaymentStatusRefunded` 语义冲突;② `NormalizeRefundStatus` 成功词表缺 "TRADE_SUCCESS"(支付宝风格),会触发保守失败关闭 | internal/paymentkit/status.go:12,16,25 | 低危但需知晓/修正词表 | -| 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-6 | 外部退款分支存在不可达死分支(复制未裁剪):`accepted := providerErr == nil` 后 `else if !accepted` 永不触发 | internal/biz/payment/payment.go:923-928 | 删除死分支 | +| ~~W-1~~ | ~~payment 空注册表导致应用启动失败。~~ **已修复:模板允许无业务模块启动,调用未配置能力时返回明确错误。** | ~~原位置~~ | ~~已完成~~ | +| ~~W-2~~ | ~~CompleteUpload 吞掉 ListChunks 底层错误。~~ **已修复:底层错误与分片数量不足已拆分处理。** | ~~internal/biz/system/media_upload.go~~ | ~~已完成~~ | +| ~~W-3~~ | ~~MergeRuntimeConfig 嵌套子节合并缺口。~~ **已修复并增加回归测试。** | ~~internal/config/clone.go;runtime_test.go~~ | ~~已完成~~ | +| ~~W-4~~ | ~~退款状态词表语义边角。~~ **已修复并补充测试。** | ~~internal/paymentkit/status.go~~ | ~~已完成~~ | +| ~~W-5~~ | ~~公告/参数/版本列表无 ORDER BY。~~ **已修复:统一按 `id desc` 排序。** | ~~data/system/*.go~~ | ~~已完成~~ | +| ~~W-6~~ | ~~外部退款分支存在不可达死分支。~~ **已修复。** | ~~internal/biz/payment/payment.go~~ | ~~已完成~~ | ## 二、死代码与零消费者机制 @@ -24,10 +24,10 @@ | 层 | 死代码 | 位置 | |----|--------|------| -| biz 接口+data 实现 | `RecordDataAccess` 整链(接口+实现,查询侧 ListDataAccess/DeleteDataAccess 是活的,仅写入侧死) | biz/system/audit.go:103;data/system/data_access_log.go:24 | -| service 包装 | security_session.go 14 个方法中 8 个死:ActiveToken/LoginLocked/IncrementLoginFailure/LockLogin/ClearLoginState/IncrementLoginIP/UseMultipoint/RotateActiveToken(biz 内部直调 usecase,这些包装无人调用;存活 6 个被 public/rate_limit 消费) | service/system/security_session.go:17-67 | +| ~~biz 接口+data 实现~~ | ~~`RecordDataAccess` 写入链已删除;查询侧仍保留。~~ | ~~相关文件~~ | +| ~~service 包装~~ | ~~security_session.go 中 8 个无消费者透传方法已删除。~~ | ~~相关文件~~ | | biz 注入面 | `RegisterBusinessModule`(W-1 的成因之一,两阶段注册+回滚补偿零调用);`PaymentBusinessModule`/`PayInternal`/`RefundInternal`/`AuthorizeRefund` 接口面仍无生产实现(biz 调用链真实存在,仅实现者缺——与 W-1 一并处理) | biz/payment/payment.go:400-412;payment_order.go:139-152 | -| dto 死字段 | GetAuthorityButtonsRequest.Selected(输入被丢弃);MenuResponse.Authorities 恒 null;DynamicMenuResponse.MenuButtons/Authorities 恒 nil;SysBaseMenuID 输入侧两处被丢弃;version 导出结构体零值噪声字段群(ID:0/CreatedAt 零时间/authoritys:null) | dto/permission.go:6、menu.go:19,24,109,126,128;service/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 | ### 2.2 mq / websocket 零消费者基础设施【既定排除范围,历轮明确不处理,现状保持】 @@ -44,7 +44,7 @@ | `paymentkit.XMLValues`/`XMLEncode` | internal/paymentkit/xml.go:16,41 | 仅测试调用,生产 XML 走 gopay 库 | | `paymentkit.NestedString` | internal/paymentkit/json.go:18 | 生产+测试均零调用(旧 shim 删除后的孤儿) | | `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 声称范围(文档漂移,非死代码) | ## 三、重复实现 / 双份维护 @@ -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/payment;internal/paymentkit~~ | ~~部分完成~~ | | 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 | 二轮 | @@ -69,11 +69,11 @@ | # | 问题 | 位置 | 轮次 | |---|------|------|------| | 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-17 剩余 | CallbackFields 已复用 paymentkit(见文末);剩余 values() 与 testRow() 近重复 | data/payment/payment.go:31-50,273-292 | 二轮(四轮部分收敛) | -| D-19 | payment 金额守恒校验四处重复(biz+vendor+wechat_v2+douyin) | biz/payment/payment.go:1198-1211 等 | 二轮 | -| D-21 | service/payment 同文件两份 30 字段映射:Order 方法内联映射与 paymentOrderResponse 重复同一张字段表 | service/payment/payment.go:14-34,65-82 | 四轮 | -| D-22 | data/payment 本地 contains 与 paymentkit.ContainsFold 重复(同 2.3) | data/payment/payment.go:520-527 | 四轮 | +| ~~D-15~~ | ~~defaults 合并逻辑三层三份。~~ **已修复:统一使用 `integrationbiz.MergeIntegrationDefaults`。** | ~~相关文件~~ | ~~已完成~~ | +| ~~D-17 剩余~~ | ~~`values()` 与 `testRow()` 已收敛为共享读取逻辑,并保留启用状态差异。~~ | ~~data/payment/payment.go~~ | ~~已完成~~ | +| ~~D-19(评估后保留)~~ | ~~payment 金额守恒校验四处重复。~~ **经复核保留:四处输入字段与供应商容错语义不同,强行合并会破坏分层;biz 最终守恒校验作为跨边界不变式。** | ~~biz/payment 与各供应商适配器~~ | ~~不改动~~ | +| ~~D-21~~ | ~~支付订单响应映射已统一复用 `paymentOrderResponse`。~~ | ~~service/payment/payment.go~~ | ~~已完成~~ | +| ~~D-22~~ | ~~同 2.3,已改用 `paymentkit.ContainsFold`。~~ | ~~data/payment/payment.go~~ | ~~已完成~~ | ## 四、过度分层:转发门面 / 透传壳 / 回调穿透 @@ -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-2 | SystemConfigService 一型仍四文件:system.go(19)+system_config.go(26)+system_init.go(38)+system_info.go(23) | service/system/* | 三轮(四轮复核未动) | -| S-3 | router 22 文件 464 行平均 21 行/文件;routes.go 手工 21 连调(与 X-1 一并解决) | server/router/* | 三轮 | -| S-4 | provider.go+providers.go 双小文件模式 ×4 子包(8 文件可并 4);modules/surface 单函数包;data/provider 单接口包;data_scope_record.go 单行别名文件 | 各处 | 三轮 | +| ~~S-1(第一批)~~ | ~~已合并 actor/data-scope 上下文载荷文件;其余微文件仍待按职责归并。~~ | ~~internal/biz/system/context.go~~ | ~~部分完成~~ | +| ~~S-2~~ | ~~SystemConfigService 已合并为单文件。~~ | ~~internal/service/system/system.go~~ | ~~已完成~~ | +| ~~S-3(第一批)~~ | ~~routes.go 已改为有序注册表;各领域路由文件仍保留。~~ | ~~internal/server/router/routes.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-6 | dto 包组织混乱:system.go 混装三域;settings.go 横跨四域 | service/dto/system.go、settings.go | 一轮 | | S-7 | data 层组织纪律:转换函数命名四种风格;PO 分布无规则;audit.go 名不副实;runtime.go 拼盘 | data/system/* | 二轮 | @@ -119,15 +119,15 @@ | P-11 | pkg/mq 去项目化(kra- 前缀)【属 2.2 排除范围交叉项】 | pkg/mq | 一轮 | | P-13 | pkg/module、pkg/task、pkg/database/migration 同层契约组维持现状 | pkg/* | 一轮 | | P-14 | httpx 移动后业务语义未剥离:CodePasswordChangeRequired=10001 与 x-token cookie 仍留在 internal/server/httpx;SetTokenCookie 注释自称 "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 | 四轮 | ## 七、分层 / 职责违规 | # | 问题 | 位置 | 轮次 | |---|------|------|------| -| L-3 | biz DO 带 json 标签:PaymentResult/PaymentTestResult(死标签);PaymentRequest(指纹编码格式锚死 DO);biz/integration Definition 家族充当前端契约 | biz/payment/payment.go:59-204;biz/integration | 二轮 | -| L-4 | DO 兼过滤器:API.OrderKey/Desc/StrictAll、SystemParameter/ExportTemplate 时间区间混入实体 | biz/system/api.go、parameter.go、export.go | 二轮 | +| ~~L-3(部分完成)~~ | ~~PaymentResult/PaymentTestResult 的死 JSON 标签已移除;PaymentRequest 字段与指纹语义保留,Definition 家族因仍被 service 消费暂不迁移。~~ | ~~biz/payment/payment.go;biz/integration~~ | ~~部分完成~~ | +| ~~L-4(部分完成)~~ | ~~`SystemParameter` 的查询字段与时间区间已拆为 `SystemParameterFilter`;API/Export 过滤字段仍待独立迁移。~~ | ~~biz/service/data system parameter~~ | ~~部分完成~~ | | L-5 | middleware 硬编码业务语义:中文消息黑名单判断审计(改文案即改审计行为);业务路径硬编码;支付回调专用逻辑内嵌通用中间件;限流策略内联 | server/middleware/* | 二轮 | | L-6 | biz 契约泄漏存储/表现原语:QueryExport 返回 []map[string]any;export DO 携带 SQL 片段;UserOptions UI 形状;AuthenticationResult 携带密码哈希 | biz/system/* | 二轮 | | L-7 | data 层纪律:Table("字符串") 绕过 PO;saveRelations 回写入参 DO;OriginSetting 裸转换 | data/system/* | 二轮 | @@ -181,7 +181,6 @@ 1. **先修 W-1 payment 启动阻塞**(应用当前无法启动,最高优先级)+ W-2/W-5(用户可感知的正确性缺陷) 2. **清 2.1 死代码残留 + 2.3 新死代码**(RecordDataAccess 链、security_session 8 方法、dto 死字段群、RegisterBusinessModule(随 W-1 一并决策)、CloneData/XML 系列等) 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 系列归位**(独立批次) 6. **X-1 路由单源化等大动作**(最后) - diff --git a/internal/biz/integration/integration_config.go b/internal/biz/integration/integration_config.go index f26a00b..f1f1c78 100644 --- a/internal/biz/integration/integration_config.go +++ b/internal/biz/integration/integration_config.go @@ -213,6 +213,11 @@ func DefaultIntegrationConfig(kind, provider string) 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)) for key, value := range defaults { out[key] = value diff --git a/internal/biz/payment/payment.go b/internal/biz/payment/payment.go index 99b1091..4f7e69d 100644 --- a/internal/biz/payment/payment.go +++ b/internal/biz/payment/payment.go @@ -158,51 +158,51 @@ type PaymentConfirmation struct { } type PaymentResult struct { - Provider string `json:"provider"` - Status string `json:"status"` - TradeNo string `json:"tradeNo"` - ProviderTradeNo string `json:"providerTradeNo"` - Amount int64 `json:"amount"` - PayerPaidAmount int64 `json:"payerPaidAmount,omitempty"` - CashPaidAmount int64 `json:"cashPaidAmount,omitempty"` - PointPaidAmount int64 `json:"pointPaidAmount,omitempty"` - DiscountAmount int64 `json:"discountAmount,omitempty"` - ProviderDiscountAmount int64 `json:"providerDiscountAmount,omitempty"` - MerchantDiscountAmount int64 `json:"merchantDiscountAmount,omitempty"` - SettlementAmount int64 `json:"settlementAmount,omitempty"` - Currency string `json:"currency"` - PayerCurrency string `json:"payerCurrency,omitempty"` - AmountBreakdownKnown bool `json:"amountBreakdownKnown"` - Duplicate bool `json:"duplicate"` - Payload json.RawMessage `json:"payload"` - OrderStatus string `json:"orderStatus,omitempty"` - FulfillmentStatus string `json:"fulfillmentStatus,omitempty"` - RefundStatus string `json:"refundStatus,omitempty"` - EventID string `json:"-"` - QueryID string `json:"-"` - SuccessAck PaymentCallbackAck `json:"-"` - FailureAck PaymentCallbackAck `json:"-"` + Provider string + Status string + TradeNo string + ProviderTradeNo string + Amount int64 + PayerPaidAmount int64 + CashPaidAmount int64 + PointPaidAmount int64 + DiscountAmount int64 + ProviderDiscountAmount int64 + MerchantDiscountAmount int64 + SettlementAmount int64 + Currency string + PayerCurrency string + AmountBreakdownKnown bool + Duplicate bool + Payload json.RawMessage + OrderStatus string + FulfillmentStatus string + RefundStatus string + EventID string + QueryID string + SuccessAck PaymentCallbackAck + FailureAck PaymentCallbackAck } // PaymentTestStage is one step of a provider connectivity test. A skipped // query/refund is reported explicitly because a prepay flow cannot be marked // paid without a sandbox payer interaction. type PaymentTestStage struct { - Name string `json:"name"` - Status string `json:"status"` - Message string `json:"message,omitempty"` - TradeNo string `json:"tradeNo,omitempty"` - Duration int64 `json:"durationMs,omitempty"` + Name string + Status string + Message string + TradeNo string + Duration int64 } type PaymentTestResult struct { - Provider string `json:"provider"` - TradeNo string `json:"tradeNo"` - Passed bool `json:"passed"` - FullFlow bool `json:"fullFlow"` - Mode string `json:"mode,omitempty"` - Stages []PaymentTestStage `json:"stages"` - Result *PaymentResult `json:"result,omitempty"` + Provider string + TradeNo string + Passed bool + FullFlow bool + Mode string + Stages []PaymentTestStage + Result *PaymentResult } type PaymentCallbackAck struct { diff --git a/internal/biz/system/access_control.go b/internal/biz/system/access_control.go deleted file mode 100644 index 9ce656d..0000000 --- a/internal/biz/system/access_control.go +++ /dev/null @@ -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) -} diff --git a/internal/biz/system/authority.go b/internal/biz/system/authority.go index bcc228f..70abc11 100644 --- a/internal/biz/system/authority.go +++ b/internal/biz/system/authority.go @@ -45,6 +45,23 @@ func NewAuthorityUsecase(repo AuthorityAccessRepo) *AuthorityUsecase { 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) { items, err := uc.ListAuthorities(ctx) if err != nil { diff --git a/internal/biz/system/media.go b/internal/biz/system/media.go index bcc673b..6722876 100644 --- a/internal/biz/system/media.go +++ b/internal/biz/system/media.go @@ -21,6 +21,25 @@ type MediaRepo interface { 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 { MediaRepo files FileStorage diff --git a/internal/biz/system/parameter.go b/internal/biz/system/parameter.go index 3972541..93805df 100644 --- a/internal/biz/system/parameter.go +++ b/internal/biz/system/parameter.go @@ -6,13 +6,18 @@ import ( ) type SystemParameter struct { - ID uint - CreatedAt time.Time - UpdatedAt time.Time + ID uint + CreatedAt time.Time + UpdatedAt time.Time + Name string + Key string + Value string + Desc string +} + +type SystemParameterFilter struct { Name string Key string - Value string - Desc string StartCreatedAt *time.Time EndCreatedAt *time.Time } @@ -23,7 +28,7 @@ type ParameterRepo interface { DeleteParameters(context.Context, []string) error FindParameterByID(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 } diff --git a/internal/biz/system/storage.go b/internal/biz/system/storage.go deleted file mode 100644 index 0ab9c85..0000000 --- a/internal/biz/system/storage.go +++ /dev/null @@ -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) -} diff --git a/internal/data/integration/migrations.go b/internal/data/integration/migrations.go index e5fae33..ff0a2de 100644 --- a/internal/data/integration/migrations.go +++ b/internal/data/integration/migrations.go @@ -3,6 +3,7 @@ package integration import ( "encoding/json" "errors" + "reflect" integrationbiz "kra/internal/biz/integration" "kra/pkg/database/migration" @@ -66,13 +67,9 @@ func ensurePaymentIntegrationConfigs(db *gorm.DB) error { return err } values := integrationObject(json.RawMessage(row.Config)) - changed := false - for key, value := range defaults { - if _, exists := values[key]; !exists { - values[key] = value - changed = true - } - } + merged := integrationbiz.MergeIntegrationDefaults(defaults, values) + changed := !reflect.DeepEqual(values, merged) + values = merged if changed { encoded, marshalErr := json.Marshal(values) if marshalErr != nil { diff --git a/internal/data/system/parameter.go b/internal/data/system/parameter.go index ff7d3cc..e062ab0 100644 --- a/internal/data/system/parameter.go +++ b/internal/data/system/parameter.go @@ -57,7 +57,7 @@ func (r *parameterRepo) FindParameterByKey(ctx context.Context, key string) (*sy } 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(¶meterPO{}) if q != nil { if q.StartCreatedAt != nil && q.EndCreatedAt != nil { diff --git a/internal/integration/payment/alipay.go b/internal/integration/payment/alipay.go index e0dfc36..7cb034b 100644 --- a/internal/integration/payment/alipay.go +++ b/internal/integration/payment/alipay.go @@ -8,6 +8,7 @@ import ( "fmt" "io" bizpayment "kra/internal/biz/payment" + "kra/internal/paymentkit" "net/http" "net/url" "strings" @@ -435,7 +436,7 @@ func alipayCreateMethod(extra, config map[string]any) (string, error) { if value == "" { value = firstAny(config, "method", "pay_method", "trade_type", "channel") } - normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) + normalized := paymentkit.NormalizePaymentMethod(value) switch normalized { case "", "create", "trade_create", "alipay_trade_create", "jsapi", "miniapp", "mini_program": return "alipay.trade.create", nil diff --git a/internal/integration/payment/allinpay.go b/internal/integration/payment/allinpay.go index 4c64e6d..8d93c53 100644 --- a/internal/integration/payment/allinpay.go +++ b/internal/integration/payment/allinpay.go @@ -12,6 +12,7 @@ import ( "fmt" "io" bizpayment "kra/internal/biz/payment" + "kra/internal/paymentkit" "net/http" "strings" "sync" @@ -207,7 +208,7 @@ func allinpayCreateMethod(extra, config map[string]any) (string, error) { if value == "" { value = firstAny(config, "method", "pay_method", "trade_type") } - normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) + normalized := paymentkit.NormalizePaymentMethod(value) switch normalized { case "", "pay", "unified", "unified_pay": return "pay", nil diff --git a/internal/integration/payment/douyin.go b/internal/integration/payment/douyin.go index a90b956..50b84cd 100644 --- a/internal/integration/payment/douyin.go +++ b/internal/integration/payment/douyin.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" bizpayment "kra/internal/biz/payment" + "kra/internal/paymentkit" "strings" "github.com/go-pay/gopay" @@ -110,7 +111,7 @@ func douyinCreateMethod(extra, config map[string]any) (string, error) { if value == "" { 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 { case "", "jsapi", "js_api", "mini", "mini_program", "miniprogram", "applet": return "jsapi", nil diff --git a/internal/integration/payment/lakala.go b/internal/integration/payment/lakala.go index 1791288..e5dda5d 100644 --- a/internal/integration/payment/lakala.go +++ b/internal/integration/payment/lakala.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" bizpayment "kra/internal/biz/payment" + "kra/internal/paymentkit" "strings" "github.com/go-pay/gopay" @@ -71,7 +72,7 @@ func lakalaCreateMethod(extra, config map[string]any) string { if method == "" { 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) { diff --git a/internal/integration/payment/saobei.go b/internal/integration/payment/saobei.go index b051b8e..fa41d77 100644 --- a/internal/integration/payment/saobei.go +++ b/internal/integration/payment/saobei.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" bizpayment "kra/internal/biz/payment" + "kra/internal/paymentkit" "strings" "time" @@ -89,7 +90,7 @@ func saobeiCreateMethod(extra, config map[string]any) (string, error) { if value == "" { value = firstAny(config, "method", "pay_method", "trade_type") } - normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) + normalized := paymentkit.NormalizePaymentMethod(value) switch normalized { case "", "mini", "mini_pay", "miniapp", "mini_app", "mini_program", "miniprogram", "jsapi", "js_api": return "mini", nil diff --git a/internal/integration/payment/wechat_v2.go b/internal/integration/payment/wechat_v2.go index 00bdb70..33a66d4 100644 --- a/internal/integration/payment/wechat_v2.go +++ b/internal/integration/payment/wechat_v2.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" bizpayment "kra/internal/biz/payment" + "kra/internal/paymentkit" "net/http" "net/url" "strings" @@ -180,7 +181,7 @@ func wechatV2CreateMethod(extra, config map[string]any) (string, error) { if value == "" { 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 { case "", "jsapi", "js_api", "mini", "miniapp", "mini_program", "miniprogram", "applet": return gopayWechat.TradeType_JsApi, nil diff --git a/internal/paymentkit/status.go b/internal/paymentkit/status.go index 93a03d5..21e93a2 100644 --- a/internal/paymentkit/status.go +++ b/internal/paymentkit/status.go @@ -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 { value, ok := values[key] if !ok { diff --git a/internal/service/integration/integration_config.go b/internal/service/integration/integration_config.go index aca5d6b..6571d55 100644 --- a/internal/service/integration/integration_config.go +++ b/internal/service/integration/integration_config.go @@ -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 { - values := make(map[string]any, len(defaults)) - for key, value := range defaults { - values[key] = value - } stored := map[string]any{} _ = json.Unmarshal(raw, &stored) - for key, value := range stored { - values[key] = value - } + values := integrationbiz.MergeIntegrationDefaults(defaults, stored) encoded, _ := json.Marshal(values) return encoded } diff --git a/internal/service/system/parameter.go b/internal/service/system/parameter.go index 5ccd659..042c0f6 100644 --- a/internal/service/system/parameter.go +++ b/internal/service/system/parameter.go @@ -24,12 +24,12 @@ func (s *ParameterService) UpdateParameterRequest(ctx context.Context, req *dto. 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) { - 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 { 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) if err != nil { return nil, 0, err