diff --git a/docs/agent-recharge/功能总结.md b/docs/agent-recharge/功能总结.md new file mode 100644 index 0000000..ddd9dad --- /dev/null +++ b/docs/agent-recharge/功能总结.md @@ -0,0 +1,227 @@ +# 代理预充值功能 + +## 功能概述 + +代理商(店铺)余额钱包的在线充值系统,支持微信在线支付和线下转账两种充值方式,具备完整的 Service/Handler/回调处理链路。充值仅针对余额钱包(`wallet_type=main`),佣金钱包通过分佣自动入账。 + +### 背景与动机 + +原有 `tb_agent_recharge_record` 表和 Store 层骨架已存在,但缺少 Service 层和 Handler 层,无法通过 API 发起充值。本次补全完整实现,并集成至支付配置管理体系(按 `payment_config_id` 动态路由至微信直连或富友通道)。 + +## 核心流程 + +### 在线充值流程(微信) + +``` +代理/平台 → POST /api/admin/agent-recharges + │ + ├─ 验证权限:代理只能充自己店铺,平台可指定任意店铺 + ├─ 验证金额范围(100 元~100 万元) + ├─ 查找目标店铺的 main 钱包 + ├─ 查询 active 支付配置 → 无配置则拒绝(返回 1175) + ├─ 记录 payment_config_id + └─ 创建充值订单(status=1 待支付) + └─ 返回订单信息(客户端支付发起【留桩】) + +支付成功 → POST /api/callback/wechat-pay 或 /api/callback/fuiou-pay + │ + ├─ 按订单号前缀 "ARCH" 识别为代理充值 + ├─ 查询充值记录,取 payment_config_id + ├─ 按配置验签 + └─ agentRechargeService.HandlePaymentCallback() + ├─ 幂等检查(WHERE status = 1) + ├─ 更新充值记录状态 → 2(已完成) + ├─ 代理主钱包余额增加(乐观锁防并发) + └─ 创建钱包流水记录 +``` + +### 线下充值流程(仅平台) + +``` +平台 → POST /api/admin/agent-recharges + └─ payment_method = "offline" + └─ 创建充值订单(status=1 待支付) + +平台确认 → POST /api/admin/agent-recharges/:id/offline-pay + ├─ 验证操作密码(二次鉴权) + └─ 事务内: + ├─ 更新充值记录状态 → 2(已完成) + ├─ 记录 paid_at、completed_at + ├─ 代理主钱包余额增加(乐观锁 version 字段) + ├─ 创建钱包流水记录 + └─ 记录审计日志 +``` + +## 接口说明 + +### 基础路径 + +`/api/admin/agent-recharges` + +**权限要求**:企业账号(`user_type=4`)在路由层被拦截,返回 `1005`。 + +### 接口列表 + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| POST | `/api/admin/agent-recharges` | 创建充值订单 | 代理(自己店铺)/ 平台(任意店铺)| +| GET | `/api/admin/agent-recharges` | 查询充值记录列表 | 代理(自己店铺)/ 平台(全部)| +| GET | `/api/admin/agent-recharges/:id` | 查询充值记录详情 | 代理(自己店铺)/ 平台(全部)| +| POST | `/api/admin/agent-recharges/:id/offline-pay` | 确认线下充值到账 | 仅平台 | + +### 创建充值订单 + +**请求体示例(在线充值)** + +```json +{ + "shop_id": 101, + "amount": 50000, + "payment_method": "wechat" +} +``` + +**请求体示例(线下充值)** + +```json +{ + "shop_id": 101, + "amount": 200000, + "payment_method": "offline" +} +``` + +**请求字段** + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| shop_id | integer | 是 | 目标店铺 ID(代理只能填自己所属店铺)| +| amount | integer | 是 | 充值金额(单位:分),范围 10000~100000000 | +| payment_method | string | 是 | `wechat`(在线)/ `offline`(线下,仅平台)| + +**成功响应** + +```json +{ + "code": 0, + "msg": "success", + "data": { + "id": 88, + "recharge_no": "ARCH20260316100001", + "shop_id": 101, + "amount": 50000, + "payment_method": "wechat", + "payment_channel": "wechat_direct", + "payment_config_id": 3, + "status": 1, + "created_at": "2026-03-16T10:00:00+08:00" + }, + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +### 线下充值确认 + +**请求体** + +```json +{ + "operation_password": "Abc123456" +} +``` + +操作密码验证通过后,事务内同步完成:余额到账 + 钱包流水 + 审计日志。 + +## 权限控制矩阵 + +| 操作 | 平台账号 | 代理账号 | 企业账号 | +|------|----------|----------|----------| +| 创建充值(在线) | ✅ 任意店铺 | ✅ 仅自己店铺 | ❌ | +| 创建充值(线下) | ✅ 任意店铺 | ❌ | ❌ | +| 线下充值确认 | ✅ | ❌ | ❌ | +| 查询充值列表 | ✅ 全部 | ✅ 仅自己店铺 | ❌ | +| 查询充值详情 | ✅ 全部 | ✅ 仅自己店铺 | ❌ | + +**越权统一响应**:代理访问他人店铺充值记录时,返回 `1121 CodeRechargeNotFound`(不区分不存在与无权限) + +## 数据模型 + +### `tb_agent_recharge_record` 新增字段 + +| 字段 | 类型 | 可空 | 说明 | +|------|------|------|------| +| `payment_config_id` | bigint | 是 | 关联支付配置 ID(线下充值为 NULL,在线充值记录实际使用的配置)| + +### 充值订单状态枚举 + +| 值 | 含义 | +|----|------| +| 1 | 待支付 | +| 2 | 已完成 | +| 3 | 已取消 | + +### 支付方式与通道 + +| payment_method | payment_channel | 说明 | +|---------------|----------------|------| +| wechat | wechat_direct | 微信直连通道(provider_type=wechat)| +| wechat | fuyou | 富友通道(provider_type=fuiou)| +| offline | offline | 线下转账 | + +> 前端统一显示"微信支付",后端根据生效配置的 `provider_type` 自动路由,前端不感知具体通道。 + +### 充值单号规则 + +前缀 `ARCH`,全局唯一,用于回调时识别订单类型。 + +## 幂等性设计 + +- 回调处理使用状态条件更新:`WHERE status = 1` +- `RowsAffected == 0` 时说明已被处理,直接返回成功,不重复入账 +- 钱包余额更新使用乐观锁(`version` 字段),并发冲突时最多重试 3 次 + +## 审计日志 + +线下充值确认(`OfflinePay`)操作记录审计日志,字段包括: + +| 字段 | 值 | +|------|-----| +| `operator_id` | 当前操作人 ID | +| `operation_type` | `offline_recharge` | +| `operation_desc` | `确认代理充值到账:充值单号 {recharge_no},金额 {amount} 分` | +| `before_data` | 操作前余额和充值记录状态 | +| `after_data` | 操作后余额和充值记录状态 | + +## 涉及文件 + +### 新增文件 + +| 层级 | 文件 | 说明 | +|------|------|------| +| DTO | `internal/model/dto/agent_recharge_dto.go` | 请求/响应 DTO | +| Service | `internal/service/agent_recharge/service.go` | 充值业务逻辑 | +| Handler | `internal/handler/admin/agent_recharge.go` | 4 个 Handler 方法 | +| 路由 | `internal/routes/agent_recharge.go` | 路由注册 | + +### 修改文件 + +| 文件 | 变更说明 | +|------|---------| +| `internal/model/agent_wallet.go` | 新增 `PaymentConfigID *uint` 字段 | +| `internal/handler/callback/payment.go` | 新增 "ARCH" 前缀分发 → agentRechargeService.HandlePaymentCallback() | +| `internal/bootstrap/` 系列 | 注册 AgentRechargeService、AgentRechargeHandler | +| `cmd/api/docs.go` / `cmd/gendocs/main.go` | 注册 AgentRechargeHandler | +| `migrations/000081_add_payment_config_id_to_agent_recharge.up.sql` | tb_agent_recharge_record 新增 payment_config_id 列 | + +## 常量定义 + +```go +// pkg/constants/wallet.go +AgentRechargeOrderPrefix = "ARCH" // 充值单号前缀 +AgentRechargeMinAmount = 10000 // 最小充值:100 元(单位:分) +AgentRechargeMaxAmount = 100000000 // 最大充值:100 万元(单位:分) +``` + +## 已知限制(留桩) + +**客户端支付发起未实现**:在线充值(`payment_method=wechat`)创建订单成功后,前端获取支付参数的接口本次未实现。充值回调处理已完整实现——等支付发起改造完成后,完整的充值支付闭环即可联通。 diff --git a/docs/wechat-config-management/功能总结.md b/docs/wechat-config-management/功能总结.md new file mode 100644 index 0000000..36a5ac8 --- /dev/null +++ b/docs/wechat-config-management/功能总结.md @@ -0,0 +1,239 @@ +# 微信参数配置管理功能 + +## 功能概述 + +在管理后台支持多套微信支付配置的 CRUD 管理,每套配置代表一套完整的"微信身份"(公众号 OAuth + 小程序 OAuth + 支付凭证),支持全局唯一激活约束和秒级切换。同时集成富友支付 SDK,作为微信直连的备选渠道。 + +### 背景与动机 + +原有微信相关参数(公众号 OAuth、小程序、支付凭证)硬编码在环境变量中,只有一套配置,无法动态切换。业务上微信公众号/小程序随时可能被封禁,需要在管理后台**秒级切换**到备用配置恢复 OAuth 登录和支付能力。同时需要接入富友支付作为备选通道,降低对微信直连的单一依赖。 + +## 核心设计 + +### 配置切换流程 + +``` +管理员激活新配置 POST /api/admin/wechat-configs/:id/activate + │ + ├─ ① BEGIN 事务 + │ ├─ UPDATE tb_wechat_config SET is_active=false WHERE is_active=true + │ └─ UPDATE tb_wechat_config SET is_active=true WHERE id=:id + ├─ ② COMMIT + ├─ ③ DEL Redis "wechat:config:active"(即时生效) + └─ ④ 记录审计日志 + │ + ├─ 新订单 → 使用新配置(记录新的 payment_config_id) + └─ 旧订单(待支付)→ 回调时按 payment_config_id 加载旧配置验签 + └─ 30 分钟超时自动取消 +``` + +### 生效配置缓存策略 + +- **Redis Key**:`wechat:config:active`(见 `pkg/constants/redis.go`) +- **TTL**:5 分钟(兜底,防 Redis 缓存与 DB 长期不一致) +- **主动失效**:激活、停用、更新生效配置、删除配置时主动 DEL 缓存 +- **空标记**:无生效配置时缓存 `"none"`,TTL 1 分钟,防止缓存穿透 +- **读取流程**:Redis GET → 命中返回 → MISS → 查 DB → SET 缓存 + +### 配置切换时在途订单处理 + +- `tb_order`、`tb_asset_recharge_record`、`tb_agent_recharge_record` 均新增 `payment_config_id` 字段(nullable) +- 下单时记录当前使用的配置 ID,配置切换后旧订单仍按 `payment_config_id` 加载旧配置验签 +- 旧待支付订单由现有 30 分钟超时自动取消机制清理 +- **有待支付订单引用的配置不允许删除**(软删除后仍可用于验签) + +### 支付回调统一分发 + +``` +回调到达 + │ + ├─ 微信回调 POST /api/callback/wechat-pay + │ └─ PowerWeChat SDK 解析 → 取 out_trade_no + │ + └─ 富友回调 POST /api/callback/fuiou-pay + └─ GBK→UTF-8 → XML 解析 → 取 mchnt_order_no + │ + └─ 按订单号前缀分发 + ├─ "ORD" → 套餐订单 → orderService.HandlePaymentCallback() + ├─ "CRCH" → 资产充值 → rechargeService.HandlePaymentCallback() + └─ "ARCH" → 代理充值 → agentRechargeService.HandlePaymentCallback() +``` + +## 接口说明 + +### 基础路径 + +`/api/admin/wechat-configs` + +**权限要求**:仅超级管理员(`user_type=1`)和平台用户(`user_type=2`)可访问,其他类型返回 `1005`。 + +### 接口列表 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/admin/wechat-configs` | 创建配置 | +| GET | `/api/admin/wechat-configs` | 查询配置列表(分页+筛选) | +| GET | `/api/admin/wechat-configs/active` | 查询当前生效配置 | +| GET | `/api/admin/wechat-configs/:id` | 查询配置详情 | +| PUT | `/api/admin/wechat-configs/:id` | 更新配置 | +| DELETE | `/api/admin/wechat-configs/:id` | 软删除配置 | +| POST | `/api/admin/wechat-configs/:id/activate` | 激活配置 | +| POST | `/api/admin/wechat-configs/:id/deactivate` | 停用配置 | +| POST | `/api/callback/fuiou-pay` | 富友支付回调(无需认证) | + +### 渠道类型(provider_type) + +| 值 | 说明 | 必填支付字段 | +|----|------|-------------| +| `wechat` | 微信直连 | `wx_mch_id`、`wx_api_v3_key`、`wx_cert_content`、`wx_key_content`、`wx_serial_no`、`wx_notify_url` | +| `fuiou` | 富友聚合支付 | `fy_ins_cd`、`fy_mchnt_cd`、`fy_term_id`、`fy_private_key`、`fy_public_key`、`fy_api_url`、`fy_notify_url` | + +### 敏感字段脱敏规则 + +接口响应中所有敏感字段均脱敏,数据库明文存储: + +| 字段类型 | 脱敏规则 | 示例 | +|---------|---------|------| +| Secret/Key(短) | 前4位 + `***` + 后4位 | `abcd***7890` | +| 证书/私钥(长) | 仅显示状态 | `[已配置]` / `[未配置]` | + +**更新脱敏字段**:不传或传空字符串 = 保留原值;传新明文值 = 替换。 + +### 删除保护规则 + +| 条件 | 错误码 | 错误消息 | +|------|--------|---------| +| 配置 `is_active=true` | `1171` | 不能删除当前生效的支付配置,请先停用 | +| 存在待支付订单引用 | `1172` | 该配置存在未完成的支付订单,暂时无法删除 | + +## 富友支付 SDK + +**位置**:`pkg/fuiou/` + +| 文件 | 说明 | +|------|------| +| `types.go` | WxPreCreateRequest/Response、NotifyRequest 等 XML 结构体 | +| `client.go` | Client 结构体、NewClient、RSA 签名/验签、HTTP 请求(XML+GBK)| +| `wxprecreate.go` | WxPreCreate 方法(公众号 JSAPI + 小程序支付下单)| +| `notify.go` | VerifyNotify(GBK→UTF-8 + XML 解析 + RSA 验签)、BuildNotifyResponse | + +**签名算法**:字典序排列参数 → GBK 编码 → MD5 哈希 → RSA 签名 → Base64 + +**新增依赖**:`golang.org/x/text`(GBK 编解码) + +## 数据库变更 + +### 新建表 `tb_wechat_config`(迁移 000078) + +| 字段组 | 字段 | 说明 | +|-------|------|------| +| 基础信息 | `id`, `name`, `description`, `provider_type`, `is_active` | 配置基础字段 | +| 公众号 OAuth | `oa_app_id`, `oa_app_secret`, `oa_token`, `oa_aes_key`, `oa_oauth_redirect_url` | 公众号相关 | +| 小程序 OAuth | `miniapp_app_id`, `miniapp_app_secret` | 小程序相关 | +| 微信直连 | `wx_mch_id`, `wx_api_v3_key`, `wx_api_v2_key`, `wx_cert_content`, `wx_key_content`, `wx_serial_no`, `wx_notify_url` | provider_type=wechat 时使用 | +| 富友 | `fy_ins_cd`, `fy_mchnt_cd`, `fy_term_id`, `fy_private_key`, `fy_public_key`, `fy_api_url`, `fy_notify_url` | provider_type=fuiou 时使用 | +| 审计 | `creator`, `updater`, `created_at`, `updated_at`, `deleted_at` | 标准审计字段 | + +### 新增字段 + +| 表 | 字段 | 类型 | 迁移文件 | +|----|------|------|---------| +| `tb_order` | `payment_config_id` | bigint, nullable | 000079 | +| `tb_asset_recharge_record` | `payment_config_id` | bigint, nullable | 000080 | +| `tb_agent_recharge_record` | `payment_config_id` | bigint, nullable | 000081 | + +## 新增错误码 + +| 错误码 | 常量 | 说明 | +|--------|------|------| +| 1170 | `CodeWechatConfigNotFound` | 微信支付配置不存在 | +| 1171 | `CodeWechatConfigActive` | 不能删除/操作当前生效的支付配置 | +| 1172 | `CodeWechatConfigHasPendingOrders` | 该配置存在未完成的支付订单 | +| 1173 | `CodeFuiouPayFailed` | 富友支付失败 | +| 1174 | `CodeFuiouCallbackInvalid` | 富友回调验签失败 | +| 1175 | `CodeNoPaymentConfig` | 当前无可用的支付配置 | + +## 审计日志 + +以下操作均记录审计日志(异步写入,失败不影响业务): + +| 操作 | operation_type | 说明 | +|------|---------------|------| +| 创建配置 | `create` | after_data 存脱敏后配置 | +| 更新配置 | `update` | before/after_data 均脱敏 | +| 删除配置 | `delete` | before_data 存脱敏后配置 | +| 激活配置 | `activate` | before_data=旧配置,after_data=新配置 | +| 停用配置 | `deactivate` | before/after_data 存状态变更 | + +## 涉及文件 + +### 新增文件 + +| 层级 | 文件 | 说明 | +|------|------|------| +| 模型 | `internal/model/wechat_config.go` | WechatConfig 模型、渠道类型常量 | +| DTO | `internal/model/dto/wechat_config_dto.go` | CRUD 请求/响应 DTO、脱敏方法 | +| Store | `internal/store/postgres/wechat_config_store.go` | CRUD + 激活/停用 + 统计 | +| Service | `internal/service/wechat_config/service.go` | 业务逻辑、缓存管理、删除保护 | +| Handler | `internal/handler/admin/wechat_config.go` | 8 个 Handler 方法 | +| 路由 | `internal/routes/wechat_config.go` | 路由注册(含平台权限中间件) | +| SDK | `pkg/fuiou/types.go` | 富友 XML 结构体 | +| SDK | `pkg/fuiou/client.go` | 富友 HTTP 客户端、签名/验签 | +| SDK | `pkg/fuiou/wxprecreate.go` | 富友支付下单 | +| SDK | `pkg/fuiou/notify.go` | 富友回调验签 | +| 迁移 | `migrations/000078_create_wechat_config_table.up.sql` | 创建 tb_wechat_config 表 | +| 迁移 | `migrations/000079_add_payment_config_id_to_order.up.sql` | tb_order 新增字段 | +| 迁移 | `migrations/000080_add_payment_config_id_to_asset_recharge.up.sql` | tb_asset_recharge_record 新增字段 | +| 迁移 | `migrations/000081_add_payment_config_id_to_agent_recharge.up.sql` | tb_agent_recharge_record 新增字段 | + +### 修改文件 + +| 文件 | 变更说明 | +|------|---------| +| `internal/model/order.go` | 新增 `PaymentConfigID *uint` 字段 | +| `internal/model/asset_wallet.go` | 新增 `PaymentConfigID *uint` 字段 | +| `internal/handler/callback/payment.go` | 支持富友回调 + 按订单前缀分发 + 按 payment_config_id 验签 | +| `internal/routes/order.go` | 新增 `/api/callback/fuiou-pay` 路由 | +| `internal/service/order/service.go` | 注入 wechatConfigService、下单时记录 payment_config_id | +| `internal/bootstrap/` 系列 | 注册 WechatConfigStore/Service/Handler | +| `cmd/api/docs.go` / `cmd/gendocs/main.go` | 注册 WechatConfigHandler | + +### 删除/精简文件(YAML 支付方案遗留清理) + +| 文件 | 变更说明 | +|------|---------| +| `pkg/config/config.go` | 删除 `PaymentConfig` 结构体 + `WechatConfig.Payment` 字段 | +| `pkg/config/defaults/config.yaml` | 删除 `wechat.payment:` 整个配置节 | +| `pkg/wechat/config.go` | 删除 `NewPaymentApp()` 函数(YAML/CertPath 方式已被 DB Base64 方案替代) | +| `cmd/api/main.go` | 删除 `validateWechatConfig` 中所有 `wechatCfg.Payment.*` 相关校验代码 | + +## 常量定义 + +```go +// pkg/constants/wallet.go(Card* 重命名为 Asset*,旧名保留为废弃别名) +AssetWalletResourceTypeIotCard // 原 CardWalletResourceTypeIotCard +AssetWalletResourceTypeDevice // 原 CardWalletResourceTypeDevice +AssetRechargeOrderPrefix // "CRCH"(原 CardRechargeOrderPrefix) +AssetRechargeMinAmount // 最小充值金额(分) +AssetRechargeMaxAmount // 最大充值金额(分) + +// pkg/constants/redis.go +RedisWechatConfigActiveKey() // "wechat:config:active" + +// internal/model/wechat_config.go +ProviderTypeWechat = "wechat" // 微信直连 +ProviderTypeFuiou = "fuiou" // 富友 +``` + +## 已知限制(留桩) + +以下功能本次**未实现**,待后续会话补全: + +- **客户端支付发起**:`WechatPayJSAPI`、`WechatPayH5`、`FuiouPayJSAPI`、`FuiouPayMiniApp` 均为留桩(返回"暂未实现"错误或 TODO 注释),当前仍保留 `wechatPayment` 单例注入 +- **OAuth 配置动态加载**:`OfficialAccountService` 仍从环境变量读取,`tb_wechat_config` 中的 `oa_*` 字段仅存储,待 H5/小程序重构时切换 + +## 部署注意事项 + +1. 执行数据库迁移(000078~000081)后,现有数据不受影响(新字段均为 nullable) +2. 原环境变量 `JUNHONG_WECHAT_PAYMENT_*` 系列已不再读取,可清理 +3. 首次上线后,需要在管理后台手动创建并激活一个微信配置,否则第三方支付功能处于禁用状态(系统自动降级为仅支持钱包/线下支付)