diff --git a/docs/iot-sim-management/表结构详细说明.md b/docs/iot-sim-management/表结构详细说明.md index ff90452..bce7efc 100644 --- a/docs/iot-sim-management/表结构详细说明.md +++ b/docs/iot-sim-management/表结构详细说明.md @@ -78,7 +78,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES ('CTCC', '中国电信', 1); ``` -**GORM 模型**: `internal/iot/model/carrier.go:5` +**GORM 模型**: `internal/model/carrier.go:5` --- @@ -132,7 +132,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - `owner_type` 和 `owner_id` 组合表示所有权 - `enable_polling = false` 的卡不参与轮询检查 -**GORM 模型**: `internal/iot/model/iot_card.go:8` +**GORM 模型**: `internal/model/iot_card.go:8` --- @@ -167,7 +167,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - `sim_slots` 取值范围 1-4 - 通过 `device_sim_bindings` 表管理设备与 IoT 卡的绑定关系 -**GORM 模型**: `internal/iot/model/device.go:5` +**GORM 模型**: `internal/model/device.go:5` --- @@ -203,7 +203,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - 使用虚拟商品编码映射运营商订单 - 从上游平台下单,不涉及本地库存管理 -**GORM 模型**: `internal/iot/model/number_card.go:8` +**GORM 模型**: `internal/model/number_card.go:8` --- @@ -235,7 +235,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - 套餐系列用于分组管理套餐 - 分佣规则可以按套餐系列统一配置 -**GORM 模型**: `internal/iot/model/package.go:7` +**GORM 模型**: `internal/model/package.go:7` --- @@ -274,7 +274,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - 停机判断基于 `virtual_data_mb` (虚流量用完即停机) - `package_type = 'formal'` 表示正式套餐,`'addon'` 表示附加套餐 -**GORM 模型**: `internal/iot/model/package.go:23` +**GORM 模型**: `internal/model/package.go:23` --- @@ -307,7 +307,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - `retail_price` 是代理的零售价格 - 佣金 = `retail_price - cost_price` -**GORM 模型**: `internal/iot/model/package.go:48` +**GORM 模型**: `internal/model/package.go:48` --- @@ -341,7 +341,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - `slot_position` 取值范围 1-4 - 同一设备的同一插槽位置只能绑定一张激活状态的 IoT 卡 -**GORM 模型**: `internal/iot/model/package.go:66` +**GORM 模型**: `internal/model/package.go:66` --- @@ -385,7 +385,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - `data_usage_mb = real_data_usage_mb + virtual_data_usage_mb` - 轮询检查时更新 `last_package_check_at` -**GORM 模型**: `internal/iot/model/package.go:85` +**GORM 模型**: `internal/model/package.go:85` --- @@ -426,7 +426,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - 可以按卡状态和运营商配置不同的轮询策略 - 优先级控制多个配置的执行顺序 -**GORM 模型**: `internal/iot/model/polling.go:7` +**GORM 模型**: `internal/model/polling.go:7` --- @@ -459,7 +459,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - `carrier_sync_data` 存储从运营商 Gateway 同步的原始数据 - 用于历史查询和统计分析 -**GORM 模型**: `internal/iot/model/data_usage.go:5` +**GORM 模型**: `internal/model/data_usage.go:5` --- @@ -510,7 +510,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - `number_card_id` 必须有值 - `package_id` 为空 -**GORM 模型**: `internal/iot/model/order.go:11` +**GORM 模型**: `internal/model/order.go:11` --- @@ -544,7 +544,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - `agent_path` 存储完整的代理路径,便于查询上级链 - 一级代理的 `parent_agent_id` 为 NULL -**GORM 模型**: `internal/iot/model/commission.go:8` +**GORM 模型**: `internal/model/commission.go:8` --- @@ -585,7 +585,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - `unfreeze_mode = 'auto'`: 时间到期 OR 流量达标,满足其一即可自动解冻 - `unfreeze_mode = 'manual'`: 需要人工审批解冻 -**GORM 模型**: `internal/iot/model/commission.go:24` +**GORM 模型**: `internal/model/commission.go:24` --- @@ -619,7 +619,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES 51+ 单: 20元/单 ``` -**GORM 模型**: `internal/iot/model/commission.go:47` +**GORM 模型**: `internal/model/commission.go:47` --- @@ -652,7 +652,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - 一次性条件立即发放 - 长期条件冻结后按规则解冻 -**GORM 模型**: `internal/iot/model/commission.go:68` +**GORM 模型**: `internal/model/commission.go:68` --- @@ -697,7 +697,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES ``` - 满足时间到期 **OR** 流量达标,任一条件满足即可自动解冻 -**GORM 模型**: `internal/iot/model/commission.go:88` +**GORM 模型**: `internal/model/commission.go:88` --- @@ -731,7 +731,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - 当分佣规则的 `unfreeze_mode = 'manual'` 时,需要创建审批记录 - 审批通过后才能解冻和发放佣金 -**GORM 模型**: `internal/iot/model/commission.go:116` +**GORM 模型**: `internal/model/commission.go:116` --- @@ -760,7 +760,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - `template_data` 存储分佣规则的完整配置 - 可以基于模板快速创建新的分佣规则 -**GORM 模型**: `internal/iot/model/commission.go:137` +**GORM 模型**: `internal/model/commission.go:137` --- @@ -794,7 +794,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - 按月统计与运营商的交易情况 - 支持结算和支付状态跟踪 -**GORM 模型**: `internal/iot/model/commission.go:155` +**GORM 模型**: `internal/model/commission.go:155` --- @@ -838,7 +838,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES } ``` -**GORM 模型**: `internal/iot/model/financial.go:8` +**GORM 模型**: `internal/model/financial.go:8` --- @@ -869,7 +869,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - 每个代理可以有独立的提现规则 - `withdrawal_fee_rate` 为 0.005 表示 0.5% 手续费 -**GORM 模型**: `internal/iot/model/financial.go:31` +**GORM 模型**: `internal/model/financial.go:31` --- @@ -907,7 +907,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES } ``` -**GORM 模型**: `internal/iot/model/financial.go:51` +**GORM 模型**: `internal/model/financial.go:51` --- @@ -940,7 +940,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES - 用于配置系统的开发能力开关 - `capability_config` 存储能力的详细配置 -**GORM 模型**: `internal/iot/model/system.go:5` +**GORM 模型**: `internal/model/system.go:5` --- @@ -977,7 +977,7 @@ INSERT INTO carriers (carrier_code, carrier_name, status) VALUES 2. 管理员审核 (status=2/3) 3. 审核通过后完成换卡 (status=4) -**GORM 模型**: `internal/iot/model/system.go:25` +**GORM 模型**: `internal/model/system.go:25` --- diff --git a/docs/refactor-iot-model-location/重构总结.md b/docs/refactor-iot-model-location/重构总结.md new file mode 100644 index 0000000..62d0869 --- /dev/null +++ b/docs/refactor-iot-model-location/重构总结.md @@ -0,0 +1,252 @@ +# IoT 模型位置重构 - 总结文档 + +## 📋 变更概述 + +**变更 ID**: `refactor-iot-model-location` + +**变更类型**: 架构重构 + +**实施日期**: 2026-01-12 + +**状态**: ✅ 已完成 + +## 🎯 重构动机 + +### 问题背景 + +在实施 IoT SIM 管理模块时,IoT 相关的数据模型被放置在 `internal/iot/model/` 目录下,这与项目的其他模型(Shop、Account、Enterprise 等)不一致。其他模型都统一放在 `internal/model/` 目录下。 + +### 存在的问题 + +1. **目录结构不一致** + - IoT 模型使用 `internal/iot/model/` + - 其他模型使用 `internal/model/` + - 导致项目结构混乱,违反统一性原则 + +2. **违反项目架构约定** + - 项目采用严格的横向四层架构:Handler → Service → Store → Model + - Model 层应该是全局统一的,不应该按业务模块分散 + - 当前的 `internal/iot/model/` 违反了横向分层原则 + +3. **导入路径冗余** + - 引用 IoT 模型时需要写 `internal/iot/model` + - 引用其他模型只需要写 `internal/model` + - 增加了开发者的认知负担 + +4. **违反 Go 语言惯用设计** + - Go 推荐扁平化包结构,避免深层嵌套 + - 包应该按功能组织,不是按业务模块分散 + - `internal/iot/model` 是不必要的复杂性 + +## 🔧 实施过程 + +### 迁移文件清单 + +共迁移 11 个 IoT 模型文件: + +| 序号 | 文件名 | 说明 | +|------|--------|------| +| 1 | carrier.go | 运营商模型 | +| 2 | commission.go | 佣金模型 | +| 3 | data_usage.go | 流量使用记录模型 | +| 4 | device.go | 设备模型 | +| 5 | financial.go | 财务记录模型 | +| 6 | iot_card.go | IoT 卡模型 | +| 7 | number_card.go | 号卡模型 | +| 8 | order.go | 订单模型 | +| 9 | package.go | 套餐模型 | +| 10 | polling.go | 轮询配置模型 | +| 11 | system.go | 系统配置模型 | + +### 实施步骤 + +1. **准备工作** + - ✅ 确认 `internal/model/` 目录存在且可写 + - ✅ 确认 `internal/iot/model/` 包含 11 个模型文件 + - ✅ 搜索并确认无 Go 代码引用旧路径 + +2. **迁移执行** + - ✅ 使用 `git mv` 命令移动所有 11 个文件到 `internal/model/` + - ✅ Git 正确识别了所有文件的重命名(保留文件历史) + +3. **清理工作** + - ✅ 验证 `internal/iot/model/` 目录为空 + - ✅ 删除空的 `internal/iot/model/` 目录 + - ✅ 保留 `internal/iot/` 目录(用于未来的 Handler/Service/Store 层) + +4. **验证工作** + - ✅ 确认项目中无 `internal/iot/model` 的 Go 代码引用 + - ✅ 验证所有模型文件包名统一为 `package model` + - ✅ 运行 `go fmt ./...` 格式检查通过 + - ✅ 运行 `go vet ./...` 静态分析通过 + - ✅ 运行 `go build` 编译成功 + +5. **文档更新** + - ✅ 更新 `docs/iot-sim-management/表结构详细说明.md` 中的 25 处引用 + - ✅ 更新 OpenSpec 变更文档(tasks.md、proposal.md) + +## 📊 影响范围 + +### 代码影响 + +- **Go 代码**: 零影响 + - 经过搜索,项目中没有任何 Go 代码引用 `internal/iot/model` + - IoT 模块尚未完全实现业务逻辑层,因此无代码依赖 + +- **文档影响**: 25 处引用更新 + - `docs/iot-sim-management/表结构详细说明.md` 中的模型路径引用已全部更新 + +- **数据库影响**: 无影响 + - 模型位置变更不影响数据库表结构 + - 数据库迁移脚本不受影响 + +- **API 影响**: 无影响 + - 模型位置变更不影响 API 接口定义 + +### 架构改进 + +**重构前的目录结构:** +``` +internal/ +├── model/ # 大部分模型 +│ ├── account.go +│ ├── shop.go +│ └── ... +└── iot/ + └── model/ # IoT 模型单独存放 ❌ + ├── carrier.go + └── ... +``` + +**重构后的目录结构:** +``` +internal/ +├── model/ # 统一的模型层 ✅ +│ ├── account.go +│ ├── shop.go +│ ├── carrier.go # IoT 模型已迁移 +│ ├── iot_card.go # IoT 模型已迁移 +│ └── ... +└── iot/ # 保留用于未来的业务逻辑层 + ├── handler.go # (未来)IoT Handler 层 + ├── service.go # (未来)IoT Service 层 + └── store.go # (未来)IoT Store 层 +``` + +## ✅ 验证结果 + +### 验收标准验证 + +| 验收标准 | 状态 | 验证方法 | +|----------|------|----------| +| 所有 IoT 模型文件已迁移到 `internal/model/` | ✅ | `ls internal/model/` 包含全部 11 个文件 | +| `internal/iot/model/` 目录已删除 | ✅ | `test ! -d internal/iot/model/` 返回成功 | +| 项目可以正常编译 | ✅ | `go build ./cmd/api` 编译成功 | +| 所有测试通过 | ✅ | 项目暂无测试 | +| 项目中不存在 `internal/iot/model` 的引用 | ✅ | 已更新文档中的引用 | + +### 代码质量验证 + +| 验证项 | 状态 | 结果 | +|--------|------|------| +| Go 格式检查 | ✅ | `go fmt ./...` 通过 | +| 静态分析 | ✅ | `go vet ./...` 无错误 | +| 编译验证 | ✅ | `go build` 成功 | +| 包名一致性 | ✅ | 所有文件包名统一为 `package model` | + +### Git 历史保留 + +- ✅ 使用 `git mv` 命令移动文件,保留了完整的 Git 历史 +- ✅ 可以使用 `git log --follow ` 追踪文件的完整历史 + +## 📈 收益总结 + +### 直接收益 + +1. **架构一致性** ✅ + - 所有模型统一放在 `internal/model/` 目录 + - 符合项目横向四层架构约定 + - 提高了代码组织的可预测性 + +2. **简化导入路径** ✅ + - 所有模型统一使用 `internal/model` 导入 + - 减少了认知负担和开发成本 + +3. **符合 Go 惯用设计** ✅ + - 扁平化包结构 + - 按功能组织,不是按业务模块分散 + - 提高了 Go 代码的地道性(idiomatic Go) + +4. **提高可维护性** ✅ + - 开发者只需要在一个地方查找所有数据模型 + - 降低了新开发者的上手难度 + - 避免了"模型应该放在哪里"的困惑 + +### 长期收益 + +1. **可扩展性** + - 为未来的 IoT 业务逻辑层(Handler/Service/Store)预留了清晰的位置 + - 符合项目长期架构演进方向 + +2. **代码质量** + - 统一的架构约定有助于保持代码质量 + - 减少了技术债务的累积 + +3. **团队协作** + - 明确的架构约定减少了团队内部的讨论成本 + - 提高了代码审查的效率 + +## 📝 经验教训 + +### 做得好的地方 + +1. **提前搜索引用** + - 在迁移前搜索了所有引用,确认无代码依赖 + - 降低了重构风险 + +2. **使用 Git 工具** + - 使用 `git mv` 保留了文件历史 + - 便于未来追溯和回滚 + +3. **完整的验证流程** + - 编译、静态分析、格式检查一个都没少 + - 确保了重构的安全性 + +4. **文档同步更新** + - 及时更新了相关文档中的引用 + - 避免了文档与代码不一致的问题 + +### 可以改进的地方 + +1. **更早发现问题** + - 如果在 IoT 模块设计初期就遵循统一的架构约定,就不需要这次重构 + - 建议:在代码审查时严格检查架构约定 + +2. **自动化检查** + - 可以添加 CI 检查,防止模型被放在错误的位置 + - 建议:添加 linter 规则检查目录结构 + +## 🔗 相关文档 + +- **变更提案**: `openspec/changes/refactor-iot-model-location/proposal.md` +- **设计文档**: `openspec/changes/refactor-iot-model-location/design.md` +- **任务清单**: `openspec/changes/refactor-iot-model-location/tasks.md` +- **项目架构规范**: `openspec/project.md` +- **IoT SIM 管理规格**: `openspec/specs/iot-card/spec.md` + +## 🎉 总结 + +本次重构成功地将 11 个 IoT 模型文件从 `internal/iot/model/` 迁移到 `internal/model/`,实现了项目架构的统一性和一致性。 + +**重构特点:** +- ✅ 零代码影响(无 Go 代码依赖) +- ✅ 完整的验证流程(编译、静态分析、格式检查) +- ✅ 保留了完整的 Git 历史 +- ✅ 同步更新了相关文档 + +**架构改进:** +- ✅ 符合横向四层架构约定 +- ✅ 符合 Go 语言惯用设计 +- ✅ 提高了代码可维护性和可扩展性 + +本次重构为项目的长期健康发展奠定了坚实的基础。 diff --git a/internal/bootstrap/dependencies.go b/internal/bootstrap/dependencies.go index 7b60b13..26b878a 100644 --- a/internal/bootstrap/dependencies.go +++ b/internal/bootstrap/dependencies.go @@ -11,9 +11,9 @@ import ( // Dependencies 封装所有基础依赖 // 这些是应用启动时初始化的核心组件 type Dependencies struct { - DB *gorm.DB // PostgreSQL 数据库连接 - Redis *redis.Client // Redis 客户端 - Logger *zap.Logger // 应用日志器 - JWTManager *auth.JWTManager // JWT 管理器 - VerificationService *verification.Service // 验证码服务 + DB *gorm.DB // PostgreSQL 数据库连接 + Redis *redis.Client // Redis 客户端 + Logger *zap.Logger // 应用日志器 + JWTManager *auth.JWTManager // JWT 管理器 + VerificationService *verification.Service // 验证码服务 } diff --git a/internal/handler/app/personal_customer.go b/internal/handler/app/personal_customer.go index bdf5db8..8b79579 100644 --- a/internal/handler/app/personal_customer.go +++ b/internal/handler/app/personal_customer.go @@ -57,7 +57,7 @@ type LoginRequest struct { // LoginResponse 登录响应 type LoginResponse struct { - Token string `json:"token"` // 访问令牌 + Token string `json:"token"` // 访问令牌 Customer *PersonalCustomerDTO `json:"customer"` // 客户信息 } diff --git a/internal/iot/model/carrier.go b/internal/model/carrier.go similarity index 100% rename from internal/iot/model/carrier.go rename to internal/model/carrier.go diff --git a/internal/iot/model/commission.go b/internal/model/commission.go similarity index 100% rename from internal/iot/model/commission.go rename to internal/model/commission.go diff --git a/internal/iot/model/data_usage.go b/internal/model/data_usage.go similarity index 100% rename from internal/iot/model/data_usage.go rename to internal/model/data_usage.go diff --git a/internal/iot/model/device.go b/internal/model/device.go similarity index 100% rename from internal/iot/model/device.go rename to internal/model/device.go diff --git a/internal/iot/model/financial.go b/internal/model/financial.go similarity index 100% rename from internal/iot/model/financial.go rename to internal/model/financial.go diff --git a/internal/iot/model/iot_card.go b/internal/model/iot_card.go similarity index 100% rename from internal/iot/model/iot_card.go rename to internal/model/iot_card.go diff --git a/internal/iot/model/number_card.go b/internal/model/number_card.go similarity index 100% rename from internal/iot/model/number_card.go rename to internal/model/number_card.go diff --git a/internal/iot/model/order.go b/internal/model/order.go similarity index 100% rename from internal/iot/model/order.go rename to internal/model/order.go diff --git a/internal/iot/model/package.go b/internal/model/package.go similarity index 100% rename from internal/iot/model/package.go rename to internal/model/package.go diff --git a/internal/model/permission_dto.go b/internal/model/permission_dto.go index e0fa2a9..46117fd 100644 --- a/internal/model/permission_dto.go +++ b/internal/model/permission_dto.go @@ -75,4 +75,4 @@ type PermissionTreeNode struct { URL string `json:"url,omitempty" description:"请求路径"` Sort int `json:"sort" description:"排序值"` Children []*PermissionTreeNode `json:"children,omitempty" description:"子权限列表"` -} \ No newline at end of file +} diff --git a/internal/iot/model/polling.go b/internal/model/polling.go similarity index 100% rename from internal/iot/model/polling.go rename to internal/model/polling.go diff --git a/internal/model/role_dto.go b/internal/model/role_dto.go index 55e10b2..66c75e4 100644 --- a/internal/model/role_dto.go +++ b/internal/model/role_dto.go @@ -65,4 +65,4 @@ type AssignPermissionsParams struct { type RemovePermissionParams struct { RoleID uint `path:"role_id" required:"true" description:"角色ID"` PermID uint `path:"perm_id" required:"true" description:"权限ID"` -} \ No newline at end of file +} diff --git a/internal/iot/model/system.go b/internal/model/system.go similarity index 100% rename from internal/iot/model/system.go rename to internal/model/system.go diff --git a/internal/service/personal_customer/service.go b/internal/service/personal_customer/service.go index 06cf2d3..b464406 100644 --- a/internal/service/personal_customer/service.go +++ b/internal/service/personal_customer/service.go @@ -68,9 +68,9 @@ func (s *Service) LoginByPhone(ctx context.Context, phone string, code string) ( // 客户不存在,创建新客户 // 注意:临时实现,使用空的微信信息(正式应该先微信授权) customer = &model.PersonalCustomer{ - WxOpenID: "", // 临时为空,后续需绑定微信 - WxUnionID: "", // 临时为空,后续需绑定微信 - Status: 1, // 默认启用 + WxOpenID: "", // 临时为空,后续需绑定微信 + WxUnionID: "", // 临时为空,后续需绑定微信 + Status: 1, // 默认启用 } if err := s.store.Create(ctx, customer); err != nil { s.logger.Error("创建个人客户失败", diff --git a/internal/store/postgres/account_store.go b/internal/store/postgres/account_store.go index 19d61ff..4cad0ee 100644 --- a/internal/store/postgres/account_store.go +++ b/internal/store/postgres/account_store.go @@ -129,4 +129,3 @@ func (s *AccountStore) List(ctx context.Context, opts *store.QueryOptions, filte return accounts, total, nil } - diff --git a/openspec/changes/archive/2026-01-12-refactor-iot-model-location/.openspec.yaml b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/.openspec.yaml new file mode 100644 index 0000000..e7e51fb --- /dev/null +++ b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-01-12 diff --git a/openspec/changes/archive/2026-01-12-refactor-iot-model-location/README.md b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/README.md new file mode 100644 index 0000000..b6d661e --- /dev/null +++ b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/README.md @@ -0,0 +1,3 @@ +# refactor-iot-model-location + +将 IoT 模型从 internal/iot/model/ 迁移到统一的 internal/model/ 目录 diff --git a/openspec/changes/archive/2026-01-12-refactor-iot-model-location/design.md b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/design.md new file mode 100644 index 0000000..74175b4 --- /dev/null +++ b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/design.md @@ -0,0 +1,230 @@ +# 设计文档:IoT 模型位置重构 + +## 问题陈述 (Problem Statement) + +当前 IoT 模块的数据模型被放置在 `internal/iot/model/` 目录下,这与项目的架构约定不一致。根据 `openspec/project.md` 的架构规范,项目采用严格的四层架构: + +``` +Handler 层 → Service 层 → Store 层 → Model 层 +``` + +其中 **Model 层** 应该是全局统一的,所有模型都应该放在 `internal/model/` 目录下。当前的 IoT 模型位置违反了这一约定。 + +## 当前架构问题分析 + +### 1. 目录结构不一致 + +**当前状态:** +``` +internal/ +├── model/ # 大部分模型在这里 +│ ├── account.go +│ ├── shop.go +│ ├── enterprise.go +│ ├── personal_customer.go +│ ├── role.go +│ ├── permission.go +│ └── ... +├── iot/ +│ └── model/ # IoT 模型单独在这里 ❌ +│ ├── carrier.go +│ ├── commission.go +│ ├── iot_card.go +│ └── ... +``` + +**存在的问题:** +- 模型分散在两个位置,违反了单一数据模型层的设计原则 +- 导入路径不一致:`internal/model` vs `internal/iot/model` +- 新开发者容易困惑,不知道应该把新模型放在哪里 + +### 2. 违反项目架构约定 + +根据项目架构规范,四层架构应该是 **横向分层**,而不是 **纵向按模块分层**: + +**正确的架构(横向分层):** +``` +internal/ +├── handler/ # 所有 Handler +│ ├── user_handler.go +│ ├── shop_handler.go +│ └── iot_handler.go # IoT 的 Handler +├── service/ # 所有 Service +│ ├── user_service.go +│ ├── shop_service.go +│ └── iot_service.go # IoT 的 Service +├── store/ # 所有 Store +│ ├── user_store.go +│ ├── shop_store.go +│ └── iot_store.go # IoT 的 Store +└── model/ # 所有 Model ✅ + ├── user.go + ├── shop.go + └── iot_card.go # IoT 的 Model +``` + +**错误的架构(纵向按模块分层):** +``` +internal/ +├── user/ # 用户模块(纵向) +│ ├── handler.go +│ ├── service.go +│ ├── store.go +│ └── model.go +├── shop/ # 店铺模块(纵向) +│ ├── handler.go +│ ├── service.go +│ ├── store.go +│ └── model.go +└── iot/ # IoT 模块(纵向)❌ + ├── handler.go + ├── service.go + ├── store.go + └── model/ # 违反横向分层原则 + └── iot_card.go +``` + +### 3. Go 语言惯用设计原则 + +根据 Go 语言的包组织最佳实践: + +- **包应该扁平化**:避免深层嵌套(最多 2-3 层) +- **包应该按功能组织**:`model` 包应该包含所有数据模型,不是按业务模块分散 +- **包名应该描述功能**:`model` 表示数据模型,而不是 `iot/model`(冗余) + +当前 `internal/iot/model` 的包名虽然是 `package model`,但实际导入路径是 `internal/iot/model`,这是不必要的复杂性。 + +## 设计决策 (Design Decision) + +**决策:** 将所有 IoT 模型迁移到 `internal/model/` 目录,与项目的其他模型保持一致。 + +### 为什么选择统一的 Model 层? + +1. **符合项目架构约定**:遵循严格的横向四层架构(Handler → Service → Store → Model) +2. **简化导入路径**:所有模型统一使用 `internal/model` 导入 +3. **提高代码可维护性**:开发者只需要在一个地方查找所有数据模型 +4. **符合 Go 语言惯用设计**:扁平化包结构,按功能组织 +5. **降低认知负担**:新开发者不需要猜测模型应该放在哪里 + +### 为什么不保留 `internal/iot/model`? + +**反驳方案 1:按业务模块组织(纵向分层)** + +``` +internal/ +├── iot/ +│ ├── model/ # IoT 模型 +│ ├── service/ # IoT 服务 +│ └── store/ # IoT 存储 +``` + +**缺点:** +- 违反项目架构约定(横向分层) +- 导致模型分散,难以统一管理 +- 跨模块调用时需要引用不同的 model 包(如 `iot/model` 和 `internal/model`) +- 不符合当前项目已有的架构实践 + +**反驳方案 2:IoT 模型使用子包(`internal/model/iot`)** + +``` +internal/ +├── model/ +│ ├── user.go +│ ├── shop.go +│ └── iot/ # IoT 子包 +│ └── iot_card.go +``` + +**缺点:** +- 引入不必要的层级嵌套 +- 导入路径变为 `internal/model/iot`,不符合项目惯例 +- 其他模型(User、Shop)没有子包,为什么 IoT 要特殊对待? +- 增加认知负担:需要记住哪些模型有子包,哪些没有 + +### 最终方案:扁平化模型目录 + +``` +internal/ +└── model/ + ├── account.go + ├── shop.go + ├── enterprise.go + ├── personal_customer.go + ├── role.go + ├── permission.go + ├── carrier.go # IoT 相关模型 + ├── commission.go # IoT 相关模型 + ├── data_usage.go # IoT 相关模型 + ├── device.go # IoT 相关模型 + ├── financial.go # IoT 相关模型 + ├── iot_card.go # IoT 相关模型 + ├── number_card.go # IoT 相关模型 + ├── order.go # IoT 相关模型 + ├── package.go # IoT 相关模型 + ├── polling.go # IoT 相关模型 + └── system.go # IoT 相关模型 +``` + +**优点:** +- 符合项目架构约定(横向分层) +- 所有模型统一管理,易于查找和维护 +- 导入路径统一(`internal/model`) +- 符合 Go 语言扁平化包结构的最佳实践 +- 与项目现有架构保持一致 + +## 实施风险评估 + +### 风险等级:低 + +**理由:** +1. **无代码引用**:经过搜索,当前项目中没有任何代码引用 `internal/iot/model`(因为 IoT 模块尚未完全实现) +2. **纯文件移动**:只需要移动文件,不需要修改文件内容(包名已经是 `package model`) +3. **无数据库影响**:模型位置变更不影响数据库表结构或迁移脚本 +4. **无 API 影响**:模型位置变更不影响 API 接口定义 + +### 潜在风险 + +**风险 1:并发开发冲突** +- **描述**:如果在重构期间有其他开发者新增了对 `internal/iot/model` 的引用 +- **缓解措施**:在重构前和重构后都执行全局搜索,确保无遗漏引用 +- **恢复方案**:如果发现遗漏引用,只需要更新导入路径即可(从 `internal/iot/model` 改为 `internal/model`) + +**风险 2:Git 历史追踪** +- **描述**:Git 的 `git log` 或 `git blame` 可能无法自动追踪文件移动历史 +- **缓解措施**:使用 `git mv` 命令移动文件(或确保提交信息清晰说明文件移动) +- **影响评估**:低(可以通过 `git log --follow` 追踪文件历史) + +## 验收标准 (Acceptance Criteria) + +1. **目录结构正确**: + - [ ] `internal/iot/model/` 目录不存在 + - [ ] 所有 11 个 IoT 模型文件都在 `internal/model/` 目录下 + +2. **包名一致性**: + - [ ] 所有模型文件的包名都是 `package model` + +3. **无遗漏引用**: + - [ ] 项目中不存在 `internal/iot/model` 的引用 + +4. **编译成功**: + - [ ] 执行 `go build` 成功,无错误 + +5. **测试通过**: + - [ ] 执行 `go test ./...` 成功(如果存在测试) + +## 后续改进建议 + +虽然本次重构只涉及模型位置,但建议后续也考虑其他架构一致性改进: + +1. **完善 IoT Handler 层**:创建 `internal/handler/iot/` 或 `internal/handler/iot_handler.go` +2. **完善 IoT Service 层**:创建 `internal/service/iot/` 或 `internal/service/iot_service.go` +3. **完善 IoT Store 层**:创建 `internal/store/postgres/iot_store.go` +4. **删除空的 `internal/iot/` 目录**(如果迁移后为空) + +## 参考资料 + +- **项目架构规范**:`openspec/project.md` +- **Go 包组织最佳实践**:[Effective Go - Package names](https://go.dev/doc/effective_go#package-names) +- **Go Code Review Comments**:[Go Wiki - Package names](https://go.dev/wiki/CodeReviewComments#package-names) +- **现有模型目录**:`internal/model/` +- **IoT 规格文档**:`openspec/specs/iot-card/spec.md` diff --git a/openspec/changes/archive/2026-01-12-refactor-iot-model-location/proposal.md b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/proposal.md new file mode 100644 index 0000000..165828e --- /dev/null +++ b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/proposal.md @@ -0,0 +1,107 @@ +# 提案:将 IoT 模型迁移到统一的 internal/model/ 目录 + +## 动机 (Motivation) + +当前 IoT 模块的数据模型被放置在 `internal/iot/model/` 目录下,这与项目的其他模型(如 Shop、Account、Enterprise 等)不一致。其他模型都统一放在 `internal/model/` 目录下。 + +**存在的问题:** + +1. **目录结构不一致**:IoT 模型使用 `internal/iot/model/`,而其他模型使用 `internal/model/`,导致项目结构混乱 +2. **导入路径冗余**:引用 IoT 模型时需要写 `internal/iot/model`,而其他模型只需要 `internal/model` +3. **违反项目约定**:根据 `openspec/project.md` 的架构规范,所有模型应该统一在 Model 层管理 +4. **可维护性下降**:新开发者容易困惑,不知道应该把模型放在哪里 + +**当前 IoT 模型文件清单(11 个文件):** + +- `internal/iot/model/carrier.go` - 运营商模型 +- `internal/iot/model/commission.go` - 佣金模型 +- `internal/iot/model/data_usage.go` - 流量使用记录模型 +- `internal/iot/model/device.go` - 设备模型 +- `internal/iot/model/financial.go` - 财务记录模型 +- `internal/iot/model/iot_card.go` - IoT 卡模型 +- `internal/iot/model/number_card.go` - 号卡模型 +- `internal/iot/model/order.go` - 订单模型 +- `internal/iot/model/package.go` - 套餐模型 +- `internal/iot/model/polling.go` - 轮询配置模型 +- `internal/iot/model/system.go` - 系统配置模型 + +## 提案内容 (Proposed Change) + +将所有 IoT 相关模型从 `internal/iot/model/` 迁移到 `internal/model/`,与项目的其他模型保持一致。 + +**迁移方案:** + +1. 将 `internal/iot/model/` 目录下的所有 `.go` 文件移动到 `internal/model/` +2. 所有文件的包名保持为 `package model`(无需修改) +3. 删除空的 `internal/iot/model/` 目录 +4. 检查是否有其他代码引用了 `internal/iot/model`,如果有则更新导入路径(当前搜索未发现引用) +5. 验证所有模型文件的包名一致性,确保都是 `package model` + +**目标结构:** + +``` +internal/ +├── model/ # 统一的模型目录 +│ ├── account.go +│ ├── shop.go +│ ├── enterprise.go +│ ├── personal_customer.go +│ ├── role.go +│ ├── permission.go +│ ├── carrier.go # ← 从 internal/iot/model/ 迁移 +│ ├── commission.go # ← 从 internal/iot/model/ 迁移 +│ ├── data_usage.go # ← 从 internal/iot/model/ 迁移 +│ ├── device.go # ← 从 internal/iot/model/ 迁移 +│ ├── financial.go # ← 从 internal/iot/model/ 迁移 +│ ├── iot_card.go # ← 从 internal/iot/model/ 迁移 +│ ├── number_card.go # ← 从 internal/iot/model/ 迁移 +│ ├── order.go # ← 从 internal/iot/model/ 迁移 +│ ├── package.go # ← 从 internal/iot/model/ 迁移 +│ ├── polling.go # ← 从 internal/iot/model/ 迁移 +│ ├── system.go # ← 从 internal/iot/model/ 迁移 +│ ├── ... +├── iot/ # IoT 业务逻辑目录 +│ ├── handler.go # (未来)IoT Handler 层 +│ ├── service.go # (未来)IoT Service 层 +│ └── store.go # (未来)IoT Store 层 +└── ... +``` + +## 影响范围 (Impact) + +**好消息:** 经过代码搜索,目前 **没有发现任何代码引用 `internal/iot/model`**,因此这是一个零影响的重构。 + +**潜在风险:** + +- 如果未来有代码在提案实施期间新增了对 `internal/iot/model` 的引用,需要同步更新 +- 数据库迁移脚本不受影响(模型位置变更不影响表结构) + +**不影响的部分:** + +- 数据库表结构 +- API 接口 +- 业务逻辑 +- 测试代码(如果存在) + +## 实施计划 (Implementation Plan) + +1. **移动文件**:将 11 个模型文件从 `internal/iot/model/` 移动到 `internal/model/` +2. **删除空目录**:删除 `internal/iot/model/` 目录 +3. **验证包名**:确认所有模型文件的包名都是 `package model` +4. **搜索引用**:再次搜索项目中是否有 `internal/iot/model` 的引用,如有则更新 +5. **运行测试**:执行 `go test ./...` 确保没有破坏性变更 +6. **构建验证**:执行 `go build` 确保项目可以正常编译 + +## 验收标准 (Acceptance Criteria) + +- [x] 所有 IoT 模型文件已迁移到 `internal/model/` 目录 +- [x] `internal/iot/model/` 目录已删除 +- [x] 项目可以正常编译(`go build` 成功) +- [x] 所有测试通过(如果存在测试) +- [x] 项目中不存在 `internal/iot/model` 的引用(已更新文档中的引用) + +## 参考资料 (References) + +- 项目架构规范:`openspec/project.md` +- 现有模型目录:`internal/model/` +- IoT 相关规格:`openspec/specs/iot-card/spec.md` diff --git a/openspec/changes/archive/2026-01-12-refactor-iot-model-location/specs/model-organization/spec.md b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/specs/model-organization/spec.md new file mode 100644 index 0000000..400842e --- /dev/null +++ b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/specs/model-organization/spec.md @@ -0,0 +1,171 @@ +# Model Organization + +## Purpose + +定义项目中数据模型(Model)的组织规范,确保所有模型遵循统一的目录结构和命名约定。 + +本规范支持: +- 统一的模型目录结构(`internal/model/`) +- 横向分层架构(Handler → Service → Store → Model) +- 扁平化包组织(符合 Go 语言最佳实践) +- 跨模块的模型共享和引用 + +## ADDED Requirements + +### Requirement: 统一的模型目录 + +系统 SHALL 将所有数据模型(GORM 模型、DTO)统一放置在 `internal/model/` 目录下,不按业务模块分散。 + +**核心原则:** +- **横向分层**:模型层(Model)是全局统一的,不按业务模块纵向分割 +- **扁平化组织**:所有模型文件直接放在 `internal/model/` 目录下,不创建子目录(除非文件数量超过 50 个) +- **统一导入路径**:所有代码引用模型时统一使用 `internal/model` 导入路径 +- **统一包名**:所有模型文件的包名统一为 `package model` + +**目录结构:** + +``` +internal/ +├── model/ # 所有数据模型统一在这里 +│ ├── account.go # 账户模型 +│ ├── shop.go # 店铺模型 +│ ├── enterprise.go # 企业模型 +│ ├── personal_customer.go # 个人客户模型 +│ ├── role.go # 角色模型 +│ ├── permission.go # 权限模型 +│ ├── carrier.go # 运营商模型(IoT 相关) +│ ├── iot_card.go # IoT 卡模型(IoT 相关) +│ ├── device.go # 设备模型(IoT 相关) +│ ├── order.go # 订单模型(IoT 相关) +│ ├── package.go # 套餐模型(IoT 相关) +│ └── ... # 其他模型 +├── handler/ # Handler 层(按功能分包) +├── service/ # Service 层(按功能分包) +└── store/ # Store 层(按功能分包) +``` + +#### Scenario: 创建新的 IoT 相关模型 + +- **WHEN** 开发者需要创建新的 IoT 相关数据模型(如 `SIMCard`) +- **THEN** 系统要求开发者在 `internal/model/sim_card.go` 创建模型,而不是在 `internal/iot/model/sim_card.go` + +#### Scenario: 引用 IoT 模型 + +- **WHEN** Service 层或 Store 层需要引用 IoT 卡模型 +- **THEN** 系统使用统一的导入路径 `internal/model`,而不是 `internal/iot/model` + +#### Scenario: 跨模块引用模型 + +- **WHEN** 用户模块(User)需要引用 IoT 卡模型(IotCard)进行关联查询 +- **THEN** 系统允许直接从 `internal/model` 导入 `IotCard`,因为所有模型都在同一个包中 + +--- + +### Requirement: 模型文件命名规范 + +系统 SHALL 遵循统一的模型文件命名规范,确保文件名清晰、一致、易于查找。 + +**命名规则:** +- 文件名使用小写下划线命名法(snake_case):`user_account.go`、`iot_card.go`、`shop_order.go` +- 文件名应该清晰描述模型的业务含义,不使用缩写(除非是广泛认可的缩写如 `iot`、`http`) +- 一个文件可以包含一个或多个相关模型(如 `iot_card.go` 可以包含 `IotCard` 和 `IotCardDTO`) +- DTO 模型应该与主模型放在同一个文件中(如 `IotCard` 和 `IotCardDTO` 都在 `iot_card.go` 中) + +**文件内容结构:** + +```go +package model + +// IotCard IoT 卡模型(GORM 模型) +type IotCard struct { + ID uint `gorm:"column:id;primaryKey" json:"id"` + ICCID string `gorm:"column:iccid;uniqueIndex" json:"iccid"` + // ... 其他字段 +} + +// TableName 指定表名 +func (IotCard) TableName() string { + return "iot_cards" +} + +// IotCardDTO IoT 卡 DTO(数据传输对象) +type IotCardDTO struct { + ID uint `json:"id"` + ICCID string `json:"iccid"` + // ... 其他字段 +} +``` + +#### Scenario: 创建新模型时命名文件 + +- **WHEN** 开发者创建新的数据模型 `DeviceBinding` +- **THEN** 系统要求文件名为 `device_binding.go`,而不是 `DeviceBinding.go` 或 `deviceBinding.go` + +#### Scenario: DTO 模型放置位置 + +- **WHEN** 开发者为 `IotCard` 模型创建 DTO(`IotCardDTO`) +- **THEN** 系统要求 DTO 定义在同一个文件 `iot_card.go` 中,而不是创建新文件 `iot_card_dto.go` + +--- + +### Requirement: 禁止按业务模块分割模型 + +系统 SHALL 禁止按业务模块(如 `iot`、`user`、`order`)创建独立的模型子目录,所有模型必须扁平化组织在 `internal/model/` 下。 + +**禁止的目录结构:** + +``` +internal/ +├── model/ +│ ├── user/ # ❌ 禁止按业务模块分子目录 +│ │ └── user.go +│ ├── iot/ # ❌ 禁止按业务模块分子目录 +│ │ └── iot_card.go +│ └── order/ # ❌ 禁止按业务模块分子目录 +│ └── order.go +``` + +``` +internal/ +├── user/ # ❌ 禁止按业务模块纵向分层 +│ ├── model.go +│ ├── handler.go +│ ├── service.go +│ └── store.go +├── iot/ # ❌ 禁止按业务模块纵向分层 +│ ├── model/ +│ │ └── iot_card.go +│ ├── handler.go +│ ├── service.go +│ └── store.go +``` + +**正确的目录结构(扁平化):** + +``` +internal/ +├── model/ # ✅ 所有模型扁平化在一个目录 +│ ├── user.go +│ ├── iot_card.go +│ └── order.go +├── handler/ # ✅ 横向分层 +├── service/ # ✅ 横向分层 +└── store/ # ✅ 横向分层 +``` + +**设计理由:** +1. **符合横向分层架构**:Handler → Service → Store → Model 是全局分层,不是模块分层 +2. **简化导入路径**:所有模型统一使用 `internal/model`,不需要记忆不同模块的路径 +3. **便于跨模块引用**:用户模块可以直接引用 IoT 模型,不需要跨包引用 +4. **符合 Go 语言惯用设计**:包应该按功能组织(model),而不是按业务模块组织(user/model, iot/model) + +#### Scenario: 代码审查拒绝纵向分层 + +- **WHEN** 开发者提交 PR,创建了 `internal/iot/model/` 目录 +- **THEN** 系统要求代码审查拒绝该 PR,并要求开发者将模型移动到 `internal/model/` + +#### Scenario: 重构现有纵向分层的模型 + +- **WHEN** 项目中存在 `internal/iot/model/` 目录 +- **THEN** 系统要求重构,将所有模型迁移到 `internal/model/`,删除 `internal/iot/model/` 目录 + diff --git a/openspec/changes/archive/2026-01-12-refactor-iot-model-location/tasks.md b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/tasks.md new file mode 100644 index 0000000..6134c36 --- /dev/null +++ b/openspec/changes/archive/2026-01-12-refactor-iot-model-location/tasks.md @@ -0,0 +1,157 @@ +# 任务列表:IoT 模型位置重构 + +## 准备工作 + +- [x] **TASK-001**: 确认 `internal/model/` 目录已存在且可写 + - 验证方法:`ls -la internal/model/` + - 预期输出:目录存在,包含现有模型文件 + - **结果**: ✅ 已确认 + +- [x] **TASK-002**: 确认 `internal/iot/model/` 目录包含 11 个模型文件 + - 验证方法:`ls internal/iot/model/ | wc -l` + - 预期输出:11 + - **结果**: ✅ 已确认 11 个文件 + +- [x] **TASK-003**: 搜索项目中所有引用 `internal/iot/model` 的代码 + - 验证方法:`rg "internal/iot/model" internal/ --type go` + - 预期输出:无结果(当前已验证) + - **结果**: ✅ 无 Go 代码引用 + +## 迁移执行 + +- [x] **TASK-004**: 移动运营商模型文件 + - 命令:`git mv internal/iot/model/carrier.go internal/model/` + - 验证:`test -f internal/model/carrier.go && echo "成功"` + - **结果**: ✅ 已完成 + +- [x] **TASK-005**: 移动佣金模型文件 + - 命令:`git mv internal/iot/model/commission.go internal/model/` + - 验证:`test -f internal/model/commission.go && echo "成功"` + - **结果**: ✅ 已完成 + +- [x] **TASK-006**: 移动流量使用记录模型文件 + - 命令:`git mv internal/iot/model/data_usage.go internal/model/` + - 验证:`test -f internal/model/data_usage.go && echo "成功"` + - **结果**: ✅ 已完成 + +- [x] **TASK-007**: 移动设备模型文件 + - 命令:`git mv internal/iot/model/device.go internal/model/` + - 验证:`test -f internal/model/device.go && echo "成功"` + - **结果**: ✅ 已完成 + +- [x] **TASK-008**: 移动财务记录模型文件 + - 命令:`git mv internal/iot/model/financial.go internal/model/` + - 验证:`test -f internal/model/financial.go && echo "成功"` + - **结果**: ✅ 已完成 + +- [x] **TASK-009**: 移动 IoT 卡模型文件 + - 命令:`git mv internal/iot/model/iot_card.go internal/model/` + - 验证:`test -f internal/model/iot_card.go && echo "成功"` + - **结果**: ✅ 已完成 + +- [x] **TASK-010**: 移动号卡模型文件 + - 命令:`git mv internal/iot/model/number_card.go internal/model/` + - 验证:`test -f internal/model/number_card.go && echo "成功"` + - **结果**: ✅ 已完成 + +- [x] **TASK-011**: 移动订单模型文件 + - 命令:`git mv internal/iot/model/order.go internal/model/` + - 验证:`test -f internal/model/order.go && echo "成功"` + - **结果**: ✅ 已完成 + +- [x] **TASK-012**: 移动套餐模型文件 + - 命令:`git mv internal/iot/model/package.go internal/model/` + - 验证:`test -f internal/model/package.go && echo "成功"` + - **结果**: ✅ 已完成 + +- [x] **TASK-013**: 移动轮询配置模型文件 + - 命令:`git mv internal/iot/model/polling.go internal/model/` + - 验证:`test -f internal/model/polling.go && echo "成功"` + - **结果**: ✅ 已完成 + +- [x] **TASK-014**: 移动系统配置模型文件 + - 命令:`git mv internal/iot/model/system.go internal/model/` + - 验证:`test -f internal/model/system.go && echo "成功"` + - **结果**: ✅ 已完成 + +## 清理工作 + +- [x] **TASK-015**: 验证 `internal/iot/model/` 目录已空 + - 验证方法:`ls internal/iot/model/` + - 预期输出:无文件 + - **结果**: ✅ 目录为空 + +- [x] **TASK-016**: 删除空的 `internal/iot/model/` 目录 + - 命令:`rmdir internal/iot/model/` + - 验证:`test ! -d internal/iot/model/ && echo "目录已删除"` + - **结果**: ✅ 目录已删除 + +- [x] **TASK-017**: 检查 `internal/iot/` 目录是否还包含其他内容 + - 验证方法:`ls -la internal/iot/` + - 如果为空,考虑删除 `internal/iot/` 目录 + - **结果**: ✅ 目录为空,但保留用于未来的 IoT Handler/Service/Store 层 + +## 验证工作 + +- [x] **TASK-018**: 再次搜索项目中所有引用 `internal/iot/model` 的代码 + - 验证方法:`rg "internal/iot/model" . --type go` + - 预期输出:无结果 + - **结果**: ✅ 无 Go 代码引用 + +- [x] **TASK-019**: 验证所有迁移的模型文件包名正确 + - 验证方法:`grep "^package " internal/model/{carrier,commission,data_usage,device,financial,iot_card,number_card,order,package,polling,system}.go` + - 预期输出:所有 11 个文件的包名都是 `package model` + - **结果**: ✅ 所有文件包名统一为 `package model` + +- [x] **TASK-020**: 运行 Go 代码格式检查 + - 命令:`go fmt ./...` + - 预期输出:无需格式化(或格式化成功) + - **结果**: ✅ 格式检查通过 + +- [x] **TASK-021**: 运行 Go 代码静态分析 + - 命令:`go vet ./...` + - 预期输出:无错误 + - **结果**: ✅ 静态分析通过 + +- [x] **TASK-022**: 编译项目 + - 命令:`go build -o /tmp/junhong_cmp_fiber ./cmd/api` + - 预期输出:编译成功,无错误 + - **结果**: ✅ 编译成功 + +- [x] **TASK-023**: 运行项目测试(如果存在) + - 命令:`go test ./... -v` + - 预期输出:所有测试通过(或无测试) + - **结果**: ✅ 跳过(项目暂无测试) + +## 文档更新 + +- [x] **TASK-024**: 检查是否需要更新项目文档 + - 验证方法:`rg "internal/iot/model" docs/ README.md CLAUDE.md 2>/dev/null` + - 预期输出:无结果(或更新找到的文档) + - **结果**: ✅ 已更新 `docs/iot-sim-management/表结构详细说明.md` 中的 25 处引用 + +- [x] **TASK-025**: 更新本次重构的总结文档 + - 创建 `docs/refactor-iot-model-location/` 目录 + - 编写重构总结(包含动机、影响、验证结果) + - **结果**: ✅ 已完成,文档位于 `docs/refactor-iot-model-location/重构总结.md` + +## 依赖关系 + +**并行任务:** +- TASK-004 到 TASK-014 可以并行执行(移动文件操作互不依赖) + +**串行依赖:** +- TASK-001, TASK-002, TASK-003 必须在迁移前完成(准备工作) +- TASK-004 到 TASK-014 必须在 TASK-015 之前完成(移动完成后才能验证) +- TASK-015 必须在 TASK-016 之前完成(验证空目录后才能删除) +- TASK-018 到 TASK-023 必须在 TASK-016 之后完成(清理完成后才能验证) + +## 预计时间 + +- 准备工作:5 分钟 +- 迁移执行:5 分钟(自动化脚本) +- 清理工作:2 分钟 +- 验证工作:5 分钟 +- 文档更新:10 分钟 + +**总计:约 30 分钟** diff --git a/openspec/specs/model-organization/spec.md b/openspec/specs/model-organization/spec.md new file mode 100644 index 0000000..164253f --- /dev/null +++ b/openspec/specs/model-organization/spec.md @@ -0,0 +1,162 @@ +# model-organization Specification + +## Purpose +TBD - created by archiving change refactor-iot-model-location. Update Purpose after archive. +## Requirements +### Requirement: 统一的模型目录 + +系统 SHALL 将所有数据模型(GORM 模型、DTO)统一放置在 `internal/model/` 目录下,不按业务模块分散。 + +**核心原则:** +- **横向分层**:模型层(Model)是全局统一的,不按业务模块纵向分割 +- **扁平化组织**:所有模型文件直接放在 `internal/model/` 目录下,不创建子目录(除非文件数量超过 50 个) +- **统一导入路径**:所有代码引用模型时统一使用 `internal/model` 导入路径 +- **统一包名**:所有模型文件的包名统一为 `package model` + +**目录结构:** + +``` +internal/ +├── model/ # 所有数据模型统一在这里 +│ ├── account.go # 账户模型 +│ ├── shop.go # 店铺模型 +│ ├── enterprise.go # 企业模型 +│ ├── personal_customer.go # 个人客户模型 +│ ├── role.go # 角色模型 +│ ├── permission.go # 权限模型 +│ ├── carrier.go # 运营商模型(IoT 相关) +│ ├── iot_card.go # IoT 卡模型(IoT 相关) +│ ├── device.go # 设备模型(IoT 相关) +│ ├── order.go # 订单模型(IoT 相关) +│ ├── package.go # 套餐模型(IoT 相关) +│ └── ... # 其他模型 +├── handler/ # Handler 层(按功能分包) +├── service/ # Service 层(按功能分包) +└── store/ # Store 层(按功能分包) +``` + +#### Scenario: 创建新的 IoT 相关模型 + +- **WHEN** 开发者需要创建新的 IoT 相关数据模型(如 `SIMCard`) +- **THEN** 系统要求开发者在 `internal/model/sim_card.go` 创建模型,而不是在 `internal/iot/model/sim_card.go` + +#### Scenario: 引用 IoT 模型 + +- **WHEN** Service 层或 Store 层需要引用 IoT 卡模型 +- **THEN** 系统使用统一的导入路径 `internal/model`,而不是 `internal/iot/model` + +#### Scenario: 跨模块引用模型 + +- **WHEN** 用户模块(User)需要引用 IoT 卡模型(IotCard)进行关联查询 +- **THEN** 系统允许直接从 `internal/model` 导入 `IotCard`,因为所有模型都在同一个包中 + +--- + +### Requirement: 模型文件命名规范 + +系统 SHALL 遵循统一的模型文件命名规范,确保文件名清晰、一致、易于查找。 + +**命名规则:** +- 文件名使用小写下划线命名法(snake_case):`user_account.go`、`iot_card.go`、`shop_order.go` +- 文件名应该清晰描述模型的业务含义,不使用缩写(除非是广泛认可的缩写如 `iot`、`http`) +- 一个文件可以包含一个或多个相关模型(如 `iot_card.go` 可以包含 `IotCard` 和 `IotCardDTO`) +- DTO 模型应该与主模型放在同一个文件中(如 `IotCard` 和 `IotCardDTO` 都在 `iot_card.go` 中) + +**文件内容结构:** + +```go +package model + +// IotCard IoT 卡模型(GORM 模型) +type IotCard struct { + ID uint `gorm:"column:id;primaryKey" json:"id"` + ICCID string `gorm:"column:iccid;uniqueIndex" json:"iccid"` + // ... 其他字段 +} + +// TableName 指定表名 +func (IotCard) TableName() string { + return "iot_cards" +} + +// IotCardDTO IoT 卡 DTO(数据传输对象) +type IotCardDTO struct { + ID uint `json:"id"` + ICCID string `json:"iccid"` + // ... 其他字段 +} +``` + +#### Scenario: 创建新模型时命名文件 + +- **WHEN** 开发者创建新的数据模型 `DeviceBinding` +- **THEN** 系统要求文件名为 `device_binding.go`,而不是 `DeviceBinding.go` 或 `deviceBinding.go` + +#### Scenario: DTO 模型放置位置 + +- **WHEN** 开发者为 `IotCard` 模型创建 DTO(`IotCardDTO`) +- **THEN** 系统要求 DTO 定义在同一个文件 `iot_card.go` 中,而不是创建新文件 `iot_card_dto.go` + +--- + +### Requirement: 禁止按业务模块分割模型 + +系统 SHALL 禁止按业务模块(如 `iot`、`user`、`order`)创建独立的模型子目录,所有模型必须扁平化组织在 `internal/model/` 下。 + +**禁止的目录结构:** + +``` +internal/ +├── model/ +│ ├── user/ # ❌ 禁止按业务模块分子目录 +│ │ └── user.go +│ ├── iot/ # ❌ 禁止按业务模块分子目录 +│ │ └── iot_card.go +│ └── order/ # ❌ 禁止按业务模块分子目录 +│ └── order.go +``` + +``` +internal/ +├── user/ # ❌ 禁止按业务模块纵向分层 +│ ├── model.go +│ ├── handler.go +│ ├── service.go +│ └── store.go +├── iot/ # ❌ 禁止按业务模块纵向分层 +│ ├── model/ +│ │ └── iot_card.go +│ ├── handler.go +│ ├── service.go +│ └── store.go +``` + +**正确的目录结构(扁平化):** + +``` +internal/ +├── model/ # ✅ 所有模型扁平化在一个目录 +│ ├── user.go +│ ├── iot_card.go +│ └── order.go +├── handler/ # ✅ 横向分层 +├── service/ # ✅ 横向分层 +└── store/ # ✅ 横向分层 +``` + +**设计理由:** +1. **符合横向分层架构**:Handler → Service → Store → Model 是全局分层,不是模块分层 +2. **简化导入路径**:所有模型统一使用 `internal/model`,不需要记忆不同模块的路径 +3. **便于跨模块引用**:用户模块可以直接引用 IoT 模型,不需要跨包引用 +4. **符合 Go 语言惯用设计**:包应该按功能组织(model),而不是按业务模块组织(user/model, iot/model) + +#### Scenario: 代码审查拒绝纵向分层 + +- **WHEN** 开发者提交 PR,创建了 `internal/iot/model/` 目录 +- **THEN** 系统要求代码审查拒绝该 PR,并要求开发者将模型移动到 `internal/model/` + +#### Scenario: 重构现有纵向分层的模型 + +- **WHEN** 项目中存在 `internal/iot/model/` 目录 +- **THEN** 系统要求重构,将所有模型迁移到 `internal/model/`,删除 `internal/iot/model/` 目录 + diff --git a/pkg/errors/codes.go b/pkg/errors/codes.go index 186e351..20bf15d 100644 --- a/pkg/errors/codes.go +++ b/pkg/errors/codes.go @@ -37,13 +37,13 @@ const ( CodePermAlreadyAssigned = 1027 // 权限已分配 // 组织相关错误 (1030-1049) - CodeShopNotFound = 1030 // 店铺不存在 - CodeShopCodeExists = 1031 // 店铺编号已存在 - CodeShopLevelExceeded = 1032 // 店铺层级超过最大值 - CodeEnterpriseNotFound = 1033 // 企业不存在 + CodeShopNotFound = 1030 // 店铺不存在 + CodeShopCodeExists = 1031 // 店铺编号已存在 + CodeShopLevelExceeded = 1032 // 店铺层级超过最大值 + CodeEnterpriseNotFound = 1033 // 企业不存在 CodeEnterpriseCodeExists = 1034 // 企业编号已存在 - CodeCustomerNotFound = 1035 // 个人客户不存在 - CodeCustomerPhoneExists = 1036 // 个人客户手机号已存在 + CodeCustomerNotFound = 1035 // 个人客户不存在 + CodeCustomerPhoneExists = 1036 // 个人客户手机号已存在 // 服务端错误 (2000-2999) -> 5xx HTTP 状态码 CodeInternalError = 2001 // 内部服务器错误 diff --git a/pkg/openapi/generator.go b/pkg/openapi/generator.go index 18de685..3cb869e 100644 --- a/pkg/openapi/generator.go +++ b/pkg/openapi/generator.go @@ -81,4 +81,4 @@ func (g *Generator) Save(filename string) error { } return os.WriteFile(filename, yamlBytes, 0644) -} \ No newline at end of file +} diff --git a/pkg/sms/types.go b/pkg/sms/types.go index af70750..cca8594 100644 --- a/pkg/sms/types.go +++ b/pkg/sms/types.go @@ -19,29 +19,29 @@ type SendResponse struct { // 错误码常量 const ( - CodeSuccess = 0 // 处理成功 - CodeEmptyUserName = 1 // 帐号名为空 - CodeAuthFailed = 2 // 帐号名或密码鉴权错误 - CodeAccountLocked = 3 // 帐号已被锁定 - CodeBusinessNotOpened = 4 // 此帐号业务未开通 - CodeInsufficientBalance = 5 // 帐号余额不足 - CodeMissingPhoneNumbers = 6 // 缺少发送号码 - CodeTooManyPhoneNumbers = 7 // 超过最大发送号码数 - CodeEmptyContent = 8 // 发送消息内容为空 - CodeInvalidIP = 10 // 非法的IP地址 - CodeTimeLimitRestriction = 11 // 24小时发送时间段限制 - CodeInvalidScheduledTime = 12 // 定时发送时间错误或超过15天 - CodeTooFrequent = 13 // 请求过于频繁 - CodeInvalidExtCode = 14 // 错误的用户扩展码 - CodeTimestampError = 16 // 时间戳差异过大 - CodeNotRealNameAuthenticated = 18 // 帐号未进行实名认证 - CodeReceiptNotEnabled = 19 // 帐号未开放回执状态 - CodeMissingParameters = 22 // 缺少必填参数 - CodeDuplicateUserName = 23 // 用户帐号名重复 - CodeNoSignatureRestriction = 24 // 用户无签名限制 - CodeSignatureMissingBrackets = 25 // 签名需要包含【】符 - CodeInvalidContentType = 98 // HTTP Content-Type错误 - CodeInvalidJSON = 99 // 错误的请求JSON字符串 + CodeSuccess = 0 // 处理成功 + CodeEmptyUserName = 1 // 帐号名为空 + CodeAuthFailed = 2 // 帐号名或密码鉴权错误 + CodeAccountLocked = 3 // 帐号已被锁定 + CodeBusinessNotOpened = 4 // 此帐号业务未开通 + CodeInsufficientBalance = 5 // 帐号余额不足 + CodeMissingPhoneNumbers = 6 // 缺少发送号码 + CodeTooManyPhoneNumbers = 7 // 超过最大发送号码数 + CodeEmptyContent = 8 // 发送消息内容为空 + CodeInvalidIP = 10 // 非法的IP地址 + CodeTimeLimitRestriction = 11 // 24小时发送时间段限制 + CodeInvalidScheduledTime = 12 // 定时发送时间错误或超过15天 + CodeTooFrequent = 13 // 请求过于频繁 + CodeInvalidExtCode = 14 // 错误的用户扩展码 + CodeTimestampError = 16 // 时间戳差异过大 + CodeNotRealNameAuthenticated = 18 // 帐号未进行实名认证 + CodeReceiptNotEnabled = 19 // 帐号未开放回执状态 + CodeMissingParameters = 22 // 缺少必填参数 + CodeDuplicateUserName = 23 // 用户帐号名重复 + CodeNoSignatureRestriction = 24 // 用户无签名限制 + CodeSignatureMissingBrackets = 25 // 签名需要包含【】符 + CodeInvalidContentType = 98 // HTTP Content-Type错误 + CodeInvalidJSON = 99 // 错误的请求JSON字符串 CodeSystemError = 500 // 系统异常 ) diff --git a/pkg/wechat/wechat.go b/pkg/wechat/wechat.go index cad2b44..2582c18 100644 --- a/pkg/wechat/wechat.go +++ b/pkg/wechat/wechat.go @@ -10,12 +10,12 @@ type Service interface { // UserInfo 微信用户信息 type UserInfo struct { - OpenID string `json:"open_id"` // 微信 OpenID - UnionID string `json:"union_id"` // 微信 UnionID(开放平台统一ID) - Nickname string `json:"nickname"` // 昵称 - Avatar string `json:"avatar"` // 头像URL - Sex int `json:"sex"` // 性别 0-未知 1-男 2-女 - Province string `json:"province"` // 省份 - City string `json:"city"` // 城市 - Country string `json:"country"` // 国家 + OpenID string `json:"open_id"` // 微信 OpenID + UnionID string `json:"union_id"` // 微信 UnionID(开放平台统一ID) + Nickname string `json:"nickname"` // 昵称 + Avatar string `json:"avatar"` // 头像URL + Sex int `json:"sex"` // 性别 0-未知 1-男 2-女 + Province string `json:"province"` // 省份 + City string `json:"city"` // 城市 + Country string `json:"country"` // 国家 }