docs: add operations capability implementation plan
Document the six-module operations roadmap and executable task breakdown for promo codes, announcements, channel monitoring, rebates, concurrency controls, and domestic payments.
This commit is contained in:
parent
f245e9bdf5
commit
059512f6ef
2240
docs/superpowers/plans/2026-05-20-运营能力补全-执行版.md
Normal file
2240
docs/superpowers/plans/2026-05-20-运营能力补全-执行版.md
Normal file
File diff suppressed because it is too large
Load Diff
846
docs/superpowers/specs/2026-05-20-运营能力补全-审阅版.md
Normal file
846
docs/superpowers/specs/2026-05-20-运营能力补全-审阅版.md
Normal file
@ -0,0 +1,846 @@
|
||||
# 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 # 校验优惠码
|
||||
```
|
||||
|
||||
请求:
|
||||
```json
|
||||
{
|
||||
"code": "SUMMER2024",
|
||||
"amount": 1000 // 计划充值/消费金额(美元分),用于校验最低门槛
|
||||
}
|
||||
```
|
||||
|
||||
响应(有效):
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"promo_code_id": 1,
|
||||
"bonus_amount": 200,
|
||||
"code": "SUMMER2024"
|
||||
}
|
||||
```
|
||||
|
||||
响应(无效):
|
||||
```json
|
||||
{
|
||||
"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 完成时处理优惠码加赠 |
|
||||
|
||||
### 验证方式
|
||||
|
||||
1. 创建优惠码 → GET 列表可见
|
||||
2. 用户 A 使用优惠码充值 → 额度增加含 bonus → usages 有记录
|
||||
3. 用户 A 再次使用同一码 → /validate 返回 already_used
|
||||
4. 达到 max_uses 后 → /validate 返回 max_uses_reached
|
||||
5. 过期后 → /validate 返回 expired
|
||||
6. 充值金额不满足门槛 → /validate 返回 below_min_recharge
|
||||
7. 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` | 修改 | 加用户端路由 |
|
||||
|
||||
### 验证方式
|
||||
|
||||
1. 创建公告(draft) → 用户端 GET 不可见
|
||||
2. 状态改为 active → 用户端可见
|
||||
3. 用户 A 标记已读 → announcement_reads 有记录
|
||||
4. 用户 A GET ?unread=true → 该公告不再出现
|
||||
5. starts_at 未来时间 → 未到时间不可见
|
||||
6. ends_at 过去 → 不可见
|
||||
7. 状态改为 archived → 不可见
|
||||
8. 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()` 则触发检查。检查流程:
|
||||
|
||||
1. 随机选一个请求模板
|
||||
2. 构造 HTTP 请求 → 发送到 target_url
|
||||
3. SSRF 校验(禁止内网 IP)
|
||||
4. 记录 latency_ms、status_code
|
||||
5. 判断状态:pass(200, latency < 阈值) / degraded(200, latency >= 阈值) / failed(非 200 或无有效响应) / error(连接失败)
|
||||
6. 写入 history
|
||||
7. 每小时汇总一次到 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` | 修改 | 加路由 |
|
||||
|
||||
### 验证方式
|
||||
|
||||
1. 创建 monitor → GET 列表可见
|
||||
2. POST /run → history 有新记录 → 状态正确
|
||||
3. 等一个间隔周期 → 自动触发检查 → history 有新记录
|
||||
4. 检查 daily_rollup 定时汇总正确
|
||||
5. SSRF:target_url 指向 127.0.0.1 → 检查失败且记录 error
|
||||
6. 模型不可用 → 状态为 failed
|
||||
7. 自定义模板列表生效 → 请求体使用自定义模板内容
|
||||
|
||||
---
|
||||
|
||||
## 模块 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` | 修改 | 加路由 |
|
||||
|
||||
### 验证方式
|
||||
|
||||
1. 用户 A 邀请用户 B → B 注册后 InviterId=A
|
||||
2. B 首次充值 → rebate_records 有新记录 → 状态 frozen
|
||||
3. A 的 AffQuota 增加对应金额
|
||||
4. 冻结到期 → 状态变 released
|
||||
5. A 手动提取 → AffQuota 减少 + 额度增加
|
||||
6. 修改 A 的返利比例为 X% → B 下次充值按 X% 返利
|
||||
7. 批量更新接口修改 10 个用户返利比例 → 全部生效
|
||||
8. 关闭返利开关 → 充值不触发返利
|
||||
|
||||
---
|
||||
|
||||
## 模块 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` | 修改 | 加管理路由 |
|
||||
|
||||
### 验证方式
|
||||
|
||||
1. 设用户 A concurrency_limit=2 → 同时发 3 个请求 → 第 3 个 429
|
||||
2. 设 channel concurrency_limit=5 → 同时发 6 个 → 第 6 个换 channel 或 429
|
||||
3. GET concurrency snapshot → 显示当前各用户/channel 并发数
|
||||
4. 请求完成后 → Redis 计数器自动 DECR
|
||||
5. 设 rpm_limit=10 → 1 分钟内 11 个请求 → 第 11 个 429
|
||||
6. Redis 宕机 → 降级跳过并发检查(不阻塞)
|
||||
7. 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 接口设计
|
||||
|
||||
```go
|
||||
// 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_direct`
|
||||
- `payment_provider`: 沿用 `trade_no` 存上游订单号
|
||||
- 可能需在 top_ups 加 `provider_payload` JSON 字段存上游原始响应(和 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 依赖 |
|
||||
|
||||
### 验证方式
|
||||
|
||||
1. 配置支付宝沙箱 → 创建订单 → 收到支付 URL → 沙箱支付 → webhook 回调 → 额度增加
|
||||
2. 配置微信沙箱 → 创建 Native 订单 → 生成二维码 → 扫码支付 → webhook 回调 → 额度增加
|
||||
3. 错误私钥 → 创建订单失败 → 不 crash
|
||||
4. 伪造 webhook → 验签失败 → 拒绝
|
||||
5. 重复 webhook → 幂等处理(top_ups 状态已 complete 则直接返回成功)
|
||||
6. 金额不一致 → 不完成订单
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:跨模块关注点
|
||||
|
||||
### 数据库兼容
|
||||
|
||||
所有新表使用 GORM AutoMigrate,不手写 SQL DDL,确保 MySQL/PostgreSQL/SQLite 三库兼容。
|
||||
|
||||
注意:
|
||||
- JSON 字段:MySQL `json` / PostgreSQL `jsonb` / SQLite `text`,用 GORM `type:text` + `serializer:json` 统一
|
||||
- 唯一索引:迁移时检查存在再创建
|
||||
- 外键:SQLite 默认不启用,用 GORM tag 但不在代码层强制执行
|
||||
|
||||
### 路由冲突
|
||||
|
||||
6 个模块不共享路由前缀,不会冲突。admin 路由统一加在 `router/admin.go`。
|
||||
|
||||
### 前端
|
||||
|
||||
本次设计只覆盖后端 API。前端(React web/default/)需要对应页面,但不在此文档范围——API 设计已确保前端可直接对接。
|
||||
|
||||
### 测试策略
|
||||
|
||||
每个模块:
|
||||
1. 单元测试:model CRUD + service 核心逻辑
|
||||
2. 集成测试:controller → service → model 全链路
|
||||
3. 手动验证:按各模块验证方式执行
|
||||
|
||||
---
|
||||
|
||||
## 附录 B:风险与兼容性
|
||||
|
||||
| 风险 | 影响 | 缓解 |
|
||||
|---|---|---|
|
||||
| 新表迁移失败 | 部分模块不可用 | 所有模块独立开关,回滚单模块不影响其他 |
|
||||
| 返利逻辑改动影响现有充值 | 充值中断 | Recharge 中 rebate 逻辑用 defer recover 包住 |
|
||||
| 支付 SDK 引入体积大 | 构建变大 | 仅模块 6 引入,不影响前 5 个模块 |
|
||||
| 并发控制 Redis 故障 | 请求被拒 | Redis 不可用时跳过并发检查 |
|
||||
| JSON 存储和独立表共存 | 公告/优惠码新旧数据不一致 | 模块 2 新表优先,旧 JSON 兼容读 |
|
||||
Loading…
x
Reference in New Issue
Block a user