重构:完善 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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-12
|
||||
@@ -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 数据库迁移策略
|
||||
|
||||
**场景 1:IoT 模块尚未部署(推荐)**
|
||||
- 删除旧的迁移脚本(如果已创建)
|
||||
- 生成新的初始迁移脚本
|
||||
- 重新运行迁移
|
||||
|
||||
**场景 2:IoT 模块已有测试数据**
|
||||
- 保留旧的迁移脚本
|
||||
- 生成新的迁移脚本(包含表重命名、字段修改)
|
||||
- 编写数据转换脚本(金额单位转换等)
|
||||
|
||||
### 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 文档
|
||||
@@ -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 会自动处理)
|
||||
@@ -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`
|
||||
- 简化模型结构减少存储开销和查询复杂度
|
||||
@@ -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,确保符合项目规范
|
||||
Reference in New Issue
Block a user