重构:完善 IoT 模型架构规范和数据库设计

- 完善 GORM 模型规范:货币字段使用 int64(分为单位)、JSONB 字段规范、模型结构规范
- 修复所有 IoT 模型的架构违规问题
- 更新 CLAUDE.md 开发指南,补充完整的数据库设计规范和模型示例
- 添加数据库迁移脚本(000006)用于架构重构
- 归档 OpenSpec 变更文档(2026-01-12-fix-iot-models-violations)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-12 17:43:12 +08:00
parent 4507de577b
commit 2150fb6ab9
21 changed files with 2774 additions and 263 deletions

124
CLAUDE.md
View File

@@ -310,28 +310,126 @@ internal/
- 短文本(名称、标题等):`VARCHAR(255)``VARCHAR(100)`
- 中等文本(描述、备注等):`VARCHAR(500)``VARCHAR(1000)`
- 长文本(内容、详情等):`TEXT` 类型
- **货币金额字段必须使用整数类型存储(分为单位)**
- Go 类型:`int64`(不是 `float64`
- 数据库类型:`BIGINT`(不是 `DECIMAL``NUMERIC`
- 示例:`CostPrice int64 gorm:"column:cost_price;type:bigint;default:0;comment:成本价(分为单位)" json:"cost_price"`
- 理由:避免浮点数精度问题,确保货币计算的准确性
- 显示时转换:金额除以 100 转换为元(如 10000 分 = 100.00 元)
- 数值字段精度必须明确定义:
- 货币金额:`DECIMAL(10, 2)``DECIMAL(18, 2)`(根据业务需求
- 百分比:`DECIMAL(5, 2)``DECIMAL(5, 4)`
- 百分比:使用 `int64` 存储千分比或万分比(如 2000 表示 20%,避免浮点精度问题
- 计数器:`INTEGER``BIGINT`
- 流量数据:`BIGINT`(如 MB、KB 为单位的流量使用量)
- 所有字段必须添加中文注释,说明字段用途和业务含义
- 必填字段必须在 GORM 标签中指定 `not null`
- 唯一字段必须在 GORM 标签中指定 `unique` 或通过数据库索引保证唯一性
- 枚举字段应该使用 `VARCHAR``INTEGER` 类型,并在代码中定义常量映射
- JSONB 字段必须使用 `datatypes.JSON` 类型(从 `gorm.io/datatypes` 包导入)
- 示例:`AccountInfo datatypes.JSON gorm:"column:account_info;type:jsonb;comment:收款账户信息" json:"account_info"`
- 不使用 `pq.StringArray` 或其他 PostgreSQL 特定类型
**字段命名示例:**
**GORM 模型结构规范:**
```go
type User struct {
ID uint `gorm:"column:id;primaryKey;comment:用户 ID" json:"id"`
UserID string `gorm:"column:user_id;type:varchar(100);uniqueIndex;not null;comment:用户唯一标识" json:"user_id"`
Email string `gorm:"column:email;type:varchar(255);uniqueIndex;not null;comment:用户邮箱" json:"email"`
Phone string `gorm:"column:phone;type:varchar(20);comment:手机号码" json:"phone"`
Nickname string `gorm:"column:nickname;type:varchar(100);comment:用户昵称" json:"nickname"`
Balance int64 `gorm:"column:balance;type:bigint;default:0;comment:账户余额(分为单位)" json:"balance"`
Status int `gorm:"column:status;type:int;default:1;comment:用户状态 1-正常 2-禁用" json:"status"`
- **所有业务实体模型必须嵌入 `gorm.Model``BaseModel`**
- `gorm.Model` 提供:`ID`(主键)、`CreatedAt``UpdatedAt``DeletedAt`(软删除支持)
- `BaseModel` 提供:`Creator``Updater`(审计字段)
- 禁止手动定义 `ID``CreatedAt``UpdatedAt``DeletedAt` 字段
- 示例:
```go
type IotCard struct {
gorm.Model
BaseModel `gorm:"embedded"`
// 业务字段...
}
```
- **日志表和只追加append-only表不需要软删除和审计字段**
- 这类表只定义 `ID` 和 `CreatedAt`,不嵌入 `gorm.Model` 或 `BaseModel`
- 示例:`DataUsageRecord`(流量使用记录)
- 示例:
```go
type DataUsageRecord struct {
ID uint `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"`
IotCardID uint `gorm:"column:iot_card_id;index;not null;comment:IoT卡ID" json:"iot_card_id"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
```
**表名命名规范:**
- **所有表名必须使用 `tb_` 前缀 + 单数形式**
- 示例:`tb_iot_card`(不是 `iot_cards`
- 示例:`tb_package`(不是 `packages`
- 示例:`tb_order`(不是 `orders`
- **必须实现 `TableName()` 方法显式指定表名**
```go
func (IotCard) TableName() string {
return "tb_iot_card"
}
```
- 禁止依赖 GORM 的自动表名推断(避免复数形式导致的不一致)
**索引和约束规范:**
- **外键字段必须添加 `index` 标签**
- 示例:`CarrierID uint gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"`
- 提高关联查询性能
- **唯一索引必须支持软删除兼容性**
- 添加 `where:deleted_at IS NULL` 条件,确保软删除后的记录不影响唯一性约束
- 示例:`gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iot_card_iccid,where:deleted_at IS NULL;not null;comment:ICCID" json:"iccid"`
- 这样同一个 ICCID 的卡可以多次创建/删除而不违反唯一性约束
- **复合索引命名规范**
- 使用 `idx_{table}_{field1}_{field2}` 格式
- 示例:`uniqueIndex:idx_device_sim_binding_device_slot,where:deleted_at IS NULL`
- 禁止定义数据库级别的外键约束Foreign Key Constraints
**完整模型示例:**
标准业务实体模型(带软删除和审计字段):
```go
type IotCard struct {
gorm.Model // 提供 ID, CreatedAt, UpdatedAt, DeletedAt
BaseModel `gorm:"embedded"` // 提供 Creator, Updater
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iot_card_iccid,where:deleted_at IS NULL;not null;comment:ICCID(唯一标识)" json:"iccid"`
CarrierID uint `gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"`
CostPrice int64 `gorm:"column:cost_price;type:bigint;default:0;comment:成本价(分为单位)" json:"cost_price"`
DistributePrice int64 `gorm:"column:distribute_price;type:bigint;default:0;comment:分销价(分为单位)" json:"distribute_price"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-未激活 2-已激活 3-已停用" json:"status"`
}
func (IotCard) TableName() string {
return "tb_iot_card"
}
```
日志表模型(不需要软删除和审计):
```go
type DataUsageRecord struct {
ID uint `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"`
IotCardID uint `gorm:"column:iot_card_id;index;not null;comment:IoT卡ID" json:"iot_card_id"`
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;not null;comment:流量使用量(MB)" json:"data_usage_mb"`
DataIncreaseMB int64 `gorm:"column:data_increase_mb;type:bigint;default:0;comment:相比上次的增量(MB)" json:"data_increase_mb"`
CheckTime time.Time `gorm:"column:check_time;not null;comment:检查时间" json:"check_time"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
}
func (DataUsageRecord) TableName() string {
return "tb_data_usage_record"
}
```
包含 JSONB 字段的模型:
```go
type Order struct {
gorm.Model
BaseModel `gorm:"embedded"`
OrderNo string `gorm:"column:order_no;type:varchar(100);uniqueIndex:idx_order_no,where:deleted_at IS NULL;not null;comment:订单号" json:"order_no"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:订单金额(分为单位)" json:"amount"`
CarrierOrderData datatypes.JSON `gorm:"column:carrier_order_data;type:jsonb;comment:运营商订单原始数据" json:"carrier_order_data"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待支付 2-已支付 3-已完成" json:"status"`
}
func (Order) TableName() string {
return "tb_order"
}
```

4
go.mod
View File

@@ -32,6 +32,7 @@ require (
require (
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
@@ -59,6 +60,7 @@ require (
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -129,4 +131,6 @@ require (
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/datatypes v1.2.7 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
)

10
go.sum
View File

@@ -1,5 +1,7 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
@@ -80,6 +82,9 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
@@ -324,10 +329,15 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=

View File

@@ -1,20 +1,21 @@
package model
import "time"
import (
"gorm.io/gorm"
)
// Carrier 运营商模型
// 存储运营商基础信息(中国移动、中国联通、中国电信)
type Carrier struct {
ID uint `gorm:"column:id;primaryKey;comment:运营商ID" json:"id"`
CarrierCode string `gorm:"column:carrier_code;type:varchar(50);uniqueIndex;not null;comment:运营商编码(CMCC/CUCC/CTCC)" json:"carrier_code"`
gorm.Model
BaseModel `gorm:"embedded"`
CarrierCode string `gorm:"column:carrier_code;type:varchar(50);uniqueIndex:idx_carrier_code,where:deleted_at IS NULL;not null;comment:运营商编码(CMCC/CUCC/CTCC)" json:"carrier_code"`
CarrierName string `gorm:"column:carrier_name;type:varchar(100);not null;comment:运营商名称(中国移动/中国联通/中国电信)" json:"carrier_name"`
Description string `gorm:"column:description;type:varchar(500);comment:运营商描述" json:"description"`
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (Carrier) TableName() string {
return "carriers"
return "tb_carrier"
}

View File

@@ -1,164 +1,160 @@
package model
import "time"
import (
"time"
"gorm.io/gorm"
)
// AgentHierarchy 代理层级关系模型
// 树形代理关系(每个代理只有一个上级)
type AgentHierarchy struct {
ID uint `gorm:"column:id;primaryKey;comment:代理层级ID" json:"id"`
AgentID uint `gorm:"column:agent_id;type:bigint;uniqueIndex;not null;comment:代理用户ID" json:"agent_id"`
ParentAgentID uint `gorm:"column:parent_agent_id;type:bigint;comment:上级代理用户ID(NULL表示顶级代理)" json:"parent_agent_id"`
gorm.Model
BaseModel `gorm:"embedded"`
AgentID uint `gorm:"column:agent_id;uniqueIndex:idx_agent_hierarchy_agent,where:deleted_at IS NULL;not null;comment:代理用户ID" json:"agent_id"`
ParentAgentID uint `gorm:"column:parent_agent_id;index;comment:上级代理用户ID(NULL表示顶级代理)" json:"parent_agent_id"`
Level int `gorm:"column:level;type:int;not null;comment:代理层级(1, 2, 3...)" json:"level"`
Path string `gorm:"column:path;type:varchar(500);comment:代理路径(如: 1/5/12)" json:"path"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (AgentHierarchy) TableName() string {
return "agent_hierarchies"
return "tb_agent_hierarchy"
}
// CommissionRule 分佣规则模型
// 三种分佣类型:一次性/长期/组合
type CommissionRule struct {
ID uint `gorm:"column:id;primaryKey;comment:分佣规则ID" json:"id"`
AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"`
gorm.Model
BaseModel `gorm:"embedded"`
AgentID uint `gorm:"column:agent_id;index;not null;comment:代理用户ID" json:"agent_id"`
BusinessType string `gorm:"column:business_type;type:varchar(50);not null;comment:业务类型" json:"business_type"`
CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型 number_card-号卡 iot_card-IoT卡" json:"card_type"`
SeriesID uint `gorm:"column:series_id;type:bigint;comment:套餐系列ID(一次性分佣时用)" json:"series_id"`
PackageID uint `gorm:"column:package_id;type:bigint;comment:套餐ID(长期分佣时用)" json:"package_id"`
SeriesID uint `gorm:"column:series_id;index;comment:套餐系列ID(一次性分佣时用)" json:"series_id"`
PackageID uint `gorm:"column:package_id;index;comment:套餐ID(长期分佣时用)" json:"package_id"`
CommissionType string `gorm:"column:commission_type;type:varchar(50);not null;comment:分佣类型 one_time-一次性 long_term-长期 combined-组合" json:"commission_type"`
CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"`
CommissionValue float64 `gorm:"column:commission_value;type:decimal(10,2);not null;comment:分佣值" json:"commission_value"`
CommissionValue int64 `gorm:"column:commission_value;type:bigint;not null;comment:分佣值(分为单位,百分比时为千分比如2000表示20%)" json:"commission_value"`
UnfreezeDays int `gorm:"column:unfreeze_days;type:int;default:0;comment:解冻天数" json:"unfreeze_days"`
MinActivationForUnfreeze int `gorm:"column:min_activation_for_unfreeze;type:int;default:0;comment:解冻最小激活量" json:"min_activation_for_unfreeze"`
ApprovalType string `gorm:"column:approval_type;type:varchar(20);default:'auto';comment:审批类型 auto-自动 manual-人工" json:"approval_type"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (CommissionRule) TableName() string {
return "commission_rules"
return "tb_commission_rule"
}
// CommissionLadder 阶梯分佣配置模型
// 支持按激活量、提货量、充值量设置阶梯佣金
type CommissionLadder struct {
ID uint `gorm:"column:id;primaryKey;comment:阶梯分佣配置ID" json:"id"`
RuleID uint `gorm:"column:rule_id;type:bigint;not null;comment:分佣规则ID" json:"rule_id"`
gorm.Model
BaseModel `gorm:"embedded"`
RuleID uint `gorm:"column:rule_id;index;not null;comment:分佣规则ID" json:"rule_id"`
LadderType string `gorm:"column:ladder_type;type:varchar(50);not null;comment:阶梯类型 activation-激活量 pickup-提货量 deposit-充值量" json:"ladder_type"`
ThresholdValue int `gorm:"column:threshold_value;type:int;not null;comment:阈值" json:"threshold_value"`
CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"`
CommissionValue float64 `gorm:"column:commission_value;type:decimal(10,2);not null;comment:分佣值" json:"commission_value"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
CommissionValue int64 `gorm:"column:commission_value;type:bigint;not null;comment:分佣值(分为单位,百分比时为千分比如2000表示20%)" json:"commission_value"`
}
// TableName 指定表名
func (CommissionLadder) TableName() string {
return "commission_ladder"
return "tb_commission_ladder"
}
// CommissionCombinedCondition 组合分佣条件模型
// 支持时间点 OR 套餐周期阈值的 OR 条件解冻
type CommissionCombinedCondition struct {
ID uint `gorm:"column:id;primaryKey;comment:组合分佣条件ID" json:"id"`
RuleID uint `gorm:"column:rule_id;type:bigint;uniqueIndex;not null;comment:分佣规则ID" json:"rule_id"`
gorm.Model
BaseModel `gorm:"embedded"`
RuleID uint `gorm:"column:rule_id;uniqueIndex:idx_commission_combined_rule,where:deleted_at IS NULL;not null;comment:分佣规则ID" json:"rule_id"`
OneTimeCommissionMode string `gorm:"column:one_time_commission_mode;type:varchar(20);comment:一次性分佣模式 fixed-固定金额 percent-百分比" json:"one_time_commission_mode"`
OneTimeCommissionValue float64 `gorm:"column:one_time_commission_value;type:decimal(10,2);comment:一次性分佣值" json:"one_time_commission_value"`
OneTimeCommissionValue int64 `gorm:"column:one_time_commission_value;type:bigint;comment:一次性分佣值(分为单位,百分比时为千分比如2000表示20%)" json:"one_time_commission_value"`
LongTermCommissionMode string `gorm:"column:long_term_commission_mode;type:varchar(20);comment:长期分佣模式 fixed-固定金额 percent-百分比" json:"long_term_commission_mode"`
LongTermCommissionValue float64 `gorm:"column:long_term_commission_value;type:decimal(10,2);comment:长期分佣值" json:"long_term_commission_value"`
LongTermCommissionValue int64 `gorm:"column:long_term_commission_value;type:bigint;comment:长期分佣值(分为单位,百分比时为千分比如2000表示20%)" json:"long_term_commission_value"`
LongTermTriggerTimePoint *time.Time `gorm:"column:long_term_trigger_time_point;comment:长期分佣触发时间点(如实名后3个月)" json:"long_term_trigger_time_point"`
LongTermTriggerPackageCycles int `gorm:"column:long_term_trigger_package_cycles;type:int;comment:长期分佣触发套餐周期数(如10个套餐周期)" json:"long_term_trigger_package_cycles"`
LongTermTriggerNetworkMonths int `gorm:"column:long_term_trigger_network_months;type:int;comment:长期分佣触发在网月数(号卡专用)" json:"long_term_trigger_network_months"`
LongTermUnfreezeDays int `gorm:"column:long_term_unfreeze_days;type:int;default:0;comment:长期分佣解冻天数" json:"long_term_unfreeze_days"`
LongTermMinActivation int `gorm:"column:long_term_min_activation;type:int;default:0;comment:长期分佣解冻最小激活量" json:"long_term_min_activation"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (CommissionCombinedCondition) TableName() string {
return "commission_combined_conditions"
return "tb_commission_combined_condition"
}
// CommissionRecord 分佣记录模型
// 记录分佣的冻结、解冻、发放状态
type CommissionRecord struct {
ID uint `gorm:"column:id;primaryKey;comment:分佣记录ID" json:"id"`
AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"`
OrderID uint `gorm:"column:order_id;type:bigint;not null;comment:订单ID" json:"order_id"`
RuleID uint `gorm:"column:rule_id;type:bigint;not null;comment:分佣规则ID" json:"rule_id"`
gorm.Model
BaseModel `gorm:"embedded"`
AgentID uint `gorm:"column:agent_id;index;not null;comment:代理用户ID" json:"agent_id"`
OrderID uint `gorm:"column:order_id;index;not null;comment:订单ID" json:"order_id"`
RuleID uint `gorm:"column:rule_id;index;not null;comment:分佣规则ID" json:"rule_id"`
CommissionType string `gorm:"column:commission_type;type:varchar(50);not null;comment:分佣类型 one_time-一次性 long_term-长期" json:"commission_type"`
Amount float64 `gorm:"column:amount;type:decimal(10,2);not null;comment:分佣金额()" json:"amount"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:分佣金额(分为单位)" json:"amount"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-已冻结 2-解冻中 3-已发放 4-已失效" json:"status"`
UnfrozenAt *time.Time `gorm:"column:unfrozen_at;comment:解冻时间" json:"unfrozen_at"`
ReleasedAt *time.Time `gorm:"column:released_at;comment:发放时间" json:"released_at"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (CommissionRecord) TableName() string {
return "commission_records"
return "tb_commission_record"
}
// CommissionApproval 分佣审批模型
// 分佣解冻审批流程
type CommissionApproval struct {
ID uint `gorm:"column:id;primaryKey;comment:分佣审批ID" json:"id"`
CommissionRecordID uint `gorm:"column:commission_record_id;type:bigint;not null;comment:分佣记录ID" json:"commission_record_id"`
ApproverID uint `gorm:"column:approver_id;type:bigint;comment:审批人用户ID" json:"approver_id"`
gorm.Model
BaseModel `gorm:"embedded"`
CommissionRecordID uint `gorm:"column:commission_record_id;index;not null;comment:分佣记录ID" json:"commission_record_id"`
ApproverID uint `gorm:"column:approver_id;index;comment:审批人用户ID" json:"approver_id"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待审批 2-已通过 3-已拒绝" json:"status"`
Reason string `gorm:"column:reason;type:text;comment:原因" json:"reason"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (CommissionApproval) TableName() string {
return "commission_approvals"
return "tb_commission_approval"
}
// CommissionTemplate 分佣模板模型
// 创建和管理分佣模板,快速为代理分配产品时设置佣金规则
type CommissionTemplate struct {
ID uint `gorm:"column:id;primaryKey;comment:分佣模板ID" json:"id"`
TemplateName string `gorm:"column:template_name;type:varchar(255);uniqueIndex;not null;comment:模板名称" json:"template_name"`
gorm.Model
BaseModel `gorm:"embedded"`
TemplateName string `gorm:"column:template_name;type:varchar(255);uniqueIndex:idx_commission_template_name,where:deleted_at IS NULL;not null;comment:模板名称" json:"template_name"`
BusinessType string `gorm:"column:business_type;type:varchar(50);not null;comment:业务类型" json:"business_type"`
CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型 number_card-号卡 iot_card-IoT卡" json:"card_type"`
CommissionType string `gorm:"column:commission_type;type:varchar(50);not null;comment:分佣类型 one_time-一次性 long_term-长期 combined-组合" json:"commission_type"`
CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;comment:分佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"`
CommissionValue float64 `gorm:"column:commission_value;type:decimal(10,2);not null;comment:分佣值" json:"commission_value"`
CommissionValue int64 `gorm:"column:commission_value;type:bigint;not null;comment:分佣值(分为单位,百分比时为千分比如2000表示20%)" json:"commission_value"`
UnfreezeDays int `gorm:"column:unfreeze_days;type:int;default:0;comment:解冻天数" json:"unfreeze_days"`
MinActivationForUnfreeze int `gorm:"column:min_activation_for_unfreeze;type:int;default:0;comment:解冻最小激活量" json:"min_activation_for_unfreeze"`
ApprovalType string `gorm:"column:approval_type;type:varchar(20);default:'auto';comment:审批类型 auto-自动 manual-人工" json:"approval_type"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (CommissionTemplate) TableName() string {
return "commission_templates"
return "tb_commission_template"
}
// CarrierSettlement 号卡运营商结算模型
// 运营商周期性结算的佣金总额,再分配给代理
type CarrierSettlement struct {
ID uint `gorm:"column:id;primaryKey;comment:运营商结算ID" json:"id"`
CommissionRecordID uint `gorm:"column:commission_record_id;type:bigint;uniqueIndex;not null;comment:分佣记录ID" json:"commission_record_id"`
AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"`
gorm.Model
BaseModel `gorm:"embedded"`
CommissionRecordID uint `gorm:"column:commission_record_id;uniqueIndex:idx_carrier_settlement_record,where:deleted_at IS NULL;not null;comment:分佣记录ID" json:"commission_record_id"`
AgentID uint `gorm:"column:agent_id;index;not null;comment:代理用户ID" json:"agent_id"`
SettlementMonth string `gorm:"column:settlement_month;type:varchar(20);not null;comment:结算月份(如 2026-01)" json:"settlement_month"`
SettlementAmount float64 `gorm:"column:settlement_amount;type:decimal(18,2);not null;comment:结算金额()" json:"settlement_amount"`
SettlementAmount int64 `gorm:"column:settlement_amount;type:bigint;not null;comment:结算金额(分为单位)" json:"settlement_amount"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待结算 2-已结算" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (CarrierSettlement) TableName() string {
return "carrier_settlements"
return "tb_carrier_settlement"
}

View File

@@ -1,12 +1,15 @@
package model
import "time"
import (
"time"
)
// DataUsageRecord 流量使用记录模型
// 记录卡的流量历史,支持流量查询和分析
// 注意:此模型是日志表,不需要软删除和审计字段
type DataUsageRecord struct {
ID uint `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"`
IotCardID uint `gorm:"column:iot_card_id;type:bigint;not null;comment:IoT卡ID" json:"iot_card_id"`
IotCardID uint `gorm:"column:iot_card_id;index;not null;comment:IoT卡ID" json:"iot_card_id"`
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;not null;comment:流量使用量(MB)" json:"data_usage_mb"`
DataIncreaseMB int64 `gorm:"column:data_increase_mb;type:bigint;default:0;comment:相比上次的增量(MB)" json:"data_increase_mb"`
CheckTime time.Time `gorm:"column:check_time;not null;comment:检查时间" json:"check_time"`
@@ -16,5 +19,5 @@ type DataUsageRecord struct {
// TableName 指定表名
func (DataUsageRecord) TableName() string {
return "data_usage_records"
return "tb_data_usage_record"
}

View File

@@ -1,13 +1,18 @@
package model
import "time"
import (
"time"
"gorm.io/gorm"
)
// Device 设备模型
// 用户的物联网设备(如 GPS 追踪器、智能传感器)
// 可绑定 1-4 张 IoT 卡,主要用于批量管理和设备操作
type Device struct {
ID uint `gorm:"column:id;primaryKey;comment:设备ID" json:"id"`
DeviceNo string `gorm:"column:device_no;type:varchar(100);uniqueIndex;not null;comment:设备编号(唯一标识)" json:"device_no"`
gorm.Model
BaseModel `gorm:"embedded"`
DeviceNo string `gorm:"column:device_no;type:varchar(100);uniqueIndex:idx_device_no,where:deleted_at IS NULL;not null;comment:设备编号(唯一标识)" json:"device_no"`
DeviceName string `gorm:"column:device_name;type:varchar(255);comment:设备名称" json:"device_name"`
DeviceModel string `gorm:"column:device_model;type:varchar(100);comment:设备型号" json:"device_model"`
DeviceType string `gorm:"column:device_type;type:varchar(50);comment:设备类型" json:"device_type"`
@@ -15,17 +20,15 @@ type Device struct {
Manufacturer string `gorm:"column:manufacturer;type:varchar(255);comment:制造商" json:"manufacturer"`
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
OwnerType string `gorm:"column:owner_type;type:varchar(20);default:'platform';not null;comment:所有者类型 platform-平台 agent-代理 user-用户" json:"owner_type"`
OwnerID uint `gorm:"column:owner_id;type:bigint;default:0;not null;comment:所有者ID" json:"owner_id"`
OwnerID uint `gorm:"column:owner_id;index;default:0;not null;comment:所有者ID" json:"owner_id"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"`
ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at"`
DeviceUsername string `gorm:"column:device_username;type:varchar(100);comment:设备登录用户名" json:"device_username"`
DevicePasswordEncrypted string `gorm:"column:device_password_encrypted;type:varchar(255);comment:设备登录密码(加密)" json:"device_password_encrypted"`
DeviceAPIEndpoint string `gorm:"column:device_api_endpoint;type:varchar(500);comment:设备API端点" json:"device_api_endpoint"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (Device) TableName() string {
return "devices"
return "tb_device"
}

View File

@@ -3,55 +3,55 @@ package model
import (
"time"
"github.com/lib/pq"
"gorm.io/datatypes"
"gorm.io/gorm"
)
// CommissionWithdrawalRequest 佣金提现申请模型
// 代理佣金提现申请、审批流程、提现记录查询
type CommissionWithdrawalRequest struct {
ID uint `gorm:"column:id;primaryKey;comment:提现申请ID" json:"id"`
AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"`
Amount float64 `gorm:"column:amount;type:decimal(18,2);not null;comment:提现金额(元)" json:"amount"`
Fee float64 `gorm:"column:fee;type:decimal(18,2);default:0;comment:手续费(元)" json:"fee"`
ActualAmount float64 `gorm:"column:actual_amount;type:decimal(18,2);comment:实际到账金额(元)" json:"actual_amount"`
gorm.Model
BaseModel `gorm:"embedded"`
AgentID uint `gorm:"column:agent_id;index;not null;comment:代理用户ID" json:"agent_id"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:提现金额(分为单位)" json:"amount"`
Fee int64 `gorm:"column:fee;type:bigint;default:0;comment:手续费(分为单位)" json:"fee"`
ActualAmount int64 `gorm:"column:actual_amount;type:bigint;comment:实际到账金额(分为单位)" json:"actual_amount"`
WithdrawalMethod string `gorm:"column:withdrawal_method;type:varchar(20);comment:提现方式 alipay-支付宝 wechat-微信 bank-银行卡" json:"withdrawal_method"`
AccountInfo pq.StringArray `gorm:"column:account_info;type:jsonb;comment:收款账户信息(姓名、账号等)" json:"account_info"`
AccountInfo datatypes.JSON `gorm:"column:account_info;type:jsonb;comment:收款账户信息(姓名、账号等)" json:"account_info"`
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-待审核 2-已通过 3-已拒绝 4-已到账" json:"status"`
ApprovedBy uint `gorm:"column:approved_by;type:bigint;comment:审批人用户ID" json:"approved_by"`
ApprovedBy uint `gorm:"column:approved_by;index;comment:审批人用户ID" json:"approved_by"`
ApprovedAt *time.Time `gorm:"column:approved_at;comment:审批时间" json:"approved_at"`
PaidAt *time.Time `gorm:"column:paid_at;comment:到账时间" json:"paid_at"`
RejectReason string `gorm:"column:reject_reason;type:text;comment:拒绝原因" json:"reject_reason"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (CommissionWithdrawalRequest) TableName() string {
return "commission_withdrawal_requests"
return "tb_commission_withdrawal_request"
}
// CommissionWithdrawalSetting 佣金提现设置模型
// 提现参数配置(最低金额、手续费率、到账时间等)
type CommissionWithdrawalSetting struct {
ID uint `gorm:"column:id;primaryKey;comment:提现设置ID" json:"id"`
MinWithdrawalAmount float64 `gorm:"column:min_withdrawal_amount;type:decimal(10,2);comment:最低提现金额(元)" json:"min_withdrawal_amount"`
FeeRate float64 `gorm:"column:fee_rate;type:decimal(5,4);comment:手续费率(如 0.01 表示 1%)" json:"fee_rate"`
gorm.Model
BaseModel `gorm:"embedded"`
MinWithdrawalAmount int64 `gorm:"column:min_withdrawal_amount;type:bigint;comment:最低提现金额(分为单位)" json:"min_withdrawal_amount"`
FeeRate int64 `gorm:"column:fee_rate;type:bigint;comment:手续费率(万分比,如100表示1%)" json:"fee_rate"`
ArrivalDays int `gorm:"column:arrival_days;type:int;comment:到账天数" json:"arrival_days"`
IsActive bool `gorm:"column:is_active;type:boolean;default:true;comment:是否生效(最新一条)" json:"is_active"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (CommissionWithdrawalSetting) TableName() string {
return "commission_withdrawal_settings"
return "tb_commission_withdrawal_setting"
}
// PaymentMerchantSetting 收款商户设置模型
// 配置支付参数(支付宝、微信等收款账户)
type PaymentMerchantSetting struct {
ID uint `gorm:"column:id;primaryKey;comment:收款商户ID" json:"id"`
UserID uint `gorm:"column:user_id;type:bigint;not null;comment:用户ID" json:"user_id"`
gorm.Model
BaseModel `gorm:"embedded"`
UserID uint `gorm:"column:user_id;index;not null;comment:用户ID" json:"user_id"`
MerchantType string `gorm:"column:merchant_type;type:varchar(20);comment:商户类型 alipay-支付宝 wechat-微信 bank-银行卡" json:"merchant_type"`
AccountName string `gorm:"column:account_name;type:varchar(255);comment:账户名称" json:"account_name"`
AccountNumber string `gorm:"column:account_number;type:varchar(255);comment:账号" json:"account_number"`
@@ -60,11 +60,9 @@ type PaymentMerchantSetting struct {
IsVerified bool `gorm:"column:is_verified;type:boolean;default:false;comment:是否已验证" json:"is_verified"`
IsDefault bool `gorm:"column:is_default;type:boolean;default:false;comment:是否默认账户" json:"is_default"`
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (PaymentMerchantSetting) TableName() string {
return "payment_merchant_settings"
return "tb_payment_merchant_setting"
}

View File

@@ -1,25 +1,30 @@
package model
import "time"
import (
"time"
"gorm.io/gorm"
)
// IotCard IoT 卡模型
// 物联网卡/流量卡的统一管理实体
// 支持平台自营、代理分销、用户购买等所有权模式
type IotCard struct {
ID uint `gorm:"column:id;primaryKey;comment:IoT 卡 ID" json:"id"`
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex;not null;comment:ICCID(唯一标识)" json:"iccid"`
gorm.Model
BaseModel `gorm:"embedded"`
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iot_card_iccid,where:deleted_at IS NULL;not null;comment:ICCID(唯一标识)" json:"iccid"`
CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型" json:"card_type"`
CardCategory string `gorm:"column:card_category;type:varchar(20);default:'normal';not null;comment:卡业务类型 normal-普通卡 industry-行业卡" json:"card_category"`
CarrierID uint `gorm:"column:carrier_id;type:bigint;not null;comment:运营商ID" json:"carrier_id"`
CarrierID uint `gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"`
IMSI string `gorm:"column:imsi;type:varchar(50);comment:IMSI" json:"imsi"`
MSISDN string `gorm:"column:msisdn;type:varchar(20);comment:MSISDN(手机号码)" json:"msisdn"`
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
Supplier string `gorm:"column:supplier;type:varchar(255);comment:供应商" json:"supplier"`
CostPrice float64 `gorm:"column:cost_price;type:decimal(10,2);default:0;comment:成本价()" json:"cost_price"`
DistributePrice float64 `gorm:"column:distribute_price;type:decimal(10,2);default:0;comment:分销价()" json:"distribute_price"`
CostPrice int64 `gorm:"column:cost_price;type:bigint;default:0;comment:成本价(分为单位)" json:"cost_price"`
DistributePrice int64 `gorm:"column:distribute_price;type:bigint;default:0;comment:分销价(分为单位)" json:"distribute_price"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"`
OwnerType string `gorm:"column:owner_type;type:varchar(20);default:'platform';not null;comment:所有者类型 platform-平台 agent-代理 user-用户 device-设备" json:"owner_type"`
OwnerID uint `gorm:"column:owner_id;type:bigint;default:0;not null;comment:所有者ID" json:"owner_id"`
OwnerID uint `gorm:"column:owner_id;index;default:0;not null;comment:所有者ID" json:"owner_id"`
ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at"`
ActivationStatus int `gorm:"column:activation_status;type:int;default:0;not null;comment:激活状态 0-未激活 1-已激活" json:"activation_status"`
RealNameStatus int `gorm:"column:real_name_status;type:int;default:0;not null;comment:实名状态 0-未实名 1-已实名(行业卡可以保持0)" json:"real_name_status"`
@@ -29,11 +34,9 @@ type IotCard struct {
LastDataCheckAt *time.Time `gorm:"column:last_data_check_at;comment:最后一次流量检查时间" json:"last_data_check_at"`
LastRealNameCheckAt *time.Time `gorm:"column:last_real_name_check_at;comment:最后一次实名检查时间" json:"last_real_name_check_at"`
LastSyncTime *time.Time `gorm:"column:last_sync_time;comment:最后一次与Gateway同步时间" json:"last_sync_time"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (IotCard) TableName() string {
return "iot_cards"
return "tb_iot_card"
}

View File

@@ -1,25 +1,26 @@
package model
import "time"
import (
"gorm.io/gorm"
)
// NumberCard 号卡模型
// 完全独立的业务线,从上游平台下单
// 使用虚拟商品编码映射运营商订单
type NumberCard struct {
ID uint `gorm:"column:id;primaryKey;comment:号卡ID" json:"id"`
VirtualProductCode string `gorm:"column:virtual_product_code;type:varchar(100);uniqueIndex;not null;comment:虚拟商品编码(用于对应运营商订单)" json:"virtual_product_code"`
gorm.Model
BaseModel `gorm:"embedded"`
VirtualProductCode string `gorm:"column:virtual_product_code;type:varchar(100);uniqueIndex:idx_number_card_code,where:deleted_at IS NULL;not null;comment:虚拟商品编码(用于对应运营商订单)" json:"virtual_product_code"`
CardName string `gorm:"column:card_name;type:varchar(255);not null;comment:号卡名称" json:"card_name"`
CardType string `gorm:"column:card_type;type:varchar(50);comment:号卡类型" json:"card_type"`
Carrier string `gorm:"column:carrier;type:varchar(50);comment:运营商" json:"carrier"`
DataAmountMB int64 `gorm:"column:data_amount_mb;type:bigint;comment:流量额度(MB)" json:"data_amount_mb"`
Price float64 `gorm:"column:price;type:decimal(10,2);comment:价格()" json:"price"`
AgentID uint `gorm:"column:agent_id;type:bigint;comment:代理用户ID" json:"agent_id"`
Price int64 `gorm:"column:price;type:bigint;comment:价格(分为单位)" json:"price"`
AgentID uint `gorm:"column:agent_id;index;comment:代理用户ID" json:"agent_id"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在售 2-下架" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (NumberCard) TableName() string {
return "number_cards"
return "tb_number_card"
}

View File

@@ -3,33 +3,33 @@ package model
import (
"time"
"github.com/lib/pq"
"gorm.io/datatypes"
"gorm.io/gorm"
)
// Order 订单模型
// 支持两种订单类型:套餐订单(单卡/设备级)、号卡订单
type Order struct {
ID uint `gorm:"column:id;primaryKey;comment:订单ID" json:"id"`
OrderNo string `gorm:"column:order_no;type:varchar(100);uniqueIndex;not null;comment:订单号(唯一标识)" json:"order_no"`
gorm.Model
BaseModel `gorm:"embedded"`
OrderNo string `gorm:"column:order_no;type:varchar(100);uniqueIndex:idx_order_no,where:deleted_at IS NULL;not null;comment:订单号(唯一标识)" json:"order_no"`
OrderType int `gorm:"column:order_type;type:int;not null;comment:订单类型 1-套餐订单 2-号卡订单" json:"order_type"`
IotCardID uint `gorm:"column:iot_card_id;type:bigint;comment:IoT卡ID(单卡套餐订单时有值)" json:"iot_card_id"`
DeviceID uint `gorm:"column:device_id;type:bigint;comment:设备ID(设备级套餐订单时有值)" json:"device_id"`
NumberCardID uint `gorm:"column:number_card_id;type:bigint;comment:号卡ID(号卡订单时有值)" json:"number_card_id"`
PackageID uint `gorm:"column:package_id;type:bigint;comment:套餐ID(套餐订单时有值)" json:"package_id"`
UserID uint `gorm:"column:user_id;type:bigint;not null;comment:用户ID" json:"user_id"`
AgentID uint `gorm:"column:agent_id;type:bigint;comment:代理用户ID" json:"agent_id"`
Amount float64 `gorm:"column:amount;type:decimal(10,2);not null;comment:订单金额()" json:"amount"`
IotCardID uint `gorm:"column:iot_card_id;index;comment:IoT卡ID(单卡套餐订单时有值)" json:"iot_card_id"`
DeviceID uint `gorm:"column:device_id;index;comment:设备ID(设备级套餐订单时有值)" json:"device_id"`
NumberCardID uint `gorm:"column:number_card_id;index;comment:号卡ID(号卡订单时有值)" json:"number_card_id"`
PackageID uint `gorm:"column:package_id;index;comment:套餐ID(套餐订单时有值)" json:"package_id"`
UserID uint `gorm:"column:user_id;index;not null;comment:用户ID" json:"user_id"`
AgentID uint `gorm:"column:agent_id;index;comment:代理用户ID" json:"agent_id"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:订单金额(分为单位)" json:"amount"`
PaymentMethod string `gorm:"column:payment_method;type:varchar(20);comment:支付方式 wallet-钱包 online-在线支付 carrier-运营商支付" json:"payment_method"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-待支付 2-已支付 3-已完成 4-已取消 5-已退款" json:"status"`
CarrierOrderID string `gorm:"column:carrier_order_id;type:varchar(255);comment:运营商订单ID" json:"carrier_order_id"`
CarrierOrderData pq.StringArray `gorm:"column:carrier_order_data;type:jsonb;comment:运营商订单原始数据(JSON)" json:"carrier_order_data"`
CarrierOrderData datatypes.JSON `gorm:"column:carrier_order_data;type:jsonb;comment:运营商订单原始数据(JSON)" json:"carrier_order_data"`
PaidAt *time.Time `gorm:"column:paid_at;comment:支付时间" json:"paid_at"`
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (Order) TableName() string {
return "orders"
return "tb_order"
}

View File

@@ -1,94 +1,95 @@
package model
import "time"
import (
"time"
"gorm.io/gorm"
)
// PackageSeries 套餐系列模型
// 套餐的分组,用于一次性分佣规则配置
type PackageSeries struct {
ID uint `gorm:"column:id;primaryKey;comment:套餐系列ID" json:"id"`
SeriesCode string `gorm:"column:series_code;type:varchar(100);uniqueIndex;not null;comment:系列编码" json:"series_code"`
gorm.Model
BaseModel `gorm:"embedded"`
SeriesCode string `gorm:"column:series_code;type:varchar(100);uniqueIndex:idx_package_series_code,where:deleted_at IS NULL;not null;comment:系列编码" json:"series_code"`
SeriesName string `gorm:"column:series_name;type:varchar(255);not null;comment:系列名称" json:"series_name"`
Description string `gorm:"column:description;type:text;comment:描述" json:"description"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (PackageSeries) TableName() string {
return "package_series"
return "tb_package_series"
}
// Package 套餐模型
// 只适用于 IoT 卡,支持真流量/虚流量共存机制
type Package struct {
ID uint `gorm:"column:id;primaryKey;comment:套餐ID" json:"id"`
PackageCode string `gorm:"column:package_code;type:varchar(100);uniqueIndex;not null;comment:套餐编码" json:"package_code"`
gorm.Model
BaseModel `gorm:"embedded"`
PackageCode string `gorm:"column:package_code;type:varchar(100);uniqueIndex:idx_package_code,where:deleted_at IS NULL;not null;comment:套餐编码" json:"package_code"`
PackageName string `gorm:"column:package_name;type:varchar(255);not null;comment:套餐名称" json:"package_name"`
SeriesID uint `gorm:"column:series_id;type:bigint;comment:套餐系列ID" json:"series_id"`
SeriesID uint `gorm:"column:series_id;index;comment:套餐系列ID" json:"series_id"`
PackageType string `gorm:"column:package_type;type:varchar(50);not null;comment:套餐类型 formal-正式套餐 addon-附加套餐" json:"package_type"`
DurationMonths int `gorm:"column:duration_months;type:int;not null;comment:套餐时长(月数) 1-月套餐 12-年套餐" json:"duration_months"`
DataType string `gorm:"column:data_type;type:varchar(20);comment:流量类型 real-真流量 virtual-虚流量" json:"data_type"`
RealDataMB int64 `gorm:"column:real_data_mb;type:bigint;default:0;comment:真流量额度(MB)" json:"real_data_mb"`
VirtualDataMB int64 `gorm:"column:virtual_data_mb;type:bigint;default:0;comment:虚流量额度(MB,用于停机判断)" json:"virtual_data_mb"`
DataAmountMB int64 `gorm:"column:data_amount_mb;type:bigint;default:0;comment:总流量额度(MB)" json:"data_amount_mb"`
Price float64 `gorm:"column:price;type:decimal(10,2);not null;comment:套餐价格()" json:"price"`
Price int64 `gorm:"column:price;type:bigint;not null;comment:套餐价格(分为单位)" json:"price"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (Package) TableName() string {
return "packages"
return "tb_package"
}
// AgentPackageAllocation 代理套餐分配模型
// 为直属下级代理分配套餐,设置佣金模式
type AgentPackageAllocation struct {
ID uint `gorm:"column:id;primaryKey;comment:分配ID" json:"id"`
AgentID uint `gorm:"column:agent_id;type:bigint;not null;comment:代理用户ID" json:"agent_id"`
PackageID uint `gorm:"column:package_id;type:bigint;not null;comment:套餐ID" json:"package_id"`
CostPrice float64 `gorm:"column:cost_price;type:decimal(10,2);not null;comment:成本价(元)" json:"cost_price"`
RetailPrice float64 `gorm:"column:retail_price;type:decimal(10,2);not null;comment:零售价(元)" json:"retail_price"`
gorm.Model
BaseModel `gorm:"embedded"`
AgentID uint `gorm:"column:agent_id;index;not null;comment:代理用户ID" json:"agent_id"`
PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"`
CostPrice int64 `gorm:"column:cost_price;type:bigint;not null;comment:成本价(分为单位)" json:"cost_price"`
RetailPrice int64 `gorm:"column:retail_price;type:bigint;not null;comment:零售价(分为单位)" json:"retail_price"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (AgentPackageAllocation) TableName() string {
return "agent_package_allocations"
return "tb_agent_package_allocation"
}
// DeviceSimBinding 设备-IoT卡绑定关系模型
// 管理设备与 IoT 卡的多对多绑定关系(1 设备绑定 1-4 张 IoT 卡)
type DeviceSimBinding struct {
ID uint `gorm:"column:id;primaryKey;comment:绑定ID" json:"id"`
DeviceID uint `gorm:"column:device_id;type:bigint;not null;comment:设备ID" json:"device_id"`
IotCardID uint `gorm:"column:iot_card_id;type:bigint;not null;comment:IoT卡ID" json:"iot_card_id"`
SlotPosition int `gorm:"column:slot_position;type:int;comment:插槽位置(1, 2, 3, 4)" json:"slot_position"`
gorm.Model
BaseModel `gorm:"embedded"`
DeviceID uint `gorm:"column:device_id;index:idx_device_slot;not null;comment:设备ID" json:"device_id"`
IotCardID uint `gorm:"column:iot_card_id;index;not null;comment:IoT卡ID" json:"iot_card_id"`
SlotPosition int `gorm:"column:slot_position;type:int;index:idx_device_slot;comment:插槽位置(1, 2, 3, 4)" json:"slot_position"`
BindStatus int `gorm:"column:bind_status;type:int;default:1;comment:绑定状态 1-已绑定 2-已解绑" json:"bind_status"`
BindTime *time.Time `gorm:"column:bind_time;comment:绑定时间" json:"bind_time"`
UnbindTime *time.Time `gorm:"column:unbind_time;comment:解绑时间" json:"unbind_time"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (DeviceSimBinding) TableName() string {
return "device_sim_bindings"
return "tb_device_sim_binding"
}
// PackageUsage 套餐使用情况模型
// 跟踪单卡套餐和设备级套餐的流量使用
type PackageUsage struct {
ID uint `gorm:"column:id;primaryKey;comment:套餐使用ID" json:"id"`
OrderID uint `gorm:"column:order_id;type:bigint;not null;comment:订单ID" json:"order_id"`
PackageID uint `gorm:"column:package_id;type:bigint;not null;comment:套餐ID" json:"package_id"`
gorm.Model
BaseModel `gorm:"embedded"`
OrderID uint `gorm:"column:order_id;index;not null;comment:订单ID" json:"order_id"`
PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"`
UsageType string `gorm:"column:usage_type;type:varchar(20);not null;comment:使用类型 single_card-单卡套餐 device-设备级套餐" json:"usage_type"`
IotCardID uint `gorm:"column:iot_card_id;type:bigint;comment:IoT卡ID(单卡套餐时有值)" json:"iot_card_id"`
DeviceID uint `gorm:"column:device_id;type:bigint;comment:设备ID(设备级套餐时有值)" json:"device_id"`
IotCardID uint `gorm:"column:iot_card_id;index;comment:IoT卡ID(单卡套餐时有值)" json:"iot_card_id"`
DeviceID uint `gorm:"column:device_id;index;comment:设备ID(设备级套餐时有值)" json:"device_id"`
DataLimitMB int64 `gorm:"column:data_limit_mb;type:bigint;not null;comment:流量限额(MB)" json:"data_limit_mb"`
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;default:0;comment:已使用流量(MB)" json:"data_usage_mb"`
RealDataUsageMB int64 `gorm:"column:real_data_usage_mb;type:bigint;default:0;comment:真流量使用(MB)" json:"real_data_usage_mb"`
@@ -97,11 +98,9 @@ type PackageUsage struct {
ExpiresAt time.Time `gorm:"column:expires_at;not null;comment:套餐过期时间" json:"expires_at"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-生效中 2-已用完 3-已过期" json:"status"`
LastPackageCheckAt *time.Time `gorm:"column:last_package_check_at;comment:最后一次套餐流量检查时间" json:"last_package_check_at"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (PackageUsage) TableName() string {
return "package_usages"
return "tb_package_usage"
}

View File

@@ -1,15 +1,18 @@
package model
import "time"
import (
"gorm.io/gorm"
)
// PollingConfig 轮询配置模型
// 支持梯度轮询策略(实名检查、卡流量检查、套餐流量检查)
type PollingConfig struct {
ID uint `gorm:"column:id;primaryKey;comment:轮询配置ID" json:"id"`
ConfigName string `gorm:"column:config_name;type:varchar(100);uniqueIndex;not null;comment:配置名称(如 未实名卡、实名卡)" json:"config_name"`
gorm.Model
BaseModel `gorm:"embedded"`
ConfigName string `gorm:"column:config_name;type:varchar(100);uniqueIndex:idx_polling_config_name,where:deleted_at IS NULL;not null;comment:配置名称(如 未实名卡、实名卡)" json:"config_name"`
Description string `gorm:"column:description;type:varchar(500);comment:配置描述" json:"description"`
CardCondition string `gorm:"column:card_condition;type:varchar(50);comment:卡状态条件 not_real_name-未实名 real_name-已实名 activated-已激活 suspended-已停用" json:"card_condition"`
CarrierID uint `gorm:"column:carrier_id;type:bigint;comment:运营商ID(NULL表示所有运营商)" json:"carrier_id"`
CarrierID uint `gorm:"column:carrier_id;index;comment:运营商ID(NULL表示所有运营商)" json:"carrier_id"`
RealNameCheckEnabled bool `gorm:"column:real_name_check_enabled;type:boolean;default:false;comment:是否启用实名检查" json:"real_name_check_enabled"`
RealNameCheckInterval int `gorm:"column:real_name_check_interval;type:int;default:60;comment:实名检查间隔(秒)" json:"real_name_check_interval"`
CardDataCheckEnabled bool `gorm:"column:card_data_check_enabled;type:boolean;default:false;comment:是否启用卡流量检查" json:"card_data_check_enabled"`
@@ -18,11 +21,9 @@ type PollingConfig struct {
PackageCheckInterval int `gorm:"column:package_check_interval;type:int;default:60;comment:套餐流量检查间隔(秒)" json:"package_check_interval"`
Priority int `gorm:"column:priority;type:int;default:100;not null;comment:优先级(数字越小优先级越高)" json:"priority"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (PollingConfig) TableName() string {
return "polling_configs"
return "tb_polling_config"
}

View File

@@ -1,45 +1,47 @@
package model
import "time"
import (
"time"
"gorm.io/gorm"
)
// DevCapabilityConfig 开发能力配置模型
// 管理 API 对接参数(AppID、AppSecret、回调地址等)
type DevCapabilityConfig struct {
ID uint `gorm:"column:id;primaryKey;comment:开发能力配置ID" json:"id"`
UserID uint `gorm:"column:user_id;type:bigint;not null;comment:用户ID(平台或代理)" json:"user_id"`
gorm.Model
BaseModel `gorm:"embedded"`
UserID uint `gorm:"column:user_id;index;not null;comment:用户ID(平台或代理)" json:"user_id"`
AppName string `gorm:"column:app_name;type:varchar(255);comment:应用名称" json:"app_name"`
AppID string `gorm:"column:app_id;type:varchar(100);uniqueIndex;comment:应用ID" json:"app_id"`
AppID string `gorm:"column:app_id;type:varchar(100);uniqueIndex:idx_dev_capability_app,where:deleted_at IS NULL;comment:应用ID" json:"app_id"`
AppSecret string `gorm:"column:app_secret;type:varchar(255);comment:应用密钥" json:"app_secret"`
CallbackURL string `gorm:"column:callback_url;type:varchar(500);comment:回调地址" json:"callback_url"`
IPWhitelist string `gorm:"column:ip_whitelist;type:text;comment:IP白名单(多个IP用逗号分隔)" json:"ip_whitelist"`
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (DevCapabilityConfig) TableName() string {
return "dev_capability_configs"
return "tb_dev_capability_config"
}
// CardReplacementRequest 换卡申请模型
// 客户提交的换卡申请管理,处理换卡申请
type CardReplacementRequest struct {
ID uint `gorm:"column:id;primaryKey;comment:换卡申请ID" json:"id"`
UserID uint `gorm:"column:user_id;type:bigint;not null;comment:申请用户ID" json:"user_id"`
gorm.Model
BaseModel `gorm:"embedded"`
UserID uint `gorm:"column:user_id;index;not null;comment:申请用户ID" json:"user_id"`
OldICCID string `gorm:"column:old_iccid;type:varchar(50);not null;comment:旧卡ICCID" json:"old_iccid"`
NewICCID string `gorm:"column:new_iccid;type:varchar(50);comment:新卡ICCID(审批时填充)" json:"new_iccid"`
Reason string `gorm:"column:reason;type:text;comment:换卡原因" json:"reason"`
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-待处理 2-已通过 3-已拒绝 4-已完成" json:"status"`
ApprovedBy uint `gorm:"column:approved_by;type:bigint;comment:处理人用户ID" json:"approved_by"`
ApprovedBy uint `gorm:"column:approved_by;index;comment:处理人用户ID" json:"approved_by"`
ApprovedAt *time.Time `gorm:"column:approved_at;comment:处理时间" json:"approved_at"`
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间(新卡激活时间)" json:"completed_at"`
RejectReason string `gorm:"column:reject_reason;type:text;comment:拒绝原因" json:"reject_reason"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名
func (CardReplacementRequest) TableName() string {
return "card_replacement_requests"
return "tb_card_replacement_request"
}

View File

@@ -0,0 +1,251 @@
-- IoT 模型架构重构回滚脚本
-- 创建时间: 2026-01-12
-- 说明: 回滚所有架构重构变更
-- 1. 恢复唯一索引(移除软删除支持)
-- 2. 恢复金额字段为 DECIMAL 类型
-- 3. 删除软删除字段和审计字段
-- 4. 恢复表名为复数形式
-- ========================================
-- 阶段 1: 恢复唯一约束(移除软删除支持)
-- ========================================
-- 1.1 运营商表
DROP INDEX IF EXISTS idx_carrier_code;
ALTER TABLE tb_carrier ADD CONSTRAINT carriers_carrier_code_key UNIQUE (carrier_code);
-- 1.2 IoT 卡表
DROP INDEX IF EXISTS idx_iot_card_iccid;
ALTER TABLE tb_iot_card ADD CONSTRAINT iot_cards_iccid_key UNIQUE (iccid);
-- 1.3 设备表
DROP INDEX IF EXISTS idx_device_no;
ALTER TABLE tb_device ADD CONSTRAINT devices_device_no_key UNIQUE (device_no);
-- 1.4 号卡表
DROP INDEX IF EXISTS idx_number_card_code;
ALTER TABLE tb_number_card ADD CONSTRAINT number_cards_virtual_product_code_key UNIQUE (virtual_product_code);
-- 1.5 套餐系列表
DROP INDEX IF EXISTS idx_package_series_code;
ALTER TABLE tb_package_series ADD CONSTRAINT package_series_series_code_key UNIQUE (series_code);
-- 1.6 套餐表
DROP INDEX IF EXISTS idx_package_code;
ALTER TABLE tb_package ADD CONSTRAINT packages_package_code_key UNIQUE (package_code);
-- 1.7 代理套餐分配表
DROP INDEX IF EXISTS idx_agent_package_allocation_agent_package;
ALTER TABLE tb_agent_package_allocation ADD CONSTRAINT uk_agent_package UNIQUE (agent_id, package_id);
-- 1.8 设备-SIM绑定表跳过保持原样
-- 1.9 订单表
DROP INDEX IF EXISTS idx_order_no;
ALTER TABLE tb_order ADD CONSTRAINT orders_order_no_key UNIQUE (order_no);
-- 1.10 轮询配置表
DROP INDEX IF EXISTS idx_polling_config_name;
ALTER TABLE tb_polling_config ADD CONSTRAINT polling_configs_config_name_key UNIQUE (config_name);
-- 1.11 代理层级表
DROP INDEX IF EXISTS idx_agent_hierarchy_agent;
ALTER TABLE tb_agent_hierarchy ADD CONSTRAINT agent_hierarchies_agent_id_key UNIQUE (agent_id);
-- 1.12 组合分佣条件表
DROP INDEX IF EXISTS idx_commission_combined_rule;
ALTER TABLE tb_commission_combined_condition ADD CONSTRAINT commission_combined_conditions_rule_id_key UNIQUE (rule_id);
-- 1.13 分佣模板表
DROP INDEX IF EXISTS idx_commission_template_name;
ALTER TABLE tb_commission_template ADD CONSTRAINT commission_templates_template_name_key UNIQUE (template_name);
-- 1.14 运营商结算表
DROP INDEX IF EXISTS idx_carrier_settlement_record;
ALTER TABLE tb_carrier_settlement ADD CONSTRAINT carrier_settlements_commission_record_id_key UNIQUE (commission_record_id);
-- 1.15 开发能力配置表
DROP INDEX IF EXISTS idx_dev_capability_app;
ALTER TABLE tb_dev_capability_config ADD CONSTRAINT dev_capability_configs_app_id_key UNIQUE (app_id);
-- ========================================
-- 阶段 2: 恢复金额字段为 DECIMAL 类型(分 / 100 = 元)
-- ========================================
-- 2.1 IoT 卡表
ALTER TABLE tb_iot_card
ALTER COLUMN cost_price TYPE DECIMAL(10,2) USING (cost_price / 100.0)::DECIMAL(10,2),
ALTER COLUMN distribute_price TYPE DECIMAL(10,2) USING (distribute_price / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_iot_card.cost_price IS '成本价(元)';
COMMENT ON COLUMN tb_iot_card.distribute_price IS '分销价(元)';
-- 2.2 号卡表
ALTER TABLE tb_number_card
ALTER COLUMN price TYPE DECIMAL(10,2) USING (price / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_number_card.price IS '价格(元)';
-- 2.3 套餐表
ALTER TABLE tb_package
ALTER COLUMN price TYPE DECIMAL(10,2) USING (price / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_package.price IS '套餐价格(元)';
-- 2.4 代理套餐分配表
ALTER TABLE tb_agent_package_allocation
ALTER COLUMN cost_price TYPE DECIMAL(10,2) USING (cost_price / 100.0)::DECIMAL(10,2),
ALTER COLUMN retail_price TYPE DECIMAL(10,2) USING (retail_price / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_agent_package_allocation.cost_price IS '成本价(元)';
COMMENT ON COLUMN tb_agent_package_allocation.retail_price IS '零售价(元)';
-- 2.5 订单表
ALTER TABLE tb_order
ALTER COLUMN amount TYPE DECIMAL(10,2) USING (amount / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_order.amount IS '订单金额(元)';
-- 2.6 分佣规则表
ALTER TABLE tb_commission_rule
ALTER COLUMN commission_value TYPE DECIMAL(10,2) USING (commission_value / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_commission_rule.commission_value IS '分佣值(元或百分比)';
-- 2.7 阶梯分佣配置表
ALTER TABLE tb_commission_ladder
ALTER COLUMN commission_value TYPE DECIMAL(10,2) USING (commission_value / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_commission_ladder.commission_value IS '分佣值(元或百分比)';
-- 2.8 组合分佣条件表
ALTER TABLE tb_commission_combined_condition
ALTER COLUMN one_time_commission_value TYPE DECIMAL(10,2) USING (one_time_commission_value / 100.0)::DECIMAL(10,2),
ALTER COLUMN long_term_commission_value TYPE DECIMAL(10,2) USING (long_term_commission_value / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_commission_combined_condition.one_time_commission_value IS '一次性分佣值(元或百分比)';
COMMENT ON COLUMN tb_commission_combined_condition.long_term_commission_value IS '长期分佣值(元或百分比)';
-- 2.9 分佣记录表
ALTER TABLE tb_commission_record
ALTER COLUMN amount TYPE DECIMAL(10,2) USING (amount / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_commission_record.amount IS '分佣金额(元)';
-- 2.10 分佣模板表
ALTER TABLE tb_commission_template
ALTER COLUMN commission_value TYPE DECIMAL(10,2) USING (commission_value / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_commission_template.commission_value IS '分佣值(元或百分比)';
-- 2.11 运营商结算表
ALTER TABLE tb_carrier_settlement
ALTER COLUMN settlement_amount TYPE DECIMAL(10,2) USING (settlement_amount / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_carrier_settlement.settlement_amount IS '结算金额(元)';
-- 2.12 佣金提现申请表
ALTER TABLE tb_commission_withdrawal_request
ALTER COLUMN amount TYPE DECIMAL(10,2) USING (amount / 100.0)::DECIMAL(10,2),
ALTER COLUMN fee TYPE DECIMAL(10,2) USING (fee / 100.0)::DECIMAL(10,2),
ALTER COLUMN actual_amount TYPE DECIMAL(10,2) USING (actual_amount / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_commission_withdrawal_request.amount IS '提现金额(元)';
COMMENT ON COLUMN tb_commission_withdrawal_request.fee IS '手续费(元)';
COMMENT ON COLUMN tb_commission_withdrawal_request.actual_amount IS '实际到账金额(元)';
-- 2.13 佣金提现设置表
ALTER TABLE tb_commission_withdrawal_setting
ALTER COLUMN min_withdrawal_amount TYPE DECIMAL(10,2) USING (min_withdrawal_amount / 100.0)::DECIMAL(10,2);
COMMENT ON COLUMN tb_commission_withdrawal_setting.min_withdrawal_amount IS '最低提现金额(元)';
-- ========================================
-- 阶段 3: 删除软删除字段和审计字段
-- ========================================
ALTER TABLE tb_carrier DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_iot_card DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_device DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_number_card DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_package_series DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_package DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_agent_package_allocation DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_device_sim_binding DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_order DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_package_usage DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_polling_config DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
-- 注意: tb_data_usage_record 没有添加这些字段,所以不需要删除
ALTER TABLE tb_agent_hierarchy DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_commission_rule DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_commission_ladder DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_commission_combined_condition DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_commission_record DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_commission_approval DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_commission_template DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_carrier_settlement DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_commission_withdrawal_request DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_commission_withdrawal_setting DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_payment_merchant_setting DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_dev_capability_config DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
ALTER TABLE tb_card_replacement_request DROP COLUMN deleted_at, DROP COLUMN creator, DROP COLUMN updater;
-- ========================================
-- 阶段 4: 恢复表名为复数形式
-- ========================================
ALTER TABLE tb_carrier RENAME TO carriers;
ALTER TABLE tb_iot_card RENAME TO iot_cards;
ALTER TABLE tb_device RENAME TO devices;
ALTER TABLE tb_number_card RENAME TO number_cards;
ALTER TABLE tb_package_series RENAME TO package_series;
ALTER TABLE tb_package RENAME TO packages;
ALTER TABLE tb_agent_package_allocation RENAME TO agent_package_allocations;
ALTER TABLE tb_device_sim_binding RENAME TO device_sim_bindings;
ALTER TABLE tb_order RENAME TO orders;
ALTER TABLE tb_package_usage RENAME TO package_usages;
ALTER TABLE tb_polling_config RENAME TO polling_configs;
ALTER TABLE tb_data_usage_record RENAME TO data_usage_records;
ALTER TABLE tb_agent_hierarchy RENAME TO agent_hierarchies;
ALTER TABLE tb_commission_rule RENAME TO commission_rules;
ALTER TABLE tb_commission_ladder RENAME TO commission_ladder;
ALTER TABLE tb_commission_combined_condition RENAME TO commission_combined_conditions;
ALTER TABLE tb_commission_record RENAME TO commission_records;
ALTER TABLE tb_commission_approval RENAME TO commission_approvals;
ALTER TABLE tb_commission_template RENAME TO commission_templates;
ALTER TABLE tb_carrier_settlement RENAME TO carrier_settlements;
ALTER TABLE tb_commission_withdrawal_request RENAME TO commission_withdrawal_requests;
ALTER TABLE tb_commission_withdrawal_setting RENAME TO commission_withdrawal_settings;
ALTER TABLE tb_payment_merchant_setting RENAME TO payment_merchant_settings;
ALTER TABLE tb_dev_capability_config RENAME TO dev_capability_configs;
ALTER TABLE tb_card_replacement_request RENAME TO card_replacement_requests;
-- ========================================
-- 阶段 5: 恢复表注释
-- ========================================
COMMENT ON TABLE carriers IS '运营商表';
COMMENT ON TABLE iot_cards IS 'IoT 卡表(物联网卡/流量卡)';
COMMENT ON TABLE devices IS '设备表(可容纳1-4张SIM卡)';
COMMENT ON TABLE number_cards IS '号卡表(虚拟商品)';
COMMENT ON TABLE package_series IS '套餐系列表';
COMMENT ON TABLE packages IS '套餐表';
COMMENT ON TABLE agent_package_allocations IS '代理套餐分配表';
COMMENT ON TABLE device_sim_bindings IS '设备-SIM卡绑定表';
COMMENT ON TABLE orders IS '订单表';
COMMENT ON TABLE package_usages IS '套餐使用表';
COMMENT ON TABLE polling_configs IS '轮询配置表';
COMMENT ON TABLE data_usage_records IS '流量使用记录表';
COMMENT ON TABLE agent_hierarchies IS '代理层级关系表';
COMMENT ON TABLE commission_rules IS '分佣规则表';
COMMENT ON TABLE commission_ladder IS '阶梯分佣配置表';
COMMENT ON TABLE commission_combined_conditions IS '组合分佣条件表';
COMMENT ON TABLE commission_records IS '分佣记录表';
COMMENT ON TABLE commission_approvals IS '分佣审批表';
COMMENT ON TABLE commission_templates IS '分佣模板表';
COMMENT ON TABLE carrier_settlements IS '号卡运营商结算表';
COMMENT ON TABLE commission_withdrawal_requests IS '佣金提现申请表';
COMMENT ON TABLE commission_withdrawal_settings IS '佣金提现设置表';
COMMENT ON TABLE payment_merchant_settings IS '收款商户设置表';
COMMENT ON TABLE dev_capability_configs IS '开发能力配置表';
COMMENT ON TABLE card_replacement_requests IS '换卡申请表';

View File

@@ -0,0 +1,349 @@
-- IoT 模型架构重构迁移脚本
-- 创建时间: 2026-01-12
-- 说明: 修改所有 IoT 相关表以符合项目架构规范
-- 1. 表名改为 tb_ 前缀 + 单数形式
-- 2. 添加软删除字段 deleted_at
-- 3. 添加审计字段 creator, updater
-- 4. 金额字段从 DECIMAL 改为 BIGINT分为单位
-- 5. 更新唯一索引以支持软删除
-- ========================================
-- 阶段 1: 重命名表(复数 -> tb_ + 单数)
-- ========================================
ALTER TABLE carriers RENAME TO tb_carrier;
ALTER TABLE iot_cards RENAME TO tb_iot_card;
ALTER TABLE devices RENAME TO tb_device;
ALTER TABLE number_cards RENAME TO tb_number_card;
ALTER TABLE package_series RENAME TO tb_package_series;
ALTER TABLE packages RENAME TO tb_package;
ALTER TABLE agent_package_allocations RENAME TO tb_agent_package_allocation;
ALTER TABLE device_sim_bindings RENAME TO tb_device_sim_binding;
ALTER TABLE orders RENAME TO tb_order;
ALTER TABLE package_usages RENAME TO tb_package_usage;
ALTER TABLE polling_configs RENAME TO tb_polling_config;
ALTER TABLE data_usage_records RENAME TO tb_data_usage_record;
ALTER TABLE agent_hierarchies RENAME TO tb_agent_hierarchy;
ALTER TABLE commission_rules RENAME TO tb_commission_rule;
ALTER TABLE commission_ladder RENAME TO tb_commission_ladder;
ALTER TABLE commission_combined_conditions RENAME TO tb_commission_combined_condition;
ALTER TABLE commission_records RENAME TO tb_commission_record;
ALTER TABLE commission_approvals RENAME TO tb_commission_approval;
ALTER TABLE commission_templates RENAME TO tb_commission_template;
ALTER TABLE carrier_settlements RENAME TO tb_carrier_settlement;
ALTER TABLE commission_withdrawal_requests RENAME TO tb_commission_withdrawal_request;
ALTER TABLE commission_withdrawal_settings RENAME TO tb_commission_withdrawal_setting;
ALTER TABLE payment_merchant_settings RENAME TO tb_payment_merchant_setting;
ALTER TABLE dev_capability_configs RENAME TO tb_dev_capability_config;
ALTER TABLE card_replacement_requests RENAME TO tb_card_replacement_request;
-- ========================================
-- 阶段 2: 添加软删除字段和审计字段
-- ========================================
-- 2.1 运营商表
ALTER TABLE tb_carrier ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_carrier ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_carrier ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.2 IoT 卡表
ALTER TABLE tb_iot_card ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_iot_card ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_iot_card ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.3 设备表
ALTER TABLE tb_device ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_device ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_device ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.4 号卡表
ALTER TABLE tb_number_card ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_number_card ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_number_card ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.5 套餐系列表
ALTER TABLE tb_package_series ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_package_series ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_package_series ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.6 套餐表
ALTER TABLE tb_package ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_package ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_package ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.7 代理套餐分配表
ALTER TABLE tb_agent_package_allocation ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_agent_package_allocation ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_agent_package_allocation ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.8 设备-SIM绑定表
ALTER TABLE tb_device_sim_binding ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_device_sim_binding ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_device_sim_binding ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.9 订单表
ALTER TABLE tb_order ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_order ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_order ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.10 套餐使用表
ALTER TABLE tb_package_usage ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_package_usage ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_package_usage ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.11 轮询配置表
ALTER TABLE tb_polling_config ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_polling_config ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_polling_config ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 注意: tb_data_usage_record 是日志表,不需要软删除和审计字段
-- 2.12 代理层级表
ALTER TABLE tb_agent_hierarchy ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_agent_hierarchy ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_agent_hierarchy ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.13 分佣规则表
ALTER TABLE tb_commission_rule ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_commission_rule ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_commission_rule ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.14 阶梯分佣配置表
ALTER TABLE tb_commission_ladder ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_commission_ladder ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_commission_ladder ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.15 组合分佣条件表
ALTER TABLE tb_commission_combined_condition ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_commission_combined_condition ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_commission_combined_condition ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.16 分佣记录表
ALTER TABLE tb_commission_record ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_commission_record ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_commission_record ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.17 分佣审批表
ALTER TABLE tb_commission_approval ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_commission_approval ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_commission_approval ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.18 分佣模板表
ALTER TABLE tb_commission_template ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_commission_template ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_commission_template ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.19 运营商结算表
ALTER TABLE tb_carrier_settlement ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_carrier_settlement ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_carrier_settlement ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.20 佣金提现申请表
ALTER TABLE tb_commission_withdrawal_request ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_commission_withdrawal_request ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_commission_withdrawal_request ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.21 佣金提现设置表
ALTER TABLE tb_commission_withdrawal_setting ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_commission_withdrawal_setting ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_commission_withdrawal_setting ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.22 收款商户设置表
ALTER TABLE tb_payment_merchant_setting ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_payment_merchant_setting ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_payment_merchant_setting ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.23 开发能力配置表
ALTER TABLE tb_dev_capability_config ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_dev_capability_config ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_dev_capability_config ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- 2.24 换卡申请表
ALTER TABLE tb_card_replacement_request ADD COLUMN deleted_at TIMESTAMP;
ALTER TABLE tb_card_replacement_request ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_card_replacement_request ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
-- ========================================
-- 阶段 3: 修改金额字段从 DECIMAL 改为 BIGINT分为单位
-- ========================================
-- 3.1 IoT 卡表:成本价、分销价(元 * 100 = 分)
ALTER TABLE tb_iot_card
ALTER COLUMN cost_price TYPE BIGINT USING (cost_price * 100)::BIGINT,
ALTER COLUMN distribute_price TYPE BIGINT USING (distribute_price * 100)::BIGINT;
COMMENT ON COLUMN tb_iot_card.cost_price IS '成本价(分为单位)';
COMMENT ON COLUMN tb_iot_card.distribute_price IS '分销价(分为单位)';
-- 3.2 号卡表:价格
ALTER TABLE tb_number_card
ALTER COLUMN price TYPE BIGINT USING (price * 100)::BIGINT;
COMMENT ON COLUMN tb_number_card.price IS '价格(分为单位)';
-- 3.3 套餐表:价格
ALTER TABLE tb_package
ALTER COLUMN price TYPE BIGINT USING (price * 100)::BIGINT;
COMMENT ON COLUMN tb_package.price IS '套餐价格(分为单位)';
-- 3.4 代理套餐分配表:成本价和零售价
ALTER TABLE tb_agent_package_allocation
ALTER COLUMN cost_price TYPE BIGINT USING (cost_price * 100)::BIGINT,
ALTER COLUMN retail_price TYPE BIGINT USING (retail_price * 100)::BIGINT;
COMMENT ON COLUMN tb_agent_package_allocation.cost_price IS '成本价(分为单位)';
COMMENT ON COLUMN tb_agent_package_allocation.retail_price IS '零售价(分为单位)';
-- 3.5 订单表:订单金额
ALTER TABLE tb_order
ALTER COLUMN amount TYPE BIGINT USING (amount * 100)::BIGINT;
COMMENT ON COLUMN tb_order.amount IS '订单金额(分为单位)';
-- 3.6 分佣规则表:分佣值
ALTER TABLE tb_commission_rule
ALTER COLUMN commission_value TYPE BIGINT USING (commission_value * 100)::BIGINT;
COMMENT ON COLUMN tb_commission_rule.commission_value IS '分佣值(分为单位,百分比时为千分比如2000表示20%)';
-- 3.7 阶梯分佣配置表:分佣值
ALTER TABLE tb_commission_ladder
ALTER COLUMN commission_value TYPE BIGINT USING (commission_value * 100)::BIGINT;
COMMENT ON COLUMN tb_commission_ladder.commission_value IS '分佣值(分为单位,百分比时为千分比如2000表示20%)';
-- 3.8 组合分佣条件表:一次性分佣值、长期分佣值
ALTER TABLE tb_commission_combined_condition
ALTER COLUMN one_time_commission_value TYPE BIGINT USING (one_time_commission_value * 100)::BIGINT,
ALTER COLUMN long_term_commission_value TYPE BIGINT USING (long_term_commission_value * 100)::BIGINT;
COMMENT ON COLUMN tb_commission_combined_condition.one_time_commission_value IS '一次性分佣值(分为单位,百分比时为千分比如2000表示20%)';
COMMENT ON COLUMN tb_commission_combined_condition.long_term_commission_value IS '长期分佣值(分为单位,百分比时为千分比如2000表示20%)';
-- 3.9 分佣记录表:分佣金额
ALTER TABLE tb_commission_record
ALTER COLUMN amount TYPE BIGINT USING (amount * 100)::BIGINT;
COMMENT ON COLUMN tb_commission_record.amount IS '分佣金额(分为单位)';
-- 3.10 分佣模板表:分佣值
ALTER TABLE tb_commission_template
ALTER COLUMN commission_value TYPE BIGINT USING (commission_value * 100)::BIGINT;
COMMENT ON COLUMN tb_commission_template.commission_value IS '分佣值(分为单位,百分比时为千分比如2000表示20%)';
-- 3.11 运营商结算表:结算金额
ALTER TABLE tb_carrier_settlement
ALTER COLUMN settlement_amount TYPE BIGINT USING (settlement_amount * 100)::BIGINT;
COMMENT ON COLUMN tb_carrier_settlement.settlement_amount IS '结算金额(分为单位)';
-- 3.12 佣金提现申请表:提现金额、手续费、实际到账金额
ALTER TABLE tb_commission_withdrawal_request
ALTER COLUMN amount TYPE BIGINT USING (amount * 100)::BIGINT,
ALTER COLUMN fee TYPE BIGINT USING (fee * 100)::BIGINT,
ALTER COLUMN actual_amount TYPE BIGINT USING (actual_amount * 100)::BIGINT;
COMMENT ON COLUMN tb_commission_withdrawal_request.amount IS '提现金额(分为单位)';
COMMENT ON COLUMN tb_commission_withdrawal_request.fee IS '手续费(分为单位)';
COMMENT ON COLUMN tb_commission_withdrawal_request.actual_amount IS '实际到账金额(分为单位)';
-- 3.13 佣金提现设置表:最低提现金额
ALTER TABLE tb_commission_withdrawal_setting
ALTER COLUMN min_withdrawal_amount TYPE BIGINT USING (min_withdrawal_amount * 100)::BIGINT;
COMMENT ON COLUMN tb_commission_withdrawal_setting.min_withdrawal_amount IS '最低提现金额(分为单位)';
-- ========================================
-- 阶段 4: 更新唯一索引以支持软删除
-- ========================================
-- 4.1 运营商表
ALTER TABLE tb_carrier DROP CONSTRAINT IF EXISTS carriers_carrier_code_key;
CREATE UNIQUE INDEX idx_carrier_code ON tb_carrier(carrier_code) WHERE deleted_at IS NULL;
-- 4.2 IoT 卡表
ALTER TABLE tb_iot_card DROP CONSTRAINT IF EXISTS iot_cards_iccid_key;
CREATE UNIQUE INDEX idx_iot_card_iccid ON tb_iot_card(iccid) WHERE deleted_at IS NULL;
-- 4.3 设备表
ALTER TABLE tb_device DROP CONSTRAINT IF EXISTS devices_device_no_key;
CREATE UNIQUE INDEX idx_device_no ON tb_device(device_no) WHERE deleted_at IS NULL;
-- 4.4 号卡表
ALTER TABLE tb_number_card DROP CONSTRAINT IF EXISTS number_cards_virtual_product_code_key;
CREATE UNIQUE INDEX idx_number_card_code ON tb_number_card(virtual_product_code) WHERE deleted_at IS NULL;
-- 4.5 套餐系列表
ALTER TABLE tb_package_series DROP CONSTRAINT IF EXISTS package_series_series_code_key;
CREATE UNIQUE INDEX idx_package_series_code ON tb_package_series(series_code) WHERE deleted_at IS NULL;
-- 4.6 套餐表
ALTER TABLE tb_package DROP CONSTRAINT IF EXISTS packages_package_code_key;
CREATE UNIQUE INDEX idx_package_code ON tb_package(package_code) WHERE deleted_at IS NULL;
-- 4.7 代理套餐分配表(复合唯一索引)
ALTER TABLE tb_agent_package_allocation DROP CONSTRAINT IF EXISTS uk_agent_package;
CREATE UNIQUE INDEX idx_agent_package_allocation_agent_package ON tb_agent_package_allocation(agent_id, package_id) WHERE deleted_at IS NULL;
-- 4.8 设备-SIM绑定表暂时跳过因为没有约束需要修改
-- 原始表使用条件唯一索引 idx_device_sim_bindings_active_card不需要修改
-- 4.9 订单表
ALTER TABLE tb_order DROP CONSTRAINT IF EXISTS orders_order_no_key;
CREATE UNIQUE INDEX idx_order_no ON tb_order(order_no) WHERE deleted_at IS NULL;
-- 4.10 轮询配置表
ALTER TABLE tb_polling_config DROP CONSTRAINT IF EXISTS polling_configs_config_name_key;
CREATE UNIQUE INDEX idx_polling_config_name ON tb_polling_config(config_name) WHERE deleted_at IS NULL;
-- 4.11 代理层级表
ALTER TABLE tb_agent_hierarchy DROP CONSTRAINT IF EXISTS agent_hierarchies_agent_id_key;
CREATE UNIQUE INDEX idx_agent_hierarchy_agent ON tb_agent_hierarchy(agent_id) WHERE deleted_at IS NULL;
-- 4.12 组合分佣条件表
ALTER TABLE tb_commission_combined_condition DROP CONSTRAINT IF EXISTS commission_combined_conditions_rule_id_key;
CREATE UNIQUE INDEX idx_commission_combined_rule ON tb_commission_combined_condition(rule_id) WHERE deleted_at IS NULL;
-- 4.13 分佣模板表
ALTER TABLE tb_commission_template DROP CONSTRAINT IF EXISTS commission_templates_template_name_key;
CREATE UNIQUE INDEX idx_commission_template_name ON tb_commission_template(template_name) WHERE deleted_at IS NULL;
-- 4.14 运营商结算表
ALTER TABLE tb_carrier_settlement DROP CONSTRAINT IF EXISTS carrier_settlements_commission_record_id_key;
CREATE UNIQUE INDEX idx_carrier_settlement_record ON tb_carrier_settlement(commission_record_id) WHERE deleted_at IS NULL;
-- 4.15 开发能力配置表
ALTER TABLE tb_dev_capability_config DROP CONSTRAINT IF EXISTS dev_capability_configs_app_id_key;
CREATE UNIQUE INDEX idx_dev_capability_app ON tb_dev_capability_config(app_id) WHERE deleted_at IS NULL;
-- ========================================
-- 阶段 5: 更新表注释
-- ========================================
COMMENT ON TABLE tb_carrier IS '运营商表';
COMMENT ON TABLE tb_iot_card IS 'IoT卡表(物联网卡/流量卡)';
COMMENT ON TABLE tb_device IS '设备表(可容纳1-4张SIM卡)';
COMMENT ON TABLE tb_number_card IS '号卡表(虚拟商品)';
COMMENT ON TABLE tb_package_series IS '套餐系列表';
COMMENT ON TABLE tb_package IS '套餐表';
COMMENT ON TABLE tb_agent_package_allocation IS '代理套餐分配表';
COMMENT ON TABLE tb_device_sim_binding IS '设备-SIM卡绑定表';
COMMENT ON TABLE tb_order IS '订单表';
COMMENT ON TABLE tb_package_usage IS '套餐使用表';
COMMENT ON TABLE tb_polling_config IS '轮询配置表';
COMMENT ON TABLE tb_data_usage_record IS '流量使用记录表';
COMMENT ON TABLE tb_agent_hierarchy IS '代理层级关系表';
COMMENT ON TABLE tb_commission_rule IS '分佣规则表';
COMMENT ON TABLE tb_commission_ladder IS '阶梯分佣配置表';
COMMENT ON TABLE tb_commission_combined_condition IS '组合分佣条件表';
COMMENT ON TABLE tb_commission_record IS '分佣记录表';
COMMENT ON TABLE tb_commission_approval IS '分佣审批表';
COMMENT ON TABLE tb_commission_template IS '分佣模板表';
COMMENT ON TABLE tb_carrier_settlement IS '号卡运营商结算表';
COMMENT ON TABLE tb_commission_withdrawal_request IS '佣金提现申请表';
COMMENT ON TABLE tb_commission_withdrawal_setting IS '佣金提现设置表';
COMMENT ON TABLE tb_payment_merchant_setting IS '收款商户设置表';
COMMENT ON TABLE tb_dev_capability_config IS '开发能力配置表';
COMMENT ON TABLE tb_card_replacement_request IS '换卡申请表';

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-01-12

View File

@@ -0,0 +1,537 @@
# 设计文档:修复 IoT 模型架构违规
## 1. 设计目标
将所有 IoT 相关数据模型重构为符合项目开发规范的标准模型,确保代码一致性、可维护性和长期可扩展性。
## 2. 核心设计原则
### 2.1 统一模型结构
所有数据模型必须遵循以下标准结构:
```go
type ModelName struct {
gorm.Model // 标准字段ID, CreatedAt, UpdatedAt, DeletedAt
BaseModel `gorm:"embedded"` // 基础字段Creator, Updater
// 业务字段(按字母顺序排列)
Field1 Type `gorm:"column:field1;..." json:"field1"`
Field2 Type `gorm:"column:field2;..." json:"field2"`
}
func (ModelName) TableName() string {
return "tb_model_name" // tb_ 前缀 + 单数
}
```
**设计理由:**
- `gorm.Model`:提供标准的主键、时间戳、软删除支持
- `BaseModel`:提供审计字段,记录创建人和更新人
- 显式 `column` 标签:明确 Go 字段和数据库列的映射关系,避免依赖 GORM 自动转换
- `tb_` 前缀单数表名:项目统一规范,便于识别业务表
### 2.2 字段定义规范
**字符串字段:**
```go
Name string `gorm:"column:name;type:varchar(100);not null;comment:名称" json:"name"`
```
- 必须显式指定 `column` 标签
- 必须指定 `type:varchar(N)` 和长度
- 必须指定 `not null`(如果必填)
- 必须添加中文 `comment`
**货币金额字段:**
```go
Amount int64 `gorm:"column:amount;type:bigint;default:0;not null;comment:金额(分)" json:"amount"`
```
- 使用 `int64` 类型(不是 `float64`
- 单位为"分"1元 = 100分
- 必须指定 `type:bigint`
- 必须指定 `default:0``not null`
- 注释中明确标注"(分)"
**设计理由:**
- 整数存储避免浮点精度问题(金融领域最佳实践)
- 分为单位便于精确计算和货币转换
**枚举字段:**
```go
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
```
- 使用 `int` 类型(不是 `string`
- 必须在注释中列举所有枚举值
- 必须指定 `default``not null`
**关联 ID 字段:**
```go
UserID uint `gorm:"column:user_id;type:bigint;not null;index;comment:用户ID" json:"user_id"`
```
- 使用 `uint` 类型(与 `gorm.Model` 的 ID 类型一致)
- 数据库类型使用 `bigint`PostgreSQL
- 必须添加 `index` 索引
- 禁止使用 GORM 关联标签(`foreignKey``references`
**可选关联 ID 字段:**
```go
ShopID *uint `gorm:"column:shop_id;type:bigint;index;comment:店铺ID可选" json:"shop_id,omitempty"`
```
- 使用指针类型 `*uint`(可为 NULL
- 不指定 `not null`
- 仍需添加 `index` 索引
- JSON 标签使用 `omitempty`
**唯一索引字段:**
```go
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iccid,where:deleted_at IS NULL;not null;comment:ICCID" json:"iccid"`
```
- 使用 `uniqueIndex` 标签
- 对于支持软删除的表,必须添加 `where:deleted_at IS NULL` 过滤条件
- 索引名命名规范:`idx_{table}_{field}``idx_{field}`
**时间字段:**
```go
ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at,omitempty"`
```
- 可选时间字段使用指针类型 `*time.Time`
- 不使用 `autoCreateTime``autoUpdateTime`(这些由 gorm.Model 提供)
- JSON 标签使用 `omitempty`
**JSONB 字段PostgreSQL**
```go
Metadata datatypes.JSON `gorm:"column:metadata;type:jsonb;comment:元数据" json:"metadata,omitempty"`
```
- 使用 `gorm.io/datatypes.JSON` 类型
- 数据库类型使用 `jsonb`PostgreSQL 优化存储)
- 使用 `omitempty`
### 2.3 表名和索引命名规范
**表名:**
- 格式:`tb_{model_name}`(单数)
- 示例:`tb_iot_card``tb_device``tb_order`
**索引名:**
- 普通索引:`idx_{table}_{field}`
- 唯一索引:`idx_{table}_{field}``uniq_{table}_{field}`
- 复合索引:`idx_{table}_{field1}_{field2}`
**设计理由:**
- 统一前缀便于识别业务表(与系统表区分)
- 单数形式符合 Go 惯用命名(类型名为单数)
- 索引名清晰表达用途和字段
### 2.4 软删除支持
所有业务数据表都应支持软删除:
```go
type BusinessModel struct {
gorm.Model // 包含 DeletedAt 字段
// ...
}
```
**不需要软删除的表:**
- 纯配置表(如 `PollingConfig``CommissionWithdrawalSetting`
- 日志表(如 `DataUsageRecord`
- 中间表(如 `DeviceSimBinding` 可选支持)
对于不需要软删除的表,可以手动定义字段:
```go
type ConfigModel struct {
ID uint `gorm:"column:id;primaryKey;comment:ID" json:"id"`
BaseModel `gorm:"embedded"`
// ...
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
}
```
## 3. 模型分类和修复策略
### 3.1 核心业务实体(必须支持软删除)
**完整模型结构gorm.Model + BaseModel**
- `IotCard`IoT 卡)
- `Device`(设备)
- `NumberCard`(号卡)
- `PackageSeries`(套餐系列)
- `Package`(套餐)
- `AgentPackageAllocation`(代理套餐分配)
- `Order`(订单)
- `AgentHierarchy`(代理层级)
- `CommissionRule`(分佣规则)
- `CommissionTemplate`(分佣模板)
- `Carrier`(运营商)
### 3.2 关联和绑定表(可选软删除)
**完整模型结构gorm.Model + BaseModel**
- `DeviceSimBinding`(设备-SIM 卡绑定)
### 3.3 使用记录和日志表(仅时间戳,不需要软删除)
**简化模型结构(手动定义 ID + BaseModel + CreatedAt/UpdatedAt**
- `PackageUsage`(套餐使用)- 保留 gorm.Model需要软删除和更新
- `DataUsageRecord`(流量记录)- 仅需 ID + CreatedAt不需要 UpdatedAt 和 DeletedAt
### 3.4 财务和审批表(必须支持软删除)
**完整模型结构gorm.Model + BaseModel**
- `CommissionRecord`(分佣记录)
- `CommissionApproval`(分佣审批)
- `CommissionWithdrawalRequest`(佣金提现申请)
- `PaymentMerchantSetting`(收款商户设置)
- `CarrierSettlement`(运营商结算)
- `CardReplacementRequest`(换卡申请)
### 3.5 阶梯和条件配置表(可选软删除)
**完整模型结构gorm.Model + BaseModel**
- `CommissionLadder`(阶梯分佣配置)
- `CommissionCombinedCondition`(组合分佣条件)
### 3.6 系统配置表(可选软删除)
**完整模型结构gorm.Model + BaseModel**
- `CommissionWithdrawalSetting`(提现设置)
- `PollingConfig`(轮询配置)
- `DevCapabilityConfig`(开发能力配置)
## 4. 货币金额处理策略
### 4.1 金额字段映射
所有货币金额从 `float64`(元)改为 `int64`(分):
| 原字段类型 | 新字段类型 | 原数据库类型 | 新数据库类型 | 说明 |
|-----------|-----------|------------|------------|-----|
| `float64` | `int64` | `DECIMAL(10,2)` | `BIGINT` | 金额单位从元改为分 |
**影响的字段:**
- `IotCard.CostPrice``IotCard.DistributePrice`
- `NumberCard.Price`
- `Package.Price`
- `AgentPackageAllocation.CostPrice``AgentPackageAllocation.RetailPrice`
- `Order.Amount`
- `CommissionRule.CommissionValue`
- `CommissionLadder.CommissionValue`
- `CommissionCombinedCondition.OneTimeCommissionValue``CommissionCombinedCondition.LongTermCommissionValue`
- `CommissionRecord.Amount`
- `CommissionTemplate.CommissionValue`
- `CarrierSettlement.SettlementAmount`
- `CommissionWithdrawalRequest.Amount``CommissionWithdrawalRequest.Fee``CommissionWithdrawalRequest.ActualAmount`
- `CommissionWithdrawalSetting.MinWithdrawalAmount`
### 4.2 业务逻辑调整
**API 输入输出:**
- API 接收的金额仍为 `float64`(元)
- Handler 层负责单位转换:元 → 分(乘以 100
- 响应时转换回:分 → 元(除以 100
**示例:**
```go
// 输入10.50 元
inputAmount := 10.50 // float64 (元)
dbAmount := int64(inputAmount * 100) // 1050 分
// 输出10.50 元
dbAmount := int64(1050) // 分
outputAmount := float64(dbAmount) / 100.0 // 10.50 元
```
### 4.3 数据库迁移
对于已有测试数据:
```sql
-- 金额从 DECIMAL(元) 转为 BIGINT(分)
ALTER TABLE iot_cards RENAME COLUMN cost_price TO cost_price_old;
ALTER TABLE iot_cards ADD COLUMN cost_price BIGINT NOT NULL DEFAULT 0;
UPDATE iot_cards SET cost_price = CAST(cost_price_old * 100 AS BIGINT);
ALTER TABLE iot_cards DROP COLUMN cost_price_old;
```
## 5. JSONB 字段处理
### 5.1 问题
原模型使用 `pq.StringArray` 类型存储 JSONB
```go
CarrierOrderData pq.StringArray `gorm:"column:carrier_order_data;type:jsonb;..."`
```
这是类型不匹配的:`pq.StringArray` 是 PostgreSQL 数组类型,不是 JSONB。
### 5.2 解决方案
使用 GORM 的 `datatypes.JSON` 类型:
```go
import "gorm.io/datatypes"
type Order struct {
// ...
CarrierOrderData datatypes.JSON `gorm:"column:carrier_order_data;type:jsonb;comment:运营商订单原始数据" json:"carrier_order_data,omitempty"`
// ...
}
```
**业务层使用:**
```go
// 写入
data := map[string]interface{}{
"order_id": "123",
"status": "paid",
}
order.CarrierOrderData, _ = json.Marshal(data)
// 读取
var data map[string]interface{}
json.Unmarshal(order.CarrierOrderData, &data)
```
## 6. 索引策略
### 6.1 唯一索引Unique Index
对于需要全局唯一的字段(如 ICCID、订单号、虚拟商品编码
```go
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iccid,where:deleted_at IS NULL;not null;comment:ICCID" json:"iccid"`
```
**关键点:**
- 必须添加 `where:deleted_at IS NULL` 过滤已软删除的记录
- 否则软删除后无法重新使用相同的唯一值
### 6.2 普通索引Index
对于频繁查询和过滤的字段(如状态、类型、关联 ID
```go
Status int `gorm:"column:status;type:int;default:1;not null;index;comment:状态" json:"status"`
UserID uint `gorm:"column:user_id;type:bigint;not null;index;comment:用户ID" json:"user_id"`
```
### 6.3 复合索引Composite Index
对于联合查询的字段组合:
```go
type DeviceSimBinding struct {
// ...
DeviceID uint `gorm:"column:device_id;type:bigint;not null;index:idx_device_slot;comment:设备ID" json:"device_id"`
SlotPosition int `gorm:"column:slot_position;type:int;index:idx_device_slot;comment:插槽位置" json:"slot_position"`
// ...
}
```
**复合索引命名:**
- `idx_device_slot`:表示 `device_id``slot_position` 的联合索引
## 7. 迁移路径
### 7.1 代码修改顺序
1. 修改所有模型文件(`internal/model/*.go`
2. 更新模型的单元测试(如有)
3. 生成新的数据库迁移脚本
4. 在开发环境测试迁移脚本
5. 验证所有模型定义正确
### 7.2 数据库迁移策略
**场景 1IoT 模块尚未部署(推荐)**
- 删除旧的迁移脚本(如果已创建)
- 生成新的初始迁移脚本
- 重新运行迁移
**场景 2IoT 模块已有测试数据**
- 保留旧的迁移脚本
- 生成新的迁移脚本(包含表重命名、字段修改)
- 编写数据转换脚本(金额单位转换等)
### 7.3 迁移脚本示例
```sql
-- 1. 重命名表(复数 → tb_ 前缀单数)
ALTER TABLE iot_cards RENAME TO tb_iot_card;
ALTER TABLE devices RENAME TO tb_device;
-- ...
-- 2. 添加新字段
ALTER TABLE tb_iot_card ADD COLUMN creator BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_iot_card ADD COLUMN updater BIGINT NOT NULL DEFAULT 0;
ALTER TABLE tb_iot_card ADD COLUMN deleted_at TIMESTAMP;
-- 3. 修改金额字段DECIMAL → BIGINT
ALTER TABLE tb_iot_card RENAME COLUMN cost_price TO cost_price_old;
ALTER TABLE tb_iot_card ADD COLUMN cost_price BIGINT NOT NULL DEFAULT 0;
UPDATE tb_iot_card SET cost_price = CAST(cost_price_old * 100 AS BIGINT);
ALTER TABLE tb_iot_card DROP COLUMN cost_price_old;
-- 4. 添加索引
CREATE UNIQUE INDEX idx_iccid ON tb_iot_card(iccid) WHERE deleted_at IS NULL;
CREATE INDEX idx_status ON tb_iot_card(status);
CREATE INDEX idx_carrier_id ON tb_iot_card(carrier_id);
```
## 8. 验证清单
修复完成后需验证:
- [ ] 所有模型嵌入 `gorm.Model` 或手动定义 `ID``CreatedAt``UpdatedAt`
- [ ] 所有业务模型嵌入 `BaseModel``Creator``Updater`
- [ ] 所有字段显式指定 `column` 标签
- [ ] 所有字符串字段指定类型和长度(`type:varchar(N)`
- [ ] 所有金额字段使用 `int64` 类型和 `type:bigint`
- [ ] 所有必填字段指定 `not null`
- [ ] 所有字段添加中文 `comment`
- [ ] 所有唯一字段添加 `uniqueIndex` 并包含 `where:deleted_at IS NULL`
- [ ] 所有关联字段添加 `index`
- [ ] 所有表名使用 `tb_` 前缀 + 单数
- [ ] 所有 JSONB 字段使用 `datatypes.JSON` 类型
- [ ] 所有模型与现有 `Account``PersonalCustomer` 模型风格一致
## 9. 风险和注意事项
### 9.1 破坏性变更
- 表名变更会导致旧代码无法运行
- 金额单位变更需要业务逻辑适配
- 新增字段需要在业务逻辑中赋值
### 9.2 迁移风险
- 表重命名可能导致迁移失败(需谨慎测试)
- 金额转换可能出现精度问题(需验证)
- 索引重建可能耗时(大表需评估)
### 9.3 开发流程影响
- 修复期间 IoT 模块功能开发需暂停
- 所有依赖 IoT 模型的代码需同步修改
- 需要重新生成数据库迁移脚本
## 10. 全局规范文档更新
### 10.1 更新目标
确保项目规范文档CLAUDE.md与实际实现的模型完全一致为未来开发提供清晰、准确的指导。
### 10.2 CLAUDE.md 更新内容
**1. 补充 GORM 模型字段规范**
在"数据库设计原则"部分添加详细的字段定义规范:
```markdown
**GORM 模型字段规范:**
**字段命名:**
- 数据库字段名必须使用下划线命名法snake_case`user_id``email_address``created_at`
- Go 结构体字段名必须使用驼峰命名法PascalCase`UserID``EmailAddress``CreatedAt`
**字段标签要求:**
- **所有字段必须显式指定数据库列名**:使用 `gorm:"column:字段名"` 标签
- 示例:`UserID uint gorm:"column:user_id;not null" json:"user_id"`
- 禁止省略 `column:` 标签,即使 GORM 能自动推断字段名
- 这确保了 Go 字段名和数据库字段名的映射关系清晰可见,避免命名歧义
- **所有字符串字段必须显式指定类型和长度**
- 短文本:`type:varchar(100)``type:varchar(255)`
- 中等文本:`type:varchar(500)``type:varchar(1000)`
- 长文本:`type:text`
- **所有字段必须添加中文注释**`comment:字段用途说明`
**货币金额字段规范:**
- **必须使用整数类型**Go 类型 `int64`,数据库类型 `bigint`
- **单位必须为"分"**1 元 = 100 分)
- **注释中必须明确标注单位**`comment:金额(分)`
- **理由**:避免浮点精度问题,符合金融系统最佳实践
示例:
```go
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:订单金额(分)" json:"amount"`
```
**唯一索引软删除兼容性:**
- 对于支持软删除的表(嵌入 `gorm.Model`),唯一索引必须包含 `where:deleted_at IS NULL` 过滤条件
- 示例:
```go
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iccid,where:deleted_at IS NULL;not null;comment:ICCID" json:"iccid"`
```
- 理由:允许软删除后重新使用相同的唯一值
**JSONB 字段规范PostgreSQL**
- 必须使用 `gorm.io/datatypes.JSON` 类型
- 数据库类型为 `jsonb`
- 示例:
```go
import "gorm.io/datatypes"
Metadata datatypes.JSON `gorm:"column:metadata;type:jsonb;comment:元数据" json:"metadata,omitempty"`
```
```
**2. 更新模型示例代码**
将现有的模型示例(如 Account更新为包含完整字段标签的版本确保所有示例都遵循规范。
**3. 添加金额单位转换说明**
在"API 设计规范"或"错误处理规范"附近添加:
```markdown
**API 层金额单位转换:**
- API 接收和返回的金额使用 `float64` 类型(元)
- 业务层和数据库使用 `int64` 类型(分)
- Handler 层负责单位转换
**输入转换API → 业务层):**
```go
// API 接收 10.50 元
inputAmount := 10.50 // float64 (元)
dbAmount := int64(inputAmount * 100) // 1050 分
```
**输出转换(业务层 → API**
```go
// 数据库存储 1050 分
dbAmount := int64(1050) // 分
outputAmount := float64(dbAmount) / 100.0 // 10.50 元
```
**注意事项:**
- 转换时注意四舍五入和边界情况
- 建议封装转换函数,避免重复代码
- 在金额字段的 DTO 注释中明确单位(元)
```
### 10.3 验证清单
更新完成后需验证:
- [ ] CLAUDE.md 中的所有模型示例包含完整的字段标签
- [ ] 所有字段定义规范清晰、完整、无歧义
- [ ] 金额字段整数存储的说明详细且易懂
- [ ] 唯一索引软删除兼容性规范已添加
- [ ] JSONB 字段使用规范已添加
- [ ] API 层金额单位转换说明已添加
- [ ] 规范文档与实际实现的模型完全一致
## 11. 后续任务
模型修复和规范文档更新完成后,需要:
1. 更新 DTO 模型(请求/响应结构体)
2. 调整 Store 层(数据访问层)
3. 调整 Service 层(业务逻辑层)- 金额单位转换
4. 调整 Handler 层API 层)- 金额单位转换
5. 生成数据库迁移脚本
6. 编写单元测试验证模型定义
7. 更新 API 文档

View File

@@ -0,0 +1,152 @@
## Why
在之前的 IoT SIM 管理系统提案2026-01-12-iot-sim-management中创建的所有数据模型存在严重的架构违规问题完全没有遵循项目的核心开发规范。这些违规导致代码不一致、可维护性差、违背项目设计原则。
**核心问题:**
1. **未使用基础模型**:所有 IoT 模型都没有嵌入 `BaseModel`,缺少统一的 `creator``updater` 字段
2. **未使用 gorm.Model**:部分模型没有嵌入 `gorm.Model`,缺少标准的 `ID``CreatedAt``UpdatedAt``DeletedAt` 字段
3. **字段命名不规范**:未显式指定 `column` 标签,依赖 GORM 自动转换(违反规范)
4. **字段定义不完整**:缺少必要的数据库约束标签(`not null``uniqueIndex`、索引等)
5. **数据类型不一致**
- 货币字段使用 `float64` 而不是整数(分为单位)
- ID 字段类型不一致(`uint` vs `bigint`
- 时间字段缺少 `autoCreateTime`/`autoUpdateTime` 标签
6. **表名不符合规范**:使用复数形式(`iot_cards`)而不是项目约定的 `tb_` 前缀单数形式
7. **缺少中文注释**:部分字段缺少清晰的中文注释说明业务含义
8. **软删除支持不一致**:某些应该支持软删除的模型缺少 `gorm.Model` 嵌入
**对比现有规范模型Account、PersonalCustomer**
**正确示例Account 模型):**
```go
type Account struct {
gorm.Model // ✅ 嵌入标准模型ID、CreatedAt、UpdatedAt、DeletedAt
BaseModel `gorm:"embedded"` // ✅ 嵌入基础模型Creator、Updater
Username string `gorm:"column:username;type:varchar(50);uniqueIndex:idx_account_username,where:deleted_at IS NULL;not null;comment:用户名" json:"username"`
// ✅ 显式 column 标签
// ✅ 明确类型和长度
// ✅ 唯一索引 + 软删除过滤
// ✅ not null 约束
// ✅ 中文注释
}
func (Account) TableName() string {
return "tb_account" // ✅ tb_ 前缀 + 单数
}
```
**错误示例IotCard 模型):**
```go
type IotCard struct {
ID uint `gorm:"column:id;primaryKey;comment:IoT 卡 ID" json:"id"`
// ❌ 没有 gorm.Model
// ❌ 没有 BaseModel
// ❌ 手动定义 ID应该由 gorm.Model 提供)
// ❌ 没有 DeletedAt无法软删除
CostPrice float64 `gorm:"column:cost_price;type:decimal(10,2);default:0;comment:成本价(元)" json:"cost_price"`
// ❌ 使用 float64 而不是整数(分为单位)
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
// ❌ 手动定义(应该由 gorm.Model 提供)
}
func (IotCard) TableName() string {
return "iot_cards" // ❌ 复数形式,没有 tb_ 前缀
}
```
**影响范围:**
需要修复以下所有 IoT 相关模型(约 25 个模型文件):
- `internal/model/iot_card.go`IotCard
- `internal/model/device.go`Device、DeviceSimBinding
- `internal/model/number_card.go`NumberCard
- `internal/model/package.go`PackageSeries、Package、AgentPackageAllocation、PackageUsage
- `internal/model/order.go`Order
- `internal/model/commission.go`AgentHierarchy、CommissionRule、CommissionLadder、CommissionCombinedCondition、CommissionRecord、CommissionApproval、CommissionTemplate、CarrierSettlement
- `internal/model/financial.go`CommissionWithdrawalRequest、CommissionWithdrawalSetting、PaymentMerchantSetting
- `internal/model/system.go`DevCapabilityConfig、CardReplacementRequest
- `internal/model/carrier.go`Carrier
- `internal/model/data_usage.go`DataUsageRecord
- `internal/model/polling.go`PollingConfig
## What Changes
- 重构所有 IoT 相关数据模型,使其完全符合项目开发规范
- 统一所有模型的字段定义、类型、约束、注释格式
- 确保所有模型与现有用户体系模型Account、PersonalCustomer保持一致的架构风格
- 更新数据库迁移脚本以反映模型变更
## Capabilities
### Modified Capabilities
#### 核心数据模型规范化
- `iot-card`: 修改 IoT 卡业务模型 - 统一字段定义,嵌入 BaseModel 和 gorm.Model修正表名为 `tb_iot_card`,使用整数存储金额,完善索引和约束
- `iot-device`: 修改设备业务模型 - 统一字段定义,嵌入 BaseModel 和 gorm.Model修正表名为 `tb_device`,规范化所有关联字段
- `iot-number-card`: 修改号卡业务模型 - 统一字段定义,嵌入 BaseModel 和 gorm.Model修正表名为 `tb_number_card`,使用整数存储金额
- `iot-package`: 修改套餐管理模型 - 统一字段定义,嵌入 BaseModel 和 gorm.Model修正表名`tb_package_series``tb_package``tb_agent_package_allocation``tb_package_usage`),使用整数存储金额
- `iot-order`: 修改订单管理模型 - 统一字段定义,嵌入 BaseModel 和 gorm.Model修正表名为 `tb_order`,使用整数存储金额,规范化 JSONB 字段
- `iot-agent-commission`: 修改代理分佣模型 - 统一所有分佣相关模型字段定义,嵌入 BaseModel 和 gorm.Model修正表名添加 `tb_` 前缀),使用整数存储金额
#### 财务和系统模型规范化
- 修改财务相关模型CommissionWithdrawalRequest、CommissionWithdrawalSetting、PaymentMerchantSetting- 统一字段定义,使用整数存储金额,完善索引和约束
- 修改系统配置模型DevCapabilityConfig、CardReplacementRequest- 统一字段定义,嵌入 BaseModel 和 gorm.Model
- 修改运营商模型Carrier- 统一字段定义,嵌入 BaseModel 和 gorm.Model修正表名为 `tb_carrier`
- 修改流量记录模型DataUsageRecord- 统一字段定义,嵌入 gorm.Model修正表名为 `tb_data_usage_record`
- 修改轮询配置模型PollingConfig- 统一字段定义,嵌入 BaseModel 和 gorm.Model修正表名为 `tb_polling_config`
## Impact
**代码变更:**
- 重构约 25 个 GORM 模型文件(`internal/model/`
- 所有模型的字段定义将发生变化(字段名、类型、标签)
- 所有表名将从复数变为 `tb_` 前缀单数形式
**数据库变更:**
- 需要生成新的数据库迁移脚本以反映模型变更
- 表名变更(如 `iot_cards``tb_iot_card`
- 字段变更(如 `cost_price DECIMAL``cost_price BIGINT`,金额从元改为分)
- 新增字段(`creator``updater``deleted_at`
- 新增索引和约束
**向后兼容性:**
-**不兼容变更**:此次修复涉及破坏性变更(表名、字段类型)
- 由于 IoT 模块尚未实际部署到生产环境,可以直接修改而无需数据迁移
- 如果已有测试数据,需要编写数据迁移脚本
**业务影响:**
- 不影响现有用户体系Account、Role、Permission 等)
- 不影响个人客户模块PersonalCustomer
- IoT 模块的 Service 层和 Handler 层代码需要相应调整(字段类型变化)
**依赖关系:**
- 必须在实现 IoT 业务逻辑Handlers、Services、Stores之前修复
- 修复后才能生成正确的数据库迁移脚本
- 修复后才能生成准确的 API 文档
**文档变更:**
- 更新 `CLAUDE.md` 中的数据库设计原则和 GORM 模型字段规范
- 补充完整的字段定义规范(显式 column 标签、类型定义、注释要求)
- 添加金额字段整数存储的详细说明和示例
- 完善表名命名规范和 BaseModel 使用说明
- 确保全局规范文档与实际实现保持一致
**明确排除的范围**(本次不涉及):
- Handler 层代码修改(将在后续任务中处理)
- Service 层代码修改(将在后续任务中处理)
- Store 层代码修改(将在后续任务中处理)
- DTO 模型调整(请求/响应结构体)
- 单元测试和集成测试
- API 文档更新
**风险和注意事项:**
- 所有金额字段从 `float64` 改为 `int64`(分为单位),需要在业务逻辑中进行单位转换
- 表名变更需要确保迁移脚本正确执行
- 新增的 `creator``updater` 字段需要在业务逻辑中正确赋值
- 软删除(`DeletedAt`的引入可能需要调整查询逻辑GORM 会自动处理)

View File

@@ -0,0 +1,458 @@
# Capability: model-organization
## MODIFIED Requirements
### Requirement: Data models MUST follow unified structure conventions
All IoT data models MUST follow unified structure conventions. 所有 IoT 相关数据模型必须与现有用户体系模型Account、PersonalCustomer保持一致的架构风格和字段定义规范。
#### Scenario: IoT 卡模型结构规范化
**Given** 系统存在 IoT 卡数据模型
**When** 开发者定义或修改 IoT 卡模型
**Then** 模型必须:
- 嵌入 `gorm.Model`(提供 ID、CreatedAt、UpdatedAt、DeletedAt 字段)
- 嵌入 `BaseModel`(提供 Creator、Updater 审计字段)
- 所有字段显式指定 `gorm:"column:字段名"` 标签
- 所有字符串字段显式指定类型和长度(如 `type:varchar(100)`
- 所有金额字段使用 `int64` 类型和 `type:bigint`,单位为"分"
- 所有必填字段添加 `not null` 约束
- 所有字段添加中文 `comment` 注释
- 所有唯一字段添加 `uniqueIndex:索引名,where:deleted_at IS NULL`
- 所有关联 ID 字段添加 `index` 索引
- 表名使用 `tb_iot_card``tb_` 前缀 + 单数)
**Example:**
```go
package model
import "gorm.io/gorm"
type IotCard struct {
gorm.Model // ID, CreatedAt, UpdatedAt, DeletedAt
BaseModel `gorm:"embedded"` // Creator, Updater
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iccid,where:deleted_at IS NULL;not null;comment:ICCID(唯一标识)" json:"iccid"`
CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型" json:"card_type"`
CardCategory string `gorm:"column:card_category;type:varchar(20);default:'normal';not null;comment:卡业务类型 normal-普通卡 industry-行业卡" json:"card_category"`
CarrierID uint `gorm:"column:carrier_id;type:bigint;not null;index;comment:运营商ID" json:"carrier_id"`
CostPrice int64 `gorm:"column:cost_price;type:bigint;default:0;not null;comment:成本价(分)" json:"cost_price"`
DistributePrice int64 `gorm:"column:distribute_price;type:bigint;default:0;not null;comment:分销价(分)" json:"distribute_price"`
Status int `gorm:"column:status;type:int;default:1;not null;index;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"`
OwnerType string `gorm:"column:owner_type;type:varchar(20);default:'platform';not null;comment:所有者类型 platform-平台 agent-代理 user-用户 device-设备" json:"owner_type"`
OwnerID uint `gorm:"column:owner_id;type:bigint;default:0;not null;index;comment:所有者ID" json:"owner_id"`
ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at,omitempty"`
}
func (IotCard) TableName() string {
return "tb_iot_card"
}
```
#### Scenario: 设备模型结构规范化
**Given** 系统存在设备数据模型
**When** 开发者定义或修改设备模型
**Then** 模型必须遵循与 IoT 卡模型相同的规范gorm.Model + BaseModel + 字段标签)
**And** 表名使用 `tb_device`
#### Scenario: 号卡模型结构规范化
**Given** 系统存在号卡数据模型
**When** 开发者定义或修改号卡模型
**Then** 模型必须遵循与 IoT 卡模型相同的规范
**And** 表名使用 `tb_number_card`
**And** 价格字段使用 `int64` 类型(分为单位)
#### Scenario: 套餐相关模型结构规范化
**Given** 系统存在套餐系列、套餐、代理套餐分配、套餐使用情况等模型
**When** 开发者定义或修改套餐相关模型
**Then** 所有套餐相关模型必须遵循统一规范:
- 套餐系列:`tb_package_series`
- 套餐:`tb_package`
- 代理套餐分配:`tb_agent_package_allocation`
- 套餐使用情况:`tb_package_usage`
**And** 所有价格字段使用 `int64` 类型(分为单位)
#### Scenario: 订单模型结构规范化
**Given** 系统存在订单数据模型
**When** 开发者定义或修改订单模型
**Then** 模型必须遵循统一规范
**And** 表名使用 `tb_order`
**And** 金额字段使用 `int64` 类型(分为单位)
**And** JSONB 字段使用 `gorm.io/datatypes.JSON` 类型(不是 `pq.StringArray`
**Example:**
```go
import (
"gorm.io/datatypes"
"gorm.io/gorm"
)
type Order struct {
gorm.Model
BaseModel `gorm:"embedded"`
OrderNo string `gorm:"column:order_no;type:varchar(100);uniqueIndex:idx_order_no,where:deleted_at IS NULL;not null;comment:订单号(唯一标识)" json:"order_no"`
OrderType int `gorm:"column:order_type;type:int;not null;index;comment:订单类型 1-套餐订单 2-号卡订单" json:"order_type"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:订单金额(分)" json:"amount"`
CarrierOrderData datatypes.JSON `gorm:"column:carrier_order_data;type:jsonb;comment:运营商订单原始数据" json:"carrier_order_data,omitempty"`
Status int `gorm:"column:status;type:int;default:1;not null;index;comment:状态 1-待支付 2-已支付 3-已完成 4-已取消 5-已退款" json:"status"`
}
func (Order) TableName() string {
return "tb_order"
}
```
#### Scenario: 分佣相关模型结构规范化
**Given** 系统存在代理层级、分佣规则、分佣记录等模型
**When** 开发者定义或修改分佣相关模型
**Then** 所有分佣相关模型必须遵循统一规范:
- 代理层级:`tb_agent_hierarchy`
- 分佣规则:`tb_commission_rule`
- 阶梯分佣配置:`tb_commission_ladder`
- 组合分佣条件:`tb_commission_combined_condition`
- 分佣记录:`tb_commission_record`
- 分佣审批:`tb_commission_approval`
- 分佣模板:`tb_commission_template`
- 运营商结算:`tb_carrier_settlement`
**And** 所有金额字段使用 `int64` 类型(分为单位)
#### Scenario: 财务相关模型结构规范化
**Given** 系统存在佣金提现申请、提现设置、收款商户设置等模型
**When** 开发者定义或修改财务相关模型
**Then** 所有财务相关模型必须遵循统一规范:
- 佣金提现申请:`tb_commission_withdrawal_request`
- 佣金提现设置:`tb_commission_withdrawal_setting`
- 收款商户设置:`tb_payment_merchant_setting`
**And** 所有金额字段使用 `int64` 类型(分为单位)
**And** JSONB 字段使用 `gorm.io/datatypes.JSON` 类型
#### Scenario: 系统配置和日志模型规范化
**Given** 系统存在运营商、轮询配置、流量记录、开发能力配置等模型
**When** 开发者定义或修改系统配置和日志模型
**Then** 模型必须遵循统一规范:
- 运营商:`tb_carrier`gorm.Model + BaseModel
- 轮询配置:`tb_polling_config`gorm.Model + BaseModel
- 流量记录:`tb_data_usage_record`(仅 ID + CreatedAt不需要 UpdatedAt 和 DeletedAt
- 开发能力配置:`tb_dev_capability_config`gorm.Model + BaseModel
- 换卡申请:`tb_card_replacement_request`gorm.Model + BaseModel
**Example (流量记录 - 简化模型):**
```go
type DataUsageRecord struct {
ID uint `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"`
IotCardID uint `gorm:"column:iot_card_id;type:bigint;not null;index;comment:IoT卡ID" json:"iot_card_id"`
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;not null;comment:流量使用量(MB)" json:"data_usage_mb"`
CheckTime time.Time `gorm:"column:check_time;not null;comment:检查时间" json:"check_time"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
}
func (DataUsageRecord) TableName() string {
return "tb_data_usage_record"
}
```
#### Scenario: 设备-SIM 卡绑定关系模型规范化
**Given** 系统存在设备-IoT 卡绑定关系模型
**When** 开发者定义或修改绑定关系模型
**Then** 模型必须遵循统一规范
**And** 表名使用 `tb_device_sim_binding`
**And** 支持复合索引(`device_id` + `slot_position`
**Example:**
```go
type DeviceSimBinding struct {
gorm.Model
BaseModel `gorm:"embedded"`
DeviceID uint `gorm:"column:device_id;type:bigint;not null;index:idx_device_slot;comment:设备ID" json:"device_id"`
IotCardID uint `gorm:"column:iot_card_id;type:bigint;not null;index;comment:IoT卡ID" json:"iot_card_id"`
SlotPosition int `gorm:"column:slot_position;type:int;index:idx_device_slot;comment:插槽位置(1, 2, 3, 4)" json:"slot_position"`
BindStatus int `gorm:"column:bind_status;type:int;default:1;not null;comment:绑定状态 1-已绑定 2-已解绑" json:"bind_status"`
BindTime *time.Time `gorm:"column:bind_time;comment:绑定时间" json:"bind_time,omitempty"`
UnbindTime *time.Time `gorm:"column:unbind_time;comment:解绑时间" json:"unbind_time,omitempty"`
}
func (DeviceSimBinding) TableName() string {
return "tb_device_sim_binding"
}
```
### Requirement: Currency amount fields MUST use integer type (unit: cents)
All currency amount fields MUST use integer type (unit: cents). 所有货币金额字段必须使用 `int64` 类型存储,单位为"分"1 元 = 100 分),避免浮点精度问题。
#### Scenario: 金额字段定义规范
**Given** 模型包含货币金额字段(如价格、成本、佣金、提现金额等)
**When** 开发者定义金额字段
**Then** 字段必须:
- 使用 `int64` Go 类型(不是 `float64`
- 数据库类型为 `bigint`(不是 `decimal``numeric`
- 默认值为 `0`
- 添加 `not null` 约束
- 注释中明确标注"(分)"单位
**Example:**
```go
CostPrice int64 `gorm:"column:cost_price;type:bigint;default:0;not null;comment:成本价(分)" json:"cost_price"`
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:订单金额(分)" json:"amount"`
```
#### Scenario: API 层金额单位转换
**Given** API 接收或返回金额数据
**When** Handler 层处理请求或响应
**Then** 必须进行单位转换:
- 输入API 接收 `float64`(元) → 业务层使用 `int64`(分)
- 输出:业务层返回 `int64`(分) → API 返回 `float64`(元)
**Example:**
```go
// 输入转换Handler 层)
type CreateOrderRequest struct {
Amount float64 `json:"amount"` // 元
}
func (h *OrderHandler) CreateOrder(c *fiber.Ctx) error {
var req CreateOrderRequest
// ... 解析请求 ...
// 转换:元 → 分
amountInCents := int64(req.Amount * 100)
// 调用 Service 层
order, err := h.orderService.CreateOrder(ctx, amountInCents, ...)
// ...
}
// 输出转换Handler 层)
type OrderResponse struct {
Amount float64 `json:"amount"` // 元
}
func (h *OrderHandler) GetOrder(c *fiber.Ctx) error {
order, err := h.orderService.GetOrder(ctx, orderID)
// ...
// 转换:分 → 元
resp := OrderResponse{
Amount: float64(order.Amount) / 100.0,
}
return response.Success(c, resp)
}
```
### Requirement: Table names MUST follow unified naming conventions
All database table names MUST follow unified naming conventions. 所有数据库表名必须遵循项目约定的 `tb_` 前缀 + 单数形式。
#### Scenario: 表名命名规范
**Given** 开发者定义数据模型
**When** 实现 `TableName()` 方法
**Then** 表名必须:
- 使用 `tb_` 前缀
- 使用单数形式(不是复数)
- 使用下划线命名法snake_case
**Example:**
```go
// ✅ 正确
func (IotCard) TableName() string {
return "tb_iot_card"
}
func (Device) TableName() string {
return "tb_device"
}
func (Order) TableName() string {
return "tb_order"
}
// ❌ 错误
func (IotCard) TableName() string {
return "iot_cards" // 缺少 tb_ 前缀,使用复数
}
func (Device) TableName() string {
return "devices" // 缺少 tb_ 前缀,使用复数
}
```
#### Scenario: 关联表和中间表命名
**Given** 模型表示多对多关系或绑定关系
**When** 定义关联表或中间表
**Then** 表名必须使用 `tb_` 前缀 + 完整描述性名称(单数)
**Example:**
```go
// 设备-SIM 卡绑定
func (DeviceSimBinding) TableName() string {
return "tb_device_sim_binding" // 不是 tb_device_sim_bindings
}
// 代理套餐分配
func (AgentPackageAllocation) TableName() string {
return "tb_agent_package_allocation" // 不是 tb_agent_package_allocations
}
```
### Requirement: All fields MUST explicitly specify database column names and types
All model fields MUST explicitly specify database column names and types. 模型字段定义必须清晰明确,不依赖 GORM 的自动转换和推断。
#### Scenario: 字段 GORM 标签完整性检查
**Given** 模型包含业务字段
**When** 开发者定义字段
**Then** 每个字段必须包含:
- `column:字段名`(显式指定数据库列名)
- `type:数据类型`(显式指定数据库类型)
- `comment:中文注释`(说明业务含义)
- 可选:`not null``default:值``index``uniqueIndex` 等约束
**Example:**
```go
// ✅ 完整的字段定义
Username string `gorm:"column:username;type:varchar(50);uniqueIndex:idx_username,where:deleted_at IS NULL;not null;comment:用户名" json:"username"`
Status int `gorm:"column:status;type:int;default:1;not null;index;comment:状态 1-启用 2-禁用" json:"status"`
Phone string `gorm:"column:phone;type:varchar(20);comment:手机号码" json:"phone,omitempty"`
// ❌ 不完整的字段定义
Username string `gorm:"comment:用户名" json:"username"` // 缺少 column 和 type
Status int `gorm:"default:1" json:"status"` // 缺少 column、type 和 comment
```
#### Scenario: 唯一索引软删除兼容
**Given** 字段需要全局唯一(如 ICCID、订单号、虚拟商品编码
**When** 模型支持软删除(嵌入 `gorm.Model`
**Then** 唯一索引必须包含 `where:deleted_at IS NULL` 过滤条件
**Example:**
```go
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex:idx_iccid,where:deleted_at IS NULL;not null;comment:ICCID(唯一标识)" json:"iccid"`
OrderNo string `gorm:"column:order_no;type:varchar(100);uniqueIndex:idx_order_no,where:deleted_at IS NULL;not null;comment:订单号" json:"order_no"`
```
**Explanation:**
- 软删除后,`deleted_at` 不为 NULL
- 索引只对 `deleted_at IS NULL` 的记录生效
- 允许软删除后重新使用相同的唯一值
### Requirement: All models MUST support audit tracking (Creator and Updater)
All business data models MUST support audit tracking (Creator and Updater). 所有业务数据模型必须记录创建人和更新人,便于审计和追溯。
#### Scenario: 嵌入 BaseModel 提供审计字段
**Given** 模型表示业务数据实体
**When** 开发者定义模型
**Then** 模型必须嵌入 `BaseModel`
**And** `BaseModel` 提供 `Creator``Updater` 字段
**Example:**
```go
type IotCard struct {
gorm.Model
BaseModel `gorm:"embedded"` // 提供 Creator 和 Updater
// 业务字段...
}
// BaseModel 定义在 internal/model/base.go
type BaseModel struct {
Creator uint `gorm:"column:creator;not null;comment:创建人ID" json:"creator"`
Updater uint `gorm:"column:updater;not null;comment:更新人ID" json:"updater"`
}
```
#### Scenario: 业务逻辑层自动填充审计字段
**Given** Service 层或 Store 层创建或更新数据
**When** 执行数据库插入或更新操作
**Then** 必须自动填充 `Creator``Updater` 字段(从上下文获取当前用户 ID
**Example:**
```go
// Service 层或 Store 层
func (s *IotCardService) CreateIotCard(ctx context.Context, req CreateIotCardRequest) (*IotCard, error) {
// 从上下文获取当前用户 ID
currentUserID := middleware.GetUserIDFromContext(ctx)
card := &IotCard{
BaseModel: BaseModel{
Creator: currentUserID,
Updater: currentUserID,
},
ICCID: req.ICCID,
CardType: req.CardType,
// ...
}
if err := s.db.Create(card).Error; err != nil {
return nil, err
}
return card, nil
}
func (s *IotCardService) UpdateIotCard(ctx context.Context, id uint, req UpdateIotCardRequest) error {
currentUserID := middleware.GetUserIDFromContext(ctx)
updates := map[string]interface{}{
"updater": currentUserID,
"card_type": req.CardType,
// ...
}
return s.db.Model(&IotCard{}).Where("id = ?", id).Updates(updates).Error
}
```
### Requirement: Log and record tables MUST use appropriate model structure
Append-only log and record tables MUST use simplified model structure. 对于只追加、不更新的日志表(如流量记录),必须使用简化的模型结构,不需要 `UpdatedAt``DeletedAt`
#### Scenario: 流量记录简化模型
**Given** 模型表示只追加的日志数据(不会被修改或删除)
**When** 开发者定义日志模型
**Then** 模型可以:
- 手动定义 `ID`(不嵌入 `gorm.Model`
- 只包含 `CreatedAt`(不需要 `UpdatedAt``DeletedAt`
- 不嵌入 `BaseModel`(如果不需要审计)
**Example:**
```go
type DataUsageRecord struct {
ID uint `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"`
IotCardID uint `gorm:"column:iot_card_id;type:bigint;not null;index;comment:IoT卡ID" json:"iot_card_id"`
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;not null;comment:流量使用量(MB)" json:"data_usage_mb"`
DataIncreaseMB int64 `gorm:"column:data_increase_mb;type:bigint;default:0;comment:相比上次的增量(MB)" json:"data_increase_mb"`
CheckTime time.Time `gorm:"column:check_time;not null;comment:检查时间" json:"check_time"`
Source string `gorm:"column:source;type:varchar(50);default:'polling';comment:数据来源 polling-轮询 manual-手动 gateway-回调" json:"source"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
}
func (DataUsageRecord) TableName() string {
return "tb_data_usage_record"
}
```
**Explanation:**
- 流量记录只追加,不修改,不需要 `UpdatedAt`
- 流量记录不删除(或物理删除),不需要 `DeletedAt`
- 简化模型结构减少存储开销和查询复杂度

View File

@@ -0,0 +1,643 @@
# Tasks
本文档列出修复 IoT 模型架构违规所需的所有任务,按优先级和依赖关系排序。
## 阶段 1: 核心业务实体模型修复(必须优先完成)
### Task 1.1: 修复 IoT 卡模型 (IotCard)
**文件**: `internal/model/iot_card.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`CostPrice``DistributePrice`)从 `float64` 改为 `int64`,数据库类型从 `decimal(10,2)` 改为 `bigint`
- 表名从 `iot_cards` 改为 `tb_iot_card`
- `ICCID` 唯一索引添加 `where:deleted_at IS NULL`
- 所有关联 ID 字段(`CarrierID``OwnerID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
- 使用 `gofmt` 格式化代码
-`Account` 模型对比,确保风格一致
---
### Task 1.2: 修复设备模型 (Device)
**文件**: `internal/model/device.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 表名从 `devices` 改为 `tb_device`
- `DeviceNo` 唯一索引添加 `where:deleted_at IS NULL`
- 所有关联 ID 字段(`OwnerID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
- 使用 `gofmt` 格式化代码
---
### Task 1.3: 修复号卡模型 (NumberCard)
**文件**: `internal/model/number_card.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`Price`)从 `float64` 改为 `int64`,数据库类型从 `decimal(10,2)` 改为 `bigint`
- 表名从 `number_cards` 改为 `tb_number_card`
- `VirtualProductCode` 唯一索引添加 `where:deleted_at IS NULL`
- 关联 ID 字段(`AgentID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
- 使用 `gofmt` 格式化代码
---
### Task 1.4: 修复运营商模型 (Carrier)
**文件**: `internal/model/carrier.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 表名从 `carriers` 改为 `tb_carrier`
- `CarrierCode` 唯一索引添加 `where:deleted_at IS NULL`
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
- 使用 `gofmt` 格式化代码
---
## 阶段 2: 套餐和订单模型修复
### Task 2.1: 修复套餐系列模型 (PackageSeries)
**文件**: `internal/model/package.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 表名从 `package_series` 改为 `tb_package_series`
- `SeriesCode` 唯一索引添加 `where:deleted_at IS NULL`
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 2.2: 修复套餐模型 (Package)
**文件**: `internal/model/package.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`Price`)从 `float64` 改为 `int64`,数据库类型从 `decimal(10,2)` 改为 `bigint`
- 表名从 `packages` 改为 `tb_package`
- `PackageCode` 唯一索引添加 `where:deleted_at IS NULL`
- 关联 ID 字段(`SeriesID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 2.3: 修复代理套餐分配模型 (AgentPackageAllocation)
**文件**: `internal/model/package.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`CostPrice``RetailPrice`)从 `float64` 改为 `int64`
- 表名从 `agent_package_allocations` 改为 `tb_agent_package_allocation`
- 关联 ID 字段(`AgentID``PackageID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 2.4: 修复套餐使用情况模型 (PackageUsage)
**文件**: `internal/model/package.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 表名从 `package_usages` 改为 `tb_package_usage`
- 关联 ID 字段(`OrderID``PackageID``IotCardID``DeviceID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 2.5: 修复设备-SIM 卡绑定模型 (DeviceSimBinding)
**文件**: `internal/model/package.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 表名从 `device_sim_bindings` 改为 `tb_device_sim_binding`
- 添加复合索引:`DeviceID``SlotPosition` 使用 `index:idx_device_slot`
- 关联 ID 字段(`IotCardID`)添加独立 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 2.6: 修复订单模型 (Order)
**文件**: `internal/model/order.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`Amount`)从 `float64` 改为 `int64`
- `CarrierOrderData``pq.StringArray` 改为 `datatypes.JSON`,添加 `import "gorm.io/datatypes"`
- 表名从 `orders` 改为 `tb_order`
- `OrderNo` 唯一索引添加 `where:deleted_at IS NULL`
- 关联 ID 字段(`IotCardID``DeviceID``NumberCardID``PackageID``UserID``AgentID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
- 检查 `datatypes.JSON` 导入是否正确
---
## 阶段 3: 分佣系统模型修复
### Task 3.1: 修复代理层级模型 (AgentHierarchy)
**文件**: `internal/model/commission.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 表名从 `agent_hierarchies` 改为 `tb_agent_hierarchy`
- `AgentID` 唯一索引添加 `where:deleted_at IS NULL`
- 关联 ID 字段(`ParentAgentID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 3.2: 修复分佣规则模型 (CommissionRule)
**文件**: `internal/model/commission.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`CommissionValue`)从 `float64` 改为 `int64`
- 表名从 `commission_rules` 改为 `tb_commission_rule`
- 关联 ID 字段(`AgentID``SeriesID``PackageID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 3.3: 修复阶梯分佣配置模型 (CommissionLadder)
**文件**: `internal/model/commission.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`CommissionValue`)从 `float64` 改为 `int64`
- 表名从 `commission_ladder` 改为 `tb_commission_ladder`
- 关联 ID 字段(`RuleID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 3.4: 修复组合分佣条件模型 (CommissionCombinedCondition)
**文件**: `internal/model/commission.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`OneTimeCommissionValue``LongTermCommissionValue`)从 `float64` 改为 `int64`
- 表名从 `commission_combined_conditions` 改为 `tb_commission_combined_condition`
- `RuleID` 唯一索引添加 `where:deleted_at IS NULL`
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 3.5: 修复分佣记录模型 (CommissionRecord)
**文件**: `internal/model/commission.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`Amount`)从 `float64` 改为 `int64`
- 表名从 `commission_records` 改为 `tb_commission_record`
- 关联 ID 字段(`AgentID``OrderID``RuleID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 3.6: 修复分佣审批模型 (CommissionApproval)
**文件**: `internal/model/commission.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 表名从 `commission_approvals` 改为 `tb_commission_approval`
- 关联 ID 字段(`CommissionRecordID``ApproverID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 3.7: 修复分佣模板模型 (CommissionTemplate)
**文件**: `internal/model/commission.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`CommissionValue`)从 `float64` 改为 `int64`
- 表名从 `commission_templates` 改为 `tb_commission_template`
- `TemplateName` 唯一索引添加 `where:deleted_at IS NULL`
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 3.8: 修复运营商结算模型 (CarrierSettlement)
**文件**: `internal/model/commission.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`SettlementAmount`)从 `float64` 改为 `int64`,数据库类型从 `decimal(18,2)` 改为 `bigint`
- 表名从 `carrier_settlements` 改为 `tb_carrier_settlement`
- `CommissionRecordID` 唯一索引添加 `where:deleted_at IS NULL`
- 关联 ID 字段(`AgentID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
## 阶段 4: 财务和系统模型修复
### Task 4.1: 修复佣金提现申请模型 (CommissionWithdrawalRequest)
**文件**: `internal/model/financial.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`Amount``Fee``ActualAmount`)从 `float64` 改为 `int64`,数据库类型从 `decimal(18,2)` 改为 `bigint`
- `AccountInfo``pq.StringArray` 改为 `datatypes.JSON`
- 表名从 `commission_withdrawal_requests` 改为 `tb_commission_withdrawal_request`
- 关联 ID 字段(`AgentID``ApprovedBy`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
- 检查 `datatypes.JSON` 导入是否正确
---
### Task 4.2: 修复佣金提现设置模型 (CommissionWithdrawalSetting)
**文件**: `internal/model/financial.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 金额字段(`MinWithdrawalAmount`)从 `float64` 改为 `int64`,数据库类型从 `decimal(10,2)` 改为 `bigint`
- 表名从 `commission_withdrawal_settings` 改为 `tb_commission_withdrawal_setting`
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 4.3: 修复收款商户设置模型 (PaymentMerchantSetting)
**文件**: `internal/model/financial.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 表名从 `payment_merchant_settings` 改为 `tb_payment_merchant_setting`
- 关联 ID 字段(`UserID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 4.4: 修复开发能力配置模型 (DevCapabilityConfig)
**文件**: `internal/model/system.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 表名从 `dev_capability_configs` 改为 `tb_dev_capability_config`
- `AppID` 唯一索引添加 `where:deleted_at IS NULL`
- 关联 ID 字段(`UserID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 4.5: 修复换卡申请模型 (CardReplacementRequest)
**文件**: `internal/model/system.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 表名从 `card_replacement_requests` 改为 `tb_card_replacement_request`
- 关联 ID 字段(`UserID``ApprovedBy`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 4.6: 修复轮询配置模型 (PollingConfig)
**文件**: `internal/model/polling.go`
**修改内容:**
- 嵌入 `gorm.Model``BaseModel`
- 移除手动定义的 `ID``CreatedAt``UpdatedAt` 字段
- 所有字段显式指定 `column` 标签
- 表名从 `polling_configs` 改为 `tb_polling_config`
- `ConfigName` 唯一索引添加 `where:deleted_at IS NULL`
- 关联 ID 字段(`CarrierID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
---
### Task 4.7: 修复流量使用记录模型 (DataUsageRecord)
**文件**: `internal/model/data_usage.go`
**修改内容:**
- **不嵌入** `gorm.Model`(简化模型,只包含 ID 和 CreatedAt
- **不嵌入** `BaseModel`(日志表不需要审计)
- 保留 `ID``CreatedAt` 字段,移除 `UpdatedAt`
- 所有字段显式指定 `column` 标签
- 表名从 `data_usage_records` 改为 `tb_data_usage_record`
- 关联 ID 字段(`IotCardID`)添加 `index` 标签
- 完善所有字段的中文注释
**验证方法:**
- 运行 `go build` 确保编译通过
- 确认模型不包含 `UpdatedAt``DeletedAt`
---
## 阶段 5: 验证和测试
### Task 5.1: 编译验证
**内容:**
- 运行 `go build ./...` 确保所有模型文件编译通过
- 运行 `gofmt -w internal/model/` 格式化所有模型文件
- 运行 `go vet ./internal/model/` 静态分析检查
**依赖**: 所有模型修复任务完成
**验证方法:**
- 无编译错误
- 无静态分析警告
---
### Task 5.2: 模型定义一致性检查
**内容:**
- 手动检查所有模型是否遵循规范(参考验证清单)
- 对比 `Account` 模型,确保风格一致
- 检查所有金额字段是否使用 `int64` 类型
- 检查所有表名是否使用 `tb_` 前缀 + 单数
- 检查所有唯一索引是否包含 `where:deleted_at IS NULL`
**依赖**: Task 5.1
**验证方法:**
- 完成验证清单(设计文档第 8 节)
---
### Task 5.3: 生成数据库迁移脚本(可选)
**内容:**
- 如果 IoT 模块尚未创建迁移脚本,跳过此任务
- 如果已有迁移脚本,生成新的迁移脚本或修改现有脚本
- 包含表重命名、字段修改、索引创建等 SQL 语句
**依赖**: Task 5.2
**验证方法:**
- 在开发环境测试迁移脚本
- 确认所有表和字段正确创建
---
### Task 5.4: 文档更新
**内容:**
- 更新 IoT SIM 管理提案(`openspec/changes/archive/2026-01-12-iot-sim-management/`)的模型定义部分(可选)
-`docs/` 目录创建模型修复总结文档(可选)
- 更新 `README.md` 添加模型规范说明(可选)
**依赖**: Task 5.2
**验证方法:**
- 文档清晰易懂,准确反映当前实现
---
### Task 5.5: 更新全局规范文档
**内容:**
- 更新 `CLAUDE.md` 中的数据库设计原则和模型规范部分
- 确保 CLAUDE.md 中的示例代码与修复后的模型风格完全一致
- 如果需要,更新 `openspec/AGENTS.md`(如果其中包含模型相关指导)
- 添加或完善以下规范内容:
- GORM 模型字段规范(显式 column 标签、类型定义、注释要求)
- 金额字段使用整数类型(分为单位)的详细说明和示例
- 表名命名规范(`tb_` 前缀 + 单数)
- BaseModel 嵌入和审计字段使用说明
- 唯一索引软删除兼容性(`where:deleted_at IS NULL`
- JSONB 字段使用 `datatypes.JSON` 类型的说明
**具体修改位置CLAUDE.md:**
1. **数据库设计原则** 部分:
- 补充完整的 GORM 模型字段定义规范
- 添加金额字段整数存储的要求和理由
- 添加字段标签完整性要求(显式 column、type、comment
2. **GORM 模型字段规范** 新增小节:
```markdown
**GORM 模型字段规范:**
- 数据库字段名必须使用下划线命名法snake_case如 `user_id`、`email_address`、`created_at`
- Go 结构体字段名必须使用驼峰命名法PascalCase如 `UserID`、`EmailAddress`、`CreatedAt`
- **所有字段必须显式指定数据库列名**:使用 `gorm:"column:字段名"` 标签明确指定数据库字段名,不依赖 GORM 的自动转换
- 示例:`UserID uint gorm:"column:user_id;not null" json:"user_id"`
- 禁止省略 `column:` 标签,即使 GORM 能自动推断字段名
- 这确保了 Go 字段名和数据库字段名的映射关系清晰可见,避免命名歧义
- 字符串字段长度必须明确定义且保持一致性:
- 短文本(名称、标题等):`VARCHAR(255)` 或 `VARCHAR(100)`
- 中等文本(描述、备注等):`VARCHAR(500)` 或 `VARCHAR(1000)`
- 长文本(内容、详情等):`TEXT` 类型
- 货币金额字段必须使用 `int64` 类型,数据库类型为 `bigint`,单位为"分"1元 = 100分
- 所有字段必须添加中文注释,说明字段用途和业务含义
```
3. **示例代码更新**
- 将现有的模型示例(如果有)更新为包含完整字段标签的版本
**依赖**: Task 5.2
**验证方法:**
- CLAUDE.md 中的规范描述与实际实现的模型完全一致
- 所有示例代码可以直接复制使用,无需修改
- 规范描述清晰、完整、无歧义
- 运行 `git diff CLAUDE.md` 检查修改内容
---
## 依赖关系图
```
阶段 1 (核心模型)
├─ Task 1.1: IotCard
├─ Task 1.2: Device
├─ Task 1.3: NumberCard
└─ Task 1.4: Carrier
阶段 2 (套餐和订单)
├─ Task 2.1: PackageSeries
├─ Task 2.2: Package (依赖 Task 2.1)
├─ Task 2.3: AgentPackageAllocation (依赖 Task 2.2)
├─ Task 2.4: PackageUsage (依赖 Task 2.2)
├─ Task 2.5: DeviceSimBinding (依赖 Task 1.1, Task 1.2)
└─ Task 2.6: Order (依赖 Task 1.1, Task 1.2, Task 1.3, Task 2.2)
阶段 3 (分佣系统)
├─ Task 3.1: AgentHierarchy
├─ Task 3.2: CommissionRule
├─ Task 3.3: CommissionLadder (依赖 Task 3.2)
├─ Task 3.4: CommissionCombinedCondition (依赖 Task 3.2)
├─ Task 3.5: CommissionRecord (依赖 Task 3.2)
├─ Task 3.6: CommissionApproval (依赖 Task 3.5)
├─ Task 3.7: CommissionTemplate
└─ Task 3.8: CarrierSettlement (依赖 Task 3.5)
阶段 4 (财务和系统)
├─ Task 4.1: CommissionWithdrawalRequest
├─ Task 4.2: CommissionWithdrawalSetting
├─ Task 4.3: PaymentMerchantSetting
├─ Task 4.4: DevCapabilityConfig
├─ Task 4.5: CardReplacementRequest
├─ Task 4.6: PollingConfig
└─ Task 4.7: DataUsageRecord (依赖 Task 1.1)
阶段 5 (验证和测试)
├─ Task 5.1: 编译验证
├─ Task 5.2: 一致性检查 (依赖 Task 5.1)
├─ Task 5.3: 生成迁移脚本 (依赖 Task 5.2, 可选)
├─ Task 5.4: 文档更新 (依赖 Task 5.2, 可选)
└─ Task 5.5: 更新全局规范文档 (依赖 Task 5.2, 必需)
```
## 估算工作量
- **阶段 1**: 约 2-3 小时4 个核心模型)
- **阶段 2**: 约 3-4 小时6 个套餐和订单模型)
- **阶段 3**: 约 4-5 小时8 个分佣系统模型)
- **阶段 4**: 约 3-4 小时7 个财务和系统模型)
- **阶段 5**: 约 2-3 小时(验证、测试和全局规范文档更新)
**总计**: 约 14-19 小时(~2-3 个工作日)
## 注意事项
1. **并行执行**: 阶段内的任务可以并行执行(除非明确依赖)
2. **增量提交**: 建议每完成一个阶段提交一次 Git commit
3. **回归测试**: 修复完成后需要运行完整的单元测试套件(如有)
4. **代码审查**: 修复完成后需要进行 Code Review确保符合项目规范