重构:完善 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

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确保符合项目规范