## 1. 常量定义

- [x] 1.1 在 `pkg/constants/` 新增 `asset_status.go`，定义资产业务状态常量：`AssetStatusInStock = 1`（在库）、`AssetStatusSold = 2`（已销售）、`AssetStatusExchanged = 3`（已换货）、`AssetStatusDeactivated = 4`（已停用），每个常量必须有中文注释
- [x] 1.2 在 `pkg/constants/` 新增 `order_source.go`，定义订单来源常量：`OrderSourceAdmin = "admin"`（后台）、`OrderSourceClient = "client"`（客户端），每个常量必须有中文注释
- [x] 1.3 在 `pkg/constants/` 新增 `operator_type.go`，定义操作人类型常量：`OperatorTypeAdminUser = "admin_user"`（后台用户）、`OperatorTypePersonalCustomer = "personal_customer"`（个人客户），每个常量必须有中文注释
- [x] 1.4 在 `pkg/constants/` 新增 `realname_link.go`，定义实名链接类型常量：`RealnameLinkTypeNone = "none"`、`RealnameLinkTypeTemplate = "template"`、`RealnameLinkTypeGateway = "gateway"`，每个常量必须有中文注释

## 2. 模型字段新增

- [x] 2.1 在 `internal/model/iot_card.go` 的 `IotCard` 结构体中新增 `AssetStatus int` 字段（gorm: `column:asset_status;type:int;not null;default:1;comment:业务状态 1-在库 2-已销售 3-已换货 4-已停用`）和 `Generation int` 字段（gorm: `column:generation;type:int;not null;default:1;comment:资产世代编号`）
- [x] 2.2 在 `internal/model/device.go` 的 `Device` 结构体中新增 `AssetStatus int` 和 `Generation int` 字段，gorm tag 同 2.1
- [x] 2.3 在 `internal/model/order.go` 的 `Order` 结构体中新增 `Source string` 字段（gorm: `column:source;type:varchar(20);not null;default:'admin';comment:订单来源 admin-后台 client-客户端`）和 `Generation int` 字段（gorm: `column:generation;type:int;not null;default:1;comment:资产世代编号`）
- [x] 2.4 在 `internal/model/package.go` 的 `PackageUsage` 结构体中新增 `Generation int` 字段（gorm: `column:generation;type:int;not null;default:1;comment:资产世代编号`）
- [x] 2.5 在 `internal/model/asset_wallet.go` 的 `AssetRechargeRecord` 结构体中新增以下字段：`OperatorType string`（`column:operator_type;type:varchar(20);not null;default:'admin_user';comment:操作人类型`）、`Generation int`（`column:generation;type:int;not null;default:1;comment:资产世代编号`）、`LinkedPackageIDs datatypes.JSON`（`column:linked_package_ids;type:jsonb;default:'[]';comment:强充关联套餐ID列表`）、`LinkedOrderType string`（`column:linked_order_type;type:varchar(20);comment:关联订单类型`）、`LinkedCarrierType string`（`column:linked_carrier_type;type:varchar(20);comment:关联载体类型`）、`LinkedCarrierID *uint`（`column:linked_carrier_id;type:bigint;comment:关联载体ID`）
- [x] 2.6 在 `internal/model/carrier.go` 的 `Carrier` 结构体中新增 `RealnameLinkType string`（`column:realname_link_type;type:varchar(20);not null;default:'none';comment:实名链接类型 none-不支持 template-模板URL gateway-Gateway接口`）和 `RealnameLinkTemplate string`（`column:realname_link_template;type:varchar(500);default:'';comment:实名链接模板URL`）
- [x] 2.7 在 `internal/model/shop_package_allocation.go` 的 `ShopPackageAllocation` 结构体中新增 `RetailPrice int64` 字段（gorm: `column:retail_price;type:bigint;not null;default:0;comment:代理面向终端客户的零售价（分）`）
- [x] 2.8 在 `internal/model/personal_customer.go` 中将 `WxOpenID` 字段的 gorm tag 从 `uniqueIndex:idx_personal_customer_wx_open_id,where:deleted_at IS NULL` 改为 `index:idx_personal_customer_wx_open_id`（唯一索引改为普通索引）

## 3. 数据库迁移

- [x] 3.1 创建迁移文件（使用 golang-migrate 工具），包含以下 ALTER TABLE 语句：
  - `tb_iot_card` ADD `asset_status int NOT NULL DEFAULT 1`、ADD `generation int NOT NULL DEFAULT 1`
  - `tb_device` ADD `asset_status int NOT NULL DEFAULT 1`、ADD `generation int NOT NULL DEFAULT 1`
  - `tb_order` ADD `source varchar(20) NOT NULL DEFAULT 'admin'`、ADD `generation int NOT NULL DEFAULT 1`
  - `tb_package_usage` ADD `generation int NOT NULL DEFAULT 1`
  - `tb_asset_recharge_record` ADD `operator_type varchar(20) NOT NULL DEFAULT 'admin_user'`、ADD `generation int NOT NULL DEFAULT 1`、ADD `linked_package_ids jsonb DEFAULT '[]'`、ADD `linked_order_type varchar(20)`、ADD `linked_carrier_type varchar(20)`、ADD `linked_carrier_id bigint`
  - `tb_carrier` ADD `realname_link_type varchar(20) NOT NULL DEFAULT 'none'`、ADD `realname_link_template varchar(500) DEFAULT ''`
  - `tb_shop_package_allocation` ADD `retail_price bigint NOT NULL DEFAULT 0`
- [x] 3.2 在同一迁移文件中添加存量数据修复 SQL：`UPDATE tb_shop_package_allocation spa SET retail_price = (SELECT suggested_retail_price FROM tb_package p WHERE p.id = spa.package_id) WHERE retail_price = 0`
- [x] 3.3 在同一迁移文件中添加索引变更：DROP `idx_personal_customer_wx_open_id` 唯一索引，CREATE 普通索引 `idx_personal_customer_wx_open_id` ON `tb_personal_customer(wx_open_id)`
- [x] 3.4 编写 down 迁移文件（回滚用），包含对应的 DROP COLUMN 和索引恢复语句
- [x] 3.5 执行迁移，使用 PostgreSQL MCP 工具验证所有字段已添加、存量 `retail_price` 已更新、索引已变更

## 4. BUG-1 修复：代理零售价

- [x] 4.1 修改 `internal/service/purchase_validation/service.go` 的 `GetPurchasePrice()` 方法（约第 172 行）：增加渠道判断，代理渠道（`card.ShopID > 0`）时查询 `ShopPackageAllocation` 获取 `RetailPrice` 并返回，平台渠道继续返回 `Package.SuggestedRetailPrice`
- [x] 4.2 修改 `internal/service/purchase_validation/service.go` 的 `validatePackages()` 方法（约第 148 行）：将 `totalPrice += pkg.SuggestedRetailPrice` 改为按渠道取价逻辑（复用 4.1 的渠道判断），代理渠道额外校验 `allocation.RetailPrice >= allocation.CostPrice`，不满足则该套餐视为不可购买
- [x] 4.3 修改 `internal/service/shop_package_batch_allocation/service.go`：创建分配记录时设置 `RetailPrice` 为 `Package.SuggestedRetailPrice`（约第 84-105 行创建 allocation 的位置）
- [x] 4.4 修改 `internal/service/shop_series_grant/service.go`：创建/更新 `ShopPackageAllocation` 时同步设置 `RetailPrice`（约第 302-352 行和第 614-685 行）
- [x] 4.5 在 `internal/service/shop_package_batch_pricing/service.go` 的 `BatchUpdatePricing()` 方法中新增 cost_price 锁定检查：更新每条 allocation 的 cost_price 前，查询是否存在下级分配记录（`ShopPackageAllocation WHERE allocator_shop_id = allocation.ShopID AND package_id = allocation.PackageID`），存在则跳过该条并记录到响应的 `skipped` 列表，附带原因"存在下级分配记录，请先回收后再修改成本价"
- [x] 4.6 在 `internal/service/shop_series_grant/service.go` 中同步添加 cost_price 锁定检查：更新现有 allocation 的 CostPrice 时（约第 634 行），查询是否存在下级分配记录，存在则拒绝修改并返回错误

## 5. 代理零售价后台管理

- [x] 5.1 修改 `internal/model/dto/shop_package_batch_pricing_dto.go` 的 `BatchUpdateCostPriceRequest`：删除 `PricingTarget` 字段，批量调价接口仅保留成本价路径
- [x] 5.2 修改 `internal/service/shop_package_batch_pricing/service.go` 的 `BatchUpdatePricing()` 方法：删除 `retail_price` 分支与默认分流逻辑，仅保留 `cost_price` 调整（包含 4.5 的锁定检查）
- [x] 5.3 在 `internal/model/dto/package_dto.go` 新增 `UpdateRetailPriceRequest` 与 `UpdateRetailPriceParams`，用于代理修改自己零售价
- [x] 5.4 在 `internal/store/postgres/shop_package_allocation_store.go` 新增 `UpdateRetailPrice(ctx context.Context, id uint, retailPrice int64, updater uint) error`
- [x] 5.5 在 `internal/service/package/service.go` 新增 `UpdateRetailPrice(ctx context.Context, packageID uint, retailPrice int64) error`：仅代理可调用、校验 `retail_price >= cost_price`
- [x] 5.6 在 `internal/handler/admin/package.go` 新增 `UpdateRetailPrice`，并在 `internal/routes/package.go` 注册 `PATCH /api/admin/packages/:id/retail-price`
- [x] 5.7 修改 `internal/model/dto/package_dto.go` 的 `PackageResponse` 结构体：新增 `RetailPrice *int64` 字段（`json:"retail_price,omitempty" description:"代理零售价（分），仅代理用户可见"`）
- [x] 5.8 修改 `internal/service/package/service.go` 的 `toResponse()` 方法（约第 530-541 行）：代理用户查询时，从 allocation 读取 `RetailPrice` 设入 `resp.RetailPrice`；同时修正 `ProfitMargin` 计算：从 `pkg.SuggestedRetailPrice - allocation.CostPrice` 改为 `allocation.RetailPrice - allocation.CostPrice`
- [x] 5.9 修改 `internal/service/package/service.go` 的 `toResponseWithAllocation()` 方法（约第 595-603 行）：同 5.8，从 allocation 读取 `RetailPrice`、修正 `ProfitMargin` 计算

## 6. Carrier 管理 DTO 更新

- [x] 6.1 修改 `internal/model/dto/carrier_dto.go`（或 Carrier 相关 DTO 文件）：在 `CarrierCreateRequest` 和 `CarrierUpdateRequest` 中新增 `RealnameLinkType *string` 字段（`json:"realname_link_type" validate:"omitempty,oneof=none template gateway" description:"实名链接类型 none-不支持 template-模板URL gateway-Gateway接口"`）和 `RealnameLinkTemplate *string` 字段（`json:"realname_link_template" validate:"omitempty,max=500" maxLength:"500" description:"实名链接模板URL，支持 {iccid}/{msisdn}/{virtual_no} 占位符"`）
- [x] 6.2 修改 Carrier Service 的 Create/Update 方法：将 DTO 中的 `RealnameLinkType` 和 `RealnameLinkTemplate` 写入 Carrier 模型；Update 时 `realname_link_type` 为 `template` 时校验 `realname_link_template` 非空
- [x] 6.3 修改 Carrier 的响应 DTO（如 `CarrierResponse`）：新增 `RealnameLinkType` 和 `RealnameLinkTemplate` 字段，后台列表/详情可展示

## 7. 后台订单 generation 快照

- [x] 7.1 修改 `internal/service/order/service.go` 的 `CreateAdminOrder()` 方法：创建 Order 时从资产（IotCard/Device）获取当前 `Generation` 值并设入 `order.Generation`，不再依赖数据库默认值 1

## 8. 资产手动停用

- [x] 8.1 在 `internal/handler/admin/asset.go`（或新建 `internal/handler/admin/asset_lifecycle.go`）新增 `DeactivateAsset` Handler 方法：接收资产类型和 ID，调用 Service 将 `asset_status` 设为 `constants.AssetStatusDeactivated`（4）
- [x] 8.2 在 `internal/service/asset/service.go`（或相关 Service）新增 `Deactivate()` 方法：校验当前 `asset_status` 为 1（在库）或 2（已销售）才允许停用，已换货（3）或已停用（4）的拒绝操作；使用条件更新 `WHERE asset_status IN (1, 2)` 确保幂等
- [x] 8.3 在 `internal/routes/` 注册停用路由：`PATCH /api/admin/iot-cards/:id/deactivate` 和 `PATCH /api/admin/devices/:id/deactivate`（或统一为 `PATCH /api/admin/assets/:type/:id/deactivate`）
- [x] 8.4 更新 `cmd/api/docs.go` 和 `cmd/gendocs/main.go`：如新增了 Handler 则同步注册到文档生成器

## 9. BUG-2 修复：一次性佣金触发条件

- [x] 9.1 修改 `internal/service/commission_calculation/service.go` 中的 `triggerOneTimeCommissionForCardInTx` 方法：将 `if order.IsPurchaseOnBehalf` 的跳过逻辑改为 `if order.IsPurchaseOnBehalf || order.Source != constants.OrderSourceClient`，即代购订单或非客户端来源订单均跳过一次性佣金
- [x] 9.2 修改 `internal/service/commission_calculation/service.go` 中的 `triggerOneTimeCommissionForDeviceInTx` 方法：同 9.1 逻辑

## 10. BUG-4 修复：充值回调事务一致性

- [x] 10.1 修改 `internal/store/postgres/asset_recharge_store.go` 的 `UpdateStatusWithOptimisticLock` 方法：新增 `tx *gorm.DB` 参数（方法签名变更），内部使用传入的 `tx` 替代 `s.db.WithContext(ctx)`；同时保留原方法签名的兼容版本或修改所有调用点
- [x] 10.2 修改 `internal/store/postgres/asset_recharge_store.go` 的 `UpdatePaymentInfo` 方法：同 10.1，新增 `tx *gorm.DB` 参数
- [x] 10.3 修改 `internal/service/recharge/service.go` 的 `HandlePaymentCallback` 方法（约第 308-321 行）：在事务闭包内调用 Store 方法时传入事务 `tx`，确保 `UpdateStatusWithOptimisticLock`、`UpdatePaymentInfo` 和钱包入账在同一事务内
- [x] 10.4 检查并更新 `UpdateStatusWithOptimisticLock` 和 `UpdatePaymentInfo` 的其他调用点（如有），确保传入正确的 db 或 tx 参数

## 11. 旧 H5 接口清理

- [x] 11.1 删除 `internal/handler/h5/` 目录下全部 5 个文件：`auth.go`、`order.go`、`recharge.go`、`package_usage.go`、`enterprise_device.go`
- [x] 11.2 删除 `internal/routes/h5.go`、`internal/routes/h5_enterprise_device.go`、`internal/routes/h5_package_usage.go`
- [x] 11.3 修改 `internal/routes/routes.go`：移除 `/api/h5` 路由组挂载（约第 31-33 行）
- [x] 11.4 修改 `internal/routes/order.go`：移除 `registerH5OrderRoutes` 函数（约第 56-105 行）
- [x] 11.5 修改 `internal/routes/recharge.go`：移除 `registerH5RechargeRoutes` 函数（约第 11-44 行）
- [x] 11.6 修改 `internal/bootstrap/handlers.go`：移除 H5 Handler 构造（H5Auth、EnterpriseDeviceH5、H5PackageUsage、H5Order、H5Recharge，约第 24、31、44、49、50 行）
- [x] 11.7 修改 `internal/bootstrap/types.go`：移除 Handlers 结构体中的 H5 Handler 字段（约第 22、29、42、47、48 行）
- [x] 11.8 修改 `internal/bootstrap/middlewares.go`：移除 `createH5AuthMiddleware` 函数和 H5 跳过路径配置（约第 72-95 行）
- [x] 11.9 修改 `pkg/openapi/handlers.go`：移除文档生成中的 H5 Handler 构造（EnterpriseDeviceH5、H5PackageUsage、H5Order、H5Recharge）
- [x] 11.10 修改 `cmd/api/main.go`：移除 `/api/h5` 限流挂载（约第 250-257 行）

## 12. 旧个人客户登录接口清理

- [x] 12.1 修改 `internal/handler/app/personal_customer.go`：删除 Login（约第 79 行）、SendCode（约第 35 行）、WechatOAuthLogin（约第 114 行）、BindWechat（约第 134 行）方法。保留 UpdateProfile 和 GetProfile
- [x] 12.2 修改 `internal/routes/personal.go`：移除指向已删除方法的路由注册（Login、SendCode、WechatOAuthLogin、BindWechat 的路由）
- [x] 12.3 检查 `internal/bootstrap/handlers.go` 和 `internal/bootstrap/types.go`：如果 PersonalCustomer Handler 有初始化引用已删除方法的依赖，同步清理

## 13. 验证

- [x] 13.1 执行 `go build ./...` 确认编译通过，无任何编译错误
- [x] 13.2 对所有修改的文件执行 `lsp_diagnostics` 确认无错误和警告
- [x] 13.3 使用 PostgreSQL MCP 工具验证数据库：确认 7 张表的新字段存在、默认值正确、存量 `retail_price` 已填充、`wx_open_id` 索引已变更
- [x] 13.4 验证删除的 H5 路由不再注册：检查代码中无 `/api/h5` 相关路由残留
- [x] 13.5 验证 `BatchUpdatePricing` 接口仅支持成本价调整；并验证 `PATCH /api/admin/packages/:id/retail-price` 可供代理修改自己的零售价
- [x] 13.6 验证代理套餐列表：确认 `PackageResponse` 包含 `retail_price` 字段，`profit_margin` 计算基于 `retail_price - cost_price`
- [x] 13.7 撰写功能总结文档 `docs/client-api-data-model-fixes/功能总结.md`，记录所有变更内容
