修复一些问题,主要是生效套餐
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s
This commit is contained in:
File diff suppressed because it is too large
Load Diff
265
docs/agent-open-api/功能总结.md
Normal file
265
docs/agent-open-api/功能总结.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# 代理开放接口对接说明
|
||||
|
||||
## 1. 接入说明
|
||||
|
||||
代理开放接口挂载在 `/api/open/v1`,面向代理店铺第三方系统调用。接入方使用代理账号登录信息进行签名认证,通过认证后即可调用卡查询、套餐查询、预充值钱包查询和套餐购买接口。
|
||||
|
||||
请通过 HTTPS 调用开放接口。接入方不要在客户端日志、浏览器控制台或异常信息中记录账号密码、签名、随机串等敏感信息。
|
||||
|
||||
## 2. 认证 Header
|
||||
|
||||
每个请求必须携带以下 Header:
|
||||
|
||||
| Header | 是什么 | 怎么来 | 示例 | 注意事项 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `X-Agent-Account` | 代理账号标识 | 填写代理登录平台使用的用户名或手机号,由平台开通代理账号时提供 | `agent001` 或 `13800000000` | 必须与签名原文最后一行 `account` 完全一致 |
|
||||
| `X-Agent-Password` | 代理账号当前登录密码 | 由代理账号持有人提供,和登录平台的密码一致 | `Passw0rd123` | 同时作为 HMAC-SHA256 签名密钥;密码变更后必须使用新密码签名;不要写入日志 |
|
||||
| `X-Agent-Timestamp` | 请求发起时间 | 调用方在每次请求前生成 | `1715400000`、`1715400000000` 或 `2024-05-11T10:00:00+08:00` | 支持 Unix 秒、Unix 毫秒、RFC3339;必须与签名原文 `timestamp` 行完全一致;客户端服务器时间需同步 |
|
||||
| `X-Agent-Nonce` | 请求随机串 | 调用方每次请求自行生成,不需要平台提前分配 | `n-1715400000-001` 或 UUID | 同一账号 5 分钟内不可重复,重复会被判定为重放请求 |
|
||||
| `X-Agent-Sign` | 请求签名 | 调用方按下方签名规则实时计算 | `5f2d...` | 不是平台分配的固定值;只要 method、path、query、body、timestamp、nonce、account 或 password 任意一个变化,签名都会变化 |
|
||||
|
||||
Header 生成顺序建议:
|
||||
|
||||
1. 先确定 `X-Agent-Account` 和 `X-Agent-Password`。
|
||||
2. 每次请求生成新的 `X-Agent-Timestamp` 和 `X-Agent-Nonce`。
|
||||
3. 按下方规则拼出签名原文。
|
||||
4. 用 `X-Agent-Password` 对签名原文做 HMAC-SHA256,得到 `X-Agent-Sign`。
|
||||
5. 带上 5 个 Header 发起请求。
|
||||
|
||||
签名原文:
|
||||
|
||||
```text
|
||||
METHOD
|
||||
PATH
|
||||
canonical_query
|
||||
body_sha256
|
||||
timestamp
|
||||
nonce
|
||||
account
|
||||
```
|
||||
|
||||
拼接规则:
|
||||
|
||||
- 使用换行符 `\n` 连接 7 行,最后一行后不追加换行。
|
||||
- `METHOD` 使用大写 HTTP 方法,例如 `GET`、`POST`。
|
||||
- `PATH` 只取请求路径,例如 `/api/open/v1/cards/status`,不包含域名和 query。
|
||||
- `canonical_query` 为 query 参数规范化结果:排除 `sign` 字段;参数名按升序排列;同名多值按值升序排列;key 和 value 使用 URL QueryEscape 编码后按 `key=value` 拼接;多个参数用 `&` 连接;无 query 时为空字符串。
|
||||
- `body_sha256` 为原始请求 body 字节的 SHA256 小写十六进制值。GET 或空 body 使用空字符串的 SHA256:`e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`。
|
||||
- POST JSON 请求必须先确定最终发送的 body 字符串,再用同一个字符串计算 `body_sha256` 并发送请求;签名后不要再重新格式化 JSON、调整字段顺序或改变空格。
|
||||
- `timestamp`、`nonce`、`account` 必须与 Header 中的值完全一致。
|
||||
|
||||
签名值为:
|
||||
|
||||
```text
|
||||
lowercase_hex(HMAC-SHA256(password, sign_payload))
|
||||
```
|
||||
|
||||
其中 `password` 为 `X-Agent-Password` 的原始值,`sign_payload` 为上面 7 行拼接后的字符串。
|
||||
|
||||
### 2.1 GET 签名示例
|
||||
|
||||
请求:
|
||||
|
||||
```text
|
||||
GET /api/open/v1/cards/status?card_no=89860000000000000001
|
||||
X-Agent-Account: agent001
|
||||
X-Agent-Password: Passw0rd123
|
||||
X-Agent-Timestamp: 1715400000
|
||||
X-Agent-Nonce: n-1715400000-001
|
||||
```
|
||||
|
||||
签名原文:
|
||||
|
||||
```text
|
||||
GET
|
||||
/api/open/v1/cards/status
|
||||
card_no=89860000000000000001
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
1715400000
|
||||
n-1715400000-001
|
||||
agent001
|
||||
```
|
||||
|
||||
Node.js 示例:
|
||||
|
||||
```javascript
|
||||
const crypto = require("crypto");
|
||||
|
||||
function sha256Hex(input) {
|
||||
return crypto.createHash("sha256").update(input).digest("hex");
|
||||
}
|
||||
|
||||
function queryEscape(value) {
|
||||
return new URLSearchParams({ v: String(value) }).toString().slice(2);
|
||||
}
|
||||
|
||||
function canonicalQuery(params) {
|
||||
return Object.entries(params)
|
||||
.filter(([key]) => key.toLowerCase() !== "sign")
|
||||
.flatMap(([key, value]) => {
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
return values.sort().map((item) => [key, item]);
|
||||
})
|
||||
.sort(([aKey, aValue], [bKey, bValue]) => {
|
||||
if (aKey === bKey) return String(aValue).localeCompare(String(bValue));
|
||||
return aKey.localeCompare(bKey);
|
||||
})
|
||||
.map(([key, value]) => `${queryEscape(key)}=${queryEscape(value)}`)
|
||||
.join("&");
|
||||
}
|
||||
|
||||
function sign({ method, path, query, body, timestamp, nonce, account, password }) {
|
||||
const payload = [
|
||||
method.toUpperCase(),
|
||||
path,
|
||||
canonicalQuery(query),
|
||||
sha256Hex(body || ""),
|
||||
timestamp,
|
||||
nonce,
|
||||
account,
|
||||
].join("\n");
|
||||
|
||||
return crypto.createHmac("sha256", password).update(payload).digest("hex");
|
||||
}
|
||||
```
|
||||
|
||||
完整 GET 调用示例:
|
||||
|
||||
```javascript
|
||||
const https = require("https");
|
||||
|
||||
const account = "agent001";
|
||||
const password = "Passw0rd123";
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const nonce = `nonce-${Date.now()}`;
|
||||
const method = "GET";
|
||||
const path = "/api/open/v1/cards/status";
|
||||
const query = { card_no: "89860000000000000001" };
|
||||
const body = "";
|
||||
const requestQuery = canonicalQuery(query);
|
||||
const requestPath = `${path}?${requestQuery}`;
|
||||
const signature = sign({ method, path, query, body, timestamp, nonce, account, password });
|
||||
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "open.example.com",
|
||||
path: requestPath,
|
||||
method,
|
||||
headers: {
|
||||
"X-Agent-Account": account,
|
||||
"X-Agent-Password": password,
|
||||
"X-Agent-Timestamp": timestamp,
|
||||
"X-Agent-Nonce": nonce,
|
||||
"X-Agent-Sign": signature,
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => console.log(data));
|
||||
}
|
||||
);
|
||||
|
||||
req.end();
|
||||
```
|
||||
|
||||
完整 POST 调用示例:
|
||||
|
||||
```javascript
|
||||
const https = require("https");
|
||||
|
||||
const account = "agent001";
|
||||
const password = "Passw0rd123";
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const nonce = `nonce-${Date.now()}`;
|
||||
const method = "POST";
|
||||
const path = "/api/open/v1/wallet/package-orders";
|
||||
const query = {};
|
||||
|
||||
const body = JSON.stringify({
|
||||
card_nos: ["89860000000000000001", "89860000000000000002"],
|
||||
package_code: "PKG001",
|
||||
});
|
||||
|
||||
const signature = sign({ method, path, query, body, timestamp, nonce, account, password });
|
||||
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "open.example.com",
|
||||
path,
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(body),
|
||||
"X-Agent-Account": account,
|
||||
"X-Agent-Password": password,
|
||||
"X-Agent-Timestamp": timestamp,
|
||||
"X-Agent-Nonce": nonce,
|
||||
"X-Agent-Sign": signature,
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => console.log(data));
|
||||
}
|
||||
);
|
||||
|
||||
req.write(body);
|
||||
req.end();
|
||||
```
|
||||
|
||||
curl 调用示例:
|
||||
|
||||
```bash
|
||||
curl -G "https://open.example.com/api/open/v1/cards/status" \
|
||||
--data-urlencode "card_no=89860000000000000001" \
|
||||
-H "X-Agent-Account: agent001" \
|
||||
-H "X-Agent-Password: Passw0rd123" \
|
||||
-H "X-Agent-Timestamp: 1715400000" \
|
||||
-H "X-Agent-Nonce: n-1715400000-001" \
|
||||
-H "X-Agent-Sign: 上面代码计算出的签名"
|
||||
```
|
||||
|
||||
## 3. 接口列表
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/api/open/v1/cards/traffic` | 按 `card_no` 查询单卡流量和套餐 |
|
||||
| `GET` | `/api/open/v1/cards/status` | 按 `card_no` 查询单卡正常/停机状态 |
|
||||
| `GET` | `/api/open/v1/cards/realname-status` | 按 `card_no` 查询实名状态 |
|
||||
| `GET` | `/api/open/v1/packages` | 查询可购买套餐列表 |
|
||||
| `POST` | `/api/open/v1/wallet/package-orders` | 使用预充值钱包给多张卡购买同一个套餐 |
|
||||
| `GET` | `/api/open/v1/wallet/balance` | 查询预充值钱包余额 |
|
||||
| `GET` | `/api/open/v1/wallet/transactions` | 查询预充值钱包流水 |
|
||||
|
||||
`card_no` 支持 ICCID、虚拟号、MSISDN。开放接口只支持单卡,设备标识会返回参数错误。
|
||||
|
||||
## 4. 字段口径
|
||||
|
||||
卡流量字段:
|
||||
|
||||
- `total_flow_mb`:套餐总流量,单位 MB。
|
||||
- `used_flow_mb`:已用流量,单位 MB。
|
||||
- `remaining_flow_mb`:剩余流量,单位 MB。
|
||||
|
||||
套餐列表字段:
|
||||
|
||||
- `cost_price`:套餐结算价,单位分。
|
||||
- `retail_price`:套餐销售价,单位分。
|
||||
- `total_flow_mb`:套餐总流量,单位 MB。
|
||||
- `valid_days`:套餐有效天数。
|
||||
|
||||
钱包购买接口一次只允许一个 `package_code`,`card_nos` 最多 100 张卡。每张卡独立创建钱包订单,允许部分成功,失败卡会返回独立错误码和原因。
|
||||
|
||||
## 5. 错误码
|
||||
|
||||
| 错误码 | 说明 |
|
||||
| --- | --- |
|
||||
| `1048` | 开放接口认证失败 |
|
||||
| `1049` | 开放接口签名无效 |
|
||||
| `1190` | 开放接口时间戳无效 |
|
||||
| `1191` | 重复请求,请勿重放 |
|
||||
| `1005` | 无权限操作该资源或资源不存在 |
|
||||
|
||||
参数校验失败统一返回 `1001`。认证类失败只返回统一错误码和消息。
|
||||
@@ -79,6 +79,7 @@
|
||||
| `virtual_total_mb` | int64 | 当前主套餐业务停机阈值 MB |
|
||||
| `virtual_used_mb` | float64 | 当前主套餐展示已用量 MB |
|
||||
| `reduction_pct` | float64 | 展示增幅比例,公式为 `(real_total_mb / virtual_total_mb) - 1` |
|
||||
| `enable_virtual_data` | bool | 当前主套餐是否启用虚流量(按套餐使用记录快照返回) |
|
||||
| `device_protect_status` | string | 保护期状态:`none` / `stop` / `start`(仅 device) |
|
||||
| `activated_at` | time | 激活时间(未来准备移除,请勿依赖) |
|
||||
| `created_at` | time | 创建时间 |
|
||||
|
||||
328
docs/deployment/生产数据库手工上线流程.md
Normal file
328
docs/deployment/生产数据库手工上线流程.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# 生产数据库手工上线流程
|
||||
|
||||
## 1. 适用场景
|
||||
|
||||
本文档用于君鸿卡管系统**首次生产上线**时的数据库初始化,目标是:
|
||||
|
||||
1. **不依赖应用启动自动建表**
|
||||
2. **人工创建生产库和表结构**
|
||||
3. **人工导入基础数据**
|
||||
4. **人工对齐 `schema_migrations` 版本**
|
||||
5. 后续继续沿用 `migrations/` 中的新增迁移
|
||||
|
||||
> 截至 **2026-05-06**,仓库内最新迁移版本为 **140**。
|
||||
> 当前仓库缺少可直接用于“全新环境从 0 建表”的 `000114_squash_baseline`,因此首次生产上线建议走“**手工 schema 基线 + 手工基础数据导入 + 手工迁移版本对齐**”。
|
||||
|
||||
---
|
||||
|
||||
## 2. 上线原则
|
||||
|
||||
### 2.1 本次建议做的事
|
||||
|
||||
1. 从你确认过的**源库**生成一份生产首发 schema 基线 SQL
|
||||
2. 在生产环境手工创建空数据库
|
||||
3. 手工执行 schema 基线 SQL 建表
|
||||
4. 导入基础数据
|
||||
5. 手工写入 `schema_migrations=140`
|
||||
6. 启动 API / Worker
|
||||
|
||||
### 2.2 本次不建议做的事
|
||||
|
||||
1. 不依赖应用自动建表
|
||||
2. 不直接把当前活跃 `migrations/` 当作全新建库链路执行
|
||||
3. 不把业务数据表(订单、卡、设备、钱包流水等)从测试库整库导入生产
|
||||
4. 不直接复用测试环境里的密钥、回调地址、商户配置而不做复核
|
||||
|
||||
---
|
||||
|
||||
## 3. 基础数据范围
|
||||
|
||||
### 3.1 建议导入的非敏感基础数据
|
||||
|
||||
以下表建议从已确认的源库导出,再导入生产库:
|
||||
|
||||
1. `tb_permission`
|
||||
2. `tb_role`
|
||||
3. `tb_role_permission`
|
||||
4. `tb_carrier`
|
||||
5. `tb_commission_withdrawal_setting`
|
||||
6. `tb_polling_config`
|
||||
7. `tb_polling_concurrency_config`
|
||||
8. `tb_polling_alert_rule`
|
||||
9. `tb_data_cleanup_config`
|
||||
|
||||
### 3.2 建议单独处理的敏感基础数据
|
||||
|
||||
以下表包含支付、公众号、小程序、证书、密钥等敏感信息,建议单独导出或手工填充:
|
||||
|
||||
1. `tb_wechat_config`
|
||||
|
||||
### 3.3 不建议首发直接导入的数据
|
||||
|
||||
以下数据不属于“生产首发基础数据”,通常不要从测试库带入生产:
|
||||
|
||||
1. `tb_account`
|
||||
2. `tb_account_role`
|
||||
3. `tb_shop`
|
||||
4. `tb_enterprise`
|
||||
5. `tb_iot_card`
|
||||
6. `tb_device`
|
||||
7. `tb_order`
|
||||
8. 各类钱包、流水、日志、审计、导入任务表
|
||||
|
||||
---
|
||||
|
||||
## 4. 关于表结构基线
|
||||
|
||||
### 4.1 建议默认排除的确认遗留表
|
||||
|
||||
以下两张表已具备明确“遗留/保留历史”的语义,生成生产首发 schema 基线时建议默认排除:
|
||||
|
||||
1. `tb_data_usage_record`
|
||||
2. `tb_card_replacement_record_legacy`
|
||||
|
||||
### 4.2 建议保守保留、后续再评估的表
|
||||
|
||||
以下表当前在代码里的活跃引用较少或暂无数据,但仍不建议在首次生产上线时贸然删除,先按兼容方式保留更稳:
|
||||
|
||||
1. `tb_card_replacement_request`
|
||||
2. `tb_number_card`
|
||||
3. `tb_payment_merchant_setting`
|
||||
4. `tb_dev_capability_config`
|
||||
|
||||
---
|
||||
|
||||
## 5. 交付文件
|
||||
|
||||
本目录对应的手工上线辅助文件位于:
|
||||
|
||||
`scripts/manual_db_release/`
|
||||
|
||||
按执行顺序主要包括:
|
||||
|
||||
1. `01_create_database.sql`
|
||||
2. `02_generate_schema_baseline.sh`
|
||||
3. `03_export_base_data_from_source.sh`
|
||||
4. `04_import_base_data_into_target.sh`
|
||||
5. `05_repair_base_data_sequences.sql`
|
||||
6. `06_mark_schema_migrations.sql`
|
||||
7. `07_verify_manual_release.sql`
|
||||
|
||||
---
|
||||
|
||||
## 6. 手工上线步骤
|
||||
|
||||
## 6.1 第一步:确定源库
|
||||
|
||||
源库应满足:
|
||||
|
||||
1. 结构版本已确认正确
|
||||
2. 基础配置完整
|
||||
3. 权限、角色、运营商、轮询配置经过业务验收
|
||||
|
||||
建议不要临时选一个“有人随手改过但未复核”的测试库。
|
||||
|
||||
---
|
||||
|
||||
## 6.2 第二步:手工创建生产数据库
|
||||
|
||||
先修改:
|
||||
|
||||
`scripts/manual_db_release/01_create_database.sql`
|
||||
|
||||
将其中的占位符替换为:
|
||||
|
||||
1. `__DB_NAME__`
|
||||
2. `__DB_USER__`
|
||||
3. `__DB_PASSWORD__`
|
||||
|
||||
然后执行:
|
||||
|
||||
```bash
|
||||
psql "postgres://postgres:你的密码@生产PG主机:5432/postgres?sslmode=disable" \
|
||||
-f scripts/manual_db_release/01_create_database.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.3 第三步:生成 schema 基线 SQL
|
||||
|
||||
使用脚本:
|
||||
|
||||
```bash
|
||||
SOURCE_DSN="postgres://源库账号:源库密码@源库主机:5432/源库名?sslmode=disable" \
|
||||
OUTPUT_SQL="/你的目录/00_schema_baseline.sql" \
|
||||
bash scripts/manual_db_release/02_generate_schema_baseline.sh
|
||||
```
|
||||
|
||||
默认会排除:
|
||||
|
||||
1. `schema_migrations`
|
||||
2. `tb_data_usage_record`
|
||||
3. `tb_card_replacement_record_legacy`
|
||||
|
||||
生成后请人工复核一次:
|
||||
|
||||
1. 是否包含你确认不需要的历史表
|
||||
2. 是否错误带入测试专用对象
|
||||
3. 是否包含意外的 `OWNER TO`
|
||||
4. 是否包含不应该进入生产的注释性对象
|
||||
|
||||
---
|
||||
|
||||
## 6.4 第四步:在生产库执行 schema 基线
|
||||
|
||||
```bash
|
||||
psql "postgres://junhong_cmp:CtCom1zBzPQbpVNf3rCNxH@cxd.whcxd.cn:16159/junhong_cmp_prod?sslmode=disable" \
|
||||
-v ON_ERROR_STOP=1 \
|
||||
-f /你的目录/00_schema_baseline.sql
|
||||
```
|
||||
|
||||
执行后先不要急着启动应用,先导入基础数据。
|
||||
|
||||
---
|
||||
|
||||
## 6.5 第五步:导出基础数据
|
||||
|
||||
导出非敏感基础数据:
|
||||
|
||||
```bash
|
||||
SOURCE_DSN="postgres://erp_pgsql:erp_2025@cxd.whcxd.cn:16159/junhong_cmp_test?sslmode=disable" \
|
||||
bash scripts/manual_db_release/03_export_base_data_from_source.sh
|
||||
```
|
||||
|
||||
如需连同微信配置一起导出:
|
||||
|
||||
```bash
|
||||
SOURCE_DSN="postgres://erp_pgsql:erp_2025@cxd.whcxd.cn:16159/junhong_cmp_test?sslmode=disable" \
|
||||
INCLUDE_SENSITIVE=1 \
|
||||
bash scripts/manual_db_release/03_export_base_data_from_source.sh
|
||||
```
|
||||
|
||||
导出结果会放到:
|
||||
|
||||
`scripts/manual_db_release/generated/`
|
||||
|
||||
> 注意:`generated/` 下的 SQL 可能包含密钥和证书内容,不要提交到 Git。
|
||||
|
||||
---
|
||||
|
||||
## 6.6 第六步:导入基础数据
|
||||
|
||||
在生产库执行:
|
||||
|
||||
```bash
|
||||
TARGET_DSN="postgres://junhong_cmp:CtCom1zBzPQbpVNf3rCNxH@cxd.whcxd.cn:16159/junhong_cmp_prod?sslmode=disable" \
|
||||
bash scripts/manual_db_release/04_import_base_data_into_target.sh
|
||||
```
|
||||
|
||||
如需导入敏感配置:
|
||||
|
||||
```bash
|
||||
TARGET_DSN="postgres://生产库账号:生产库密码@生产库主机:5432/生产库名?sslmode=disable" \
|
||||
INCLUDE_SENSITIVE=1 \
|
||||
bash scripts/manual_db_release/04_import_base_data_into_target.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.7 第七步:修正序列值
|
||||
|
||||
```bash
|
||||
psql "postgres://junhong_cmp:CtCom1zBzPQbpVNf3rCNxH@cxd.whcxd.cn:16159/junhong_cmp_prod?sslmode=disable" \
|
||||
-v ON_ERROR_STOP=1 \
|
||||
-f scripts/manual_db_release/05_repair_base_data_sequences.sql
|
||||
```
|
||||
|
||||
这一步用于避免显式插入 ID 后,自增序列仍停留在旧值。
|
||||
|
||||
---
|
||||
|
||||
## 6.8 第八步:手工对齐迁移版本
|
||||
|
||||
```bash
|
||||
psql "postgres://生产库账号:生产库密码@生产库主机:5432/生产库名?sslmode=disable" \
|
||||
-v ON_ERROR_STOP=1 \
|
||||
-f scripts/manual_db_release/06_mark_schema_migrations.sql
|
||||
```
|
||||
|
||||
执行后,生产库会被视为已处于 `140` 版本。
|
||||
后续如果仓库新增 `141+` 迁移,就可以继续正常执行 `migrate up`。
|
||||
|
||||
> 如果你上线时仓库最新迁移版本已经不是 `140`,记得同步修改 `06_mark_schema_migrations.sql`。
|
||||
|
||||
---
|
||||
|
||||
## 6.9 第九步:上线前校验
|
||||
|
||||
```bash
|
||||
psql "postgres://生产库账号:生产库密码@生产库主机:5432/生产库名?sslmode=disable" \
|
||||
-f scripts/manual_db_release/07_verify_manual_release.sql
|
||||
```
|
||||
|
||||
重点关注:
|
||||
|
||||
1. `schema_migrations.version=140`
|
||||
2. 权限、角色、角色权限数量不为 0
|
||||
3. 运营商配置数量不为 0
|
||||
4. 轮询配置数量不为 0
|
||||
5. `tb_wechat_config` 是否按预期导入
|
||||
|
||||
---
|
||||
|
||||
## 6.10 第十步:启动应用
|
||||
|
||||
确认以下环境变量改为生产值:
|
||||
|
||||
1. `JUNHONG_DATABASE_HOST`
|
||||
2. `JUNHONG_DATABASE_PORT`
|
||||
3. `JUNHONG_DATABASE_USER`
|
||||
4. `JUNHONG_DATABASE_PASSWORD`
|
||||
5. `JUNHONG_DATABASE_DBNAME`
|
||||
6. `JUNHONG_REDIS_*`
|
||||
7. `JUNHONG_JWT_SECRET_KEY`
|
||||
|
||||
特别注意:
|
||||
|
||||
1. 当前仓库里的 `docker-compose.prod.yml` 示例仍是测试库参数,不能原样直接用于生产
|
||||
2. 如果生产库里没有超级管理员账号,应用启动时会自动创建一个超级管理员账号
|
||||
3. 如不希望使用默认超管账号,请提前设置 `JUNHONG_DEFAULT_ADMIN_USERNAME`、`JUNHONG_DEFAULT_ADMIN_PASSWORD`、`JUNHONG_DEFAULT_ADMIN_PHONE`
|
||||
|
||||
---
|
||||
|
||||
## 7. 推荐执行顺序汇总
|
||||
|
||||
```text
|
||||
手工创建生产库
|
||||
-> 生成 schema 基线 SQL
|
||||
-> 执行 schema 基线 SQL
|
||||
-> 导出基础数据
|
||||
-> 导入基础数据
|
||||
-> 修正序列
|
||||
-> 写入 schema_migrations=140
|
||||
-> 校验
|
||||
-> 启动 API / Worker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险提醒
|
||||
|
||||
### 8.1 关于 `tb_wechat_config`
|
||||
|
||||
该表包含敏感配置,建议:
|
||||
|
||||
1. 单独导出
|
||||
2. 单独传输
|
||||
3. 单独审批
|
||||
4. 导入后立刻校验回调地址、商户号、证书内容
|
||||
|
||||
### 8.2 关于账号初始化
|
||||
|
||||
本次流程**不建议**直接从测试库导入 `tb_account`。
|
||||
首发生产环境建议只导入权限体系和系统配置,让生产超管账号在受控条件下独立创建。
|
||||
|
||||
### 8.3 关于后续迁移
|
||||
|
||||
本次只是首次生产上线的“人工基线落地”。
|
||||
后续版本发布仍应继续补齐正式的 `000114_squash_baseline`,避免长期依赖手工基线流程。
|
||||
85
docs/series-binding-bulk-selection/功能总结.md
Normal file
85
docs/series-binding-bulk-selection/功能总结.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 套餐系列批量绑定选择方式优化
|
||||
|
||||
## 背景
|
||||
|
||||
运营人员为卡或设备设置套餐系列时,原接口只能提交当前页勾选的 `iccids` 或 `device_ids`。卡/设备列表存在分页限制,遇到按批次、店铺、运营商等条件批量处理时,需要跨页反复勾选,操作成本高。
|
||||
|
||||
## 涉及接口
|
||||
|
||||
- `PATCH /api/admin/iot-cards/series-binding`
|
||||
- `PATCH /api/admin/devices/series-binding`
|
||||
|
||||
## 请求方式
|
||||
|
||||
两个接口都保留旧请求兼容:
|
||||
|
||||
```json
|
||||
{
|
||||
"iccids": ["8986000000000000001"],
|
||||
"series_id": 12
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"device_ids": [1001],
|
||||
"series_id": 12
|
||||
}
|
||||
```
|
||||
|
||||
新增 `selection_type` 后支持三种选择方式:
|
||||
|
||||
| selection_type | 卡接口 | 设备接口 | 用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `list` | `iccids` | `device_ids` | 显式列表选择,兼容旧勾选逻辑 |
|
||||
| `range` | `iccid_start` + `iccid_end` | `virtual_no_start` + `virtual_no_end` | 按连续号段/虚拟号范围批量处理 |
|
||||
| `filter` | 卡列表筛选条件 | 设备列表筛选条件 | 按完整筛选结果批量处理,不受分页限制 |
|
||||
|
||||
## 示例
|
||||
|
||||
按卡批次和运营商批量绑定:
|
||||
|
||||
```json
|
||||
{
|
||||
"selection_type": "filter",
|
||||
"batch_no": "BATCH-2026-05",
|
||||
"carrier_id": 1,
|
||||
"series_id": 12
|
||||
}
|
||||
```
|
||||
|
||||
按 ICCID 号段清除绑定:
|
||||
|
||||
```json
|
||||
{
|
||||
"selection_type": "range",
|
||||
"iccid_start": "8986000000000000001",
|
||||
"iccid_end": "8986000000000000999",
|
||||
"series_id": 0
|
||||
}
|
||||
```
|
||||
|
||||
按设备筛选条件批量绑定:
|
||||
|
||||
```json
|
||||
{
|
||||
"selection_type": "filter",
|
||||
"shop_id": 10,
|
||||
"batch_no": "DEVICE-2026-05",
|
||||
"series_id": 12
|
||||
}
|
||||
```
|
||||
|
||||
## 权限与兼容性
|
||||
|
||||
- `series_id=0` 仍表示清除套餐系列关联。
|
||||
- 代理用户仍只能操作自己店铺名下的卡/设备,保持旧接口权限语义不变。
|
||||
- 代理用户设置非零 `series_id` 时,仍会校验当前店铺是否拥有该套餐系列授权。
|
||||
- 旧请求不传 `selection_type` 时,只要传了 `iccids` 或 `device_ids`,后端仍按列表选择处理。
|
||||
|
||||
## 验证
|
||||
|
||||
- `go build ./...`
|
||||
- `go vet ./...`
|
||||
- `go run ./cmd/gendocs`
|
||||
- `ccc index`
|
||||
@@ -66,6 +66,7 @@
|
||||
| `package_remain_mb` | 来自后台资产解析结果 | `virtual_total_mb` / `virtual_used_mb` | 不再返回 remain |
|
||||
| 无 | 无 | `current_package_usage_id` | 当前主套餐使用记录 ID |
|
||||
| 无 | 无 | `reduction_pct` | 展示增幅比例 |
|
||||
| 无 | `tb_package_usage.enable_virtual_data_snapshot` | `enable_virtual_data` | 返回当前主套餐快照,供前端区分是否启用虚流量 |
|
||||
|
||||
### 3.5 C 端 `GET /api/c/v1/asset/package-history`
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ curl -X GET "http://localhost:8080/api/c/v1/asset/info?identifier=<identifier>"
|
||||
|
||||
- 仅返回当前主套餐摘要
|
||||
- 无主套餐时五字段为 `0`,`current_package_usage_id = null`
|
||||
- 当前主套餐摘要包含 `enable_virtual_data`
|
||||
- 不再出现 `package_total_mb` / `package_used_mb` / `package_remain_mb`
|
||||
|
||||
### 4.2 套餐历史
|
||||
|
||||
Reference in New Issue
Block a user