优化结构
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# Kra 管理后台
|
||||
|
||||
## 范围
|
||||
|
||||
Kra 管理后台由 Kratos 应用承载生命周期,Gin 提供兼容管理端的 HTTP 路由。后端按 `service -> biz -> data` 分层,前端位于 `web/`。
|
||||
|
||||
保留的业务模块:
|
||||
|
||||
- 初始化、认证、用户和个人中心
|
||||
- 角色、菜单、API、Casbin 和按钮权限
|
||||
- 部门、岗位和数据范围
|
||||
- 字典、字典项和系统参数
|
||||
- 安全策略、系统配置和 API Token
|
||||
- Excel 导入导出与版本管理
|
||||
- 操作日志、登录日志、数据访问日志、错误日志和文件日志
|
||||
- 定时任务、SSE、媒体上传和附件分类
|
||||
- 公告管理与邮件发送
|
||||
|
||||
不包含代码生成、智能辅助、模块安装发布、客户示例和测试页面。
|
||||
|
||||
## 目录
|
||||
|
||||
| 目录 | 职责 |
|
||||
|---|---|
|
||||
| `internal/server` | Gin 路由、中间件和 Kratos transport 生命周期 |
|
||||
| `internal/service` | HTTP DTO 与领域对象转换、用例编排 |
|
||||
| `internal/biz` | 领域对象、业务规则和仓储接口 |
|
||||
| `internal/data` | GORM、MySQL、Redis 和本地文件存储实现 |
|
||||
| `web` | Vue 3 管理端 |
|
||||
|
||||
## 兼容约定
|
||||
|
||||
- HTTP 响应为 `{code, data, msg}`,成功码为 `0`。
|
||||
- JWT 接受 `x-token` Header 和同名 Cookie。
|
||||
- 初始化创建后台数据表、管理员、菜单,并从 Gin 路由同步 API 权限数据。
|
||||
- 超级管理员角色 `888` 保留全权限旁路,其他角色由 Casbin 控制。
|
||||
- SMTP 配置位于 `admin.email`;未配置时错误邮件告警保持关闭。
|
||||
- 媒体文件默认使用本地 `uploads/file` 存储。
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
go generate ./cmd
|
||||
go test ./...
|
||||
go vet ./...
|
||||
go build ./...
|
||||
|
||||
cd web
|
||||
pnpm install
|
||||
pnpm run lint
|
||||
pnpm run build
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
+410
@@ -0,0 +1,410 @@
|
||||
# 支付接入设计
|
||||
|
||||
## 代码分层
|
||||
|
||||
支付渠道适配器统一放在 `internal/data/payment`,订单仓储和运行时入口保留在
|
||||
`internal/data`;这与 `internal/data/storage` 集中管理 OSS provider 的方式一致,
|
||||
避免各渠道协议散落在 data 根目录。它们仍属于 data 层,因为需要读取
|
||||
`sys_integration_configs`、调用运行时客户端、转换 `biz` 对象并在数据库事务中推进
|
||||
`pay_orders` 状态。无状态的协议基础能力已下沉到领域化公共包:
|
||||
|
||||
- `pkg/paymentkit`:金额整数化、支付状态归一化、JSON 路径读取、微信 v2 XML、
|
||||
微信 v2/聚合渠道签名等纯函数;不依赖 `biz`、`data` 或数据库。
|
||||
- `internal/data/payment`:GoPay 和配置驱动渠道的 adapter、SDK 配置映射、
|
||||
回调验签/解密以及 DO 结果归一化;GoPay/driver 类型不越过 data 边界。
|
||||
- `pkg/osskit`:跨对象存储 provider 的流式合并和 MD5 计算;不依赖具体 OSS SDK。
|
||||
- `internal/data/storage`:本地磁盘、七牛、阿里云 OSS、华为 OBS、腾讯 COS、
|
||||
S3/MinIO/R2 的客户端创建、配置映射和 `biz.FileStorage` 适配仍保留在 data。
|
||||
|
||||
因此不能把整个 `internal/data` 或所有 OSS provider 直接移动到 `pkg`:那会让公共包
|
||||
反向依赖内部领域模型和运行时配置,破坏分层。后续新增 provider 时,优先把纯签名、
|
||||
金额和报文转换放入 `pkg/paymentkit`,把配置、HTTP 客户端、数据库和业务结果转换留在
|
||||
`internal/data`。
|
||||
|
||||
支付模块创建一张 `pay_orders` 表,用于记录一次支付尝试的金额、渠道状态、发货状态、退款状态和并发租约。业务模块继续拥有自己的业务订单,通过 `business_type + business_id` 关联支付订单。渠道配置继续使用已有的 `sys_integration_configs`,`kind = payment`;不创建独立回调事件表或支付日志表。
|
||||
|
||||
## 订单表边界
|
||||
|
||||
`pay_orders` 一行代表一次支付尝试,而不是业务订单:
|
||||
|
||||
- `(provider, trade_no)` 唯一,同一个业务订单可以创建多个不同渠道或不同批次的支付尝试。
|
||||
- `(provider, provider_trade_no)` 唯一,禁止一个平台交易绑定两个本地订单。
|
||||
- `request_fingerprint` 防止相同商户订单号被不同金额、币种或业务对象重复使用。
|
||||
- `payment_status`、`fulfillment_status`、`refund_status` 相互独立,退款不会抹掉原始支付和发货事实。
|
||||
- `original_amount` 是业务原价;`amount` 是提交给第三方平台的订单总额,二者均使用币种最小单位整数。
|
||||
- `payer_paid_amount` 是用户实际支付金额;`cash_paid_amount` / `point_paid_amount` 分别表示现金和积分/平台资产部分;`discount_amount` 是订单总额减用户实付。
|
||||
- `provider_discount_amount` / `merchant_discount_amount` 只有渠道明确返回出资拆分时才填写;未知字段不能推测。`settlement_amount` 是渠道最终结算商户金额,不等于用户实付。
|
||||
- `amount_breakdown_known` 只有在上述核心金额满足守恒校验时才为 `true`,否则仍保留总额校验,但不伪造优惠拆分。
|
||||
- `confirmation_id` 根据 `provider + trade_no` 稳定生成,是业务发货最终幂等键。
|
||||
- 只保存平台响应哈希和必要状态;不保存完整回调原文、签名密钥或请求头。
|
||||
- `fulfillment_token` / `refund_token` 和租约时间用于跨实例互斥,进程退出后租约到期可安全重试。
|
||||
|
||||
## GoPay 渠道矩阵(v1.5.122)
|
||||
|
||||
本分支固定使用 `github.com/go-pay/gopay v1.5.122`。下面的九行是 GoPay
|
||||
稳定版本中可复用的渠道族;微信在本地拆成 `wechat-v2` 和 `wechat-v3` 两个
|
||||
provider,因此渠道族仍然是九个,而不是把微信重复计算成两个渠道。
|
||||
|
||||
| GoPay 渠道族 | 本地 provider | 当前适配的 GoPay 能力 | 回调/退款边界 |
|
||||
| --- | --- | --- | --- |
|
||||
| Alipay(旧网关协议) | `alipay` | `TradeCreate`、`TradePay`、`TradePrecreate`、`TradeAppPay`、`TradePagePay`、`TradeWapPay`、查单、退款、通知验签 | 支持服务端验签后主动查单 |
|
||||
| Alipay V3 | `alipay-v3` | GoPay V3 `TradeCreate`、`TradePay`、`TradePrecreate`、`TradeAppPay`、`TradePagePay`、`TradeWapPay`、查单、退款、证书响应验签、通知证书验签 | 必须配置应用公钥证书、支付宝根证书和支付宝公钥证书;确认仍以主动查单为准 |
|
||||
| WeChat | `wechat-v2`、`wechat-v3` | v2 `UnifiedOrder`/`Micropay`/`QueryOrder`/`Refund`;v3 JSAPI、App、Native、H5、CodePay 付款码下单、查单、退款、通知验签/解密 | v2 使用商户密钥;v3 使用平台证书和 API v3 key |
|
||||
| Apple | `apple-iap` | App Store Server API 交易查询、JWS/证书链处理 | 下单由客户端 StoreKit 驱动;服务端不提供主动退款 |
|
||||
| PayPal | `paypal` | `CreateOrder`、`OrderDetail`、`OrderCapture`、`PaymentCaptureRefund`、Webhook 验签 | 买家批准后由查单链路捕获;退款需要已持久化的 capture ID |
|
||||
| Douyin | `douyin` | App、JSAPI、H5、Native 下单,按商户订单号查单、退款、通知验签和解密 | 平台证书必须配置 |
|
||||
| QQ | `qq` | `UnifiedOrder`、`OrderQuery`、`Refund`、通知解析和验签 | 退款需要商户证书、私钥或 PKCS#12 |
|
||||
| AllinPay | `allinpay` | `Pay`、`ScanPay`、`NativePay`、`Query`、`Refund` | 当前不接收通知;使用主动查单/对账确认 |
|
||||
| Lakala | `lakala` | JSAPI、H5、小程序、Native/二维码、Native JSAPI、SDK、Web Gateway、线下条码/二维码下单,`OrderStatus`、`Refund`、通知解析和验签 | 回调字段以 GoPay 返回模型为准 |
|
||||
| Saobei | `saobei` | `MiniPay`、`BarcodePay`、`Query`、`Refund` | 当前不接收通知;使用主动查单/对账确认 |
|
||||
|
||||
`CMB`(招商银行)不在固定的 v1.5.122 模块中,不能作为本分支已接入渠道;
|
||||
上游未发布版本中的目录或提交不构成稳定依赖,故这里明确排除。
|
||||
|
||||
AllinPay 和 Saobei 的 adapter 会拒绝回调入口,因为固定版本没有可复用且能在
|
||||
本项目边界内完成验签的通知路径。对这两个渠道,生产流程必须依赖主动查单、
|
||||
定时对账和幂等状态推进,不得把未验签的通知当作支付事实。
|
||||
|
||||
本矩阵只说明代码和 GoPay 方法的接入情况,不代表真实商户沙箱或生产联调已经完成。
|
||||
当前仓库仅做单元测试和本地 mock/HTTP 响应验证;上线前仍须使用实际商户凭证、
|
||||
证书、平台回调和退款报文逐项联调。
|
||||
|
||||
统一 adapter 面向普通商户的核心下单、查单、退款和回调流程。微信服务商/合单、
|
||||
PayPal `AUTHORIZE` 意图后的授权捕获,以及账单、分账、转账等能力需要不同的业务
|
||||
状态机和持久化字段,不能仅靠透传 `extra` 安全接入;这些扩展应按实际业务合同
|
||||
单独建模,不属于“稳定渠道族已接入”的含义。
|
||||
|
||||
## 其他已保留渠道
|
||||
|
||||
| Provider | 实现方式 |
|
||||
| --- | --- |
|
||||
| `alipay`、`alipay-v3`、`wechat-v2`、`wechat-v3`、`apple-iap`、`paypal`、`douyin`、`qq`、`allinpay`、`lakala`、`saobei` | 统一走上面的 GoPay v1.5.122 adapter;业务层只接收 `paymentbiz.PaymentResult` |
|
||||
| `chinaums` | 配置驱动的银联商务 JSON 签名适配器 |
|
||||
| `sft` | 配置驱动的商福通 JSON/MD5 适配器 |
|
||||
| `supper-pay` | 配置驱动的 Supper Pay HMAC 适配器 |
|
||||
| `wechat-game-pay` | 配置驱动的微信小游戏虚拟支付 2.0 适配器,支持 access token |
|
||||
| `douyin-game-pay` | 配置驱动的抖音小游戏支付签名适配器 |
|
||||
|
||||
上表后五个 provider 不是 GoPay v1.5.122 的九个稳定渠道族,仍保留现有的
|
||||
配置驱动协议适配器。它们的协议字段依商户合同和产品版本不同,因此没有强行
|
||||
假设某一个固定请求格式,必须在 `sys_integration_configs.config` 中声明查单
|
||||
字段和金额单位。
|
||||
|
||||
## 回调安全流程
|
||||
|
||||
创建支付按以下顺序处理:
|
||||
|
||||
1. 根据 `business_type` 调用业务模块注册的 `PaymentOrderSource`。
|
||||
2. 业务模块返回可信的金额、币种、标题、业务 ID 和商户订单号;客户端提交的金额不是权威数据。
|
||||
3. 先插入 `pay_orders`,利用唯一索引和请求指纹实现本地下单幂等。
|
||||
4. 再调用支付平台创建订单;平台创建响应只会把本地状态推进到 `pending`,不能直接认定已支付。
|
||||
5. 平台调用成功后保存客户端后续支付所需的创建响应。数据库记录失败时,可使用相同商户订单号安全重试平台创建接口。
|
||||
|
||||
每次支付回调按以下顺序处理:
|
||||
|
||||
1. 根据渠道配置加载适配器。
|
||||
2. 校验渠道签名、证书/JWS/通知解密,并校验应用号、商户号等身份字段。
|
||||
3. 从已验签的通知中提取商户订单号。
|
||||
4. **主动调用对应平台查单接口**,不以回调中的支付状态、金额或币种作为最终依据。
|
||||
5. 查单状态只允许 `success`、`pending`、`failed`;未知状态直接失败。成功状态必须同时包含商户订单号、平台交易号、正整数金额和币种。
|
||||
6. 查单状态不是成功时,不发货;后续回调或业务主动查询可以再次确认。
|
||||
7. 从 `pay_orders` 读取本地权威金额、币种、业务类型和业务 ID。
|
||||
8. 严格比较渠道、商户订单号、平台交易号、金额、币种;任一关键字段缺失或不一致都拒绝更新为已支付。
|
||||
9. 在数据库事务中把支付状态推进到 `paid`,再竞争发货租约。
|
||||
10. 根据 `business_type` 查找 `PaymentFulfillmentHandler`,使用稳定 `confirmation_id` 执行业务发货。
|
||||
11. 发货成功后将 `fulfillment_status` 更新为 `succeeded`;失败记录为 `failed` 并允许重试。重复回调和主动查询不会重复执行已完成发货。
|
||||
|
||||
支付模块不会接受客户端传入的金额、标题或币种作为权威订单数据。持久化流程启用后,未注册 `PaymentOrderSource` 的业务类型会直接拒绝下单。
|
||||
|
||||
## 业务扩展接口
|
||||
|
||||
业务侧必须把同一个实现作为 `PaymentBusinessModule` 注册。该接口同时要求实现
|
||||
`PaymentOrderSource`(返回权威金额、币种和标题)、`PaymentFulfillmentHandler`
|
||||
(按 `confirmation_id` 幂等发货)和 `PaymentRefundAuthorizer`(校验业务订单是否
|
||||
允许退款)。应用组合阶段调用 `PaymentUsecase.RegisterBusinessModule(...)`;
|
||||
只注册支付 adapter 而不注册业务模块是不完整的接入。
|
||||
|
||||
当前仓库没有注册任何生产业务模块,因此直接调用持久化支付创建流程会返回
|
||||
`支付业务订单来源未注册`。这不是渠道配置错误,接入具体商品、订单或订阅业务时
|
||||
必须在应用启动组装处完成注册。
|
||||
|
||||
每个业务模块先实现可信订单来源:
|
||||
|
||||
```go
|
||||
import paymentbiz "kra/internal/biz/payment"
|
||||
|
||||
type GameItemPayment struct {
|
||||
orders GameItemOrderRepo
|
||||
}
|
||||
|
||||
func (GameItemPayment) Type() string { return "game_item" }
|
||||
|
||||
func (p GameItemPayment) PreparePayment(ctx context.Context, provider, tradeNo, businessID string) (*paymentbiz.PaymentIntent, error) {
|
||||
order, err := p.orders.FindPayable(ctx, businessID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &paymentbiz.PaymentIntent{
|
||||
Provider: provider, TradeNo: tradeNo,
|
||||
BusinessType: "game_item", BusinessID: businessID,
|
||||
Subject: order.Title, Amount: order.PayableAmount, Currency: order.Currency,
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
然后注册按业务类型分发的发货处理器:
|
||||
|
||||
```go
|
||||
func (p GameItemPayment) Fulfill(ctx context.Context, c *paymentbiz.PaymentConfirmation) error {
|
||||
return p.orders.Transaction(ctx, func(tx GameItemOrderTx) error {
|
||||
// confirmation_id 必须有唯一约束。已处理时直接返回 nil。
|
||||
if tx.HasPaymentConfirmation(c.ID) {
|
||||
return nil
|
||||
}
|
||||
if err := tx.Deliver(c.BusinessID); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.SavePaymentConfirmation(c.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func (p GameItemPayment) AuthorizeRefund(ctx context.Context, order *paymentbiz.PaymentOrder, amount int64) error {
|
||||
return p.orders.CheckRefundable(ctx, order.BusinessID, amount)
|
||||
}
|
||||
```
|
||||
|
||||
启动时将同一个实现分别注册到 `PaymentOrderSourceRegistry` 和 `PaymentFulfillmentRegistry`。`PaymentConfirmation.ID` 同一支付尝试永远不变。业务处理器必须在自己的事务中以该 ID 建唯一约束,才能覆盖“业务已发货但进程在更新 `pay_orders` 前退出”的极端窗口。
|
||||
|
||||
推荐直接注册完整业务模块:
|
||||
|
||||
```go
|
||||
if err := paymentUsecase.RegisterBusinessModule(GameItemPayment{orders: gameOrders}); err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
## 一致性边界
|
||||
|
||||
- 本地下单:唯一索引和 `request_fingerprint` 保证同一支付号不可换金额或业务对象。
|
||||
- 平台确认:只有验签后的主动查单结果可以把订单推进到 `paid`,状态不会从已支付回退到待支付或失败。
|
||||
- 多实例发货:数据库行锁、处理令牌和租约保证同一时刻只有一个实例执行发货。
|
||||
- 最终发货幂等:业务模块必须在自己的事务中对 `confirmation_id` 建唯一约束。
|
||||
- 退款:一次只允许一个在途退款;每次退款都有持久化 `refund_no`,网络超时会复用同一个退款号重试。渠道响应提供退款号回显时,adapter 必须校验其与本地 `refund_no` 一致,并要求独立的平台退款号非空;平台接受退款后状态为 `pending`,只有携带匹配 `refund_no` 的退款通知或对账任务调用 `ConfirmRefund` 后才增加 `refunded_amount`。
|
||||
- 退款授权:业务模块必须实现 `AuthorizeRefund`,支付模块不会仅凭渠道、订单号和金额执行退款。
|
||||
- 外部平台调用和本地数据库无法组成单个 ACID 事务,因此采用“本地先落单、平台接口幂等重试、主动查单、数据库状态机、业务最终幂等”的组合保证最终一致性。
|
||||
|
||||
## 配置示例
|
||||
|
||||
支付宝:
|
||||
|
||||
```json
|
||||
{"app_id":"","private_key":"PEM","public_key":"PEM","environment":"production","sign_type":"RSA2","gateway_url":"https://openapi.alipay.com/gateway.do","method":"alipay.trade.create"}
|
||||
```
|
||||
|
||||
`method` 可选 `alipay.trade.create`、`alipay.trade.pay`、`alipay.trade.precreate`、
|
||||
`alipay.trade.app.pay`、`alipay.trade.page.pay` 或 `alipay.trade.wap.pay`。
|
||||
付款码支付可使用 `barcode` / `micropay` 别名,并在订单 `extra.auth_code` 中传入付款码。
|
||||
|
||||
支付宝 V3:
|
||||
|
||||
```json
|
||||
{"app_id":"","private_key":"PEM","app_cert":"PEM 或文件路径","root_cert":"PEM 或文件路径","public_cert":"PEM 或文件路径","environment":"production","api_base_url":"https://openapi.alipay.com","gateway_url":"https://openapi.alipay.com/gateway.do","method":"alipay.trade.create"}
|
||||
```
|
||||
|
||||
`alipay-v3` 独立于旧 `alipay` provider。`app_cert`、`root_cert` 和 `public_cert`
|
||||
分别对应 GoPay `ClientV3.SetCert` 的应用公钥证书、支付宝根证书和支付宝公钥证书;
|
||||
也可使用 `*_content` / `*_path` 以及 `alipay_root_cert*`、`alipay_public_cert*`
|
||||
兼容别名。V3 HTTP 接口使用 `api_base_url`(测试代理可指向本地 mock),页面/APP
|
||||
调起参数使用 GoPay 已封装的 `TradeAppPay`、`TradePagePay` 和 `TradeWapPay`;
|
||||
`method` 支持 `alipay.trade.create`、`alipay.trade.pay`、`alipay.trade.precreate`、
|
||||
`alipay.trade.app.pay`、`alipay.trade.page.pay` 和 `alipay.trade.wap.pay`。V3 的
|
||||
REST 响应只有在证书验签通过后才会进入业务层,通知回调同样使用 GoPay 的证书验签。
|
||||
|
||||
微信支付 v2:
|
||||
|
||||
```json
|
||||
{"app_id":"","merchant_id":"","mch_key":"","sign_type":"MD5","trade_type":"NATIVE","client_cert":"PEM","client_key":"PEM"}
|
||||
```
|
||||
|
||||
`client_cert` / `client_key` 在退款等双向 TLS 请求中使用;也接受
|
||||
`appid`、`mch_id`、`api_key` 等兼容别名。自定义测试端点可分别配置
|
||||
`create_url`、`query_url` 和 `refund_url`。`trade_type` 支持 `JSAPI`、`APP`、
|
||||
`NATIVE`、`MWEB`;付款码支付可使用 `micropay` / `barcode` 别名,并在订单
|
||||
`extra.auth_code`(或渠道配置同名字段)传入付款码。未知下单方式会直接拒绝;
|
||||
付款码下单返回的平台交易号和金额会校验,客户端调起数据只保存在 `Payload`。
|
||||
|
||||
微信支付 v3:
|
||||
|
||||
```json
|
||||
{"app_id":"","merchant_id":"","serial_no":"","private_key":"PEM","api_v3_key":"32-byte key","platform_cert":"PEM","platform_serial_no":"","trade_type":"jsapi"}
|
||||
```
|
||||
|
||||
`trade_type` 支持 `jsapi`、`app`、`native`、`h5` 和 `codepay` / `micropay`;
|
||||
JSAPI/小程序下单还需在订单 `extra.openid` 中传入用户标识,付款码支付则需在
|
||||
订单 `extra.auth_code` 中传入用户付款码(兼容 `authcode` / `barcode` 字段名)。
|
||||
|
||||
Apple 内购:
|
||||
|
||||
```json
|
||||
{"issuer_id":"","key_id":"","bundle_id":"","private_key":"PEM","price_divisor":"10","environment":"production"}
|
||||
```
|
||||
|
||||
Apple Server API 的交易 `price` 使用平台返回的单位;`price_divisor` 必须按业务订单使用的最小货币单位配置。金额不能整除时,支付模块拒绝确认。
|
||||
|
||||
Apple 多币种可使用 `price_divisors` 对不同 `currency` 分别配置比例;服务端不会把 Apple 的价格字段默认当作人民币分。
|
||||
|
||||
Apple 没有传统服务端“预下单”。创建接口要求 `tradeNo` 是 UUID,并把它作为 `appAccountToken` 返回给客户端;客户端发起 StoreKit 购买时必须原样传入。回调使用 `appAccountToken` 关联本地订单,使用 `transactionId` 调 Apple Server API 主动查单,两者不会混用。最终确认还会把签名载荷的 `productId` 与业务订单持久化的 `extra.product_id` 精确匹配;缺少 `appAccountToken`、Bundle ID、商品 ID 或环境不一致、JWS 算法不是 ES256、证书链校验失败时均拒绝确认。
|
||||
adapter 在调用 GoPay 解码前还会绑定 `x5c[0]` 叶子到 `x5c[1]`/`x5c[2]`,并检查 Apple App Store 签名证书扩展,避免仅凭叶子公钥验签。
|
||||
|
||||
Apple IAP 的购买流程由客户端发起,退款/撤销由 App Store 管理。本项目的
|
||||
`apple-iap` adapter 不提供商户服务端主动退款,业务侧应通过 Apple 的退款流程和
|
||||
后续通知/查询更新状态。
|
||||
|
||||
PayPal:
|
||||
|
||||
```json
|
||||
{"client_id":"","client_secret":"","webhook_id":"","environment":"sandbox","return_url":"https://merchant.example/paypal/return","cancel_url":"https://merchant.example/paypal/cancel","amount_scales":{"USD":"100","JPY":"1"}}
|
||||
```
|
||||
|
||||
启用 PayPal 配置必须提供 `webhook_id`,回调验签会将其传给 GoPay;默认金额比例按
|
||||
PayPal 币种处理,也可使用 `amount_scales`/`currency_scales` 覆盖。`CAPTURE` 意图的
|
||||
订单在买家批准后会由查单流程调用 GoPay `OrderCapture` 完成捕获,只有捕获成功才会
|
||||
进入支付成功和发货;可显式配置 `auto_capture=false` 关闭。退款需要订单状态中已
|
||||
持久化的 capture ID。
|
||||
|
||||
抖音支付:
|
||||
|
||||
```json
|
||||
{"app_id":"","merchant_id":"","serial_no":"","api_key":"32-byte key","private_key":"PEM","platform_cert":"PEM","platform_serial_no":"","trade_type":"jsapi","environment":"production"}
|
||||
```
|
||||
|
||||
当前 adapter 支持 `app`、`jsapi`、`h5`、`native`;固定版本的抖音客户端按
|
||||
生产接口工作,`environment` 只能使用 `production`/`prod`。平台证书序列号也接受
|
||||
GoPay 模型使用的兼容字段 `platform_cert_serial`。
|
||||
|
||||
QQ 支付:
|
||||
|
||||
```json
|
||||
{"mch_id":"","api_key":"","sign_type":"MD5","trade_type":"NATIVE","cert_file":"/secure/qq/apiclient_cert.pem","key_file":"/secure/qq/apiclient_key.pem","environment":"production"}
|
||||
```
|
||||
|
||||
`sign_type` 可用 `MD5` 或 `HMAC-SHA256`。退款必须配置
|
||||
`cert_file` + `key_file`,或 `pkcs12_file`;也可使用对应的 `*_content` 字段。
|
||||
|
||||
通联支付(AllinPay):
|
||||
|
||||
```json
|
||||
{"cus_id":"","app_id":"","private_key":"PEM","public_key":"PEM","org_id":"","pay_type":"W02","query_order_type":"reqsn","currency":"CNY","environment":"production"}
|
||||
```
|
||||
|
||||
`query_order_type` 只允许 `reqsn` 或 `trxid`,默认使用商户订单号 `reqsn`。
|
||||
选择 `trxid` 时,下单响应必须返回交易号并将其持久化为后续查单、退款标识;
|
||||
Native 下单不会返回该标识,因此不能与 `trxid` 模式组合。该 provider 只接入
|
||||
下单、查单和退款,不接收通知;支付确认由主动查单/对账触发。
|
||||
|
||||
拉卡拉(Lakala):
|
||||
|
||||
```json
|
||||
{"partner_code":"","credential_code":"","channel":"Wechat","method":"jsapi","currency":"JPY","environment":"production"}
|
||||
```
|
||||
|
||||
`method` 支持 `jsapi`、`h5`、`mini`、`native`、`qrcode`、`native_jsapi`、
|
||||
`sdk`、`web`、`retail` 和 `retail_qrcode`,并映射到 GoPay v1.5.122 对应的
|
||||
创建方法;未知值会直接拒绝。下单响应中的 `order_id` 是持久化查单键,二维码、
|
||||
跳转 URL 和 SDK 参数只放在创建结果 `Payload`。支持查单、退款和 GoPay 通知验签。
|
||||
当前固定版本客户端只允许生产环境配置。
|
||||
|
||||
扫呗(Saobei):
|
||||
|
||||
```json
|
||||
{"inst_no":"","key":"","merchant_no":"","terminal_id":"","access_token":"","pay_type":"010","currency":"CNY","environment":"production"}
|
||||
```
|
||||
|
||||
该 provider 只接入条码/小程序下单、查单和退款,不接收通知;支付确认由主动
|
||||
查单/对账触发。条码支付在订单 `extra.auth_no` 中传入付款码,也兼容
|
||||
`auth_code`、`authcode`、`barcode` 和 `pay_code` 字段名。
|
||||
|
||||
非 GoPay 的配置驱动聚合/小游戏渠道最少需要:
|
||||
|
||||
```json
|
||||
{
|
||||
"protocol_version":"以商户协议为准",
|
||||
"app_id":"",
|
||||
"merchant_id":"",
|
||||
"create_url":"",
|
||||
"query_url":"",
|
||||
"refund_url":"",
|
||||
"app_key":"",
|
||||
"query_status_field":"data.status",
|
||||
"query_success_values":"SUCCESS,PAID",
|
||||
"query_trade_no_field":"data.trade_no",
|
||||
"query_provider_trade_no_field":"data.transaction_id",
|
||||
"query_amount_field":"data.amount",
|
||||
"query_currency_field":"data.currency",
|
||||
"query_amount_scale":"1",
|
||||
"callback_status_field":"data.status",
|
||||
"callback_success_values":"SUCCESS,PAID",
|
||||
"callback_trade_no_field":"data.trade_no",
|
||||
"callback_provider_trade_no_field":"data.transaction_id"
|
||||
}
|
||||
```
|
||||
|
||||
`query_amount_scale = 1` 表示响应已经是最小货币单位整数;`100` 表示响应是元/主货币单位并转换为分;其他值必须是 10 的幂。字段路径使用点号访问 JSON 对象。
|
||||
|
||||
聚合/小游戏渠道如需保存优惠和内部资产拆分,可额外配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"query_payer_paid_amount_field":"data.payer_total",
|
||||
"query_cash_paid_amount_field":"data.cash_fee",
|
||||
"query_point_paid_amount_field":"data.point_fee",
|
||||
"query_discount_amount_field":"data.discount",
|
||||
"query_provider_discount_amount_field":"data.provider_discount",
|
||||
"query_merchant_discount_amount_field":"data.merchant_discount",
|
||||
"query_settlement_amount_field":"data.settlement_amount",
|
||||
"query_payer_currency_field":"data.payer_currency",
|
||||
"query_payer_paid_amount_scale":"100",
|
||||
"query_cash_paid_amount_scale":"100"
|
||||
}
|
||||
```
|
||||
|
||||
金额拆分字段缺失时,适配器把 `amount_breakdown_known` 设为 `false`,不会将未知优惠归给平台或商户。字段存在但不能按配置的整数比例精确换算时,查单失败并要求修正渠道配置。
|
||||
|
||||
内部支付使用 `provider = internal`、`payment_mode = internal`,不调用第三方平台。业务模块实现 `PayInternal`,以 `trade_no` 原子、幂等扣减积分、余额或其他内部资产,并返回授权号和完整金额拆分;仍然走同一套 `pay_orders`、查单/确认、发货和退款状态机。内部退款必须由业务模块以同一订单幂等执行 `RefundInternal`。
|
||||
|
||||
Chinaums、SFT、Supper Pay、微信小游戏和抖音小游戏没有在代码中假设所有商户都使用同一合同版本。启用前必须拿实际商户文档和沙箱报文逐项确认请求字段、签名串、金额单位、回调字段和成功状态;缺少 `protocol_version` 或明确回调字段映射时配置校验会拒绝启用。这里提供的是严格失败的适配框架,不应在未完成渠道联调测试时标记为生产可用。
|
||||
|
||||
## 回调 ACK
|
||||
|
||||
回调接口不会返回内部错误文本、平台原始查询结果或业务发货信息。支付宝、微信 v2、微信 v3 和 Apple 使用各自固定 ACK;聚合渠道默认返回纯文本 `success` / `failure`,并可按实际协议配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"callback_success_status":"200",
|
||||
"callback_success_content_type":"application/json",
|
||||
"callback_success_body":"{\"code\":\"SUCCESS\"}",
|
||||
"callback_failure_status":"500",
|
||||
"callback_failure_content_type":"application/json",
|
||||
"callback_failure_body":"{\"code\":\"FAIL\"}"
|
||||
}
|
||||
```
|
||||
|
||||
状态码只允许 `200-599`,Content-Type 禁止换行,响应体最大 64KiB。验签、主动查单、订单解析、金额校验或发货失败时返回失败 ACK,让支持重试的平台再次通知;查单为 `pending` / `failed` 时不发货,但通知已被安全处理,因此返回成功 ACK。
|
||||
|
||||
## HTTP 接口
|
||||
|
||||
- `GET /integration/configs/payment`
|
||||
- `GET /integration/configs/payment/:provider`
|
||||
- `PUT /integration/configs/payment/:provider`
|
||||
- `DELETE /integration/configs/payment/:provider`
|
||||
- `POST /payment/order`
|
||||
- `POST /payment/create`
|
||||
- `POST /payment/query`
|
||||
- `POST /payment/refund`
|
||||
- `POST /payment/callback/:provider`
|
||||
|
||||
`amount` 使用整数,单位是业务约定的最小货币单位。`extra` 传递渠道特有字段,例如 `openid`、`trade_type`、`product_id`。
|
||||
|
||||
## 日志
|
||||
|
||||
支付日志在 `internal/biz/payment/payment_log.go` 通过 `PaymentLogger` 独立抽象,包含下单、查单失败、回调查单、金额校验、重复回调和发货结果等结构化事件。日志只记录渠道、商户订单号、业务类型、业务 ID、确认 ID 等审计字段,不记录私钥、密钥、证书内容或完整敏感回调原文。
|
||||
@@ -0,0 +1,261 @@
|
||||
# 代码审查问题清单(internal + pkg)
|
||||
|
||||
- 审查日期:2026-08-27(三轮)。第三轮(2026-08-27):核查前两轮修复落地情况 + 以"高内聚低耦合 / 简单实现复杂化 / 过分拆分 / 包归属(可抽 pkg/utils)"为重点重审全库
|
||||
- 审查方式:第一轮 codegraph + 5 路并行深读;第二轮 6 路逐文件深读;第三轮 5 路并行(修复核查 + 重审)。所有死代码结论均经全仓库 Grep 反查调用方验证;第三轮附带 `go build ./...` 编译验证通过
|
||||
- 第三轮修复核查结论:**S0×17、P0×6、P1-1/2/3/4/5/6/7/10、P4-4、S4 两条全部真实落地**(已从本文档删除,留痕见文末"已修复确认清单");**未修复**:S1 死代码大部分、P2/P3/P4 结构问题大部分、1-8/1-8b/1-9/2-10/3-8/3-9
|
||||
- 当前待办集中在:T0 修复回归缺陷 → S1/T3 死代码 → T1 过分拆分合并 → T2 归属移动 → S2 重复消除 → P2/P4 结构
|
||||
|
||||
优先级定义:P0 = 零风险可直接删;P1 = 低风险去重;P2 = 中风险删转发层;P3 = 涉及 import 路径批量修改的移位;P4 = 大动作结构合并/文档修正。
|
||||
|
||||
---
|
||||
|
||||
## P1 重复实现 / 双份维护(遗留)
|
||||
|
||||
| # | 问题 | 位置 | 建议 |
|
||||
|---|------|------|------|
|
||||
| 1-8 | DSN 一致性双写:`persistDatabaseConfig`、`refreshDatabaseSource`、`InitializeDatabase` 三处维护"结构化字段→Source"不变式,refresh 用"先清空再恢复"绕开 databaseDSN 短路 | internal/data/config_store.go:78-94;internal/data/initialization_backend.go:42-59,135-140 | 固化进 databaseDSN 唯一入口 |
|
||||
| 1-8b | "storage/email 缺省则沿用现值"策略散布 4 处(文件/DB 配置合并规则;原 5 处已收敛为 4 处,但无统一 MergeRuntimeConfig) | internal/config/runtime.go:275-286;internal/data/config_store.go:173-183;internal/data/initialization_backend.go:77-85,171-186 | config 包提供唯一 `MergeRuntimeConfig` |
|
||||
| 1-9 | config.Store 与 runtimeconfig.Store 各写一套同构的 listener 注册/通知/克隆机制(语义确有差异:文件全量 vs DB 集成窄通道,可辩护为有意分离,需明确决策) | internal/config/runtime.go:29,150-196;internal/integration/runtimeconfig/store.go:62-167;桥接靠 data 层三处 Replace(data.go:417、config_store.go:258、initialization_backend.go:214) | 抽泛型 notifier,或写决策注释固化现状 |
|
||||
| 1-11 | mq.Registry 接口面积翻倍且恶化:legacy API(Subscribe/Unsubscribe/Publish)内部转译为声明式 Register,双面并存;且整个 mq 体系业务消费者为零 | pkg/mq/mq.go;internal/integration/mq/emqx.go:539-589 | 见 T3:整体裁撤或收缩 |
|
||||
|
||||
## P2 转发 shim / 门面层(过度分层)
|
||||
|
||||
| # | 问题 | 位置 |
|
||||
|---|------|------|
|
||||
| 2-1 | payment 回调 ack 穿透仍四层(第三轮部分收敛:定义已单源化到 paymentkit、biz 用别名零拷贝;但 biz/service/handler 三层转发仍在,且 dto/payment.go:58-62 重复声明同构 3 字段结构体,加字段需同步三处) | pkg/paymentkit/callback.go:11-83 → biz/payment/payment.go:206-215 → service/payment/payment.go:131-148 → handler/payment.go:173-184 |
|
||||
| 2-2 | integration/payment/result.go 自称 "Compatibility shims":11 个纯转发函数全部仍在(normalizePaymentStatus/parseIntegerAmount/jsonObject/nestedString 等) | internal/integration/payment/result.go:68-110 |
|
||||
| 2-3 | service 根包 25 个 `type X = systemservice.X` 别名门面 + 函数转发(迁移脚手架,注释自辩 "one import instead of four";使 handler 无法感知真实包结构) | internal/service/service.go:18-51 |
|
||||
| 2-4 | handler/http.go 纯转发别名层:3 常量+2 类型+6 函数一对一转发到 pkg/httpx 与 middleware.Claims,仅为省一个 import(每个 handler 文件仍要同时认识两个包) | internal/server/handler/http.go:14-29 |
|
||||
| 2-5 | service/task 双面 API:DO 签名版与 DTO 版并存;DO 版 Create/Update/Tasks 仅被同文件 DTO 版内部调用,handler 只用 `*Request` 版 | internal/service/task/task.go:35-46 |
|
||||
| 2-6 | TaskUsecase 嵌入透传且加重:仍嵌入 TaskRepo 透传 9 方法给 worker;新增 TaskApplicationUsecase 再包一层,其 6 方法纯转发——双 usecase 并存(worker 持前者、service 持后者,wire_gen.go:115-119) | internal/biz/task/task.go:76-79,159-237 |
|
||||
| 2-7 | `openWithDriver` 单调用点便捷转发(第三轮部分修复:已拆出 openWithDriverConfig;wrapper 仍有 1 个生产调用 + 约 20 处测试调用) | internal/data/database.go:134-136(生产调用 :248) |
|
||||
| 2-8 | Backend 接口三层缝合:biz `InitializationRepo` → initialize.Repo(内嵌 6 方法 Backend + seed 回调单传入点)→ data.Data;initialize 包只为"转手 6 个同名方法 + 注入 catalog"存在 | internal/initialize/initialize.go:17-46;internal/data/initialization_backend.go:17-217;cmd/wire.go:45 |
|
||||
| 2-9 | data-scope 审计回调 `dataScopeAuditEnqueue` 穿透 6 层签名,生产实现唯一 | internal/data/data.go:238,245,392 → runtime_clients.go:168 → data_scope.go:19-136 |
|
||||
| 2-10 | `*Data` 方法约 10 处模板式 `if d == nil` 防御(构造归 Wire 管理,不可能为 nil);`NewIntegrationRuntime` 的 nil→空 Store 回退掩盖错误状态 | internal/data/data.go:41,72,82,88,97,119,126,373-375;initialization_backend.go:18 |
|
||||
|
||||
## P3 包归属问题(pkg 应为可对外复用、无业务语义)
|
||||
|
||||
| # | 包 | 问题 | 建议 |
|
||||
|---|----|------|------|
|
||||
| 3-1 | pkg/paymentkit | 目录名≠包名(实际 `package paymentutil`);17 个支付渠道常量是本项目商户目录(与 biz/integration 中文渠道定义一一对应);回调 ack 硬编码微信/支付宝协议、中文业务文案;调用方 100% 在 internal | 整体并入 internal/integration/payment,顺带消灭 17 常量双份导出(biz/payment/payment.go:20-43 逐个重命名再导出一遍)、三层金额/回调转发、text/firstAny 双份 |
|
||||
| 3-2 | pkg/logging | source.go:54-61 硬编码 internal 目录布局(效果等同反向依赖);zap.go:346-383 中文上报文案、:342 特判遗留 gva 项目文件名;AGENTS.md 声称的 `internal/logging/` 目录实际不存在 | 内移为 internal/logging,同步修 AGENTS.md |
|
||||
| 3-3 | pkg/httpx | response.go:17 业务状态码 10001(PasswordChangeRequired);:60-69 硬编码 `x-token` cookie 契约;调用方全部在 internal/server | 移入 internal/server |
|
||||
| 3-4 | internal/initialize/configuration.go | `management*` 家族(:221-318)手工构造 camelCase JSON,属 service 层 DTO 塑形职责;根因是 biz `InitializationRepo` 以 `json.RawMessage` 为出入参,表现形状泄漏进 repo 层 | JSON 形状定义移至 internal/service/dto;snakeCase/normalizeDuration 等纯函数与掩码逻辑分文件(当前 464 行混杂三种职责) |
|
||||
| 3-5 | pkg/database/pagination、gormkit | 调用方 100% 在 internal/data(分页纯数学无业务语义,轻度) | 可下沉 internal/data(低优先级) |
|
||||
| 3-6 | pkg/mq | rabbitmq.go:127 硬编码项目名前缀 `kra-`;subscription.go 的 Contributor 面向本项目 module 概念 | 去项目化(低优先级) |
|
||||
| 3-7 | pkg/module、pkg/task、pkg/database/migration | 只被本仓库消费,但互为依赖构成同层契约组(module 依赖 migration/task,单独搬会破坏依赖方向一致性) | 维持现状 |
|
||||
| 3-8 | internal/data/runtime_clients.go | `newReloadableDB` 在通用热切换构造器里调用领域函数 `registerDataScopeCallbacks`,越界 | 该调用移到 data.go 与 replacePrimaryDB 同址 |
|
||||
| 3-9 | internal/data/data_scope_record.go | 整文件只有一行类型别名 `dataAccessLogPO = datasystem.DataAccessLogPO` | 删除文件并入 data_scope.go |
|
||||
|
||||
## P4 结构性设计(双份数据 / 文档漂移,大动作需决策)
|
||||
|
||||
| # | 问题 | 位置 | 建议 |
|
||||
|---|------|------|------|
|
||||
| 4-1 | router 与 routecatalog 双声明:21 个 router 文件纯声明式注册 method+path;routecatalog 又用一张 map 声明同一批路由的元数据(public/audit/group),靠契约测试强制对齐——改一条路径要同时改两处 | internal/server/router/*(routes.go:22-43 手工 21 连调);internal/routecatalog/catalog.go:41- | 表驱动合并为单一声明源(method+path+handlerFunc+元数据),可同时消掉对齐测试 |
|
||||
| 4-2 | 新增一个资源实际要触碰 7 处:dto、service/system、handler、router/资源文件、router/routes.go、routecatalog、Set/provider | — | 同 4-1 |
|
||||
| 4-3 | 同一份集成配置三种形状两条通道:`config.Storage` 强类型 ↔ `sys_integration_configs` JSON 行 ↔ 运行时客户端;storage/email 走 config.Store(email 即时读快照、storage 手动 Replace),mq/websocket 走 runtimeconfig 订阅——同一"集成"概念两套配置源两种重载模式 | internal/data/integration_config.go:43-104,157-179;internal/data/integration/runtime.go:13-29;storage/reloadable.go:18-32;email/email.go:25-34 | 评估统一为一条解析-分发通道 |
|
||||
| 4-5 | swagger 运行时文档:约 160 行 map 手拼 Swagger 2.0 JSON + 正则加工,全局单例仍在 server 根包 | internal/server/swagger.go:21-159 | 可辩护(README 有意为之),但按 server/README 自己的规则更宜独立子包 |
|
||||
| 4-6 | 错误日志热路径做磁盘 IO + go/parser AST 解析(为错误上报附上出错方法源码) | pkg/logging/source.go:71-104;pkg/logging/zap.go:373-379 | 展示性需求不该在日志关键路径,缓存或降级 |
|
||||
| 4-7 | dto 包文件组织混乱:dto/system.go 混装 Login/User/ServerInfo 三域;dto/settings.go 横跨 Dictionary/SystemParameter/APIToken/SecurityConfig 四域(名不副实) | internal/service/dto/system.go:5-105;settings.go:5-172 | 按域拆分重命名 |
|
||||
| 4-8 | 校验双轨制:handler 手工 if 校验与 dto binding 标签并存;menu dto 无 binding 标签全靠 handler 手补 | internal/server/handler/user.go:27-64;internal/service/dto/settings.go:46-48 | 统一为 binding 标签 |
|
||||
| 4-9 | 单方法 handler 各占结构体+构造器+Set 字段:Session/Navigation 可并入相邻资源 handler(Set 已膨胀到 23 个字段、provider 23 个构造器) | internal/server/handler/session.go:10-12;navigation.go:9-11;set.go:3-26 | 合并 |
|
||||
| 4-10 | local 存储两套入口逻辑:staticfiles 直读 config 本地盘语义 vs integration/storage 的 Reloadable 体系 | internal/server/staticfiles/staticfiles.go:49-92;internal/integration/storage/local.go | 边界收敛(低优先级) |
|
||||
|
||||
## 审查后认为合理、不建议改动的部分
|
||||
|
||||
- **modules 与 routecatalog 分离**:前者是启动期模块装配(wire/种子/迁移),后者是请求期热路径策略查询(有 byMethod 桶等性能优化),消费方零重叠,仅存在 modules→routecatalog 的合理单向依赖。
|
||||
- **Provider 接口缝模式**(子包不反向 import 根 data 避免成环 + 各子包测试假 Data):模式本身正当,仅 1-1 所述两处重复需合并。
|
||||
- **config.Store(YAML 文件)与 runtimeconfig.Store(DB 集成表)职责分离**:不重叠,不建议合并包;仅需评估统一 pub-sub 实现(1-9)。
|
||||
- **utils/routepath 与 routecatalog**:互补非重复(前者剥离配置前缀供落库/Casbin 用,后者纯策略匹配)。
|
||||
- **TaskScheduler 多锁**:每处均有并发正确性注释论证,是真实并发需求的代价。
|
||||
- **worker→biz 正向依赖 + biz 经 TaskRuntime/TaskReloader 接口反向倒置**:整个任务链路最规范的一段。
|
||||
- **三个"registry"实为三种角色**(pkg/task=实现、biz/task=接口缝、worker=贡献者):不是重复机制,但命名误导(建议重命名 worker/task_registry.go)。
|
||||
- **data 根 11 个文件同包共享 Data 状态与重载锁**:符合 data/README 既定约定,不建议拆包。
|
||||
|
||||
---
|
||||
|
||||
## 第二轮深审发现(S 系列编号)
|
||||
|
||||
### S1 死代码补充(第一轮 P0 之外;第三轮核查:约 15 组仍存在,仅 PersistConfig 属误报已更正)
|
||||
|
||||
**biz 层死接口方法/死注入面**:
|
||||
- PermissionRepo 3/4 方法死(Buttons/SetAuthorityButtons/AuthorityButtonIDs)— internal/biz/system/permission.go:6-8
|
||||
- APITokenRepo.DisableAPIToken — api_token.go:26;UserRepo.CreateUser/UpdateUserWithAuthorities — user.go:45,49
|
||||
- MediaMetadataRepo.FindMediaByHash — media_metadata.go:44;MenuRepo.AuthorityMenuIDs — menu.go:61
|
||||
- SecurityUsecase.ActiveTokenMatches — security.go:243-249;EmailUsecase.Alert — email.go:31-48
|
||||
- PaymentUsecase 注册面全死:SetHooks/SetOrderSourceRegistry/SetFulfillmentRegistry/RegisterBusinessModule(含两阶段注册+回滚补偿)— biz/payment/payment.go:296-326;PayInternal/RefundInternal/AuthorizeRefund 无生产实现,service 却作为正式 API 暴露(Fulfill)
|
||||
|
||||
**data 层死实现**(与上述接口配套):data/system/export.go:430-440、user.go:317-319,474-491、api_token.go:108-114、media.go:67-73、permission.go:113-117、menu.go:283-287
|
||||
|
||||
**service 层死方法**:MenuService.List(menu.go:14-20,handler 实际用 Tree,连带 biz MenuUsecase.List 也死)、RecordLogin/RecordDataAccess(audit.go:56-58,91-93,后者整链含 biz 接口+data 实现全死)、IsTokenDisabled(api_token.go:60-62)、security_session.go 14 方法中 8 个死(:17-67)、AuditRecorder.CreateErrorRequest+recordedErrorDomain(audit_error.go:17-31)
|
||||
|
||||
**integration 层零消费者基础设施**:
|
||||
- mq legacy 发布/订阅 API 全链零生产调用(Publish/Subscribe/Unsubscribe 及 To 变体、Register/Unregister、订阅簿记/dispatcher/reconcile 约 350 行空转)— integration/mq/emqx.go:358-630;连带 cmd/main.go:69 `_ mq.Client` 幻影参数与 integration/provider.go:28,29 两条死 wire 绑定(外加 :31 的 platformws.Hub 死绑定)
|
||||
- websocket:Hub 接口零消费者(pkg/websocket/melody.go:28-40);integration 层 On* 四注册方法零调用(server.go:326-373),双层 handler 登记机制两层都永远为空(server.go:28-31,169-180 vs melody.go:65-118)
|
||||
- s3 多 provider endpoint 五个死分支(唯一调用点只传 "minio")— storage/s3_storage.go:29-41
|
||||
- namedClient/Client() 整型死代码 — emqx.go:632-650
|
||||
- 零散:wechat_v2.go:584 `stringOr`、paypal.go:651 `normalizePayPalOrderState`、paypal.go:388-391 `paypalOrderTradeNo`、result.go:98,148-155 `nestedString`/`amountFromDecimalField`(shim 连调用者都没有)、middleware/capture.go:72-74 `isBootstrapPath`
|
||||
|
||||
**仅测试调用(生产死代码)**:decryptWechatV3(wechat_v3.go:600-605)、alipaySignContent(alipay.go:572-585)、validatePaymentConfig(data/payment/payment.go:551-553)
|
||||
|
||||
**dto 死类型/死字段**:LoginLogRequest(dto/system.go:68-75)、DataAccessRecordRequest(dto/audit.go:21-30)、GetAuthorityButtonsRequest.Selected、MenuResponse.Authorities 恒 null(biz 无此字段)、DynamicMenuResponse.MenuButtons 恒 nil、SysBaseMenuID 输入被静默丢弃(dto/menu.go:21,28)、version 导出结构体大量零值噪声字段(ID:0/CreatedAt 零时间/authoritys:null,version.go:23-94)
|
||||
|
||||
### S2 重复模式补充(大块可消除,估算合计 1000+ 行)
|
||||
|
||||
- **data/system List 样板 13 处同构**(构建 db→逐字段翻译 filter→Count→分页→Find→循环 toBiz),可抽泛型 helper 消 200+ 行 — user.go:281-315、api.go:119-181、data_access_log.go:32-55、operation_log.go:39-68、error_record.go:83-109 等 13 处
|
||||
- **payment 渠道适配器家族重复**(合计约 500 行):下单方式归一化骨架 9 份(Replacer 归一化行逐字出现 9 次);退款身份校验 4 份同构;状态归一化 switch 10 份(可收敛为 `normalizeState(state, successWords, failedWords)` + 词表);金额拆分守恒 5 份 + biz 层再校验第 6 份;SDK 客户端构造 10 份同构;firstNonEmpty 三胞胎(alipay.go:475/qq.go:146/paypal.go:707);mustMarshalAlipayPayload ≡ mustJSON
|
||||
- **storage provider 家族重复**(约 200 行):key/unkey/file 三件套 5 份逐行相同(aliyun:35-48/aws:63-76/huawei:31-44/s3:61-74/tencent:45-58);DeletePrefix 分页循环 5 份(可提 `deletePrefixViaList`);Compose 一行委托 7 份;limit 守卫 7 份;构造尾部样板 5 份
|
||||
- **handler 四段式样板约 70 处**(ShouldBindJSON→Fail→service→Write),抽 `bindJSON`+`respond` 两个 helper 可消一半
|
||||
- **biz 接口嵌入透传 12 处 usecase**(API/APIRepo/Token/LogViewer/Audit/AuditRecorder/Authority/Dictionary/Export/Media/Parameter/Permission/Position/Version),与同包 9 个私有字段风格并存
|
||||
- **middleware**:请求体读取三处(access_log.go:56-74 / audit.go:49-67 fallback / handler/media.go:17-21,同一请求体三层 MaxBytesReader);脱敏/截断三处且分散(capture.go:41-68 / audit.go:118-136 / redact 词表在 redact.go 但函数散在 access_log.go);双重脱敏双重截断(audit.go:47-48,93 对已 mask 已截断的正文再处理一遍)
|
||||
- mq/websocket 两包各写一套 map 解码 helper 且逐字符相同(emqx.go:226-260 vs server.go:189-240);TestConfig 探测骨架三处同构(emqx.go:92-133/server.go:65-126/connectivity.go)
|
||||
- authority 树构建算法两份(biz authority.go:48-78 vs menu.go:80-99);CreateAuthority/CopyAuthority 前 8 行校验逐字重复(data authority.go:133-151,211-245)
|
||||
- service:authorityResponse 与 convertAuthority 逐字重复(authority.go:30-36 vs user_conversion.go:8-14);"单条 DTO helper + for-append"样板 9 处;"Request 包装 + Filter 包装 + 裸方法"三重入口家族(audit/parameter/export/dictionary/media);user 空对象兜底三连
|
||||
- data/payment:callbackFields/first 整函数复制(payment.go:507-540 vs result.go:14-56);values() 与 testRow() 近重复;渠道"免 notify_url 方法"同义词表在 data 层重抄一份(payment.go:390-426,与各适配器 createMethod 表双份维护)
|
||||
- gorm.DeletedAt→*time.Time 转换 4 处逐字重复(user.go:99-104、menu.go:17-21,241-245,250-255)
|
||||
- version 阶段→消息映射两份(handler version.go:107-121 vs 158-167);engine.Routes()→dto 转换两份(api.go:161-167 vs public.go:135-139)
|
||||
- defaults 合并逻辑三层三份(service/integration:86-98 / biz/integration:215-224 / data/integration/migrations.go:69-75)
|
||||
- media 分片根目录逻辑两份(biz media.go:126-132 vs media_upload.go:61-69)
|
||||
- media_upload 三处复制会话校验样板(biz media_upload.go:120-129,176-184,244-250,顺带吞掉 ErrUploadSessionNotFound 可判定性)
|
||||
|
||||
### S3 分层/归属违规补充
|
||||
|
||||
- **data 直依赖 integration**:data/payment/payment.go:17 import `kra/internal/integration/payment`;TestProvider 是 145 行业务编排(本地建单、500ms×3 重试轮询、退款闭环)长在 data 层;还自建第二 paymentOrderRepo 实例绕过 wire 单一构造点
|
||||
- **service 层混入业务/存储细节**:system_init.go:13-26 DSN/驱动连接串与回退规则;system_info.go:14-41 直连 gopsutil 采集(每请求阻塞 200ms);export_excel.go:63-71 SQL 别名/前缀归一;api_token.go:28-42 发币编排与到期规则;user.go:36-37,85-96 密码策略编排;api.go:60-82 Groups 分组推导;version.go:184-221 导入导出全编排
|
||||
- **biz DO 带 json 标签**:PaymentResult/PaymentTestResult(死标签,service 逐字段转 dto);PaymentRequest(把指纹编码格式锚死在 DO);biz/integration 的 Definition/Field/Option 家族直接充当前端契约(dto/integration_config.go:21 内嵌 biz 类型,biz 事实上兼任 DTO 提供方)
|
||||
- **DO 兼过滤器**:API.OrderKey/Desc/StrictAll、SystemParameter/ExportTemplate.StartCreatedAt/EndCreatedAt 混入实体(LoginLog 已改为独立 Filter)
|
||||
- **middleware 硬编码业务语义**:error_audit.go:70-77 靠中文消息黑名单判断是否审计(改文案即改审计行为);error_audit.go:25,55-67 硬编码业务路径;access_log.go:82-203 支付回调专用逻辑内嵌通用中间件;rate_limit.go:33-51 限流策略参数内联;A8 限流挂在全局链却只匹配两个 public 路由
|
||||
- **biz 契约泄漏存储/表现原语**:QueryExport 返回 `[]map[string]any`;LogViewer 的文件读取器细节(NextCursor/LimitedByBytes);export DO 字面携带 SQL/Join/Table 片段(Export 域整体是查询引擎不是领域逻辑,应下沉 data);UserOptions `{Label,Value}` UI 形状进 biz;AuthenticationResult 携带含密码哈希的完整 User
|
||||
- **data 层纪律**:转换函数命名四种风格违反 new<X>/toBiz 契约(FromPO×16/ToPO×4/ToBiz×2/new×2);PO 分布无规则(models.go 集中 6 个 + 散落 30+,models.go:98-100 还混 repo 声明);Table("字符串") 绕过已有 PO 7 处(user.go:58,70,88,231,259、announcement.go:100、security.go:92);saveRelations 回写入参 DO 约 10 处;audit.go 名不副实(只有构造器,实现在 5 个文件);runtime.go 拼盘(settings + tokenIssuer 无关联)
|
||||
- **编排类文件过重**:seedSystem 单函数 126 行 10 类职责(seed.go:30-156);authority.go 834 行四类职责(CRUD/严格权限引擎被 4 个文件 15+ 处借用/DataScope 域解析/用户-角色关联,权限引擎应独立 accessGuard);BuildVersionBundle 105 行五职责(version.go:76-181);migrations.go 两个通信 surface 迁移互为重复子集(:36-130 vs :132-173)
|
||||
- **dto 契约问题**:ID 类型三处分叉(int/uint/string 混用,迫使 handler 做转换);AuthorityResponse.DeletedAt 泄漏且破坏全库 `json:"-"` 约定(dto/authority.go:34);ErrorRecordMutationRequest 一半指针一半值不自洽;DTO 反向依赖 biz 类型(dto/integration_config.go:21)
|
||||
- **错误体系**:错误定义双体系(errors.go 仅 3 个 kratos 类型错误,其余 stdlib errors.New 散落 13+ 文件,无 reason 码);错误包装 Error+Unwrap 与 Error+Is 两机制混用;PasswordPolicyError 类型定义在 service(security.go:11-19)
|
||||
- **List 契约三种风格并存**(过滤结构体内含分页 / 位置参数+指针 DO / 裸标量 5-6 参)
|
||||
- **data 分页三风格**(pagination.ApplyRequired/Apply/手写 Limit-Offset),手写版 page=0 产生负 offset(position.go:89、export.go:208-210 无防护,media.go:112-115 有)
|
||||
|
||||
### S4 简单实现复杂化补充(遗留)
|
||||
|
||||
- payment Create 过度防御:先拷 9+1 字段再逐一回比 + Extra 双次 JSON 序列化,同一不可变保证指纹层(RequestFingerprint + 落库后二次指纹比对)已做两道 — biz/payment/payment.go:385-402,898-907,588-590
|
||||
- loadUser/loadUsers 双实现(单实体 6 次串行查询 vs 批量实现,可复用)— data/system/user.go:53-97,115-218
|
||||
- JWT 签名双重检查(validateSigningOptions 后 signToken 再查一遍)— data/system/token.go:34-51
|
||||
- 中间件 nil 防御四处(gin.go:28-30 已兜底,access.go:33/access_log.go:36/audit.go:27/cors.go:22-25 仍各自检查)
|
||||
- public.go captchaConfig 恒真分支与无效首调用(public.go:29-43)
|
||||
- 媒体上传三重大小防御(limitMultipartBody + rejectMediaTooLarge + header.Size 检查)— handler/media.go:38-50,226-240
|
||||
- emqx.go 恒真 ctx 判断(:561-569,592-600)
|
||||
- PaymentLogger 单实现接口(仅为包装 *slog.Logger)— biz/payment/payment_log.go:10-32
|
||||
- dictionary.go Tree 解析结果被丢弃(handler dictionary.go:211-213,byType 分支不用 id/parseErr)
|
||||
- DictionaryRepo/MediaRepo 组合式子接口无独立消费方(biz dictionary.go:56-59、media.go:19-22)
|
||||
|
||||
### 第二轮结构性发现(遗留)
|
||||
|
||||
- **mq 与 websocket 是"零消费者基础设施"**:大量生命周期/重放/簿记机制空转,要么接入首个真实业务消费者,要么裁掉 legacy 半区保留最小面(TestConfig 探测 + Enabled/Path/HandleRequest)
|
||||
- **payment 域 data/integration 边界倒置**:paymentRepo 实为"配置读取+适配器编排+ack 组装"的编排层,仅 4 个纯转发方法符合 repo 形态;渠道知识(同义词表)在 data 与 integration 双份维护必然漂移
|
||||
- **gopay.go/gopay_helpers.go 名实相反**:gopay.go 是窄基座(4 个函数);gopay_helpers.go 是杂物间(7 类职责混装,渠道专属谓词/状态机应下沉各渠道文件,公共函数 mergeMap 反而散在 alipay.go)
|
||||
- **vendor.go**:vendorSupperPay 是死枚举(26,38 定义注册,switch 无 case);金额拆分 14 键配置 DSL 疑似投机通用性(无内置默认使用);通用渠道定义 18 字段全 Required=true
|
||||
- **gva/ 目录是遗留参考库**(独立 module 不参与 kra 编译),pkg/logging 为其保留文件名特判(zap.go:342)
|
||||
|
||||
---
|
||||
|
||||
## 第三轮审查发现(T 系列编号,2026-08-27)
|
||||
|
||||
### T0 修复回归缺陷与残留(S0 修复核查时发现,优先处理)
|
||||
|
||||
| # | 问题 | 位置 |
|
||||
|---|------|------|
|
||||
| T0-1 | **vendor 退款静默受理残留(资金安全,高危)**:S0-1 修复覆盖了"FAIL 状态字段"场景,但 create/refund 响应 HTTP 200 且 body 非 JSON、或 JSON 无可识别状态字段时仍默认 `created`;biz `validatePaymentRefundResult` 接受 created → 真实被渠道拒绝但响应无状态字段的退款会被永久记为已受理。建议:refund 端点响应无状态字段时报错或至少 pending | internal/integration/payment/vendor.go:135-163;biz/payment/payment.go:1020-1021,837-855 |
|
||||
| T0-2 | Excel `[]byte` 全转文本副作用:MySQL 数值列也会变文本单元格,大面积"数字以文本存储"警告(原值保真达成,展示体验回退);可结合模板列类型区分处理 | internal/service/system/export_excel.go:85-86 |
|
||||
| T0-3 | ErrAuthoritiesRequired 错误透传缺失:biz 返回具体错误后 handler 只回"修改失败",用户看不到"至少一个角色";data 层 setUserAuthorities 还残留一处重复中文防御错误(双轨) | internal/server/handler/user.go:208-212;internal/data/system/user.go:563-565 |
|
||||
| T0-4 | 版本导入非原子残留:落库成功但留痕 CreateVersion 失败时返回错误,用户误判"导入失败"(幂等查重使重试可控) | internal/service/system/version.go:256-270 |
|
||||
| T0-5 | 任务元数据双源漂移已发生:种子描述("**定时**清理数据库过期日志…")与注册方法描述("清理数据库过期日志…")不一致——两处维护必然继续漂移;建议 worker 注册时复用 catalog TimedTask 元数据 | internal/modules/task/definition.go:14-15 vs internal/worker/task_registry.go:31,45 |
|
||||
| T0-6 | export SQL/ImportSQL 摆设字段:dto 保留但 ValidateExportTemplate 拒绝非空,只能提交空值且响应回显空值 | internal/service/dto/export.go:26-27;internal/biz/system/export.go:72-74 |
|
||||
| T0-7 | payment 回调读体失败分支未入 Gin 错误链(S0-12 修复只覆盖 service 调用错误) | internal/server/handler/payment.go:149-153 |
|
||||
| T0-8 | LoginLogFilter 收敛为仅 Username/Status 两字段:若前端需按 IP/时间筛选登录日志则能力缺失(设计取舍需确认) | internal/biz/system/audit.go:42-45 |
|
||||
| T0-9 | 任务种子与注册方法的"必经链路"提示:PaymentUsecase 注册面(SetHooks 等 4 个方法)零调用时,生产装配下支付主链路 Create/Refund/Fulfill 必然在 preparePaymentRequest 报"支付业务订单来源未注册"——模板未完成态,建议 Wire/cmd 层提供默认注册或 fail-fast 提示 | internal/biz/payment/payment.go:296-326,407-410 |
|
||||
|
||||
### T1 过分拆分清单(用户重点维度,量化)
|
||||
|
||||
**根因模式三条**:①零逻辑 usecase 壳(wire 强制每域一个构造器放大);②"每资源 N 文件"机械切分(dto+biz+service+handler+router 各一个);③为 import 美观引入的中间缝合包/门面。
|
||||
|
||||
**A. biz/system:38 文件 3430 行,其中 17 个非测试文件 <60 行(合计约 575 行,保守可归并 8-10 个文件)**:
|
||||
|
||||
| 文件 | 行数 | 内容 | 合并目标 |
|
||||
|---|---|---|---|
|
||||
| errors.go | 14 | 4 个错误变量(token.go:35-42 另有 6 个,同类分散) | 并入 user.go 或统一 errors |
|
||||
| cache.go | 15 | Cache 接口 4 方法 | 并入 security.go(主消费者) |
|
||||
| maintenance.go | 19 | Repo 接口+纯透传壳 | 并入 user.go 或保留(worker 消费) |
|
||||
| actor.go | 19 | ctx 携带 helper | 并入 authority.go(唯一消费者) |
|
||||
| access_control.go | 20 | 透传壳 2 方法 | 并入 authority.go/api.go |
|
||||
| storage.go | 26 | FileStorage 接口 | 并入 media.go(主消费者) |
|
||||
| data_scope.go | 27 | DataScope 类型+ctx helper | 并入 authority.go |
|
||||
| upload_session.go | 38 | DO+Repo 13 方法无 usecase | 并入 media_upload.go(同域) |
|
||||
| parameter.go | 33 | DO+Repo+零方法壳 | 壳删后 28 行 |
|
||||
| settings.go | 43 | 3 设置类型+接口 | 可保留(wire 5 处消费) |
|
||||
| department.go | 46 | DO+Repo+改名壳 | 壳删后 32 行 |
|
||||
| position.go | 48 | DO+Repo | 与 department.go 合并为 organization.go |
|
||||
| token.go | 48 | AuthClaims+TokenIssuer | 并入 authentication.go(唯一 biz 消费) |
|
||||
| media_metadata.go | 53 | DO+Repo | 并入 media.go(media 域 4 文件最典型同域碎片) |
|
||||
| email.go / api_token.go / version.go | 48/55/57 | — | 保留(有逻辑)或去壳 |
|
||||
|
||||
**B. 纯透传壳 usecase 12 个**(整个 struct 无自有逻辑/纯改名转发,service 可直依赖 biz repo 接口——repo 接口仍在 biz,分层契约不破;wire 链物证 wire_gen.go:88-91 permission 全程零逻辑):
|
||||
ParameterUsecase(parameter.go:29)、PermissionUsecase(permission.go:14)、VersionUsecase(version.go:55)、AnnouncementUsecase(announcement.go:39,7 方法全一行透传)、MaintenanceUsecase(maintenance.go:11)、AccessControlUsecase(access_control.go:5)、DepartmentUsecase(department.go:34,2 改名)、AuthorityUsecase(authority.go:42,仅 Tree 有逻辑)、TaskUsecase(task.go:76)、MenuUsecase(menu.go:72,10 处透传)、UserUsecase(user.go:59,13 处透传)、SystemConfigUsecase(system_init.go:50,7 处透传)。
|
||||
对照组(有真实逻辑应保留):Authentication/Security/Media/Email/IntegrationConfig/Payment/TaskApplication。
|
||||
|
||||
**C. service/system 碎片**:
|
||||
- SystemConfigService 一型拆四文件:system.go(19)+system_config.go(26)+system_init.go(38)+system_info.go(42)=125 行 4 文件
|
||||
- security_session.go:17-68:SecurityService 14 方法全部一行透传(零 DTO 工作)
|
||||
- audit.go 与 audit_error.go 同属 AuditService/AuditRecorder 可合并(audit_log_file.go 是独立 LogViewerService,保留)
|
||||
- email.go(16)/permission.go(24)/access_control.go(28,仅 5 行逻辑) 近纯透传小文件
|
||||
|
||||
**D. router 碎片**:22 文件 464 行,平均 21 行/文件(最小 email.go 12 行);routes.go:18-43 手工 21 连调——纯注册碎片无内聚(与 4-1 表驱动合并一并解决)。
|
||||
|
||||
**E. 单符号包/微文件**:
|
||||
- internal/modules/surface:整包只有一个 10 行函数(surface.go:10-19),2 个调用方
|
||||
- internal/data/provider:整包只有一个 7 行 2 方法接口(provider.go:5-8)——中性缝可辩护,建议与 provider.go 别名使用方注释互指
|
||||
- provider.go+providers.go 双小文件模式 ×4 子包(integration 12+5 行、payment 8+5、task 7+5、system 17+29)→ 8 文件并 4
|
||||
- data/system/bootstrap.go 27 行仅 seed.go 使用可并入;worker/worker.go 7 行、initialize/provider.go 5 行、pkg/task/provider.go 3 行、biz/payment/provider.go 8 行均为纯 wire-set 微文件
|
||||
- data_scope_record.go 整文件 1 行别名(同 3-9)
|
||||
|
||||
**F. 巨微并存两极**:
|
||||
- biz/payment/payment.go 单文件 1150 行(usecase+常量再导出+5 validator+指纹工具)vs 同域 payment_log.go 32 行/provider.go 8 行微文件
|
||||
- dto:authentication.go 8 行/email.go 7 行超小文件 vs settings.go 170 行横跨四域(同 4-7)
|
||||
|
||||
### T2 包归属移动建议(从 → 到)
|
||||
|
||||
| # | 从 | 到 | 动作 | 影响 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | pkg/httpx | internal/server/httpx | 整包内移(消费者 100% 在 internal/server 7 文件;中文文案+x-token 是本项目契约;logging/source.go:59 已预留该路径 marker) | 7 文件 |
|
||||
| 2 | pkg/logging | internal/logging | 整包内移(source.go:47-69 硬编码本仓库 internal 路径;zap.go:366-382 中文文案;模块路由硬编码服务日志查看器语义) | 约 5 文件 |
|
||||
| 3 | pkg/module | internal/modules | 契约内移(Menu/API/Surface/TimedTask 是本项目模块系统契约;import gin;15 消费者全在本仓库) | 15 文件 |
|
||||
| 4 | pkg/paymentkit 渠道常量(provider.go) | internal/biz/payment 本体 | 常量归位并删 biz/payment/payment.go:20-43 的 17+2 个别名转发层;目录更名 paymentutil 对齐包名;通用工具(signing/json/amount/xml)可留 pkg 或并入 internal/utils | 约 8 文件 |
|
||||
| 5 | pkg/mq + pkg/websocket(Hub) | 收缩或合并进 internal/integration/mq | 全链零业务消费者(详见 T3);保留 TestConfig 探测+基础驱动,删 Registry/Client()/namedClient/legacy API/Hub/ApplySubscriptions | 约 7 文件 |
|
||||
| 6 | integration/mq emqx.go:226-260 与 integration/websocket server.go:189-240 的重复 JSON map helper | internal/utils/jsonvalue | 上收去重(configText≡text、configBool≡boolValue 逐字符相同) | 2-3 文件 |
|
||||
| 7 | storage:aws-sdk-v2 栈与 minio-go 栈双 S3 实现并存 | 统一 S3 兼容单栈(可选) | qiniu/aliyun/huawei/tencent 原生 SDK 均有 S3 兼容端点,可收敛删 4 实现+s3 死分支 | 5-6 文件 |
|
||||
| 8 | handler/query.go:13-61(Gin 工具)、middleware/request.go:67-102(纯函数)、handler/announcement.go parseTime(被 export.go 跨域借用) | pkg 或 internal/utils 候选 | 通用无状态逻辑上收 | 3 文件 |
|
||||
|
||||
依赖方向合规确认(第三轮验证):pkg 无 import internal(仅 zap_test.go:131 字符串字面量);integration 无 import data/service;service→biz→data 无反向。
|
||||
|
||||
### T3 死代码回归与新增(S1 现状核查)
|
||||
|
||||
- **S1 清单约 15 组死方法仍存在**(biz 接口+data 实现+service 包装三层残留);已更正误报:service 层 PersistConfig 实为活代码
|
||||
- mq 零消费者机制未清理且**规模扩大**:emqx.go 约 710 行 + mq.go 106 行 + subscription.go 55 行(声明式订阅全套无注册者);legacy API 转译层又加了一层(1-11 恶化)
|
||||
- websocket Hub 死绑定、s3 五个不可达 endpoint 分支(local.go:39-48 只让 minio 进 newS3Storage)、namedClient/Client()、`_ mq.Client` 幻影参数(main.go:65)、isBootstrapPath 死函数——全部未清理
|
||||
- 新增死代码:wechat_v2.go:47 私有 wechatV2Sign 变为仅测试调用(可移测试文件);pkg/task RegisterAll 仅测试调用;**NewGinEngine 生产死代码**(gin.go:23-25,wire 用 NewGinEngineWithRuntime);TaskScheduler.Trigger(task) 可未导出(仅 TriggerID 内部用)
|
||||
- P1-3 修复残留:result.go:58-64 与 payment_helpers.go:5-11 两包仍保留同名本地包装转发 paymentutil(micro-shim 未拆)
|
||||
|
||||
### 已修复确认清单(第三轮验证后从本文档删除,留痕)
|
||||
|
||||
- P0 全部 6 条:protoutil 整包删除;WechatV2Sign 死实现删除;Data 三死方法删除;Bootstrap 别名删除;静态任务注册链删除(重建为活的 TimedTasks 单源链路);Schedule(task) 删除
|
||||
- P1-1/2/4/5/6/7/10:provider.Database 中性接口(task/security 两处仅剩一行别名);种子单源化(含防回归测试);validateMenuRequest;surface.APIsForPrefix;persistConfig Locked 变体删除;NewData/reloadConfig 均用 rollback 收集器;websocket snapshotHandlers 泛型收敛
|
||||
- P1-3:paymentkit.Text/FirstText 落地(残留 micro-shim 见 T3)
|
||||
- P4-4:AGENTS.md 已更新为真实栈
|
||||
- S0 全部 17 条(含回归验证):登录日志三态过滤、claims 注入、时间戳服务端生成、x-user-id 删除+CORS 收紧(含防回归测试)、export 安全查询构建器(双保险)、ListAuthorities 委托统一入口、版本错误返回、失败关闭、SecurityConfig nil+error、限流白名单闭环、回调错误入链、Excel 文本保真、Apple JWS 固定 Root CA G3 真实指纹(经官方根证书清单核实)+完整链校验+签名时刻验证、ErrAuthoritiesRequired、OriginSetting 错误传播、cache Lua 原子补 TTL(语义正确)
|
||||
- S4 两条:QueryExport 单次严格解析;saveRelations 单 replace 参数
|
||||
|
||||
## 处置建议总览(三轮合并,按优先级)
|
||||
|
||||
1. **先修 T0 修复回归缺陷**(T0-1 资金安全高危优先)
|
||||
2. **删 S1+T3 死代码**(纯减法零风险:biz/data/service 三层死方法约 15 组 + mq/websocket 零消费者机制 + 微死码,估算 800+ 行)
|
||||
3. **T1 过分拆分合并**(透传壳 usecase 12 个、biz/system 小文件归并、SystemConfigService 四合一、provider/providers 双文件 ×4——零行为变更的文件级减法)
|
||||
4. **T2 归属移动**(httpx/logging/module/paymentkit 内移 + jsonvalue 上收,按表逐项决策)
|
||||
5. **S2 重复消除三大块**(data/system List 泛型、payment 渠道骨架、storage provider 基座,估算 900+ 行)
|
||||
6. **P2/P4 结构收敛**(service 根门面、Backend 三层缝合、router+routecatalog 表驱动合并)
|
||||
@@ -0,0 +1,87 @@
|
||||
# `` 到 `pkg` 复用性审查
|
||||
|
||||
审查原则:公共包只提供跨模块稳定的机制、协议或无状态纯函数;不能依赖
|
||||
`app/*/internal`,也不承载 system 的业务表、用例、provider 客户端或运行时
|
||||
配置。
|
||||
|
||||
## 已抽取到 `pkg`
|
||||
|
||||
| 能力 | 公共位置 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| HTTP JSON 响应契约 | `pkg/httpx` | `Response`、`PageResult`、状态码和 Gin 响应助手;`server/httpx/response.go` 仅保留 system 适配。 |
|
||||
| protobuf JSON 局部合并 | `pkg/protoutil` | 与业务无关的字段归一化和局部反序列化;初始化直接使用公共包。 |
|
||||
| 支付 provider/mode 标识 | `pkg/paymentkit` | provider 常量、支持列表、金额/签名/JSON 等跨模块协议;`biz/payment` 与 `biz/integration` 直接复用。 |
|
||||
| 支付回调 ACK | `pkg/paymentkit` | 回调应答、失败包装和默认 provider 应答;具体渠道 SDK 留在 `internal/integration/payment`。 |
|
||||
| WebSocket 通用收发 | `pkg/websocket` | Melody 的连接、事件、点对点发送、广播和会话查询封装;`internal/integration` 管理配置与生命周期。 |
|
||||
| 消息队列 | `pkg/mq` | Broker 无关的发布、订阅、JSON 和 QoS 接口;EMQX/Paho 与 RabbitMQ/AMQP 客户端由 `internal/integration` 管理。 |
|
||||
| 模块、任务和迁移协议 | `pkg/module`、`pkg/task`、`pkg/database/migration` | 供不同业务模块注册贡献,不带 system 业务语义。 |
|
||||
|
||||
## system 内部保留边界
|
||||
|
||||
- `app`:运行时组合根,汇总依赖注入后的路由和任务贡献。
|
||||
- `modules`:静态模块 catalog,汇总各模块迁移、菜单、API 和默认任务。
|
||||
- `modules/system`:system 模块的 Definition,声明系统表迁移。
|
||||
- `modules/integration`:integration 配置迁移和管理面贡献。
|
||||
- `modules/task`:定时任务迁移和默认任务贡献。
|
||||
- `modules/payment`:payment 模块的 Definition,声明支付迁移和支付管理面。
|
||||
- `biz/system`:用户、权限、菜单、审计、媒体和系统配置等系统领域模型与用例。
|
||||
- `biz/payment`:支付订单、支付流程、支付接口和支付日志。
|
||||
- `biz/integration`:支付/消息队列/WebSocket 集成配置定义与校验。
|
||||
- `biz/task`:定时任务模型、任务用例和任务注册协议。
|
||||
- `conf`:system 配置 proto、运行时快照和生成代码。
|
||||
- `data`:共享数据库生命周期与配置 watcher;PO/仓储按 `system`、`integration`、`task`、`payment` 子包隔离;后台 JWT claims 与签发/解析位于 `data/system/token.go`。
|
||||
- `initialize`:首次安装、配置迁移、种子编排和运行时重载。
|
||||
- `integration`:Redis、邮件、存储、支付、WebSocket、EMQX 和 RabbitMQ 的 provider 生命周期。
|
||||
- `routecatalog`:HTTP 公开性、操作审计、请求体策略和 API 分组/说明的统一目录。
|
||||
- `service`:HTTP DTO(`service/dto`)、DTO 与 DO 转换和应用服务。
|
||||
- `server`:Gin 生命周期;handler、middleware、router、HTTP 适配按子包维护。
|
||||
- `worker`:任务调度、执行器、SSE 订阅及其并发状态。
|
||||
|
||||
这些代码都带有 system 的 API、配置、数据表或生命周期语义,不应为了减少
|
||||
文件数量搬到 `pkg`。
|
||||
|
||||
## 目录分类约定
|
||||
|
||||
```text
|
||||
internal/
|
||||
app/ # 应用组合根
|
||||
modules/ # 静态 catalog、业务模块定义及其模块级贡献
|
||||
biz/
|
||||
system/ # 系统领域
|
||||
payment/ # 支付领域
|
||||
integration/# 集成配置领域
|
||||
task/ # 定时任务领域
|
||||
config/ # Viper 配置、快照和热更新
|
||||
global/ # 进程级共享资源入口
|
||||
data/
|
||||
system/ # 系统表与系统仓储,含后台 JWT token.go
|
||||
integration/# 集成配置表与仓储
|
||||
task/ # 定时任务表与仓储
|
||||
payment/ # 支付表与仓储
|
||||
initialize/ # 首次安装和配置编排
|
||||
integration/ # 外部 I/O provider
|
||||
server/ # Gin 生命周期,内部按 handler/middleware/router/httpx 分类
|
||||
service/ # 应用服务;DTO 集中在 dto 子包
|
||||
worker/ # 任务运行时
|
||||
```
|
||||
|
||||
技术角色使用子目录表达,子目录内部使用资源名,例如 `handler/payment.go`、
|
||||
`router/payment.go`、`dto/payment.go`。只有少量代码且没有独立边界时不建立
|
||||
新包;同一角色文件较多时也不应全部堆在父目录。
|
||||
|
||||
## 暂不抽取的候选
|
||||
|
||||
1. token cookie 名称、SameSite 和反向代理策略:目前是 system 认证策略。
|
||||
2. operation-audit 脱敏和请求采集:包含 system context key 与审计字段。
|
||||
3. biz 查询选项和错误:当前绑定 system 的领域接口及 API reason。
|
||||
|
||||
只有当其他模块出现相同、稳定且不带 system 语义的契约时,才新增公共包;不要
|
||||
直接把单个 system 类型搬到 `pkg`。
|
||||
|
||||
## 验证
|
||||
|
||||
```text
|
||||
go test ./...
|
||||
```
|
||||
|
||||
当前全仓测试通过,且未发现对已删除旧路径的 Go import。
|
||||
@@ -0,0 +1,63 @@
|
||||
# `internal` 目录结构优化结论
|
||||
|
||||
参考 Go Kratos 的分层方式,顶层保留 `app`、`modules`、`biz`、`config`、`global`、
|
||||
`data`、`initialize`、`integration`、`security`、`server`、`service`、`worker` 等
|
||||
稳定职责。目录不是越少越好:同一技术角色文件较多时,应在所属层下分组,避免
|
||||
一个目录堆积几十个文件。
|
||||
|
||||
## 当前结构
|
||||
|
||||
```text
|
||||
internal/
|
||||
app/ # 应用组合根
|
||||
modules/ # 静态 catalog 和模块定义
|
||||
modules/payment/ # payment 模块定义
|
||||
biz/ # DO、usecase、repo interface
|
||||
config/ # Viper 配置、快照和热更新
|
||||
global/ # 进程级共享资源
|
||||
data/ # PO、repo、数据库和迁移
|
||||
initialize/ # 首次安装和配置编排
|
||||
integration/ # 外部 I/O provider
|
||||
security/ # JWT 和安全实现
|
||||
server/
|
||||
handler/ # Gin handler,按资源命名
|
||||
middleware/ # 认证、审计、限流、恢复等中间件
|
||||
router/ # 各资源路由及 Routes 聚合
|
||||
httpx/ # system HTTP 响应和 cookie 适配
|
||||
service/
|
||||
dto/ # HTTP 请求、响应和查询 DTO,按模块命名
|
||||
worker/ # 任务运行时
|
||||
```
|
||||
|
||||
## 本次调整
|
||||
|
||||
- DTO 从 `service` 根目录归档到 `service/dto`。
|
||||
- handler、middleware、router、HTTP helper 分别归档到 `server` 子包。
|
||||
- 子目录内文件直接使用资源名,例如 `handler/payment.go`、
|
||||
`router/payment.go`、`dto/payment.go`,不保留重复角色前缀。
|
||||
- Wire 直接装配 `handler.ProviderSet` 和 `router.ProviderSet`,server 根目录只
|
||||
负责 Gin/Swagger 生命周期。
|
||||
- `security/adminauth/token.go` 合并为 `security/token.go`;单文件子包没有
|
||||
独立边界时不继续拆分。
|
||||
- 删除只转发 `pkg/protoutil` 的 `utils/configutil`。
|
||||
|
||||
## `internal/app` 为什么只保留应用组合
|
||||
|
||||
`modules/catalog.go` 是静态模块注册点,负责按依赖顺序汇总各模块
|
||||
`Definition()`;`app/runtime.go` 只负责任务注册和依赖注入后的路由组合。
|
||||
这样模块定义不再和应用组合逻辑混在一起,也不能误并入 `biz`、`service` 或
|
||||
`data`。
|
||||
|
||||
Catalog 只能自动汇总静态模块贡献;新增模块若提供运行时路由或依赖型任务,仍需
|
||||
在 cmd/Wire 中显式注册。
|
||||
|
||||
## 其他目录审查
|
||||
|
||||
- `data` 与 `integration` 已按仓储或 provider 分类,边界和生命周期明确。
|
||||
- `biz`、`service` 根目录采用一资源一文件;进一步拆成资源子包会改变 Go 包
|
||||
边界并容易引入循环依赖,本次不做纯视觉拆分。
|
||||
- `security` 当前只有一个文件,但安全实现是明确的依赖边界,后续认证机制也会
|
||||
在此扩展,因此保留顶层包。
|
||||
|
||||
新增目录应至少满足独立依赖方向、状态生命周期或稳定技术角色之一。不要回到
|
||||
一文件一目录,也不要为了减少目录数量把大量不同角色重新铺平。
|
||||
Reference in New Issue
Block a user