Document the six-module operations roadmap and executable task breakdown for promo codes, announcements, channel monitoring, rebates, concurrency controls, and domestic payments.
29 KiB
new-api 运营能力补全 — 6 模块设计方案
日期:2026-05-20 状态:待审阅
概览
实施顺序
| # | 模块 | 优先级 | 复杂度 | 预计工时 |
|---|---|---|---|---|
| 1 | 优惠码 | P0 | 低 | 1-2d |
| 2 | 公告系统 | P1 | 中 | 2-3d |
| 3 | 渠道监控 | P1 | 中 | 2-3d |
| 4 | 邀请返利 | P2 | 中 | 2-3d |
| 5 | 账号并发 | P2 | 中 | 2-3d |
| 6 | 国内支付 | P0 | 大 | 3-5d |
技术栈
- Go 1.25 + Gin + GORM
- 数据库:MySQL/PostgreSQL/SQLite 三兼容
- 缓存:Redis
- 参考实现:sub2api (
/Users/qingse/Sync/local-git/sub2api/)
模块 1:优惠码(Promo Code)
背景
new-api 现状:有兑换码(Redemption)系统支持额度兑换和订阅兑换,也有充值金额折扣阶梯(AmountDiscount),但无独立优惠码系统。运营需要「充值/订阅时输入优惠码获得额外赠送」的能力。
目标与非目标
目标:
- 管理员创建/管理优惠码(固定加赠额度)
- 用户充值或订阅时输入优惠码,校验通过后获得额外额度加赠
- 每人每码仅可用一次,支持使用次数上限和过期时间
- 可设最低充值/消费门槛
非目标:
- 不实现百分比折扣(本次只做固定加赠)
- 不实现满减阶梯
- 不做优惠码和现有兑换码的合并
数据模型
promo_codes(新表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | uint | PK, AUTO_INCREMENT | |
| code | varchar(32) | UNIQUE, NOT NULL | 优惠码,大写 |
| bonus_amount | int | NOT NULL, DEFAULT 0 | 加赠额度(美元分) |
| max_uses | int | NOT NULL, DEFAULT 0 | 最大使用次数,0=不限制 |
| used_count | int | NOT NULL, DEFAULT 0 | 已使用次数 |
| status | tinyint | NOT NULL, DEFAULT 1 | 1=启用, 0=禁用 |
| min_recharge_amount | int | NOT NULL, DEFAULT 0 | 最低充值/消费门槛(美元分),0=无门槛 |
| expires_at | timestamp | NULL | 过期时间,NULL=永不过期 |
| notes | varchar(255) | DEFAULT '' | 内部备注 |
| created_at | timestamp | NOT NULL | |
| updated_at | timestamp | NOT NULL |
promo_code_usages(新表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | uint | PK, AUTO_INCREMENT | |
| promo_code_id | int | FK, NOT NULL | 关联 promo_codes |
| user_id | int | FK, NOT NULL | 关联 users |
| used_at | timestamp | NOT NULL | 使用时间 |
唯一索引:(promo_code_id, user_id)
API
管理端
GET /api/admin/promo-codes # 列表(分页,支持搜索、状态筛选)
POST /api/admin/promo-codes # 创建
GET /api/admin/promo-codes/:id # 详情
PUT /api/admin/promo-codes/:id # 编辑(已启用且有使用的码不可改 bonus_amount)
DELETE /api/admin/promo-codes/:id # 删除(软删除或仅禁用)
GET /api/admin/promo-codes/:id/usages # 使用记录分页
用户端
POST /api/promo-codes/validate # 校验优惠码
请求:
{
"code": "SUMMER2024",
"amount": 1000 // 计划充值/消费金额(美元分),用于校验最低门槛
}
响应(有效):
{
"valid": true,
"promo_code_id": 1,
"bonus_amount": 200,
"code": "SUMMER2024"
}
响应(无效):
{
"valid": false,
"reason": "expired" // expired / disabled / max_uses_reached / already_used / below_min_recharge
}
业务流程
充值页 → 用户输入码 → POST /validate →
有效:展示"加赠 $2.00" → 用户确认支付 →
支付成功回调 → Recharge() 中:
1. 执行业务充值(加额度)
2. 查询已校验的 promo_code
3. 检查 (promo_code_id, user_id) 未使用
4. 增加 bonus_amount 额度
5. 记录 usage + 更新 used_count
6. 记录日志 Log{Type: "topup", Content: "promo_code:SUMMER2024 bonus:200"}
和现有兑换码的关系
| 兑换码 Redemption | 优惠码 PromoCode | |
|---|---|---|
| 触发方式 | 独立兑换(POST /api/redemption) |
支付时附带 |
| 类型 | 额度/订阅 | 固定加赠 |
| 依赖 | 不依赖支付 | 依赖支付订单完成 |
| 表 | redemptions |
promo_codes + promo_code_usages |
互不影响,独立运作。
文件变更清单
| 文件 | 操作 | 说明 |
|---|---|---|
model/promo_code.go |
新增 | GORM 模型 + 表创建 + CRUD 方法 |
model/promo_code_usage.go |
新增 | GORM 模型 |
controller/promo_code.go |
新增 | 管理端 handler |
controller/promo_code_user.go |
新增 | 用户端 validate handler |
router/admin.go |
修改 | 加路由 |
router/api.go |
修改 | 加路由 |
service/topup.go 或 service/payment.go |
修改 | Recharge 完成时处理优惠码加赠 |
验证方式
- 创建优惠码 → GET 列表可见
- 用户 A 使用优惠码充值 → 额度增加含 bonus → usages 有记录
- 用户 A 再次使用同一码 → /validate 返回 already_used
- 达到 max_uses 后 → /validate 返回 max_uses_reached
- 过期后 → /validate 返回 expired
- 充值金额不满足门槛 → /validate 返回 below_min_recharge
- MySQL / PostgreSQL / SQLite 三库建表通过
模块 2:公告系统
背景
new-api 现状:公告以 JSON 字符串存在 ConsoleSetting.Announcements 字段,通过 /api/status 下发。无已读追踪,无状态管理,无定时发布。运营需要更精细的公告管理能力。
目标与非目标
目标:
- 独立
announcements表,支持 Markdown 内容 - 状态机:draft → active → archived
- 已读追踪(
announcement_reads) - 定时发布(starts_at / ends_at)
- 弹窗/静默两种通知模式
- json_rules JSON 字段,预留定向规则
非目标:
- 不做复杂的规则引擎(定向规则暂不走解析,存 JSON 备用)
- 不做站内通知中心 (in-app notification center)
- 不删除旧 JSON 存储方式(兼容期)
数据模型
announcements(新表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | uint | PK | |
| title | varchar(255) | NOT NULL | 标题 |
| content | text | NOT NULL | 内容,Markdown |
| content_html | text | 渲染后的 HTML,服务端预渲染 | |
| status | varchar(16) | NOT NULL, DEFAULT 'draft' | draft / active / archived |
| notify_mode | varchar(16) | NOT NULL, DEFAULT 'silent' | silent / popup |
| json_rules | text | JSON 字符串,定向展示规则(预留) | |
| starts_at | timestamp | NULL | 开始展示时间 |
| ends_at | timestamp | NULL | 结束展示时间 |
| created_at | timestamp | NOT NULL | |
| updated_at | timestamp | NOT NULL | |
| created_by | int | 创建人 user_id |
announcement_reads(新表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | uint | PK | |
| announcement_id | int | FK, NOT NULL | |
| user_id | int | FK, NOT NULL | |
| read_at | timestamp | NOT NULL | 首次阅读时间 |
唯一索引:(announcement_id, user_id)
API
管理端
GET /api/admin/announcements # 列表(分页,状态筛选)
POST /api/admin/announcements # 创建
GET /api/admin/announcements/:id # 详情
PUT /api/admin/announcements/:id # 编辑
DELETE /api/admin/announcements/:id # 删除
GET /api/admin/announcements/:id/read-status # 已读/未读用户列表
用户端
GET /api/announcements # 当前可见公告(status=active, 在时间范围内)
GET /api/announcements?unread=true # 仅未读
POST /api/announcements/:id/read # 标记已读
路由兼容
/api/status 中同步返回公告(从新表查 active 公告),保持现有前端兼容。用 ConsoleSetting.AnnouncementsEnabled 总开关。
文件变更清单
| 文件 | 操作 | 说明 |
|---|---|---|
model/announcement.go |
新增 | GORM 模型 + 表创建 + CRUD |
model/announcement_read.go |
新增 | GORM 模型 |
controller/announcement.go |
新增 | 管理端 handler |
controller/announcement_user.go |
新增 | 用户端 handler |
controller/misc.go |
修改 | /api/status 公告来源切换到新表 |
router/admin.go |
修改 | 加管理端路由 |
router/api.go |
修改 | 加用户端路由 |
验证方式
- 创建公告(draft) → 用户端 GET 不可见
- 状态改为 active → 用户端可见
- 用户 A 标记已读 → announcement_reads 有记录
- 用户 A GET ?unread=true → 该公告不再出现
- starts_at 未来时间 → 未到时间不可见
- ends_at 过去 → 不可见
- 状态改为 archived → 不可见
- notify_mode=popup → 前端根据该字段弹出(前端配合)
模块 3:渠道监控
背景
new-api 现状:MonitorSetting 有 auto-test-channel 和 auto-disable-channel 功能,但检查结果无独立存储,无历史查询,无请求模板,无日报汇总。运维需要可追溯的监控数据。
目标与非目标
目标:
channel_monitors表管理监控配置(多模型、间隔、超时、请求模板)channel_monitor_history表记录每次检查结果channel_monitor_daily_rollup日报汇总(各模型可用率)- 支持自定义请求模板列表(用户可配,内置 20 个兜底)
- admin CRUD + 手动触发检查 + 历史查询
- SSRF 校验(禁止内网地址)
非目标:
- 不替换现有 auto-test / auto-disable 机制(共存,后续可合并)
- 不做实时告警推送(只做数据采集和展示,告警后续加)
- 用户端只读(不需要用户创建 monitor)
数据模型
channel_monitors(新表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | uint | PK | |
| name | varchar(128) | NOT NULL | 监控名称 |
| channel_id | int | FK | 关联 channels,0=不限 |
| target_url | varchar(512) | NOT NULL | 目标 URL |
| primary_model | varchar(64) | NOT NULL | 主测模型 |
| extra_models | text | JSON 数组,额外测试模型 | |
| check_interval | int | NOT NULL, DEFAULT 300 | 检查间隔(秒),15-3600 |
| timeout | int | NOT NULL, DEFAULT 30 | 超时(秒) |
| request_templates | text | JSON 数组,自定义请求模板 | |
| custom_headers | text | JSON 对象,自定义请求头 | |
| custom_body | text | 自定义请求体(raw string) | |
| status | tinyint | NOT NULL, DEFAULT 1 | 1=启用, 0=禁用 |
| created_at / updated_at | timestamp |
channel_monitor_history(新表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | uint | PK | |
| monitor_id | int | FK, NOT NULL | |
| channel_id | int | FK | |
| model_name | varchar(64) | NOT NULL | 测试的模型 |
| status | varchar(16) | NOT NULL | pass / degraded / failed / error |
| status_code | int | HTTP 状态码 | |
| latency_ms | int | 响应延迟(毫秒) | |
| error_message | text | 错误信息 | |
| request_body | text | 发送的请求内容 | |
| response_snippet | text | 响应片段(前 500 字符) | |
| checked_at | timestamp | NOT NULL |
数据保留:原始记录保留 7 天(定时清理 job)。
channel_monitor_daily_rollup(新表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | uint | PK | |
| monitor_id | int | FK, NOT NULL | |
| channel_id | int | FK | |
| model_name | varchar(64) | NOT NULL | |
| date | date | NOT NULL | 汇总日期 |
| total_checks | int | NOT NULL, DEFAULT 0 | 检查总数 |
| pass_count | int | NOT NULL, DEFAULT 0 | 通过次数 |
| degraded_count | int | NOT NULL, DEFAULT 0 | 降级次数 |
| failed_count | int | NOT NULL, DEFAULT 0 | 失败次数 |
| avg_latency_ms | int | 平均延迟 |
唯一索引:(monitor_id, channel_id, model_name, date)
API
管理端
GET /api/admin/channel-monitors # 列表
POST /api/admin/channel-monitors # 创建
GET /api/admin/channel-monitors/:id # 详情
PUT /api/admin/channel-monitors/:id # 编辑
DELETE /api/admin/channel-monitors/:id # 删除
POST /api/admin/channel-monitors/:id/run # 手动触发一次检查
GET /api/admin/channel-monitors/:id/history # 历史记录(按时间范围、模型筛选)
GET /api/admin/channel-monitors/:id/rollup # 日报(按日期范围)
用户端
GET /api/channel-monitors/:id/status # 公开状态页(可选)
内置请求模板
内置 20 个简短英文问题,覆盖不同主题(general、coding、math、translation、science),每次检查随机抽取 1 个。用户可以自定义模板列表覆盖默认。
示例(4/20):
"Explain what a binary tree is in one sentence."
"Write a simple Python function to check if a number is prime."
"What is 15 percent of 200? Return only the number."
"Translate 'Hello, how are you?' into French."
Runner 设计
后台 goroutine,每分钟扫描启用的 monitor,检查各 monitor 的 last_checked_at + check_interval < now() 则触发检查。检查流程:
- 随机选一个请求模板
- 构造 HTTP 请求 → 发送到 target_url
- SSRF 校验(禁止内网 IP)
- 记录 latency_ms、status_code
- 判断状态:pass(200, latency < 阈值) / degraded(200, latency >= 阈值) / failed(非 200 或无有效响应) / error(连接失败)
- 写入 history
- 每小时汇总一次到 daily_rollup
文件变更清单
| 文件 | 操作 | 说明 |
|---|---|---|
model/channel_monitor.go |
新增 | monitor 模型 |
model/channel_monitor_history.go |
新增 | history 模型 |
model/channel_monitor_daily_rollup.go |
新增 | rollup 模型 |
service/channel_monitor_runner.go |
新增 | 后台 runner |
service/channel_monitor_checker.go |
新增 | 单次检查逻辑 |
service/channel_monitor_ssrf.go |
新增 | SSRF 校验 |
controller/channel_monitor.go |
新增 | 管理端 handler |
router/admin.go |
修改 | 加路由 |
验证方式
- 创建 monitor → GET 列表可见
- POST /run → history 有新记录 → 状态正确
- 等一个间隔周期 → 自动触发检查 → history 有新记录
- 检查 daily_rollup 定时汇总正确
- SSRF:target_url 指向 127.0.0.1 → 检查失败且记录 error
- 模型不可用 → 状态为 failed
- 自定义模板列表生效 → 请求体使用自定义模板内容
模块 4:邀请返利增强
背景
new-api 现状有基础邀请系统:AffCode(邀请码)、AffCount(邀请数)、AffQuota(待提取返利额度)、AffHistoryQuota(累计返利)、InviterId(邀请人 ID)。返利逻辑在 model/user.go 的 Insert() 和 inviteUser() 中,使用全局 QuotaForInviter/QuotaForInvitee 常量。
缺失:
- 无独立返利记录表(只有 Log 表 Type=system 的日志)
- 无从用户级返利比例覆盖
- 无冻结期机制
- 无管理面板
目标与非目标
目标:
rebate_records表:记录每次返利发生- 全局返利比例 + per-user 返利比例覆盖
- 冻结期机制:返利入账后冻结 N 天,解冻后才可提取
- 管理面板:邀请概览、邀请记录、返利记录、转账记录、批量改比例
非目标:
- 不改变现有邀请注册逻辑(AffCode 生成、InviterId 绑定)
- 不做多级分销(只做一级)
- 不做返利自动转入余额(保持用户手动 TransferAffQuotaToQuota)
数据模型
现有字段增强(users 表)
新增字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| aff_rebate_rate_percent | int | per-user 返利比例覆盖(百分比),0=使用全局默认 |
| aff_rebate_frozen_days | int | per-user 冻结期覆盖(天),0=使用全局默认 |
rebate_records(新表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | uint | PK | |
| inviter_id | int | FK, NOT NULL | 邀请人 |
| invitee_id | int | FK, NOT NULL | 被邀请人 |
| order_id | int | FK | 关联 top_ups 或 subscription_orders |
| order_type | varchar(16) | NOT NULL | topup / subscription |
| order_amount | int | NOT NULL | 订单金额(美元分) |
| rebate_amount | int | NOT NULL | 返利金额(美元分) |
| rate_percent | int | NOT NULL | 使用的返利比例 |
| status | varchar(16) | NOT NULL, DEFAULT 'frozen' | frozen / released |
| frozen_until | timestamp | 冻结到期时间 | |
| released_at | timestamp | 解冻时间 | |
| created_at | timestamp | NOT NULL |
全局配置(option 表)
| Key | 说明 |
|---|---|
| AffRebateRatePercent | 全局默认返利比例(百分比) |
| AffRebateFrozenDays | 全局默认冻结天数 |
| AffRebateEnabled | 返利总开关 |
API
管理端
GET /api/admin/affiliates/overview # 概览:总邀请数、返利总额、待解冻总额
GET /api/admin/affiliates/invites # 邀请记录(分页,按时间/用户筛选)
GET /api/admin/affiliates/rebates # 返利记录(分页,按状态/时间/用户筛选)
GET /api/admin/affiliates/transfers # 转账/提取记录
GET /api/admin/affiliates/users # 用户返利设置列表
GET /api/admin/affiliates/users/:user_id/overview # 单用户概览
PUT /api/admin/affiliates/users/:user_id # 更新单用户返利比例/冻结期
POST /api/admin/affiliates/users/batch-rate # 批量更新返利比例
DELETE /api/admin/affiliates/users/:user_id # 清除用户特殊设置(回退到全局默认)
PUT /api/admin/affiliates/config # 更新全局配置
返利流程变更
现有 inviteUser() 逻辑增强:
inviteUser(inviterId, invitee):
1. inviter.AffCount += 1
2. invitee.Quota += QuotaForInvitee // 不变
3. 不在注册时给返利,改为在 invitee 首次充值完成后触发
onRechargeComplete(inviteeUser):
如果 inviteeUser.InviterId > 0 且是首次充值:
rate = inviteeUser.Inviter.AffRebateRatePercent > 0
? inviteeUser.Inviter.AffRebateRatePercent
: Option.AffRebateRatePercent
rebate = orderAmount * rate / 100
frozenDays = inviteeUser.Inviter.AffRebateFrozenDays > 0
? inviteeUser.Inviter.AffRebateFrozenDays
: Option.AffRebateFrozenDays
inviter.AffQuota += rebate // 入账到冻结额度
inviter.AffHistoryQuota += rebate
insert rebate_record {
status: frozenDays > 0 ? 'frozen' : 'released',
frozen_until: now + frozenDays
}
定时任务(每小时):releaseExpiredRebates()
扫描 status=frozen AND frozen_until <= now()
→ status=released, released_at=now
→ inviter.AffQuota 不变(已在入账时加过),用户可正常提取
文件变更清单
| 文件 | 操作 | 说明 |
|---|---|---|
model/rebate_record.go |
新增 | 返利记录模型 |
model/user.go |
修改 | 加 aff_rebate_rate_percent / aff_rebate_frozen_days 字段 |
model/option.go |
修改 | 加 AffRebateRatePercent / AffRebateFrozenDays / AffRebateEnabled |
service/rebate.go |
新增 | 返利计算、冻结/解冻逻辑 |
controller/affiliate.go |
新增 | 管理端 handler |
controller/topup_*.go |
修改 | Recharge 完成时触发 rebate 检查 |
router/admin.go |
修改 | 加路由 |
验证方式
- 用户 A 邀请用户 B → B 注册后 InviterId=A
- B 首次充值 → rebate_records 有新记录 → 状态 frozen
- A 的 AffQuota 增加对应金额
- 冻结到期 → 状态变 released
- A 手动提取 → AffQuota 减少 + 额度增加
- 修改 A 的返利比例为 X% → B 下次充值按 X% 返利
- 批量更新接口修改 10 个用户返利比例 → 全部生效
- 关闭返利开关 → 充值不触发返利
模块 5:账号并发控制
背景
new-api 现状有:全局 API 限流、模型级限流、channel polling 锁。但没有 per-user 或 per-upstream-account 的并发控制。每个上游 API Key 有其并发上限,超过后要么排队要么拒绝。
目标与非目标
目标:
- per-user 并发限制(可配,默认不限制)
- per-upstream-account(channel key)并发槽位管理
- Redis 原子计数实现
- 超时等待队列(可选,不直接拒绝)
- 管理面板实时并发监控
- admin 可调整单个用户的并发数
非目标:
- 不替换现有 rate-limit 体系(并发和速率是两个维度)
- 不做模型级并发隔离(先用 channel key 粒度)
- 不做跨节点排队(单节点等待队列即可)
数据模型
users 表新增字段
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| concurrency_limit | int | 0 | per-user 最大并发数,0=不限制 |
| rpm_limit | int | 0 | per-user 每分钟最大请求数,0=不限制 |
channels 表新增字段
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| concurrency_limit | int | 0 | 该 channel key 最大并发数,0=不限制 |
Redis 数据结构
user:concurrency:{user_id} → int (当前并发数)
channel:concurrency:{channel_id} → int (当前并发数)
user:rpm:{user_id}:{minute} → int (当前分钟请求数)
流程
relay 请求 → middleware:
1. user_concurrency_limit > 0:
INCR user:concurrency:{user_id}
若 > limit → DECR → 返回 429(或进入等待队列)
2. rpm_limit > 0:
INCR user:rpm:{user_id}:{current_minute}
若 > limit → 返回 429
3. 选择 channel → channel.concurrent_limit > 0:
INCR channel:concurrency:{channel_id}
若 > limit → DECR → 换 channel 或 429
4. relay 完成(成功或失败)→ defer DECR
等待队列(可选):
若希望排队而非直接拒绝:
- 用 Redis BLPOP 等待,设置 timeout(如 30s)
- 获到槽位后执行
API
管理端
GET /api/admin/ops/concurrency # 全局实时并发 snapshot
GET /api/admin/ops/concurrency/users # per-user 当前并发排行
GET /api/admin/ops/concurrency/channels # per-channel 当前并发
PUT /api/admin/users/:id/concurrency # 修改单用户并发/RPM 限制
文件变更清单
| 文件 | 操作 | 说明 |
|---|---|---|
model/user.go |
修改 | 加 concurrency_limit / rpm_limit |
model/channel.go |
修改 | 加 concurrency_limit |
service/concurrency.go |
新增 | Redis 并发槽位管理 |
middleware/concurrency.go |
新增 | relay 前并发检查 |
controller/ops_concurrency.go |
新增 | 管理端 handler |
router/relay.go 或 router/api.go |
修改 | relay 路由注入 middleware |
router/admin.go |
修改 | 加管理路由 |
验证方式
- 设用户 A concurrency_limit=2 → 同时发 3 个请求 → 第 3 个 429
- 设 channel concurrency_limit=5 → 同时发 6 个 → 第 6 个换 channel 或 429
- GET concurrency snapshot → 显示当前各用户/channel 并发数
- 请求完成后 → Redis 计数器自动 DECR
- 设 rpm_limit=10 → 1 分钟内 11 个请求 → 第 11 个 429
- Redis 宕机 → 降级跳过并发检查(不阻塞)
- concurrency_limit=0 → 不检查
模块 6:国内支付直连
背景
new-api 现有支付:Epay(易支付聚合)、Stripe、Creem、Waffo。都是国际渠道或聚合渠道,没有支付宝/微信直连。需要新增直连接入以降低支付费率、扩展国内用户群。
目标与非目标
目标:
- 统一 Provider 接口抽象
- 支付宝直连(WAP/PC 支付)
- 微信直连(Native/H5/JSAPI)
- Webhook 回调验签 + 订单完成
- 新渠道用 Provider 接口,老渠道不动
非目标:
- 不重构 Stripe/Creem/Waffo/Epay 到 Provider 接口
- 不做 EasyPay 聚合(后续再加)
- 不做分账、退款(第一期只做支付)
支付 Provider 接口设计
// payment/types.go
type PaymentRequest struct {
OrderID string
Amount int // 美元分
Currency string // USD / CNY
Description string
ReturnURL string // 支付完成跳转
NotifyURL string // webhook 地址
ClientIP string
Metadata map[string]string
// WxPay specific
TradeType string // NATIVE / H5 / JSAPI
OpenID string // JSAPI 必填
}
type PaymentResponse struct {
ProviderOrderID string // 上游订单号
PaymentURL string // 用户跳转支付 URL(H5/WAP)
QRCode string // 扫码支付 URL(Native)
RawResponse string // 上游原始响应
}
type OrderStatus struct {
ProviderOrderID string
Status string // pending / success / failed / closed
Amount int
Currency string
PaidAt *time.Time
}
type Provider interface {
Name() string
CreatePayment(ctx context.Context, req *PaymentRequest) (*PaymentResponse, error)
QueryOrder(ctx context.Context, providerOrderID string) (*OrderStatus, error)
VerifyNotification(ctx context.Context, body []byte, headers map[string]string) (*OrderStatus, error)
// Refund 第一期空实现
Refund(ctx context.Context, providerOrderID string, amount int) error
}
支付宝直连
配置项(setting/payment_alipay.go):
- AppID
- 应用私钥 (PKCS8)
- 支付宝公钥
- 签约的产品码 (FAST_INSTANT_TRADE_PAY)
- 支付模式:WAP / PC
流程:
POST /api/payment/alipay/create
→ 签名参数
→ 返回支付宝支付 URL
→ 用户跳转 → 支付宝页面 → 支付完成
→ 支付宝 POST notify → VerifyNotification() 验签
→ 订单完成 → 加额度
验签逻辑:支付宝 SDK 提供 RSA 签名验证。
微信直连
配置项(setting/payment_wxpay.go):
- AppID
- MchID (商户号)
- APIv3 Key
- 商户私钥 (PEM)
- 商户证书序列号
- 支付模式:NATIVE / H5 / JSAPI
流程:
POST /api/payment/wxpay/create
→ 调用微信统一下单 APIv3
→ NATIVE: 返回 code_url (二维码)
→ H5: 返回 h5_url (跳转链接)
→ 用户支付 → 微信 POST notify → VerifyNotification() 验签
→ 订单完成 → 加额度
验签逻辑:微信 APIv3 使用平台证书公钥验证回调签名,回调体是加密 JSON,需先解密再验签。
数据库
复现现有 top_ups 表,字段已够用:
payment_method: 新增值alipay_direct,wxpay_directpayment_provider: 沿用trade_no存上游订单号- 可能需在 top_ups 加
provider_payloadJSON 字段存上游原始响应(和 subscription_orders 对齐)
API
新 Provider 风格的支付路由:
POST /api/payment/alipay/create # 创建支付宝订单
GET /api/payment/alipay/return # 支付宝同步回调(跳转)
POST /api/payment/webhook/alipay # 支付宝异步通知
POST /api/payment/webhook/wxpay # 微信异步通知
POST /api/payment/wxpay/create # 创建微信订单
文件变更清单
| 文件 | 操作 | 说明 |
|---|---|---|
payment/types.go |
新增 | Provider 接口 + 请求/响应类型 |
payment/provider/alipay.go |
新增 | 支付宝 Provider 实现 |
payment/provider/wxpay.go |
新增 | 微信 Provider 实现 |
payment/crypto.go |
新增 | 配置敏感字段 AES 加解密 |
setting/payment_alipay.go |
新增 | 支付宝配置项 |
setting/payment_wxpay.go |
新增 | 微信配置项 |
controller/payment_alipay.go |
新增 | 支付宝 handler |
controller/payment_wxpay.go |
新增 | 微信 handler |
model/option.go |
修改 | 加配置 key 注册 |
model/topup.go |
修改 | 加 provider_payload 字段 |
router/api.go |
修改 | 加支付路由 |
go.mod |
修改 | 加支付宝/微信 SDK 依赖 |
验证方式
- 配置支付宝沙箱 → 创建订单 → 收到支付 URL → 沙箱支付 → webhook 回调 → 额度增加
- 配置微信沙箱 → 创建 Native 订单 → 生成二维码 → 扫码支付 → webhook 回调 → 额度增加
- 错误私钥 → 创建订单失败 → 不 crash
- 伪造 webhook → 验签失败 → 拒绝
- 重复 webhook → 幂等处理(top_ups 状态已 complete 则直接返回成功)
- 金额不一致 → 不完成订单
附录 A:跨模块关注点
数据库兼容
所有新表使用 GORM AutoMigrate,不手写 SQL DDL,确保 MySQL/PostgreSQL/SQLite 三库兼容。
注意:
- JSON 字段:MySQL
json/ PostgreSQLjsonb/ SQLitetext,用 GORMtype:text+serializer:json统一 - 唯一索引:迁移时检查存在再创建
- 外键:SQLite 默认不启用,用 GORM tag 但不在代码层强制执行
路由冲突
6 个模块不共享路由前缀,不会冲突。admin 路由统一加在 router/admin.go。
前端
本次设计只覆盖后端 API。前端(React web/default/)需要对应页面,但不在此文档范围——API 设计已确保前端可直接对接。
测试策略
每个模块:
- 单元测试:model CRUD + service 核心逻辑
- 集成测试:controller → service → model 全链路
- 手动验证:按各模块验证方式执行
附录 B:风险与兼容性
| 风险 | 影响 | 缓解 |
|---|---|---|
| 新表迁移失败 | 部分模块不可用 | 所有模块独立开关,回滚单模块不影响其他 |
| 返利逻辑改动影响现有充值 | 充值中断 | Recharge 中 rebate 逻辑用 defer recover 包住 |
| 支付 SDK 引入体积大 | 构建变大 | 仅模块 6 引入,不影响前 5 个模块 |
| 并发控制 Redis 故障 | 请求被拒 | Redis 不可用时跳过并发检查 |
| JSON 存储和独立表共存 | 公告/优惠码新旧数据不一致 | 模块 2 新表优先,旧 JSON 兼容读 |