feat: 套餐系统升级 - Worker 重构、流量重置、文档与规范更新
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
- 重构 Worker 启动流程,引入 bootstrap 模块统一管理依赖注入 - 实现套餐流量重置服务(日/月/年周期重置) - 新增套餐激活排队、加油包绑定、囤货待实名激活逻辑 - 新增订单创建幂等性防重(Redis 业务键 + 分布式锁) - 更新 AGENTS.md/CLAUDE.md:新增注释规范、幂等性规范,移除测试要求 - 添加套餐系统升级完整文档(API文档、使用指南、功能总结、运维指南) - 归档 OpenSpec package-system-upgrade 变更,同步 specs 到主目录 - 新增 queue types 抽象和 Redis 常量定义
This commit is contained in:
277
docs/package-system-upgrade/API文档.md
Normal file
277
docs/package-system-upgrade/API文档.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# 套餐系统升级 - API 文档
|
||||
|
||||
## 客户端 API
|
||||
|
||||
### 查询我的流量使用情况
|
||||
|
||||
获取当前用户绑定的卡/设备的套餐流量使用情况。
|
||||
|
||||
**请求**
|
||||
|
||||
```http
|
||||
GET /api/h5/packages/my-usage
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"main_package": {
|
||||
"package_usage_id": 101,
|
||||
"package_id": 1,
|
||||
"package_name": "月度套餐 30G",
|
||||
"data_limit_mb": 30720,
|
||||
"data_usage_mb": 15360,
|
||||
"status": 1,
|
||||
"priority": 1,
|
||||
"activated_at": "2025-02-01T00:00:00Z",
|
||||
"expires_at": "2025-02-28T23:59:59Z",
|
||||
"data_reset_cycle": "monthly",
|
||||
"last_reset_at": "2025-02-01T00:00:00Z",
|
||||
"next_reset_at": "2025-03-01T00:00:00Z"
|
||||
},
|
||||
"addon_packages": [
|
||||
{
|
||||
"package_usage_id": 102,
|
||||
"package_id": 5,
|
||||
"package_name": "加油包 5G",
|
||||
"data_limit_mb": 5120,
|
||||
"data_usage_mb": 2048,
|
||||
"status": 1,
|
||||
"priority": 2,
|
||||
"master_usage_id": 101,
|
||||
"activated_at": "2025-02-10T00:00:00Z",
|
||||
"expires_at": "2025-02-28T23:59:59Z"
|
||||
}
|
||||
],
|
||||
"total": {
|
||||
"total_mb": 35840,
|
||||
"used_mb": 17408,
|
||||
"remaining_mb": 18432
|
||||
}
|
||||
},
|
||||
"timestamp": 1707667200
|
||||
}
|
||||
```
|
||||
|
||||
**响应字段说明**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `main_package` | object | 主套餐信息(可能为 null) |
|
||||
| `addon_packages` | array | 加油包列表 |
|
||||
| `total.total_mb` | int64 | 总流量(MB) |
|
||||
| `total.used_mb` | int64 | 已用流量(MB) |
|
||||
| `total.remaining_mb` | int64 | 剩余流量(MB) |
|
||||
|
||||
**套餐状态 status**
|
||||
|
||||
| 值 | 说明 |
|
||||
|----|------|
|
||||
| 0 | 待生效 |
|
||||
| 1 | 生效中 |
|
||||
| 2 | 已用完 |
|
||||
| 3 | 已过期 |
|
||||
| 4 | 已失效 |
|
||||
|
||||
---
|
||||
|
||||
## 后台管理 API
|
||||
|
||||
### 查询套餐流量详单
|
||||
|
||||
查询指定套餐的每日流量使用记录。
|
||||
|
||||
**请求**
|
||||
|
||||
```http
|
||||
GET /api/admin/package-usage/{id}/daily-records
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**Query 参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `start_date` | string | 是 | 开始日期(YYYY-MM-DD) |
|
||||
| `end_date` | string | 是 | 结束日期(YYYY-MM-DD) |
|
||||
|
||||
**响应**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"package_usage_id": 101,
|
||||
"package_name": "月度套餐 30G",
|
||||
"records": [
|
||||
{
|
||||
"date": "2025-02-01",
|
||||
"daily_usage_mb": 1024,
|
||||
"cumulative_usage_mb": 1024
|
||||
},
|
||||
{
|
||||
"date": "2025-02-02",
|
||||
"daily_usage_mb": 512,
|
||||
"cumulative_usage_mb": 1536
|
||||
},
|
||||
{
|
||||
"date": "2025-02-03",
|
||||
"daily_usage_mb": 2048,
|
||||
"cumulative_usage_mb": 3584
|
||||
}
|
||||
],
|
||||
"total_usage_mb": 15360
|
||||
},
|
||||
"timestamp": 1707667200
|
||||
}
|
||||
```
|
||||
|
||||
**错误码**
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|-------|------|
|
||||
| 400 | 参数错误(日期格式不正确) |
|
||||
| 403 | 无权限访问该套餐 |
|
||||
| 404 | 套餐不存在 |
|
||||
|
||||
---
|
||||
|
||||
### 创建套餐(扩展字段)
|
||||
|
||||
创建套餐时支持的新字段。
|
||||
|
||||
**请求**
|
||||
|
||||
```http
|
||||
POST /api/admin/packages
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"package_name": "月度套餐 30G",
|
||||
"package_type": "main",
|
||||
"data_limit_mb": 30720,
|
||||
"price": 9900,
|
||||
"calendar_type": "natural_month",
|
||||
"duration_months": 1,
|
||||
"data_reset_cycle": "monthly",
|
||||
"enable_realname_activation": false
|
||||
}
|
||||
```
|
||||
|
||||
**新增字段说明**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `calendar_type` | string | 是 | 有效期类型:`natural_month`(自然月)、`by_day`(按天) |
|
||||
| `duration_months` | int | 条件必填 | 自然月套餐的月数(calendar_type=natural_month 时必填) |
|
||||
| `duration_days` | int | 条件必填 | 按天套餐的天数(calendar_type=by_day 时必填) |
|
||||
| `data_reset_cycle` | string | 是 | 流量重置周期:`daily`、`monthly`、`yearly`、`none` |
|
||||
| `enable_realname_activation` | bool | 否 | 是否需要实名后激活(默认 false) |
|
||||
|
||||
**calendar_type 取值**
|
||||
|
||||
| 值 | 说明 | 有效期计算 |
|
||||
|----|------|-----------|
|
||||
| `natural_month` | 自然月 | 激活月份 + N 个月,月末过期 |
|
||||
| `by_day` | 按天 | 激活日期 + N 天 |
|
||||
|
||||
**data_reset_cycle 取值**
|
||||
|
||||
| 值 | 说明 | 重置时间 |
|
||||
|----|------|---------|
|
||||
| `daily` | 日重置 | 每天 00:00:00 |
|
||||
| `monthly` | 月重置 | 自然月套餐:每月1号<br>按天套餐:每30天 |
|
||||
| `yearly` | 年重置 | 每年1月1日 |
|
||||
| `none` | 不重置 | 不重置 |
|
||||
|
||||
---
|
||||
|
||||
### 更新套餐(扩展字段)
|
||||
|
||||
更新套餐时支持的新字段。
|
||||
|
||||
**请求**
|
||||
|
||||
```http
|
||||
PUT /api/admin/packages/{id}
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**
|
||||
|
||||
```json
|
||||
{
|
||||
"calendar_type": "by_day",
|
||||
"duration_days": 30,
|
||||
"data_reset_cycle": "none",
|
||||
"enable_realname_activation": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 查询套餐详情(扩展字段)
|
||||
|
||||
获取套餐详情时返回的新字段。
|
||||
|
||||
**响应**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": 1,
|
||||
"package_name": "月度套餐 30G",
|
||||
"package_type": "main",
|
||||
"data_limit_mb": 30720,
|
||||
"price": 9900,
|
||||
"calendar_type": "natural_month",
|
||||
"duration_months": 1,
|
||||
"duration_days": 0,
|
||||
"data_reset_cycle": "monthly",
|
||||
"enable_realname_activation": false,
|
||||
"status": 1,
|
||||
"created_at": "2025-01-01T00:00:00Z",
|
||||
"updated_at": "2025-01-15T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码汇总
|
||||
|
||||
| 错误码 | HTTP 状态码 | 说明 |
|
||||
|-------|------------|------|
|
||||
| `CodePackageActivationConflict` | 409 | 套餐正在激活中,请稍后重试 |
|
||||
| `CodeNoMainPackage` | 400 | 必须有主套餐才能购买加油包 |
|
||||
| `CodeRealnameRequired` | 403 | 设备/卡必须先完成实名认证才能购买套餐 |
|
||||
| `CodeMixedOrderForbidden` | 400 | 同订单不能同时购买正式套餐和加油包 |
|
||||
|
||||
---
|
||||
|
||||
## 数据权限
|
||||
|
||||
### 客户端 API
|
||||
|
||||
- 只能查询当前用户绑定的卡/设备的套餐信息
|
||||
- 用户身份通过 JWT Token 识别
|
||||
|
||||
### 后台管理 API
|
||||
|
||||
- 代理商:只能查询自己店铺及下级店铺的套餐
|
||||
- 企业用户:只能查询自己企业的套餐
|
||||
- 平台用户:可查询所有套餐
|
||||
- 越权访问返回 403 错误
|
||||
278
docs/package-system-upgrade/使用指南.md
Normal file
278
docs/package-system-upgrade/使用指南.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# 套餐系统升级 - 使用指南
|
||||
|
||||
## 场景一:囤货待实名激活
|
||||
|
||||
### 业务场景
|
||||
|
||||
代理商后台为未实名的卡/设备预先购买套餐,用户实名后自动激活。
|
||||
|
||||
### 操作流程
|
||||
|
||||
```
|
||||
1. 代理商登录后台
|
||||
2. 选择未实名的卡/设备
|
||||
3. 购买套餐(选择支持实名激活的套餐)
|
||||
4. 套餐状态:待激活(status=0, pending_realname_activation=true)
|
||||
5. 用户完成实名认证
|
||||
6. 系统自动激活套餐
|
||||
```
|
||||
|
||||
### 前置条件
|
||||
|
||||
- 套餐必须启用 `enable_realname_activation=true`
|
||||
- 卡/设备当前未实名
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 囤货套餐在实名前不会计算有效期
|
||||
- 实名后,有效期从激活日期开始计算
|
||||
- 如果卡/设备已实名,套餐会立即激活
|
||||
|
||||
---
|
||||
|
||||
## 场景二:主套餐排队
|
||||
|
||||
### 业务场景
|
||||
|
||||
用户当前有生效中的主套餐,想提前购买下一个套餐。
|
||||
|
||||
### 操作流程
|
||||
|
||||
```
|
||||
1. 用户购买新主套餐
|
||||
2. 系统检测到已有生效中主套餐
|
||||
3. 新套餐进入排队状态(status=0, priority=N+1)
|
||||
4. 当前主套餐过期
|
||||
5. 系统自动激活排队中的下一个套餐
|
||||
```
|
||||
|
||||
### 排队规则
|
||||
|
||||
| 情况 | 新套餐状态 |
|
||||
|------|-----------|
|
||||
| 无生效中主套餐 | 立即激活(status=1, priority=1) |
|
||||
| 有生效中主套餐 | 排队等待(status=0, priority=MAX+1) |
|
||||
|
||||
### 查看排队情况
|
||||
|
||||
```http
|
||||
GET /api/h5/packages/my-usage
|
||||
|
||||
// 响应
|
||||
{
|
||||
"main_package": {
|
||||
"package_name": "月度套餐",
|
||||
"status": 1, // 生效中
|
||||
"expires_at": "2025-03-31T23:59:59Z"
|
||||
},
|
||||
"queued_packages": [
|
||||
{
|
||||
"package_name": "季度套餐",
|
||||
"status": 0, // 排队中
|
||||
"priority": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 场景三:加油包购买
|
||||
|
||||
### 业务场景
|
||||
|
||||
用户主套餐流量不够用,需要购买加油包补充流量。
|
||||
|
||||
### 操作流程
|
||||
|
||||
```
|
||||
1. 确认用户有生效中或待生效的主套餐
|
||||
2. 用户选择加油包
|
||||
3. 系统自动绑定到当前主套餐
|
||||
4. 加油包立即生效
|
||||
5. 流量扣减时优先使用加油包
|
||||
```
|
||||
|
||||
### 购买限制
|
||||
|
||||
| 限制项 | 说明 |
|
||||
|-------|------|
|
||||
| 必须有主套餐 | 无主套餐无法购买加油包 |
|
||||
| 混买禁止 | 同一订单不能同时购买主套餐和加油包 |
|
||||
|
||||
### 加油包生命周期
|
||||
|
||||
```
|
||||
主套餐过期 → 加油包自动失效(status=4)
|
||||
```
|
||||
|
||||
### 加油包有效期
|
||||
|
||||
| 类型 | 有效期计算 |
|
||||
|------|-----------|
|
||||
| 随主套餐 | 与主套餐同时过期(has_independent_expiry=false) |
|
||||
| 独立有效期 | 从购买日期开始计算(has_independent_expiry=true) |
|
||||
|
||||
---
|
||||
|
||||
## 场景四:流量查询
|
||||
|
||||
### 客户端查询我的流量
|
||||
|
||||
```http
|
||||
GET /api/h5/packages/my-usage
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"main_package": {
|
||||
"package_usage_id": 101,
|
||||
"package_name": "月度套餐 30G",
|
||||
"data_limit_mb": 30720,
|
||||
"data_usage_mb": 15360,
|
||||
"status": 1,
|
||||
"activated_at": "2025-02-01T00:00:00Z",
|
||||
"expires_at": "2025-02-28T23:59:59Z"
|
||||
},
|
||||
"addon_packages": [
|
||||
{
|
||||
"package_usage_id": 102,
|
||||
"package_name": "加油包 5G",
|
||||
"data_limit_mb": 5120,
|
||||
"data_usage_mb": 2048,
|
||||
"status": 1,
|
||||
"priority": 2
|
||||
}
|
||||
],
|
||||
"total": {
|
||||
"total_mb": 35840,
|
||||
"used_mb": 17408,
|
||||
"remaining_mb": 18432
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 后台查询套餐流量详单
|
||||
|
||||
```http
|
||||
GET /api/admin/package-usage/101/daily-records?start_date=2025-02-01&end_date=2025-02-15
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"package_usage_id": 101,
|
||||
"package_name": "月度套餐 30G",
|
||||
"records": [
|
||||
{
|
||||
"date": "2025-02-01",
|
||||
"daily_usage_mb": 1024,
|
||||
"cumulative_usage_mb": 1024
|
||||
},
|
||||
{
|
||||
"date": "2025-02-02",
|
||||
"daily_usage_mb": 512,
|
||||
"cumulative_usage_mb": 1536
|
||||
}
|
||||
],
|
||||
"total_usage_mb": 15360
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 套餐状态说明
|
||||
|
||||
| 状态码 | 名称 | 说明 |
|
||||
|-------|------|------|
|
||||
| 0 | 待生效 | 排队中或待实名激活 |
|
||||
| 1 | 生效中 | 正在使用 |
|
||||
| 2 | 已用完 | 流量已耗尽 |
|
||||
| 3 | 已过期 | 超过有效期 |
|
||||
| 4 | 已失效 | 主套餐过期导致加油包失效 |
|
||||
|
||||
---
|
||||
|
||||
## 流量重置说明
|
||||
|
||||
### 重置类型
|
||||
|
||||
| 类型 | 套餐类型 | 重置时间 | 适用场景 |
|
||||
|------|---------|---------|---------|
|
||||
| 日重置 | 所有 | 每天 00:00:00 | 日租卡 |
|
||||
| 月重置 | 自然月 | 每月1号 00:00:00 | 自然月套餐 |
|
||||
| 月重置 | 按天 | 从激活日起每30天 | 按天套餐 |
|
||||
| 年重置 | 所有 | 每年1月1日 | 年度套餐 |
|
||||
| 不重置 | 所有 | 不重置 | 一次性流量包 |
|
||||
|
||||
### 重置行为
|
||||
|
||||
```
|
||||
重置前:data_usage_mb = 25600
|
||||
重置后:data_usage_mb = 0
|
||||
```
|
||||
|
||||
- 重置只清空已用流量,不影响有效期
|
||||
- 流量用完的套餐(status=2)重置后恢复为生效中(status=1)
|
||||
|
||||
---
|
||||
|
||||
## 停复机说明
|
||||
|
||||
### 停机条件
|
||||
|
||||
所有生效套餐流量用完:
|
||||
- 主套餐 status=2
|
||||
- 所有加油包 status=2
|
||||
|
||||
### 复机条件
|
||||
|
||||
- 购买新套餐
|
||||
- 套餐流量重置
|
||||
- 排队套餐激活
|
||||
|
||||
### 停机记录
|
||||
|
||||
```sql
|
||||
-- 停机记录在 tb_iot_card 表
|
||||
stopped_at: 停机时间
|
||||
stop_reason: 停机原因(如 "流量耗尽")
|
||||
|
||||
-- 复机后
|
||||
resumed_at: 复机时间
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 为什么购买加油包提示"必须有主套餐"?
|
||||
|
||||
A: 加油包必须绑定到主套餐,请先购买主套餐再购买加油包。
|
||||
|
||||
### Q: 主套餐过期后加油包还能用吗?
|
||||
|
||||
A: 不能。主套餐过期后,绑定的加油包会自动失效(status=4)。
|
||||
|
||||
### Q: 套餐排队后可以取消吗?
|
||||
|
||||
A: 目前不支持取消排队中的套餐,请联系客服处理。
|
||||
|
||||
### Q: 流量重置后为什么还是停机状态?
|
||||
|
||||
A: 流量重置后系统会自动触发复机,如果仍是停机状态,请检查运营商接口是否正常。
|
||||
|
||||
### Q: H5 端未实名用户如何购买套餐?
|
||||
|
||||
A: H5 端必须先完成实名认证才能购买套餐。代理商可在后台为未实名用户囤货。
|
||||
183
docs/package-system-upgrade/功能总结.md
Normal file
183
docs/package-system-upgrade/功能总结.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# 套餐系统升级 - 功能总结
|
||||
|
||||
## 概述
|
||||
|
||||
本次升级实现了完整的套餐生命周期管理,支持主套餐排队激活、加油包绑定主套餐、囤货待实名激活、流量按优先级扣减等核心功能。
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 套餐有效期计算
|
||||
|
||||
| 类型 | 计算方式 | 示例 |
|
||||
|------|---------|------|
|
||||
| 自然月 | 激活月份 + N 个月,月末 23:59:59 | 2月15日激活3个月 → 5月31日 23:59:59 过期 |
|
||||
| 按天 | 激活日期 + N 天,当天 23:59:59 | 2月15日激活30天 → 3月16日 23:59:59 过期 |
|
||||
|
||||
### 2. 主套餐排队机制
|
||||
|
||||
```
|
||||
卡/设备 购买主套餐 A → 立即激活(status=1, priority=1)
|
||||
购买主套餐 B → 排队等待(status=0, priority=2)
|
||||
主套餐 A 过期 → 自动激活主套餐 B
|
||||
```
|
||||
|
||||
- 同一卡/设备同时只能有一个生效中的主套餐
|
||||
- 新购买的主套餐自动进入排队状态
|
||||
- 过期检查每 10 秒执行一次
|
||||
|
||||
### 3. 加油包绑定主套餐
|
||||
|
||||
```
|
||||
加油包必须绑定到当前生效的主套餐(master_usage_id)
|
||||
├── 加油包与主套餐同时生效
|
||||
├── 主套餐过期时,加油包自动失效(status=4)
|
||||
└── 流量扣减时,先扣加油包,再扣主套餐
|
||||
```
|
||||
|
||||
- 购买加油包时必须有生效中或待生效的主套餐
|
||||
- 加油包可设置独立有效期(`has_independent_expiry=true`)
|
||||
|
||||
### 4. 囤货待实名激活
|
||||
|
||||
```
|
||||
后台为未实名卡/设备购买套餐
|
||||
├── 套餐 status=0, pending_realname_activation=true
|
||||
├── 用户完成实名
|
||||
├── 轮询系统检测到实名状态变更
|
||||
└── 自动激活套餐(status=1)
|
||||
```
|
||||
|
||||
- 仅当套餐 `enable_realname_activation=true` 时触发此机制
|
||||
- H5 端未实名用户无法直接购买套餐
|
||||
|
||||
### 5. 流量扣减优先级
|
||||
|
||||
扣减顺序:**加油包(按 priority ASC)→ 主套餐**
|
||||
|
||||
```go
|
||||
// 示例:卡有 3 个生效套餐
|
||||
主套餐:1000MB,已用 500MB
|
||||
加油包1:100MB,已用 0MB,priority=2
|
||||
加油包2:200MB,已用 50MB,priority=3
|
||||
|
||||
// 本次使用 180MB
|
||||
扣减顺序:
|
||||
1. 加油包1 扣 100MB(用完,status=2)
|
||||
2. 加油包2 扣 80MB
|
||||
3. 主套餐不变
|
||||
```
|
||||
|
||||
### 6. 流量重置周期
|
||||
|
||||
| 周期 | 套餐类型 | 重置时间 | 说明 |
|
||||
|------|---------|---------|------|
|
||||
| 日重置 | 所有 | 每天 00:00:00 | `data_reset_cycle=daily` |
|
||||
| 月重置 | 自然月 | 每月1号 00:00:00 | `calendar_type=natural_month` |
|
||||
| 月重置 | 按天 | 从激活日期起每30天 | `calendar_type=by_day` |
|
||||
| 年重置 | 所有 | 每年1月1日 00:00:00 | `data_reset_cycle=yearly` |
|
||||
|
||||
### 7. 停复机机制
|
||||
|
||||
- **停机条件**:所有生效套餐流量用完(主套餐 + 所有加油包 status=2)
|
||||
- **复机条件**:购买新套餐或套餐激活后自动复机
|
||||
|
||||
## 数据库变更
|
||||
|
||||
### 新增表
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| `tb_package_usage_daily_record` | 套餐流量日记录 |
|
||||
| `tb_card_daily_usage` | 卡每日流量使用汇总 |
|
||||
|
||||
### 扩展字段
|
||||
|
||||
**tb_package 表**:
|
||||
- `calendar_type`: 有效期类型(natural_month/by_day)
|
||||
- `data_reset_cycle`: 流量重置周期(daily/monthly/yearly/none)
|
||||
- `enable_realname_activation`: 是否需要实名后激活
|
||||
- `duration_days`: 按天套餐的有效天数
|
||||
|
||||
**tb_package_usage 表**:
|
||||
- `priority`: 套餐优先级
|
||||
- `master_usage_id`: 主套餐 ID(加油包使用)
|
||||
- `has_independent_expiry`: 加油包是否有独立有效期
|
||||
- `pending_realname_activation`: 是否待实名激活
|
||||
- `data_reset_cycle`: 流量重置周期
|
||||
- `last_reset_at`: 上次重置时间
|
||||
- `next_reset_at`: 下次重置时间
|
||||
|
||||
**tb_iot_card 表**:
|
||||
- `stopped_at`: 停机时间
|
||||
- `resumed_at`: 复机时间
|
||||
- `stop_reason`: 停机原因
|
||||
|
||||
**tb_carrier 表**:
|
||||
- `billing_day`: 运营商计费日(用于流量查询接口的计费周期计算,联通=27,其他=1)
|
||||
|
||||
## API 端点
|
||||
|
||||
### 新增端点
|
||||
|
||||
| 端点 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/h5/packages/my-usage` | GET | 客户端查询我的流量使用情况 |
|
||||
| `/api/admin/package-usage/:id/daily-records` | GET | 查询套餐流量详单 |
|
||||
|
||||
### 扩展端点
|
||||
|
||||
套餐管理 API 支持新字段:
|
||||
- `calendar_type`: 有效期类型
|
||||
- `duration_days`: 有效天数
|
||||
- `data_reset_cycle`: 重置周期
|
||||
- `enable_realname_activation`: 实名激活开关
|
||||
|
||||
## 轮询任务
|
||||
|
||||
| 任务 | 调度频率 | 说明 |
|
||||
|------|---------|------|
|
||||
| 套餐激活检查 | 每 10 秒 | 检查过期主套餐,激活排队套餐 |
|
||||
| 流量重置调度 | 每 10 秒 | 执行日/月/年流量重置 |
|
||||
| 实名状态检查 | 配置化 | 检测首次实名,触发套餐激活 |
|
||||
|
||||
## Asynq 任务
|
||||
|
||||
| 任务类型 | 说明 |
|
||||
|---------|------|
|
||||
| `task:package:first_activation` | 首次实名激活套餐 |
|
||||
| `task:package:queue_activation` | 排队主套餐激活 |
|
||||
|
||||
## 错误码
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|-------|------|
|
||||
| `CodePackageActivationConflict` | 套餐正在激活中 |
|
||||
| `CodeNoMainPackage` | 必须有主套餐才能购买加油包 |
|
||||
| `CodeRealnameRequired` | 必须先完成实名认证才能购买套餐 |
|
||||
| `CodeMixedOrderForbidden` | 同订单不能同时购买正式套餐和加油包 |
|
||||
|
||||
## 技术实现
|
||||
|
||||
### Service 层
|
||||
|
||||
| 服务 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| ActivationService | `activation_service.go` | 套餐激活(实名激活、排队激活) |
|
||||
| UsageService | `usage_service.go` | 流量扣减、停机检查 |
|
||||
| ResetService | `reset_service.go` | 流量重置(日/月/年) |
|
||||
| CustomerViewService | `customer_view_service.go` | 客户端流量查询 |
|
||||
| DailyRecordService | `daily_record_service.go` | 套餐流量详单 |
|
||||
| StopResumeService | `stop_resume_service.go` | 停复机操作 |
|
||||
|
||||
### 工具函数
|
||||
|
||||
| 函数 | 说明 |
|
||||
|------|------|
|
||||
| `CalculateExpiryTime()` | 计算套餐过期时间 |
|
||||
| `CalculateNextResetTime()` | 计算下次重置时间 |
|
||||
|
||||
## 性能优化
|
||||
|
||||
- 流量重置分批处理:每批最多 10000 条
|
||||
- 使用 Redis 分布式锁避免套餐激活并发问题
|
||||
- Asynq 任务重试策略:MaxRetry(3), Timeout(30s)
|
||||
279
docs/package-system-upgrade/运维指南.md
Normal file
279
docs/package-system-upgrade/运维指南.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# 套餐系统升级 - 运维指南
|
||||
|
||||
## 监控指标
|
||||
|
||||
### Asynq 队列监控
|
||||
|
||||
| 指标 | 说明 | 正常范围 | 告警阈值 |
|
||||
|------|------|---------|---------|
|
||||
| `asynq_queue_size{queue="default"}` | 默认队列长度 | < 100 | > 1000 |
|
||||
| `asynq_queue_latency_seconds` | 任务处理延迟 | < 5s | > 30s |
|
||||
| `asynq_processed_total` | 已处理任务数 | 持续增长 | - |
|
||||
| `asynq_failed_total` | 失败任务数 | 接近 0 | > 10/min |
|
||||
|
||||
### 套餐激活监控
|
||||
|
||||
| 指标 | 说明 | 正常范围 | 告警阈值 |
|
||||
|------|------|---------|---------|
|
||||
| 排队套餐激活延迟 | 主套餐过期到下一个激活的时间 | < 30s | > 1min |
|
||||
| 实名激活延迟 | 实名完成到套餐激活的时间 | < 30s | > 1min |
|
||||
| 待激活套餐堆积 | `status=0` 的套餐数量 | 正常波动 | 持续增长 |
|
||||
|
||||
### API 性能监控
|
||||
|
||||
| 指标 | 端点 | 正常范围 | 告警阈值 |
|
||||
|------|------|---------|---------|
|
||||
| 响应时间 P95 | `/api/h5/packages/my-usage` | < 100ms | > 200ms |
|
||||
| 响应时间 P99 | `/api/h5/packages/my-usage` | < 200ms | > 500ms |
|
||||
| 响应时间 P95 | `/api/admin/package-usage/:id/daily-records` | < 150ms | > 300ms |
|
||||
|
||||
### 数据库监控
|
||||
|
||||
| 指标 | 说明 | 正常范围 | 告警阈值 |
|
||||
|------|------|---------|---------|
|
||||
| 流量重置执行时间 | 单批次重置耗时 | < 5s | > 10s |
|
||||
| 套餐表行数增长 | `tb_package_usage` 每日新增 | 正常波动 | 异常增长 |
|
||||
| 日记录表行数 | `tb_package_usage_daily_record` | 正常增长 | - |
|
||||
|
||||
---
|
||||
|
||||
## 告警规则
|
||||
|
||||
### Prometheus 告警规则示例
|
||||
|
||||
```yaml
|
||||
groups:
|
||||
- name: package_system_alerts
|
||||
rules:
|
||||
# 套餐激活延迟告警
|
||||
- alert: PackageActivationDelayHigh
|
||||
expr: histogram_quantile(0.95, rate(package_activation_duration_seconds_bucket[5m])) > 60
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "套餐激活延迟过高"
|
||||
description: "套餐激活 P95 延迟超过 1 分钟,当前值: {{ $value }}s"
|
||||
|
||||
# Asynq 队列堆积告警
|
||||
- alert: AsynqQueueBacklog
|
||||
expr: asynq_queue_size{queue="default"} > 1000
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Asynq 任务队列堆积"
|
||||
description: "默认队列任务数超过 1000,当前值: {{ $value }}"
|
||||
|
||||
# 任务失败率告警
|
||||
- alert: AsynqTaskFailureRateHigh
|
||||
expr: rate(asynq_failed_total[5m]) > 0.1
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Asynq 任务失败率过高"
|
||||
description: "任务失败率超过 10%,当前值: {{ $value }}/s"
|
||||
|
||||
# API 响应时间告警
|
||||
- alert: PackageAPILatencyHigh
|
||||
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{path=~"/api/h5/packages.*"}[5m])) > 0.2
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "套餐 API 响应时间过高"
|
||||
description: "套餐相关 API P95 响应时间超过 200ms"
|
||||
|
||||
# 流量重置执行时间告警
|
||||
- alert: DataResetDurationHigh
|
||||
expr: package_data_reset_duration_seconds > 10
|
||||
for: 1m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "流量重置执行时间过长"
|
||||
description: "流量重置批次执行时间超过 10 秒"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 回滚预案
|
||||
|
||||
### 场景一:代码回滚
|
||||
|
||||
**触发条件**:
|
||||
- API 接口异常
|
||||
- 业务逻辑错误
|
||||
- 性能严重下降
|
||||
|
||||
**回滚步骤**:
|
||||
|
||||
```bash
|
||||
# 1. 切换到上一个稳定版本
|
||||
git checkout <上一个稳定版本 tag>
|
||||
|
||||
# 2. 重新构建镜像
|
||||
make build-docker
|
||||
|
||||
# 3. 重新部署
|
||||
kubectl rollout restart deployment/cmp-api
|
||||
kubectl rollout restart deployment/cmp-worker
|
||||
|
||||
# 4. 验证服务正常
|
||||
curl -s http://api-host/health | jq
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- 代码回滚不会回滚数据库迁移
|
||||
- 需要确保旧代码兼容新数据库结构
|
||||
- 新增字段使用默认值,不影响旧代码运行
|
||||
|
||||
### 场景二:数据库回滚
|
||||
|
||||
**触发条件**:
|
||||
- 迁移脚本有问题
|
||||
- 数据损坏
|
||||
- 需要完全撤销功能
|
||||
|
||||
**前置条件**:
|
||||
- 确认已备份数据库
|
||||
- 确认代码已回滚到兼容版本
|
||||
|
||||
**回滚步骤**:
|
||||
|
||||
```bash
|
||||
# 1. 停止 API 和 Worker 服务
|
||||
kubectl scale deployment/cmp-api --replicas=0
|
||||
kubectl scale deployment/cmp-worker --replicas=0
|
||||
|
||||
# 2. 执行数据库回滚
|
||||
make migrate-down STEPS=1
|
||||
|
||||
# 3. 验证数据库结构
|
||||
psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "\d tb_package"
|
||||
psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "\d tb_package_usage"
|
||||
|
||||
# 4. 重新启动服务
|
||||
kubectl scale deployment/cmp-api --replicas=3
|
||||
kubectl scale deployment/cmp-worker --replicas=2
|
||||
```
|
||||
|
||||
**回滚脚本位置**:
|
||||
`migrations/000055_package_system_upgrade.down.sql`
|
||||
|
||||
### 场景三:数据修复
|
||||
|
||||
**情况 1:套餐状态异常**
|
||||
|
||||
```sql
|
||||
-- 查找状态异常的套餐
|
||||
SELECT id, status, activated_at, expires_at
|
||||
FROM tb_package_usage
|
||||
WHERE status = 1 AND expires_at < NOW();
|
||||
|
||||
-- 修复:将过期套餐标记为已过期
|
||||
UPDATE tb_package_usage
|
||||
SET status = 3, updated_at = NOW()
|
||||
WHERE status = 1 AND expires_at < NOW();
|
||||
```
|
||||
|
||||
**情况 2:加油包未正确失效**
|
||||
|
||||
```sql
|
||||
-- 查找主套餐已过期但加油包仍生效的记录
|
||||
SELECT pu.id, pu.status, pu.master_usage_id, master.status as master_status
|
||||
FROM tb_package_usage pu
|
||||
JOIN tb_package_usage master ON pu.master_usage_id = master.id
|
||||
WHERE pu.status = 1 AND master.status = 3;
|
||||
|
||||
-- 修复:将这些加油包标记为失效
|
||||
UPDATE tb_package_usage
|
||||
SET status = 4, updated_at = NOW()
|
||||
WHERE id IN (
|
||||
SELECT pu.id
|
||||
FROM tb_package_usage pu
|
||||
JOIN tb_package_usage master ON pu.master_usage_id = master.id
|
||||
WHERE pu.status = 1 AND master.status = 3
|
||||
);
|
||||
```
|
||||
|
||||
**情况 3:流量重置时间错误**
|
||||
|
||||
```sql
|
||||
-- 查找下次重置时间异常的套餐
|
||||
SELECT id, data_reset_cycle, next_reset_at
|
||||
FROM tb_package_usage
|
||||
WHERE data_reset_cycle = 'daily' AND next_reset_at < NOW() - INTERVAL '1 day';
|
||||
|
||||
-- 修复:重新计算下次重置时间
|
||||
UPDATE tb_package_usage
|
||||
SET next_reset_at = DATE_TRUNC('day', NOW()) + INTERVAL '1 day',
|
||||
updated_at = NOW()
|
||||
WHERE data_reset_cycle = 'daily' AND next_reset_at < NOW() - INTERVAL '1 day';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 日常运维
|
||||
|
||||
### 手动触发流量重置
|
||||
|
||||
```bash
|
||||
# 通过 API 触发
|
||||
curl -X POST http://api-host/api/admin/internal/trigger-data-reset \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN"
|
||||
```
|
||||
|
||||
### 查看 Asynq 队列状态
|
||||
|
||||
```bash
|
||||
# 查看队列概览
|
||||
asynq stats
|
||||
|
||||
# 查看待处理任务
|
||||
asynq list pending
|
||||
|
||||
# 查看失败任务
|
||||
asynq list archived
|
||||
```
|
||||
|
||||
### 重试失败任务
|
||||
|
||||
```bash
|
||||
# 重试所有失败任务
|
||||
asynq task run archived --all
|
||||
|
||||
# 重试特定任务
|
||||
asynq task run archived --id=<task_id>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 容量规划
|
||||
|
||||
### 数据增长预估
|
||||
|
||||
| 表 | 每日增量 | 月增量 | 年增量 |
|
||||
|----|---------|--------|--------|
|
||||
| `tb_package_usage` | ~1000 行 | ~30000 行 | ~360000 行 |
|
||||
| `tb_package_usage_daily_record` | ~10000 行 | ~300000 行 | ~3600000 行 |
|
||||
| `tb_card_daily_usage` | ~10000 行 | ~300000 行 | ~3600000 行 |
|
||||
|
||||
### 存储预估
|
||||
|
||||
| 表 | 单行大小 | 年存储量 |
|
||||
|----|---------|---------|
|
||||
| `tb_package_usage_daily_record` | ~100 bytes | ~360 MB |
|
||||
| `tb_card_daily_usage` | ~80 bytes | ~288 MB |
|
||||
|
||||
### 清理策略
|
||||
|
||||
```sql
|
||||
-- 清理 180 天前的日记录(可选)
|
||||
DELETE FROM tb_package_usage_daily_record
|
||||
WHERE date < NOW() - INTERVAL '180 days';
|
||||
|
||||
DELETE FROM tb_card_daily_usage
|
||||
WHERE usage_date < NOW() - INTERVAL '180 days';
|
||||
```
|
||||
Reference in New Issue
Block a user