实现 IoT SIM 管理模块数据模型和数据库结构

- 添加 IoT 核心业务表:运营商、IoT 卡、设备、号卡、套餐、订单等
- 添加分佣系统表:分佣规则、分佣记录、运营商结算等
- 添加轮询和流量管理表:轮询配置、流量使用记录等
- 添加财务和系统管理表:佣金提现、换卡申请等
- 实现完整的 GORM 模型和常量定义
- 添加数据库迁移脚本和详细文档
- 集成 OpenSpec 工作流工具(opsx 命令和 skills)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-12 15:44:23 +08:00
parent 743db126f7
commit 034f00e2e7
48 changed files with 11675 additions and 1 deletions

View File

@@ -0,0 +1,940 @@
# IoT SIM 管理系统 - 分佣系统说明
## 概述
IoT SIM 管理系统实现了一套灵活的多级代理分佣体系,支持三种分佣模式(一次性分佣、长期分佣、组合分佣),支持阶梯奖励机制,支持自动解冻和手动审批,支持 OR 条件解冻逻辑。
---
## 分佣架构
### 多级代理树形结构
```
┌─────────────────────────────────────────────────────────┐
│ 平台(Platform) │
│ Level 0 │
└─────────────────────────────────────────────────────────┘
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 一级代理 A │ │ 一级代理 B │ │ 一级代理 C │
│ Level 1 │ │ Level 1 │ │ Level 1 │
│ Path: /A/ │ │ Path: /B/ │ │ Path: /C/ │
└──────────────┘ └──────────────┘ └──────────────┘
│ │
├────┬────┐ ├────┬────┐
▼ ▼ ▼ ▼ ▼ ▼
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
│A-1 │ │A-2 │ │A-3 │ │B-1 │ │B-2 │ │B-3 │
│L2 │ │L2 │ │L2 │ │L2 │ │L2 │ │L2 │
└────┘ └────┘ └────┘ └────┘ └────┘ └────┘
```
**代理层级关系表**: `agent_hierarchies`
---
## 三种分佣模式
### 1. 一次性分佣 (One-time Commission)
**特点**: 订单完成后立即发放佣金
**适用场景**:
- 首次激活奖励
- 推广奖励
- 快速返佣
**示例**:
```
用户购买套餐 → 订单完成 → 立即发放佣金给上级代理
```
**配置示例**:
```sql
INSERT INTO commission_rules (
rule_name,
rule_type,
package_series_id,
commission_type,
commission_value,
status
) VALUES (
'一次性分佣-套餐激活奖励',
'one_time',
1, -- 套餐系列 ID
'fixed', -- 固定金额
10.00, -- 10元
1 -- 启用
);
```
**业务流程**:
```
订单创建 → 订单支付 → 订单完成
└─→ 创建分佣记录 (status=1 待发放)
└─→ 自动发放 (status=2 已发放)
```
---
### 2. 长期分佣 (Long-term Commission)
**特点**: 订单完成后冻结佣金,满足解冻条件后发放
**适用场景**:
- 续费奖励
- 留存奖励
- 长期激励
**解冻条件**:
- **时间条件**: 冻结 N 天后自动解冻
- **流量条件**: IoT 卡累计使用 M MB 流量后解冻
- **OR 逻辑**: 时间到期 **OR** 流量达标,满足任一条件即可解冻
**示例**:
```
用户购买套餐 → 订单完成 → 冻结佣金 (30天或1GB流量)
时间到期 OR 流量达标
自动解冻发放
```
**配置示例**:
```sql
INSERT INTO commission_rules (
rule_name,
rule_type,
package_series_id,
commission_type,
commission_value,
freeze_days,
freeze_data_mb,
unfreeze_mode,
status
) VALUES (
'长期分佣-续费奖励',
'long_term',
1,
'percentage', -- 百分比
0.10, -- 10%
30, -- 冻结30天
1024, -- 或使用1GB流量
'auto', -- 自动解冻
1
);
```
**业务流程**:
```
订单创建 → 订单支付 → 订单完成
└─→ 创建分佣记录 (status=3 已冻结)
├─→ 时间检查: 30天后 → 自动解冻 (status=2 已发放)
└─→ 流量检查: 使用1GB流量后 → 自动解冻 (status=2 已发放)
```
**解冻条件数据结构**:
```json
{
"time_based": {
"days": 30,
"deadline": "2025-02-10T00:00:00Z"
},
"data_based": {
"data_mb": 1024,
"iot_card_id": 12345
}
}
```
---
### 3. 组合分佣 (Combined Commission)
**特点**: 同时包含一次性分佣和长期分佣,订单完成后部分立即发放,部分冻结
**适用场景**:
- 首充奖励(立即发放) + 留存奖励(冻结发放)
- 灵活激励机制
**示例**:
```
用户购买套餐 → 订单完成 → 立即发放 5元 + 冻结 10元 (30天后发放)
```
**配置示例**:
```sql
-- 1. 创建组合分佣规则
INSERT INTO commission_rules (
rule_name,
rule_type,
package_series_id,
status
) VALUES (
'组合分佣-首充+留存',
'combined',
1,
1
);
-- 2. 配置一次性条件
INSERT INTO commission_combined_conditions (
rule_id,
condition_type,
commission_type,
commission_value
) VALUES (
1, -- 上面创建的规则 ID
'one_time',
'fixed',
5.00 -- 立即发放 5元
);
-- 3. 配置长期条件
INSERT INTO commission_combined_conditions (
rule_id,
condition_type,
commission_type,
commission_value,
freeze_days,
freeze_data_mb
) VALUES (
1,
'long_term',
'fixed',
10.00, -- 冻结 10元
30, -- 30天
1024 -- 或1GB流量
);
```
**业务流程**:
```
订单创建 → 订单支付 → 订单完成
├─→ 创建一次性分佣记录 (status=1 待发放) → 立即发放 (status=2)
└─→ 创建长期分佣记录 (status=3 已冻结) → 满足条件后解冻
```
---
## 阶梯奖励机制
### 阶梯奖励说明
阶梯奖励允许根据订单数量设置不同的分佣标准,订单数量越多,分佣越高。
**示例配置**:
```
1-10 单: 10元/单
11-50 单: 15元/单
51+ 单: 20元/单
```
### 配置示例
```sql
-- 1. 创建支持阶梯的分佣规则
INSERT INTO commission_rules (
rule_name,
rule_type,
package_series_id,
enable_ladder,
status
) VALUES (
'阶梯分佣-月度订单量',
'one_time',
1,
true, -- 启用阶梯
1
);
-- 2. 配置阶梯奖励
INSERT INTO commission_ladder (rule_id, min_quantity, max_quantity, commission_type, commission_value) VALUES
(1, 1, 10, 'fixed', 10.00),
(1, 11, 50, 'fixed', 15.00),
(1, 51, NULL, 'fixed', 20.00); -- NULL 表示无上限
```
### 阶梯计算逻辑
```go
// 伪代码
func CalculateLadderCommission(agentID uint, ruleID uint, currentMonth string) float64 {
// 1. 查询阶梯配置
ladders := db.FindCommissionLadders(ruleID)
// 2. 统计当月订单数量
orderCount := db.CountOrders(agentID, currentMonth)
// 3. 匹配阶梯
for _, ladder := range ladders {
if orderCount >= ladder.MinQuantity &&
(ladder.MaxQuantity == nil || orderCount <= ladder.MaxQuantity) {
if ladder.CommissionType == "fixed" {
return ladder.CommissionValue
} else if ladder.CommissionType == "percentage" {
orderAmount := db.GetOrderAmount(orderID)
return orderAmount * ladder.CommissionValue
}
}
}
return 0
}
```
---
## 分佣计算方式
### 1. 固定金额 (Fixed)
**说明**: 每笔订单固定分佣 N 元
**示例**:
```sql
commission_type = 'fixed'
commission_value = 10.00
```
**计算公式**:
```
分佣金额 = commission_value = 10.00 元
```
---
### 2. 百分比 (Percentage)
**说明**: 按订单金额的 N% 分佣
**示例**:
```sql
commission_type = 'percentage'
commission_value = 0.10 -- 10%
```
**计算公式**:
```
分佣金额 = 订单金额 × commission_value
= 100.00 × 0.10
= 10.00 元
```
---
## 分佣记录表
### 表结构: `commission_records`
分佣记录表记录每笔分佣的详细信息:
```sql
CREATE TABLE commission_records (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL,
agent_id BIGINT NOT NULL,
rule_id BIGINT NOT NULL,
commission_type VARCHAR(50) NOT NULL,
commission_amount DECIMAL(10,2) NOT NULL,
status INT NOT NULL DEFAULT 1,
freeze_days INT DEFAULT 0,
freeze_data_mb BIGINT DEFAULT 0,
unfreeze_conditions JSONB,
unfrozen_at TIMESTAMPTZ,
distributed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
### 状态说明
| status | 状态 | 说明 |
|--------|------|------|
| 1 | 待发放 | 一次性分佣,等待发放 |
| 2 | 已发放 | 已发放到代理账户 |
| 3 | 已冻结 | 长期分佣,冻结中 |
| 4 | 已取消 | 订单取消或退款,分佣取消 |
---
## OR 条件解冻逻辑
### 解冻条件设计
长期分佣支持 **OR 条件解冻**,即时间到期 **OR** 流量达标,满足任一条件即可自动解冻。
**解冻条件数据结构**:
```json
{
"time_based": {
"days": 30,
"deadline": "2025-02-10T00:00:00Z"
},
"data_based": {
"data_mb": 1024,
"iot_card_id": 12345
}
}
```
### 解冻检查逻辑
```go
// 伪代码
func CheckUnfreezeConditions(record *CommissionRecord) bool {
var conditions struct {
TimeBased struct {
Days int `json:"days"`
Deadline time.Time `json:"deadline"`
} `json:"time_based"`
DataBased struct {
DataMB int64 `json:"data_mb"`
IotCardID uint `json:"iot_card_id"`
} `json:"data_based"`
}
json.Unmarshal(record.UnfreezeConditions, &conditions)
// 检查时间条件
if time.Now().After(conditions.TimeBased.Deadline) {
return true // 时间到期,可以解冻
}
// 检查流量条件
if conditions.DataBased.IotCardID > 0 {
card := db.FindIotCardByID(conditions.DataBased.IotCardID)
if card.DataUsageMB >= conditions.DataBased.DataMB {
return true // 流量达标,可以解冻
}
}
return false // 条件均未满足
}
```
### 自动解冻定时任务
```go
// 伪代码
func UnfreezeCommissionTask() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
// 查询所有冻结中的分佣记录
records := db.FindCommissionRecords("status = 3")
for _, record := range records {
if CheckUnfreezeConditions(&record) {
// 解冻
record.Status = 2 // 已发放
record.UnfrozenAt = time.Now()
record.DistributedAt = time.Now()
db.Save(&record)
// 发放到代理账户
DistributeCommission(record.AgentID, record.CommissionAmount)
logger.Info("分佣解冻成功",
zap.Uint("record_id", record.ID),
zap.Uint("agent_id", record.AgentID),
zap.Float64("amount", record.CommissionAmount),
)
}
}
}
}
```
---
## 分佣审批流程
### 自动审批 vs 手动审批
分佣规则的 `unfreeze_mode` 字段控制解冻模式:
- **auto**: 自动解冻,满足条件后自动发放
- **manual**: 手动审批,需要人工审核通过后才能发放
### 手动审批流程
```
订单完成 → 创建分佣记录 (status=3 已冻结)
满足解冻条件
创建审批记录 (approval_status=1 待审批)
审批人审核
├─→ 通过 (approval_status=2) → 发放佣金 (status=2 已发放)
└─→ 拒绝 (approval_status=3) → 取消分佣 (status=4 已取消)
```
### 审批表: `commission_approvals`
```sql
CREATE TABLE commission_approvals (
id BIGSERIAL PRIMARY KEY,
commission_record_id BIGINT UNIQUE NOT NULL,
agent_id BIGINT NOT NULL,
approval_status INT NOT NULL DEFAULT 1,
approver_id BIGINT,
approval_reason TEXT,
approved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
### 审批状态
| approval_status | 状态 | 说明 |
|----------------|------|------|
| 1 | 待审批 | 等待审批人审核 |
| 2 | 已通过 | 审批通过,发放佣金 |
| 3 | 已拒绝 | 审批拒绝,取消分佣 |
---
## 分佣模板
### 模板设计
分佣模板用于快速创建分佣规则,避免重复配置。
**表结构**: `commission_templates`
```sql
CREATE TABLE commission_templates (
id BIGSERIAL PRIMARY KEY,
template_name VARCHAR(255) NOT NULL,
template_data JSONB NOT NULL,
description TEXT,
status INT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
### 模板数据格式
```json
{
"rule_type": "combined",
"package_series_id": 1,
"conditions": [
{
"condition_type": "one_time",
"commission_type": "fixed",
"commission_value": 5.00
},
{
"condition_type": "long_term",
"commission_type": "fixed",
"commission_value": 10.00,
"freeze_days": 30,
"freeze_data_mb": 1024
}
]
}
```
### 使用模板创建规则
```go
// 伪代码
func CreateRuleFromTemplate(templateID uint, seriesID uint) error {
template := db.FindTemplateByID(templateID)
var data struct {
RuleType string `json:"rule_type"`
PackageSeriesID uint `json:"package_series_id"`
Conditions []struct {
ConditionType string `json:"condition_type"`
CommissionType string `json:"commission_type"`
CommissionValue float64 `json:"commission_value"`
FreezeDays int `json:"freeze_days"`
FreezeDataMB int64 `json:"freeze_data_mb"`
} `json:"conditions"`
}
json.Unmarshal(template.TemplateData, &data)
// 创建分佣规则
rule := CommissionRule{
RuleName: template.TemplateName,
RuleType: data.RuleType,
PackageSeriesID: seriesID,
Status: 1,
}
db.Create(&rule)
// 创建组合条件
for _, cond := range data.Conditions {
condition := CommissionCombinedCondition{
RuleID: rule.ID,
ConditionType: cond.ConditionType,
CommissionType: cond.CommissionType,
CommissionValue: cond.CommissionValue,
FreezeDays: cond.FreezeDays,
FreezeDataMB: cond.FreezeDataMB,
}
db.Create(&condition)
}
return nil
}
```
---
## 运营商结算
### 结算表: `carrier_settlements`
记录与运营商的月度结算情况:
```sql
CREATE TABLE carrier_settlements (
id BIGSERIAL PRIMARY KEY,
carrier_id BIGINT NOT NULL,
settlement_month VARCHAR(7) NOT NULL,
total_orders INT DEFAULT 0,
total_amount DECIMAL(10,2) DEFAULT 0,
settlement_status INT NOT NULL DEFAULT 1,
settled_at TIMESTAMPTZ,
paid_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
### 结算状态
| settlement_status | 状态 | 说明 |
|------------------|------|------|
| 1 | 待结算 | 月度未结束 |
| 2 | 已结算 | 已统计金额 |
| 3 | 已支付 | 已支付给运营商 |
### 月度结算流程
```
每月1号 → 统计上月订单数据
创建结算记录 (settlement_status=1 待结算)
财务审核
确认结算 (settlement_status=2 已结算)
支付运营商 (settlement_status=3 已支付)
```
### 结算计算逻辑
```go
// 伪代码
func GenerateCarrierSettlement(carrierID uint, month string) error {
// 1. 统计上月订单
orders := db.FindOrders("carrier_id = ? AND DATE_FORMAT(completed_at, '%Y-%m') = ?", carrierID, month)
totalOrders := len(orders)
totalAmount := 0.0
for _, order := range orders {
totalAmount += order.Amount
}
// 2. 创建结算记录
settlement := CarrierSettlement{
CarrierID: carrierID,
SettlementMonth: month,
TotalOrders: totalOrders,
TotalAmount: totalAmount,
SettlementStatus: 1, // 待结算
}
db.Create(&settlement)
return nil
}
```
---
## 提现管理
### 提现申请表: `commission_withdrawal_requests`
```sql
CREATE TABLE commission_withdrawal_requests (
id BIGSERIAL PRIMARY KEY,
agent_id BIGINT NOT NULL,
withdrawal_amount DECIMAL(10,2) NOT NULL,
withdrawal_method VARCHAR(20) NOT NULL,
account_info JSONB NOT NULL,
status INT NOT NULL DEFAULT 1,
reviewer_id BIGINT,
review_reason TEXT,
reviewed_at TIMESTAMPTZ,
paid_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
### 提现状态
| status | 状态 | 说明 |
|--------|------|------|
| 1 | 待审核 | 等待审核 |
| 2 | 已通过 | 审核通过,等待打款 |
| 3 | 已拒绝 | 审核拒绝 |
| 4 | 已打款 | 已打款到账户 |
| 5 | 已取消 | 用户取消 |
### 提现流程
```
代理提交提现申请 (status=1 待审核)
财务审核
├─→ 通过 (status=2 已通过) → 打款 (status=4 已打款)
└─→ 拒绝 (status=3 已拒绝)
```
### 提现设置表: `commission_withdrawal_settings`
```sql
CREATE TABLE commission_withdrawal_settings (
id BIGSERIAL PRIMARY KEY,
agent_id BIGINT UNIQUE NOT NULL,
min_withdrawal_amount DECIMAL(10,2) DEFAULT 0,
max_withdrawal_amount DECIMAL(10,2) DEFAULT 0,
withdrawal_fee_rate DECIMAL(5,4) DEFAULT 0,
auto_approval_enabled BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
### 提现规则检查
```go
// 伪代码
func ValidateWithdrawalRequest(agentID uint, amount float64) error {
setting := db.FindWithdrawalSetting(agentID)
// 检查最小金额
if amount < setting.MinWithdrawalAmount {
return fmt.Errorf("提现金额不能低于 %.2f 元", setting.MinWithdrawalAmount)
}
// 检查最大金额
if setting.MaxWithdrawalAmount > 0 && amount > setting.MaxWithdrawalAmount {
return fmt.Errorf("提现金额不能高于 %.2f 元", setting.MaxWithdrawalAmount)
}
// 检查账户余额
balance := db.GetAgentBalance(agentID)
fee := amount * setting.WithdrawalFeeRate
totalAmount := amount + fee
if balance < totalAmount {
return fmt.Errorf("余额不足,需要 %.2f 元(含手续费 %.2f 元)", totalAmount, fee)
}
return nil
}
```
---
## 分佣业务流程示例
### 示例 1: 一次性分佣
```
1. 用户购买套餐(100元)
2. 订单完成
3. 触发分佣计算
- 规则: 一次性分佣,固定金额 10元
- 创建分佣记录: agent_id=123, commission_amount=10.00, status=1 待发放
4. 自动发放
- 更新分佣记录: status=2 已发放, distributed_at=NOW()
- 更新代理账户余额: balance += 10.00
5. 完成
```
---
### 示例 2: 长期分佣(OR 条件解冻)
```
1. 用户购买套餐(100元)
2. 订单完成
3. 触发分佣计算
- 规则: 长期分佣,10%,冻结30天 OR 使用1GB流量
- 创建分佣记录: agent_id=123, commission_amount=10.00, status=3 已冻结
- 解冻条件: {"time_based": {"days": 30}, "data_based": {"data_mb": 1024}}
4. 定时任务检查解冻条件
- 时间检查: 30天后 → 满足条件 → 解冻
- 流量检查: 使用1GB流量后 → 满足条件 → 解冻
5. 自动解冻
- 更新分佣记录: status=2 已发放, unfrozen_at=NOW(), distributed_at=NOW()
- 更新代理账户余额: balance += 10.00
6. 完成
```
---
### 示例 3: 组合分佣
```
1. 用户购买套餐(100元)
2. 订单完成
3. 触发分佣计算
- 规则: 组合分佣
- 一次性条件: 固定金额 5元
- 长期条件: 固定金额 10元,冻结30天
4. 创建两条分佣记录
- 记录1: agent_id=123, commission_amount=5.00, status=1 待发放
- 记录2: agent_id=123, commission_amount=10.00, status=3 已冻结
5. 立即发放一次性分佣
- 记录1: status=2 已发放
- 代理账户余额: balance += 5.00
6. 30天后自动解冻长期分佣
- 记录2: status=2 已发放
- 代理账户余额: balance += 10.00
7. 完成
```
---
## 监控和统计
### 分佣统计指标
1. **代理分佣总额**
- 待发放金额
- 已发放金额
- 已冻结金额
2. **分佣发放效率**
- 平均发放时长
- 平均解冻时长
3. **提现统计**
- 提现申请数量
- 提现成功率
- 提现金额统计
### SQL 查询示例
```sql
-- 1. 代理分佣总额统计
SELECT
agent_id,
SUM(CASE WHEN status = 1 THEN commission_amount ELSE 0 END) AS pending_amount,
SUM(CASE WHEN status = 2 THEN commission_amount ELSE 0 END) AS distributed_amount,
SUM(CASE WHEN status = 3 THEN commission_amount ELSE 0 END) AS frozen_amount
FROM commission_records
WHERE agent_id = 123
GROUP BY agent_id;
-- 2. 月度分佣统计
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
COUNT(*) AS total_records,
SUM(commission_amount) AS total_amount
FROM commission_records
WHERE agent_id = 123
AND status = 2
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY month DESC;
-- 3. 提现统计
SELECT
status,
COUNT(*) AS request_count,
SUM(withdrawal_amount) AS total_amount
FROM commission_withdrawal_requests
WHERE agent_id = 123
GROUP BY status;
```
---
## 最佳实践
### 1. 合理设置冻结条件
- **过短**: 可能导致代理流失
- **过长**: 影响代理积极性
- **建议**: 根据业务特点和用户留存数据设置合理的冻结期
### 2. 使用 OR 条件解冻
- **优势**: 提高解冻灵活性,代理满足任一条件即可获得佣金
- **示例**: 30天 OR 1GB流量,满足其一即可解冻
### 3. 启用阶梯奖励
- **优势**: 激励代理提高订单量
- **示例**: 月订单量越多,单笔佣金越高
### 4. 定期审查分佣规则
- 定期分析分佣数据,优化分佣规则
- 根据代理反馈调整冻结条件和佣金比例
---
## 总结
IoT SIM 管理系统的分佣系统具有以下特点:
1. **三种分佣模式**: 一次性分佣、长期分佣、组合分佣
2. **阶梯奖励机制**: 支持根据订单数量设置不同的分佣标准
3. **OR 条件解冻**: 时间到期 OR 流量达标,满足任一条件即可解冻
4. **自动 + 手动审批**: 支持自动解冻和手动审批两种模式
5. **分佣模板**: 快速创建分佣规则,避免重复配置
6. **运营商结算**: 记录与运营商的月度结算情况
7. **提现管理**: 完善的提现申请和审批流程
8. **多级代理**: 支持无限层级的代理树形结构
通过灵活配置和使用分佣系统,可以激励代理积极性,提高销售业绩,实现平台与代理的双赢。
---
**文档版本**: v1.0
**最后更新**: 2026-01-12
**维护人员**: Claude Sonnet 4.5

View File

@@ -0,0 +1,491 @@
# IoT SIM 管理系统 - 数据模型层实施总结
## 项目信息
- **项目名称**: IoT SIM 管理系统 - 数据模型层
- **实施日期**: 2026-01-12
- **实施人员**: Claude Sonnet 4.5
- **OpenSpec 变更 ID**: iot-sim-management
---
## 实施范围
本次实施严格按照 OpenSpec 规范,仅完成数据模型层的实现,包括:
### ✅ 已完成
1. **数据库迁移脚本**
- UP 迁移脚本 (000005_create_iot_sim_management_tables.up.sql)
- DOWN 迁移脚本 (000005_create_iot_sim_management_tables.down.sql)
- 26 张数据库表
- 完整的索引定义
- 中文注释
- 三大运营商初始数据
2. **GORM 模型定义**
- 11 个模型文件
- 26 个模型结构体
- 遵循项目规范的字段定义
- 无外键约束,无 ORM 关联
3. **业务常量定义**
- 100+ 业务常量
- 5 大分类
- 统一常量管理
4. **代码质量保证**
- `go fmt` 格式化
- `goimports` 导入整理
- `golangci-lint` 质量检查(0 issues)
- `go build` 编译通过
5. **数据库迁移测试**
- UP 迁移成功 (616ms)
- DOWN 迁移成功 (602ms)
- 版本切换正确
6. **完整文档**
- 数据模型总结.md
- 表结构详细说明.md
- 轮询机制说明.md
- 分佣系统说明.md
- 实施总结.md (本文档)
### ❌ 不在本阶段范围
根据 OpenSpec 规范,以下工作不在本阶段实施范围:
- API Handler 层
- Service 业务逻辑层
- Store 数据访问层
- 单元测试
- 集成测试
- API 文档生成
---
## 数据库表清单
### 核心业务表 (4张)
| 表名 | 说明 | 记录数 |
|------|------|--------|
| carriers | 运营商 | 3 (预置) |
| iot_cards | IoT 卡 | 0 |
| devices | 设备 | 0 |
| number_cards | 号卡 | 0 |
### 套餐与流量管理表 (7张)
| 表名 | 说明 | 记录数 |
|------|------|--------|
| package_series | 套餐系列 | 0 |
| packages | 套餐 | 0 |
| agent_package_allocations | 代理套餐分配 | 0 |
| device_sim_bindings | 设备-IoT卡绑定 | 0 |
| package_usages | 套餐使用情况 | 0 |
| polling_configs | 轮询配置 | 0 |
| data_usage_records | 流量使用记录 | 0 |
### 订单管理表 (1张)
| 表名 | 说明 | 记录数 |
|------|------|--------|
| orders | 订单 | 0 |
### 分佣系统表 (8张)
| 表名 | 说明 | 记录数 |
|------|------|--------|
| agent_hierarchies | 代理层级关系 | 0 |
| commission_rules | 分佣规则 | 0 |
| commission_ladder | 分佣阶梯 | 0 |
| commission_combined_conditions | 组合分佣条件 | 0 |
| commission_records | 分佣记录 | 0 |
| commission_approvals | 分佣审批 | 0 |
| commission_templates | 分佣模板 | 0 |
| carrier_settlements | 运营商结算 | 0 |
### 财务管理表 (3张)
| 表名 | 说明 | 记录数 |
|------|------|--------|
| commission_withdrawal_requests | 提现申请 | 0 |
| commission_withdrawal_settings | 提现设置 | 0 |
| payment_merchant_settings | 收款商户设置 | 0 |
### 系统管理表 (2张)
| 表名 | 说明 | 记录数 |
|------|------|--------|
| dev_capability_configs | 开发能力配置 | 0 |
| card_replacement_requests | 换卡申请 | 0 |
**总计**: 26 张表
---
## 文件清单
### 数据库迁移脚本
```
migrations/
├── 000005_create_iot_sim_management_tables.up.sql (1102 行)
└── 000005_create_iot_sim_management_tables.down.sql (27 行)
```
### GORM 模型文件
```
internal/iot/model/
├── carrier.go (17 行)
├── iot_card.go (40 行)
├── device.go (27 行)
├── number_card.go (26 行)
├── package.go (108 行)
├── order.go (36 行)
├── polling.go (29 行)
├── data_usage.go (20 行)
├── commission.go (175 行)
├── financial.go (70 行)
└── system.go (46 行)
```
**总计**: 11 个文件, 594 行代码
### 常量定义文件
```
pkg/constants/
└── iot.go (164 行)
```
### 文档文件
```
docs/iot-sim-management/
├── 数据模型总结.md
├── 表结构详细说明.md
├── 轮询机制说明.md
├── 分佣系统说明.md
└── 实施总结.md (本文档)
```
---
## 核心技术特性
### 1. 无外键约束设计
- 数据库表之间没有外键约束
- 关联关系通过存储关联 ID 字段手动维护
- 提高灵活性和性能
- 便于分布式扩展
### 2. GORM 模型规范
- 所有字段显式指定 `column:` 标签
- 禁止使用 ORM 关联关系
- 所有字段添加中文注释
- 字符串字段明确长度
- 数值字段明确精度
### 3. 多所有者模式
- `owner_type` + `owner_id` 实现多态所有权
- 支持 platform/agent/user/device 四种所有者类型
- 灵活管理资源所有权
### 4. 三层轮询机制
- 实名检查进程
- 卡流量检查进程
- 套餐流量检查进程
- 支持梯度轮询策略
- 独立的轮询配置
### 5. 三种分佣模式
- 一次性分佣 (立即发放)
- 长期分佣 (冻结后发放)
- 组合分佣 (部分立即,部分冻结)
- OR 条件解冻 (时间 OR 流量)
- 阶梯奖励机制
### 6. 真流量/虚流量共存
- 真流量额度 (`real_data_mb`)
- 虚流量额度 (`virtual_data_mb`)
- 总流量额度 (`data_amount_mb`)
- 停机判断基于虚流量
### 7. 行业卡 vs 普通卡
- 行业卡无需实名认证
- 普通卡必须实名才能激活
- 通过 `card_category` 字段区分
---
## 代码质量指标
### 编译和格式化
-`go fmt` 格式化通过
-`goimports` 导入整理通过
-`go build` 编译通过
-`go mod tidy` 依赖管理通过
### 静态分析
-`golangci-lint run` 质量检查通过
- ✅ 0 issues
- ✅ 无语法错误
- ✅ 无命名冲突
### 数据库迁移测试
- ✅ UP 迁移成功 (耗时 616ms)
- ✅ DOWN 迁移成功 (耗时 602ms)
- ✅ 版本切换正确 (4 → 5 → 4 → 5)
- ✅ 表结构验证通过
---
## 解决的问题
### 1. 常量命名冲突
**问题**: 发现订单状态常量在 `constants.go``iot.go` 中重复定义
**解决方案**:
- 将 IoT 模块的订单状态常量重命名为 `IotOrderStatus*`
- 避免与现有常量冲突
- 保持常量命名的一致性
### 2. 依赖管理
**问题**: `github.com/lib/pq` 应该作为直接依赖
**解决方案**:
- 运行 `go mod tidy` 添加直接依赖
- 确保所有依赖正确管理
---
## 关键决策记录
### 1. 字段命名规范
**决策**: 所有 GORM 模型字段必须显式指定 `column:` 标签
**理由**:
- 明确 Go 字段名和数据库字段名的映射关系
- 避免 GORM 自动转换可能带来的歧义
- 提高代码可读性和可维护性
### 2. 无外键约束
**决策**: 数据库表之间不建立外键约束
**理由**:
- 提高灵活性,业务逻辑完全在代码中控制
- 提高性能,无数据库层面的引用完整性检查开销
- 简化数据库 schema,迁移更容易
- 分布式友好,便于后续微服务拆分
### 3. 轮询机制设计
**决策**: 实现三层独立的轮询机制
**理由**:
- 实名检查、卡流量检查、套餐流量检查业务逻辑独立
- 可以针对不同场景设置不同的轮询间隔
- 提高系统可维护性和可扩展性
### 4. 分佣系统 OR 条件解冻
**决策**: 长期分佣支持时间 OR 流量两种解冻条件
**理由**:
- 提高解冻灵活性
- 代理满足任一条件即可获得佣金
- 提高代理积极性和用户留存率
---
## 性能考虑
### 索引设计
1. **主键索引**: 所有表的 `id` 字段
2. **唯一索引**:
- `iccid` (IoT 卡唯一标识)
- `order_no` (订单号)
- `carrier_code` (运营商编码)
等唯一字段
3. **组合索引**:
- `(carrier_id, status)` - IoT 卡查询优化
- `(owner_type, owner_id)` - 所有权查询优化
- `(status, created_at)` - 订单列表查询优化
4. **单列索引**:
- 外键字段
- 常用查询字段
### 查询优化建议
1. **避免 N+1 查询**: 在业务层使用批量查询
2. **分页查询**: 列表查询必须分页,避免一次性加载大量数据
3. **异步任务**: 使用 Asynq 处理轮询任务,避免阻塞主线程
4. **缓存策略**: 运营商信息、套餐信息等静态数据可以缓存
---
## 安全考虑
### 1. 数据脱敏
- 敏感信息(API 凭证、账户信息)使用 JSONB 存储
- 业务层需要实现加密/解密逻辑
### 2. 权限控制
- 数据权限通过 `owner_type` + `owner_id` 实现
- 业务层需要实现权限检查逻辑
### 3. 审计日志
- 所有表包含 `created_at``updated_at` 时间戳
- 关键操作(分佣审批、提现审批)记录审批人 ID
---
## 后续工作建议
根据 OpenSpec 规范,数据模型层完成后,建议按以下顺序进行后续开发:
### 1. Store 数据访问层
- 实现 GORM 数据访问接口
- 实现事务管理
- 实现查询优化
### 2. Service 业务逻辑层
- 实现核心业务逻辑
- 实现轮询调度器
- 实现分佣计算引擎
- 实现运营商 Gateway 对接
### 3. Handler API 层
- 实现 RESTful API 接口
- 实现参数验证
- 实现错误处理
### 4. 测试
- 单元测试
- 集成测试
- 性能测试
### 5. 文档
- API 文档
- 部署文档
- 运维手册
---
## 团队协作建议
### 1. 代码审查要点
- 检查是否遵循项目规范
- 检查是否有外键约束或 ORM 关联
- 检查字段命名是否符合规范
- 检查常量是否统一管理
### 2. 数据库变更流程
- 所有数据库变更必须通过迁移脚本
- 迁移脚本必须同时编写 UP 和 DOWN
- 迁移脚本必须在测试环境验证后才能上生产
### 3. 文档维护
- 数据模型变更时同步更新文档
- 文档使用中文编写,便于团队理解
- 文档放在 `docs/` 目录统一管理
---
## 风险和挑战
### 1. 无外键约束的数据一致性
**风险**: 删除父记录时可能遗留子记录
**应对措施**:
- 在业务层实现级联删除逻辑
- 定期运行数据一致性检查脚本
- 使用软删除(status 字段)代替物理删除
### 2. 轮询任务性能
**风险**: 大量 IoT 卡可能导致轮询任务堆积
**应对措施**:
- 使用 Asynq 任务队列,支持横向扩展
- 实现限流保护,避免过度调用运营商 API
- 根据卡状态动态调整轮询间隔
### 3. 分佣计算复杂度
**风险**: 多级代理分佣计算可能影响性能
**应对措施**:
- 使用异步任务处理分佣计算
- 实现分佣计算缓存
- 定期优化分佣计算逻辑
---
## 总结
本次实施严格按照 OpenSpec 规范完成了 IoT SIM 管理系统的数据模型层实现,包括 26 张数据库表、11 个 GORM 模型文件、100+ 业务常量,以及完整的文档。
### 成果总结
- ✅ 数据库迁移脚本 (UP + DOWN)
- ✅ GORM 模型定义 (11 个文件, 594 行代码)
- ✅ 业务常量定义 (164 行代码)
- ✅ 代码质量保证 (0 issues)
- ✅ 数据库迁移测试通过
- ✅ 完整技术文档 (5 个文档)
### 技术亮点
1. **无外键约束设计**: 提高灵活性和性能
2. **三层轮询机制**: 实名检查、卡流量检查、套餐流量检查相互独立
3. **三种分佣模式**: 一次性、长期、组合分佣,支持 OR 条件解冻
4. **多所有者模式**: 统一管理资源所有权
5. **真流量/虚流量共存**: 灵活的流量管理机制
6. **行业卡支持**: 区分行业卡和普通卡的不同业务流程
### 遵循的规范
- ✅ Go 代码风格规范 (Effective Go)
- ✅ GORM 模型规范 (显式 column 标签, 无 ORM 关联)
- ✅ 数据库设计规范 (无外键约束, 完整索引)
- ✅ 常量管理规范 (统一定义, 分类管理)
- ✅ 文档规范 (中文编写, 结构清晰)
- ✅ OpenSpec 流程规范 (只实现数据模型层)
---
**实施完成时间**: 2026-01-12
**实施人员**: Claude Sonnet 4.5
**文档版本**: v1.0

View File

@@ -0,0 +1,658 @@
# IoT SIM 管理系统 - 数据模型总结
## 概述
本文档总结了 IoT SIM 管理系统的数据模型层实现,包括 26 张数据库表和对应的 GORM 模型定义。
## 实现范围
- ✅ 数据库迁<E5BA93><E8BF81><EFBFBD>脚本 (migrations/000005_create_iot_sim_management_tables.up/down.sql)
- ✅ GORM 模型定义 (internal/iot/model/*.go)
- ✅ 业务常量定义 (pkg/constants/iot.go)
- ❌ API Handler 层 (不在本阶段范围)
- ❌ Service 业务逻辑层 (不在本阶段范围)
- ❌ Store 数据访问层 (不在本阶段范围)
## 核心业务实体
### 1. 运营商管理 (Carrier)
**表名**: `carriers`
**核心字段**:
- `carrier_code`: 运营商编码 (CMCC/CUCC/CTCC)
- `carrier_name`: 运营商名称 (中国移动/中国联通/中国电信)
- `api_endpoint`: API 接口地址
- `api_credentials`: API 凭证 (JSONB)
**预置数据**: 初始化三大运营商数据
**文件位置**: `internal/iot/model/carrier.go:5`
---
### 2. IoT 卡管理 (IotCard)
**表名**: `iot_cards`
**核心字段**:
- `iccid`: IoT 卡唯一标识 (20 位数字)
- `card_category`: 卡业务类型 (normal-普通卡, industry-行业卡)
- `carrier_id`: 所属运营商 ID
- `owner_type`: 所有者类型 (platform/agent/user/device)
- `owner_id`: 所有者 ID
- `activation_status`: 激活状态 (0-未激活, 1-已激活)
- `real_name_status`: 实名状态 (0-未实名, 1-已实名)
- `network_status`: 网络状态 (0-停机, 1-开机)
- `enable_polling`: 是否参与轮询
**特殊机制**:
- 支持行业卡(无需实名)和普通卡(需实名)
- 多所有者模式 (owner_type + owner_id)
- 轮询开关控制 (enable_polling)
- 流量使用累计 (data_usage_mb)
- Gateway 同步时间戳 (last_sync_time)
**文件位置**: `internal/iot/model/iot_card.go:8`
---
### 3. 设备管理 (Device)
**表名**: `devices`
**核心字段**:
- `device_code`: 设备唯一编码
- `device_name`: 设备名称
- `device_type`: 设备类型
- `sim_slots`: SIM 卡槽数量 (1-4)
- `owner_type`: 所有者类型 (platform/agent/user)
- `owner_id`: 所有者 ID
**关联关系**:
- 通过 `device_sim_bindings` 表关联 1-4 张 IoT 卡
**文件位置**: `internal/iot/model/device.go:5`
---
### 4. 号卡管理 (NumberCard)
**表名**: `number_cards`
**核心字段**:
- `virtual_product_code`: 虚拟商品编码 (用于对应运营商订单)
- `card_name`: 号卡名称
- `carrier`: 运营商
- `data_amount_mb`: 流量额度 (MB)
- `price`: 价格 (元)
**业务说明**: 号卡是完全独立的业务线,从上游平台下单,使用虚拟商品编码映射运营商订单。
**文件位置**: `internal/iot/model/number_card.go:8`
---
## 套餐与流量管理
### 5. 套餐系列 (PackageSeries)
**表名**: `package_series`
**核心字段**:
- `series_code`: 系列编码
- `series_name`: 系列名称
- `description`: 描述
**用途**: 套餐分组,用于一次性分佣规则配置。
**文件位置**: `internal/iot/model/package.go:7`
---
### 6. 套餐 (Package)
**表名**: `packages`
**核心字段**:
- `package_code`: 套餐编码
- `package_name`: 套餐名称
- `series_id`: 所属套餐系列 ID
- `package_type`: 套餐类型 (formal-正式套餐, addon-附加套餐)
- `duration_months`: 套餐时长 (月数)
- `data_type`: 流量类型 (real-真流量, virtual-虚流量)
- `real_data_mb`: 真流量额度 (MB)
- `virtual_data_mb`: 虚流量额度 (MB)
- `data_amount_mb`: 总流量额度 (MB)
- `price`: 套餐价格 (元)
**特殊机制**:
- 支持真流量/虚流量共存
- 停机判断基于虚流量
- 支持正式套餐和附加套餐
**文件位置**: `internal/iot/model/package.go:23`
---
### 7. 代理套餐分配 (AgentPackageAllocation)
**表名**: `agent_package_allocations`
**核心字段**:
- `agent_id`: 代理用户 ID
- `package_id`: 套餐 ID
- `cost_price`: 成本价 (元)
- `retail_price`: 零售价 (元)
**用途**: 为直属下级代理分配套餐,设置佣金模式。
**文件位置**: `internal/iot/model/package.go:48`
---
### 8. 设备-IoT 卡绑定 (DeviceSimBinding)
**表名**: `device_sim_bindings`
**核心字段**:
- `device_id`: 设备 ID
- `iot_card_id`: IoT 卡 ID
- `slot_position`: 插槽位置 (1, 2, 3, 4)
- `bind_status`: 绑定状态 (1-已绑定, 2-已解绑)
- `bind_time`: 绑定时间
- `unbind_time`: 解绑时间
**用途**: 管理设备与 IoT 卡的多对多绑定关系 (1 设备绑定 1-4 张 IoT 卡)。
**文件位置**: `internal/iot/model/package.go:66`
---
### 9. 套餐使用情况 (PackageUsage)
**表名**: `package_usages`
**核心字段**:
- `order_id`: 订单 ID
- `package_id`: 套餐 ID
- `usage_type`: 使用类型 (single_card-单卡套餐, device-设备级套餐)
- `iot_card_id`: IoT 卡 ID (单卡套餐时有值)
- `device_id`: 设备 ID (设备级套餐时有值)
- `data_limit_mb`: 流量限额 (MB)
- `data_usage_mb`: 已使用流量 (MB)
- `real_data_usage_mb`: 真流量使用 (MB)
- `virtual_data_usage_mb`: 虚流量使用 (MB)
- `activated_at`: 套餐生效时间
- `expires_at`: 套餐过期时间
- `status`: 状态 (1-生效中, 2-已用完, 3-已过期)
- `last_package_check_at`: 最后一次套餐流量检查时间
**用途**: 跟踪单卡套餐和设备级套餐的流量使用。
**文件位置**: `internal/iot/model/package.go:85`
---
### 10. 轮询配置 (PollingConfig)
**表名**: `polling_configs`
**核心字段**:
- `config_name`: 配置名称 (如 未实名卡、实名卡)
- `card_condition`: 卡状态条件 (not_real_name/real_name/activated/suspended)
- `carrier_id`: 运营商 ID (NULL 表示所有运营商)
- `real_name_check_enabled`: 是否启用实名检查
- `real_name_check_interval`: 实名检查间隔 (秒)
- `card_data_check_enabled`: 是否启用卡流量检查
- `card_data_check_interval`: 卡流量检查间隔 (秒)
- `package_check_enabled`: 是否启用套餐流量检查
- `package_check_interval`: 套餐流量检查间隔 (秒)
- `priority`: 优先级 (数字越小优先级越高)
**特殊机制**: 支持梯度轮询策略 (实名检查、卡流量检查、套餐流量检查)。
**文件位置**: `internal/iot/model/polling.go:7`
---
### 11. 流量使用记录 (DataUsageRecord)
**表名**: `data_usage_records`
**核心字段**:
- `iot_card_id`: IoT 卡 ID
- `usage_date`: 使用日期
- `data_usage_mb`: 流量使用 (MB)
- `carrier_sync_data`: 运营商同步数据 (JSONB)
- `synced_at`: 同步时间
**用途**: 记录 IoT 卡每日流量使用情况 (历史数据)。
**文件位置**: `internal/iot/model/data_usage.go:5`
---
## 订单管理
### 12. 订单 (Order)
**表名**: `orders`
**核心字段**:
- `order_no`: 订单号 (唯一标识)
- `order_type`: 订单类型 (1-套餐订单, 2-号卡订单)
- `iot_card_id`: IoT 卡 ID (单卡套餐订单时有值)
- `device_id`: 设备 ID (设备级套餐订单时有值)
- `number_card_id`: 号卡 ID (号卡订单时有值)
- `package_id`: 套餐 ID (套餐订单时有值)
- `user_id`: 用户 ID
- `agent_id`: 代理用户 ID
- `amount`: 订单金额 (元)
- `payment_method`: 支付方式 (wallet/online/carrier)
- `status`: 状态 (1-待支付, 2-已支付, 3-已完成, 4-已取消, 5-已退款)
- `carrier_order_id`: 运营商订单 ID
- `carrier_order_data`: 运营商订单原始数据 (JSONB)
**支持场景**:
- 套餐订单 (单卡套餐 / 设备级套餐)
- 号卡订单
**文件位置**: `internal/iot/model/order.go:11`
---
## 分佣系统
### 13. 代理层级关系 (AgentHierarchy)
**表名**: `agent_hierarchies`
**核心字段**:
- `agent_id`: 代理用户 ID
- `parent_agent_id`: 上级代理用户 ID
- `agent_level`: 代理层级 (1-一级代理, 2-二级代理, ...)
- `agent_path`: 代理路径 (如 /1/2/3/)
**用途**: 管理代理的树形层级关系。
**文件位置**: `internal/iot/model/commission.go:8`
---
### 14. 分佣规则 (CommissionRule)
**表名**: `commission_rules`
**核心字段**:
- `rule_name`: 规则名称
- `rule_type`: 规则类型 (one_time-一次性分佣, long_term-长期分佣, combined-组合分佣)
- `package_series_id`: 套餐系列 ID
- `commission_type`: 分佣方式 (fixed-固定金额, percentage-百分比)
- `commission_value`: 分佣值
- `target_level`: 目标层级 (NULL 表示所有层级)
- `enable_ladder`: 是否启用阶梯
- `freeze_days`: 冻结天数 (长期分佣)
- `freeze_data_mb`: 冻结流量 (MB, 长期分佣)
- `unfreeze_mode`: 解冻模式 (auto-自动, manual-手动)
**分佣类型说明**:
- **一次性分佣**: 订单完成后立即发放
- **长期分佣**: 订单完成后冻结,满足解冻条件后发放
- **组合分佣**: 同时包含一次性和长期分佣
**文件位置**: `internal/iot/model/commission.go:24`
---
### 15. 分佣阶梯 (CommissionLadder)
**表名**: `commission_ladder`
**核心字段**:
- `rule_id`: 分佣规则 ID
- `min_quantity`: 最小数量
- `max_quantity`: 最大数量
- `commission_type`: 分佣方式 (fixed/percentage)
- `commission_value`: 分佣值
**用途**: 为分佣规则配置阶梯奖励 (订单数量越多,分佣越高)。
**文件位置**: `internal/iot/model/commission.go:47`
---
### 16. 组合分佣条件 (CommissionCombinedCondition)
**表名**: `commission_combined_conditions`
**核心字段**:
- `rule_id`: 分佣规则 ID
- `condition_type`: 条件类型 (one_time-一次性, long_term-长期)
- `commission_type`: 分佣方式 (fixed/percentage)
- `commission_value`: 分佣值
- `freeze_days`: 冻结天数 (长期分佣)
- `freeze_data_mb`: 冻结流量 (MB, 长期分佣)
**用途**: 定义组合分佣规则的具体条件 (一次性部分 + 长期部分)。
**文件位置**: `internal/iot/model/commission.go:68`
---
### 17. 分佣记录 (CommissionRecord)
**表名**: `commission_records`
**核心字段**:
- `order_id`: 订单 ID
- `agent_id`: 代理用户 ID
- `rule_id`: 分佣规则 ID
- `commission_type`: 分佣类型 (one_time/long_term/combined)
- `commission_amount`: 分佣金额 (元)
- `status`: 状态 (1-待发放, 2-已发放, 3-已冻结, 4-已取消)
- `freeze_days`: 冻结天数
- `freeze_data_mb`: 冻结流量 (MB)
- `unfreeze_conditions`: 解冻条件 (JSONB)
- `unfrozen_at`: 解冻时间
- `distributed_at`: 发放时间
**特殊机制**: 支持 OR 条件解冻 (时间到期 OR 流量达标,满足其一即可解冻)。
**文件位置**: `internal/iot/model/commission.go:88`
---
### 18. 分佣审批 (CommissionApproval)
**表名**: `commission_approvals`
**核心字段**:
- `commission_record_id`: 分佣记录 ID
- `agent_id`: 代理用户 ID
- `approval_status`: 审批状态 (1-待审批, 2-已通过, 3-已拒绝)
- `approver_id`: 审批人 ID
- `approval_reason`: 审批原因
- `approved_at`: 审批时间
**用途**: 管理需要手动审批的分佣记录。
**文件位置**: `internal/iot/model/commission.go:116`
---
### 19. 分佣模板 (CommissionTemplate)
**表名**: `commission_templates`
**核心字段**:
- `template_name`: 模板名称
- `template_data`: 模板数据 (JSONB)
- `description`: 描述
**用途**: 快速创建分佣规则的预设模板。
**文件位置**: `internal/iot/model/commission.go:137`
---
### 20. 运营商结算 (CarrierSettlement)
**表名**: `carrier_settlements`
**核心字段**:
- `carrier_id`: 运营商 ID
- `settlement_month`: 结算月份 (YYYY-MM)
- `total_orders`: 总订单数
- `total_amount`: 总金额 (元)
- `settlement_status`: 结算状态 (1-待结算, 2-已结算, 3-已支付)
- `settled_at`: 结算时间
- `paid_at`: 支付时间
**用途**: 记录与运营商的月度结算情况。
**文件位置**: `internal/iot/model/commission.go:155`
---
## 财务管理
### 21. 提现申请 (CommissionWithdrawalRequest)
**表名**: `commission_withdrawal_requests`
**核心字段**:
- `agent_id`: 代理用户 ID
- `withdrawal_amount`: 提现金额 (元)
- `withdrawal_method`: 提现方式 (bank_card/alipay/wechat)
- `account_info`: 账户信息 (JSONB)
- `status`: 状态 (1-待审核, 2-已通过, 3-已拒绝, 4-已打款, 5-已取消)
- `reviewer_id`: 审核人 ID
- `review_reason`: 审核原因
- `reviewed_at`: 审核时间
- `paid_at`: 打款时间
**用途**: 管理代理用户的佣金提现申请。
**文件位置**: `internal/iot/model/financial.go:8`
---
### 22. 提现设置 (CommissionWithdrawalSetting)
**表名**: `commission_withdrawal_settings`
**核心字段**:
- `agent_id`: 代理用户 ID
- `min_withdrawal_amount`: 最小提现金额 (元)
- `max_withdrawal_amount`: 最大提现金额 (元)
- `withdrawal_fee_rate`: 提现手续费率 (小数)
- `auto_approval_enabled`: 是否启用自动审批
**用途**: 配置代理用户的提现规则。
**文件位置**: `internal/iot/model/financial.go:31`
---
### 23. 收款商户设置 (PaymentMerchantSetting)
**表名**: `payment_merchant_settings`
**核心字段**:
- `merchant_name`: 商户名称
- `merchant_type`: 商户类型 (alipay/wechat/bank)
- `merchant_config`: 商户配置 (JSONB)
- `is_default`: 是否默认商户
**用途**: 配置收款商户信息 (支付宝、微信、银行)。
**文件位置**: `internal/iot/model/financial.go:51`
---
## 系统管理
### 24. 开发能力配置 (DevCapabilityConfig)
**表名**: `dev_capability_configs`
**核心字段**:
- `capability_name`: 能力名称
- `capability_code`: 能力编码
- `capability_config`: 能力配置 (JSONB)
- `description`: 描述
**用途**: 管理系统开发能力配置 (如 API 开关、功能权限等)。
**文件位置**: `internal/iot/model/system.go:5`
---
### 25. 换卡申请 (CardReplacementRequest)
**表名**: `card_replacement_requests`
**核心字段**:
- `old_iot_card_id`: 旧卡 ID
- `new_iot_card_id`: 新卡 ID
- `user_id`: 用户 ID
- `replacement_reason`: 换卡原因
- `status`: 状态 (1-待审核, 2-已通过, 3-已拒绝, 4-已完成)
- `reviewer_id`: 审核人 ID
- `reviewed_at`: 审核时间
- `completed_at`: 完成时间
**用途**: 管理 IoT 卡的换卡申请流程。
**文件位置**: `internal/iot/model/system.go:25`
---
## 数据库设计原则
### 1. 无外键约束
- 数据库表之间**禁止建立外键约束** (Foreign Key Constraints)
- 关联关系通过存储关联 ID 字段手动维护
- 关联数据查询在代码层面显式执行
**设计理由**:
- 灵活性:业务逻辑完全在代码中控制
- 性能:无数据库层面的引用完整性检查开销
- 可控性:开发者完全掌控何时查询关联数据
- 分布式友好:在微服务场景下更容易扩展
---
### 2. GORM 模型规范
- 所有字段必须显式指定 `column:` 标签
- 禁止使用 ORM 关联关系 (`foreignKey`, `references`, `hasMany`, `belongsTo`)
- 所有字段必须添加中文注释
- 字符串字段长度必须明确定义 (VARCHAR(100)/VARCHAR(255)/TEXT)
- 数值字段精度必须明确定义 (DECIMAL(10,2)/BIGINT)
- 时间字段使用 GORM 自动管理 (`autoCreateTime`, `autoUpdateTime`)
---
### 3. 命名规范
- 数据库字段名:下划线命名法 (snake_case),如 `user_id`, `created_at`
- Go 结构体字段名:驼峰命名法 (PascalCase),如 `UserID`, `CreatedAt`
- 表名:复数形式,如 `iot_cards`, `orders`, `commission_rules`
- 常量名:大写驼峰 + 前缀,如 `IotOrderStatusPending`, `CarrierCodeCMCC`
---
## 常量定义
所有业务常量统一定义在 `pkg/constants/iot.go` 文件中,包括:
- 卡类型、卡业务类型、激活状态、实名状态、网络状态
- 所有者类型、卡状态、设备状态
- 套餐类型、流量类型、套餐使用状态
- 订单类型、订单状态、支付方式
- 轮询卡状态条件
- 分佣规则类型、分佣方式、分佣状态、解冻模式
- 提现状态、提现方式、商户类型
- 换卡申请状态、审批状态
**文件位置**: `pkg/constants/iot.go:1`
---
## 数据库迁移
### 迁移脚本
- **UP 脚本**: `migrations/000005_create_iot_sim_management_tables.up.sql`
- 创建 26 张表
- 创建所有必需的索引
- 添加完整的中文注释
- 初始化三大运营商数据
- **DOWN 脚本**: `migrations/000005_create_iot_sim_management_tables.down.sql`
- 按反向依赖顺序删除所有表
### 迁移测试
```bash
# 应用迁移
source .env && migrate -database "postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}" -path migrations up
# 回滚迁移
source .env && migrate -database "postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}" -path migrations down 1
# 查看版本
source .env && migrate -database "postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}" -path migrations version
```
### 测试结果
- ✅ UP 迁移成功 (耗时 616ms)
- ✅ DOWN 迁移成功 (耗时 602ms)
- ✅ 数据库版本正确切换 (4 → 5 → 4 → 5)
---
## 文件清单
### 数据库迁移脚本
- `migrations/000005_create_iot_sim_management_tables.up.sql` (1102 行)
- `migrations/000005_create_iot_sim_management_tables.down.sql` (27 行)
### GORM 模型定义
- `internal/iot/model/carrier.go` (17 行)
- `internal/iot/model/iot_card.go` (40 行)
- `internal/iot/model/device.go` (27 行)
- `internal/iot/model/number_card.go` (26 行)
- `internal/iot/model/package.go` (108 行)
- `internal/iot/model/order.go` (36 行)
- `internal/iot/model/polling.go` (29 行)
- `internal/iot/model/data_usage.go` (20 行)
- `internal/iot/model/commission.go` (175 行)
- `internal/iot/model/financial.go` (70 行)
- `internal/iot/model/system.go` (46 行)
### 常量定义
- `pkg/constants/iot.go` (164 行)
---
## 代码质量
-`go fmt` 格式化通过
-`goimports` 导入整理通过
-`golangci-lint` 质量检查通过 (0 issues)
-`go build` 编译通过
-`go mod tidy` 依赖管理通过
---
## 下一步工作
根据 OpenSpec 规范,本阶段只实现数据模型层,以下工作不在本阶段范围:
- ❌ API Handler 层
- ❌ Service 业务逻辑层
- ❌ Store 数据访问层
- ❌ 单元测试
- ❌ 集成测试
- ❌ API 文档生成
这些工作将在后续阶段按照 OpenSpec 流程逐步实现。
---
## 参考文档
- [表结构详细说明](./表结构详细说明.md)
- [轮询机制说明](./轮询机制说明.md)
- [分佣系统说明](./分佣系统说明.md)
---
**文档版本**: v1.0
**最后更新**: 2026-01-12
**维护人员**: Claude Sonnet 4.5

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,776 @@
# IoT SIM 管理系统 - 轮询机制说明
## 概述
IoT SIM 管理系统实现了一套灵活的三层轮询机制,用于定期检查 IoT 卡的实名状态、流量使用情况和套餐流量情况。轮询机制支持梯度策略配置,可以针对不同卡状态、不同运营商设置不同的轮询间隔和优先级。
---
## 轮询架构
### 三层轮询体系
```
┌──────────────────────────────────────────────────────────┐
│ 轮询调度器 │
│ (Polling Scheduler) │
│ - 读取轮询配置表 │
│ - 按优先级和间隔时间调度任务 │
│ - 使用 Asynq 异步任务队列 │
└──────────────────────────────────────────────────────────┘
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 实名检查进程 │ │ 卡流量检查进程 │ │ 套餐流量检查进程 │
│ (Real Name) │ │ (Card Data) │ │ (Package Data) │
├────────────────┤ ├────────────────┤ ├────────────────┤
│ - 查询未实名卡 │ │ - 查询激活的卡 │ │ - 查询生效中套餐 │
│ - 调用运营商API │ │ - 同步流量使用 │ │ - 检查流量使用 │
│ - 更新实名状态 │ │ - 更新 IoT 卡 │ │ - 判断是否停机 │
└────────────────┘ └────────────────┘ └────────────────┘
```
---
## 轮询配置表
### 表结构: `polling_configs`
轮询配置表支持灵活的梯度轮询策略:
```sql
CREATE TABLE polling_configs (
id BIGSERIAL PRIMARY KEY,
config_name VARCHAR(100) UNIQUE NOT NULL,
description VARCHAR(500),
card_condition VARCHAR(50),
carrier_id BIGINT,
real_name_check_enabled BOOLEAN DEFAULT false,
real_name_check_interval INT DEFAULT 60,
card_data_check_enabled BOOLEAN DEFAULT false,
card_data_check_interval INT DEFAULT 60,
package_check_enabled BOOLEAN DEFAULT false,
package_check_interval INT DEFAULT 60,
priority INT NOT NULL DEFAULT 100,
status INT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
### 配置字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| config_name | VARCHAR(100) | 配置名称,如"未实名卡-移动"、"已激活卡-联通" |
| card_condition | VARCHAR(50) | 卡状态条件: `not_real_name`/`real_name`/`activated`/`suspended` |
| carrier_id | BIGINT | 运营商ID,NULL 表示所有运营商 |
| real_name_check_enabled | BOOLEAN | 是否启用实名检查 |
| real_name_check_interval | INT | 实名检查间隔(秒) |
| card_data_check_enabled | BOOLEAN | 是否启用卡流量检查 |
| card_data_check_interval | INT | 卡流量检查间隔(秒) |
| package_check_enabled | BOOLEAN | 是否启用套餐流量检查 |
| package_check_interval | INT | 套餐流量检查间隔(秒) |
| priority | INT | 优先级(数字越小优先级越高) |
| status | INT | 状态: 1-启用, 2-禁用 |
---
## 轮询配置示例
### 示例 1: 未实名卡快速轮询
```sql
INSERT INTO polling_configs (
config_name,
description,
card_condition,
carrier_id,
real_name_check_enabled,
real_name_check_interval,
card_data_check_enabled,
card_data_check_interval,
priority,
status
) VALUES (
'未实名卡-快速轮询',
'对未实名卡每30秒检查一次实名状态',
'not_real_name',
NULL, -- 所有运营商
true, -- 启用实名检查
30, -- 30秒间隔
false, -- 不检查流量
0,
10, -- 高优先级
1 -- 启用
);
```
**说明**: 未实名卡需要频繁检查实名状态,以便及时发现已完成实名认证的卡。
---
### 示例 2: 已激活卡流量监控
```sql
INSERT INTO polling_configs (
config_name,
description,
card_condition,
carrier_id,
real_name_check_enabled,
real_name_check_interval,
card_data_check_enabled,
card_data_check_interval,
package_check_enabled,
package_check_interval,
priority,
status
) VALUES (
'已激活卡-流量监控',
'对已激活卡每60秒检查流量使用',
'activated',
NULL,
false, -- 不检查实名
0,
true, -- 启用卡流量检查
60, -- 60秒间隔
true, -- 启用套餐流量检查
60, -- 60秒间隔
20, -- 中优先级
1
);
```
**说明**: 已激活卡需要监控流量使用,防止超额使用和及时停机。
---
### 示例 3: 移动运营商特殊策略
```sql
INSERT INTO polling_configs (
config_name,
description,
card_condition,
carrier_id,
real_name_check_enabled,
real_name_check_interval,
card_data_check_enabled,
card_data_check_interval,
priority,
status
) VALUES (
'移动-已激活卡-慢速轮询',
'移动运营商已激活卡每180秒检查一次流量',
'activated',
1, -- 中国移动 carrier_id
false,
0,
true,
180, -- 180秒间隔
50, -- 低优先级
1
);
```
**说明**: 可以针对特定运营商设置不同的轮询策略,优化 API 调用频率。
---
## 三种轮询进程
### 1. 实名检查进程 (Real Name Check)
**目标**: 检查未实名的 IoT 卡是否已完成实名认证
**工作流程**:
1. 查询符合条件的 IoT 卡:
- `card_category = 'normal'` (普通卡需要实名)
- `real_name_status = 0` (未实名)
- `enable_polling = true` (参与轮询)
- 根据 `last_real_name_check_at` 判断是否到达检查间隔
2. 调用运营商 Gateway API 查询实名状态
3. 更新 IoT 卡的 `real_name_status``last_real_name_check_at`
4. 记录日志和异常情况
**轮询间隔控制**:
```go
// 伪代码
if time.Since(card.LastRealNameCheckAt) >= config.RealNameCheckInterval {
// 执行实名检查
result := gateway.CheckRealName(card.ICCID)
card.RealNameStatus = result.Status
card.LastRealNameCheckAt = time.Now()
db.Save(&card)
}
```
**配置参数**:
- `real_name_check_enabled`: 是否启用
- `real_name_check_interval`: 检查间隔(秒)
**注意事项**:
- 行业卡 (`card_category = 'industry'`) 无需实名检查
- 已实名的卡 (`real_name_status = 1`) 不再参与轮询
---
### 2. 卡流量检查进程 (Card Data Check)
**目标**: 同步 IoT 卡的流量使用情况
**工作流程**:
1. 查询符合条件的 IoT 卡:
- `activation_status = 1` (已激活)
- `enable_polling = true` (参与轮询)
- 根据 `last_data_check_at` 判断是否到达检查间隔
2. 调用运营商 Gateway API 查询流量使用
3. 更新 IoT 卡的 `data_usage_mb``last_data_check_at`
4. 记录流量使用历史到 `data_usage_records`
5. 判断是否需要停机:
- 如果流量超过套餐的虚流量额度,触发停机逻辑
**轮询间隔控制**:
```go
// 伪代码
if time.Since(card.LastDataCheckAt) >= config.CardDataCheckInterval {
// 执行流量检查
usage := gateway.GetDataUsage(card.ICCID)
card.DataUsageMB = usage.TotalMB
card.LastDataCheckAt = time.Now()
db.Save(&card)
// 记录历史数据
record := DataUsageRecord{
IotCardID: card.ID,
UsageDate: time.Now().Format("2006-01-02"),
DataUsageMB: usage.TodayMB,
CarrierSyncData: usage.RawData,
SyncedAt: time.Now(),
}
db.Create(&record)
}
```
**配置参数**:
- `card_data_check_enabled`: 是否启用
- `card_data_check_interval`: 检查间隔(秒)
**停机判断逻辑**:
```go
// 伪代码
if card.DataUsageMB >= package.VirtualDataMB {
// 触发停机
card.NetworkStatus = 0 // 停机
gateway.SuspendCard(card.ICCID)
}
```
---
### 3. 套餐流量检查进程 (Package Check)
**目标**: 检查套餐流量使用情况,判断套餐状态
**工作流程**:
1. 查询符合条件的套餐使用记录:
- `status = 1` (生效中)
- `expires_at > NOW()` (未过期)
- 根据 `last_package_check_at` 判断是否到达检查间隔
2. 计算套餐的流量使用情况:
- 单卡套餐: 统计该卡的流量使用
- 设备级套餐: 统计该设备绑定的所有卡的流量使用
3. 更新套餐使用记录的 `data_usage_mb``last_package_check_at`
4. 判断套餐状态:
- 如果流量用完: `status = 2` (已用完)
- 如果时间过期: `status = 3` (已过期)
5. 如果套餐用完或过期,触发停机逻辑
**轮询间隔控制**:
```go
// 伪代码
if time.Since(packageUsage.LastPackageCheckAt) >= config.PackageCheckInterval {
// 执行套餐检查
var totalUsage int64
if packageUsage.UsageType == "single_card" {
// 单卡套餐
card := db.FindIotCardByID(packageUsage.IotCardID)
totalUsage = card.DataUsageMB
} else {
// 设备级套餐
bindings := db.FindDeviceSimBindings(packageUsage.DeviceID)
for _, binding := range bindings {
card := db.FindIotCardByID(binding.IotCardID)
totalUsage += card.DataUsageMB
}
}
packageUsage.DataUsageMB = totalUsage
packageUsage.LastPackageCheckAt = time.Now()
// 判断状态
if totalUsage >= packageUsage.DataLimitMB {
packageUsage.Status = 2 // 已用完
// 触发停机
}
if time.Now().After(packageUsage.ExpiresAt) {
packageUsage.Status = 3 // 已过期
// 触发停机
}
db.Save(&packageUsage)
}
```
**配置参数**:
- `package_check_enabled`: 是否启用
- `package_check_interval`: 检查间隔(秒)
**停机判断逻辑**:
```go
// 伪代码
if packageUsage.Status == 2 || packageUsage.Status == 3 {
// 套餐用完或过期,触发停机
if packageUsage.UsageType == "single_card" {
card := db.FindIotCardByID(packageUsage.IotCardID)
card.NetworkStatus = 0 // 停机
gateway.SuspendCard(card.ICCID)
} else {
// 设备级套餐,停掉所有绑定的卡
bindings := db.FindDeviceSimBindings(packageUsage.DeviceID)
for _, binding := range bindings {
card := db.FindIotCardByID(binding.IotCardID)
card.NetworkStatus = 0
gateway.SuspendCard(card.ICCID)
}
}
}
```
---
## 轮询调度器设计
### 调度器架构
```go
// 伪代码
type PollingScheduler struct {
db *gorm.DB
queue *asynq.Client
}
func (s *PollingScheduler) Start() {
// 启动三个独立的调度协程
go s.scheduleRealNameCheck()
go s.scheduleCardDataCheck()
go s.schedulePackageCheck()
}
func (s *PollingScheduler) scheduleRealNameCheck() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
configs := s.loadPollingConfigs("real_name_check_enabled = true")
for _, config := range configs {
// 查询需要检查的卡
cards := s.findCardsForRealNameCheck(config)
for _, card := range cards {
// 使用 Asynq 异步任务队列
task := asynq.NewTask("iot:realname:check", map[string]interface{}{
"card_id": card.ID,
"config_id": config.ID,
})
s.queue.Enqueue(task, asynq.ProcessIn(0))
}
}
}
}
```
### 任务队列设计
使用 Asynq 异步任务队列处理轮询任务:
```go
// 伪代码
type RealNameCheckHandler struct {
db *gorm.DB
gateway *CarrierGateway
}
func (h *RealNameCheckHandler) ProcessTask(ctx context.Context, task *asynq.Task) error {
var payload struct {
CardID uint `json:"card_id"`
ConfigID uint `json:"config_id"`
}
if err := json.Unmarshal(task.Payload(), &payload); err != nil {
return err
}
// 加载卡信息
card := h.db.FindIotCardByID(payload.CardID)
// 调用运营商 API
result, err := h.gateway.CheckRealName(card.ICCID)
if err != nil {
return err
}
// 更新卡状态
card.RealNameStatus = result.Status
card.LastRealNameCheckAt = time.Now()
h.db.Save(&card)
return nil
}
```
---
## 轮询优先级和并发控制
### 优先级机制
轮询配置表的 `priority` 字段控制执行优先级:
- **高优先级 (1-30)**: 紧急任务,如未实名卡检查
- **中优先级 (31-70)**: 常规任务,如流量监控
- **低优先级 (71-100)**: 非紧急任务,如历史数据同步
调度器按优先级排序执行:
```sql
SELECT * FROM polling_configs
WHERE status = 1
ORDER BY priority ASC, id ASC;
```
### 并发控制
使用 Asynq 的并发控制功能:
```go
// 伪代码
queue := asynq.NewClient(asynq.RedisClientOpt{Addr: "localhost:6379"})
// 设置队列并发数
queues := map[string]int{
"iot:realname": 10, // 实名检查队列,10个并发
"iot:carddata": 20, // 卡流量检查队列,20个并发
"iot:package": 20, // 套餐检查队列,20个并发
}
server := asynq.NewServer(
asynq.RedisClientOpt{Addr: "localhost:6379"},
asynq.Config{Queues: queues},
)
```
### 限流保护
为了避免过度调用运营商 API,需要实现限流保护:
```go
// 伪代码
type RateLimiter struct {
limiter *rate.Limiter
}
func NewRateLimiter(carrierID uint) *RateLimiter {
// 每秒最多 10 次 API 调用
return &RateLimiter{
limiter: rate.NewLimiter(rate.Limit(10), 10),
}
}
func (r *RateLimiter) Wait(ctx context.Context) error {
return r.limiter.Wait(ctx)
}
```
---
## 轮询间隔策略
### 推荐配置
| 卡状态 | 实名检查间隔 | 卡流量检查间隔 | 套餐流量检查间隔 | 优先级 |
|--------|-------------|---------------|----------------|-------|
| 未实名卡 | 30秒 | - | - | 10 (高) |
| 已实名未激活 | - | - | - | - |
| 已激活卡(正常) | - | 60秒 | 60秒 | 20 (中) |
| 已激活卡(套餐即将用完) | - | 30秒 | 30秒 | 15 (高) |
| 已停用卡 | - | - | - | - |
### 动态调整策略
根据卡的流量使用情况动态调整轮询间隔:
```go
// 伪代码
func calculateCheckInterval(packageUsage *PackageUsage) int {
usagePercent := float64(packageUsage.DataUsageMB) / float64(packageUsage.DataLimitMB)
if usagePercent >= 0.9 {
return 30 // 90%以上,30秒检查一次
} else if usagePercent >= 0.7 {
return 60 // 70-90%,60秒检查一次
} else {
return 180 // 70%以下,180秒检查一次
}
}
```
---
## 轮询开关控制
### 全局开关
IoT 卡的 `enable_polling` 字段控制是否参与轮询:
```sql
-- 禁用某张卡的轮询
UPDATE iot_cards SET enable_polling = false WHERE iccid = '89860123456789012345';
-- 启用某张卡的轮询
UPDATE iot_cards SET enable_polling = true WHERE iccid = '89860123456789012345';
```
### 配置开关
轮询配置表的 `status` 字段控制整个配置是否启用:
```sql
-- 禁用某个轮询配置
UPDATE polling_configs SET status = 2 WHERE config_name = '未实名卡-快速轮询';
-- 启用某个轮询配置
UPDATE polling_configs SET status = 1 WHERE config_name = '未实名卡-快速轮询';
```
### 单项开关
轮询配置表的 `*_check_enabled` 字段控制具体检查类型:
```sql
-- 只启用实名检查,禁用流量检查
UPDATE polling_configs
SET real_name_check_enabled = true,
card_data_check_enabled = false,
package_check_enabled = false
WHERE config_name = '未实名卡-快速轮询';
```
---
## 错误处理和重试
### 错误处理策略
轮询任务可能因为以下原因失败:
1. 运营商 API 超时
2. 运营商 API 返回错误
3. 数据库连接失败
4. 网络故障
使用 Asynq 的重试机制:
```go
// 伪代码
task := asynq.NewTask("iot:realname:check", payload)
// 设置重试策略
opts := []asynq.Option{
asynq.MaxRetry(3), // 最多重试 3 次
asynq.Timeout(30 * time.Second), // 任务超时时间 30 秒
}
queue.Enqueue(task, opts...)
```
### 失败日志记录
记录失败的轮询任务:
```go
// 伪代码
type PollingLog struct {
ID uint `gorm:"primaryKey"`
TaskType string // realname/carddata/package
CardID uint
ConfigID uint
Success bool
ErrorMsg string
ExecutedAt time.Time
CreatedAt time.Time
}
func logPollingResult(taskType string, cardID, configID uint, err error) {
log := PollingLog{
TaskType: taskType,
CardID: cardID,
ConfigID: configID,
Success: err == nil,
ErrorMsg: fmt.Sprintf("%v", err),
ExecutedAt: time.Now(),
CreatedAt: time.Now(),
}
db.Create(&log)
}
```
---
## 监控和告警
### 监控指标
1. **轮询任务执行成功率**
- 实名检查成功率
- 卡流量检查成功率
- 套餐流量检查成功率
2. **轮询任务延迟**
- 任务入队时间到执行时间的延迟
- 平均延迟、P95、P99
3. **运营商 API 调用统计**
- 每分钟 API 调用次数
- API 响应时间
- API 错误率
4. **卡状态统计**
- 未实名卡数量
- 已激活卡数量
- 已停用卡数量
### 告警规则
1. **高失败率告警**
- 如果某类轮询任务 5 分钟内失败率超过 50%,触发告警
2. **高延迟告警**
- 如果轮询任务延迟超过 5 分钟,触发告警
3. **API 异常告警**
- 如果运营商 API 连续失败 10 次,触发告警
4. **流量异常告警**
- 如果某张卡流量使用突增(1小时内增加超过 100MB),触发告警
---
## 最佳实践
### 1. 合理设置轮询间隔
- **频繁轮询的代价**: 增加运营商 API 调用次数,可能触发限流
- **稀疏轮询的风险**: 流量超额检测不及时,可能导致停机延迟
- **建议**: 根据业务需求和运营商 API 限制平衡轮询频率
### 2. 使用批量查询
```go
// 不推荐: 逐个查询
for _, card := range cards {
usage := gateway.GetDataUsage(card.ICCID)
card.DataUsageMB = usage.TotalMB
db.Save(&card)
}
// 推荐: 批量查询
iccids := []string{}
for _, card := range cards {
iccids = append(iccids, card.ICCID)
}
usages := gateway.BatchGetDataUsage(iccids) // 批量查询
for _, card := range cards {
card.DataUsageMB = usages[card.ICCID]
}
db.Save(&cards) // 批量更新
```
### 3. 实现幂等性
轮询任务可能会重复执行,必须保证幂等性:
```go
// 伪代码
func ProcessRealNameCheck(cardID uint) error {
// 加锁,防止重复执行
lockKey := fmt.Sprintf("iot:realname:lock:%d", cardID)
lock := redis.SetNX(lockKey, "1", 60*time.Second)
if !lock {
return errors.New("task already running")
}
defer redis.Del(lockKey)
// 执行检查
card := db.FindIotCardByID(cardID)
result := gateway.CheckRealName(card.ICCID)
card.RealNameStatus = result.Status
card.LastRealNameCheckAt = time.Now()
db.Save(&card)
return nil
}
```
### 4. 记录详细日志
```go
// 伪代码
logger.Info("开始实名检查",
zap.Uint("card_id", card.ID),
zap.String("iccid", card.ICCID),
zap.Uint("config_id", config.ID),
)
result, err := gateway.CheckRealName(card.ICCID)
if err != nil {
logger.Error("实名检查失败",
zap.Uint("card_id", card.ID),
zap.String("iccid", card.ICCID),
zap.Error(err),
)
return err
}
logger.Info("实名检查成功",
zap.Uint("card_id", card.ID),
zap.String("iccid", card.ICCID),
zap.Int("real_name_status", result.Status),
)
```
---
## 总结
IoT SIM 管理系统的轮询机制具有以下特点:
1. **三层轮询体系**: 实名检查、卡流量检查、套餐流量检查相互独立
2. **灵活配置**: 支持按卡状态、运营商、优先级配置不同的轮询策略
3. **异步任务队列**: 使用 Asynq 实现高并发、可重试的任务处理
4. **梯度策略**: 支持根据流量使用情况动态调整轮询间隔
5. **开关控制**: 支持全局、配置、单项的轮询开关
6. **错误处理**: 完善的重试机制和错误日志记录
7. **监控告警**: 实时监控轮询任务执行情况和运营商 API 调用
通过合理配置和使用轮询机制,可以实现 IoT 卡的自动化管理,提高运营效率,降低人工成本。
---
**文档版本**: v1.0
**最后更新**: 2026-01-12
**维护人员**: Claude Sonnet 4.5