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

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

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

124
CLAUDE.md
View File

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