实现 IoT SIM 管理模块数据模型和数据库结构
- 添加 IoT 核心业务表:运营商、IoT 卡、设备、号卡、套餐、订单等 - 添加分佣系统表:分佣规则、分佣记录、运营商结算等 - 添加轮询和流量管理表:轮询配置、流量使用记录等 - 添加财务和系统管理表:佣金提现、换卡申请等 - 实现完整的 GORM 模型和常量定义 - 添加数据库迁移脚本和详细文档 - 集成 OpenSpec 工作流工具(opsx 命令和 skills) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
940
docs/iot-sim-management/分佣系统说明.md
Normal file
940
docs/iot-sim-management/分佣系统说明.md
Normal 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
|
||||
491
docs/iot-sim-management/实施总结.md
Normal file
491
docs/iot-sim-management/实施总结.md
Normal 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
|
||||
658
docs/iot-sim-management/数据模型总结.md
Normal file
658
docs/iot-sim-management/数据模型总结.md
Normal 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
|
||||
1020
docs/iot-sim-management/表结构详细说明.md
Normal file
1020
docs/iot-sim-management/表结构详细说明.md
Normal file
File diff suppressed because it is too large
Load Diff
776
docs/iot-sim-management/轮询机制说明.md
Normal file
776
docs/iot-sim-management/轮询机制说明.md
Normal 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
|
||||
Reference in New Issue
Block a user