# 需求03/07/11/12/13:简单改动合集 --- ## 需求03:店铺列表搜索新增联系电话 ### 后端 `Shop` 表已有 `contact_phone` 字段。仅需在列表查询接口新增过滤条件。 **文件**:`internal/store/postgres/shop_store.go`(列表查询 Store 方法) ```go // 现有过滤条件基础上追加 if req.ContactPhone != "" { query = query.Where("contact_phone = ?", req.ContactPhone) } ``` **DTO 变更**:`internal/model/dto/shop_dto.go` 的 `ShopListRequest` 新增: ```go ContactPhone string `json:"contact_phone" query:"contact_phone" validate:"omitempty,len=11" minLength:"11" maxLength:"11" description:"联系人电话(精确匹配,11位)"` ``` ### 前端 店铺列表搜索栏新增"联系电话"输入框,填入后带入 `contact_phone` 参数请求。 --- ## 需求07:IoT卡/设备管理新增已实名/未实名筛选 ### IoT 卡 `IotCard.real_name_status` 已有(0=未实名, 1=已实名),`ListStandaloneIotCardRequest` 无该过滤字段,需新增。 **DTO 变更**(`internal/model/dto/iot_card_dto.go` → `ListStandaloneIotCardRequest` 新增): ```go RealNameStatus *int `json:"real_name_status" query:"real_name_status" validate:"omitempty,oneof=0 1" description:"实名状态 (0:未实名, 1:已实名)"` ``` **Store 追加**(`internal/store/postgres/iot_card_store.go`): ```go if req.RealNameStatus != nil { query = query.Where("real_name_status = ?", *req.RealNameStatus) } ``` ### 设备 设备本身目前无 `real_name_status` 字段。语义为:任意一张绑定卡已实名 = 设备已实名。 为避免列表查询时走 EXISTS 子查询,改为**快照方案**:在 `Device` 表落盘,轮询时维护。 #### 迁移 `tb_devices` 新增字段: ```sql ALTER TABLE tb_devices ADD COLUMN real_name_status int NOT NULL DEFAULT 0 COMMENT '实名状态快照(0=未实名,1=已实名),任意绑定卡已实名则为1,由轮询异步维护'; ``` **Model**(`internal/model/device.go`): ```go RealNameStatus int `gorm:"column:real_name_status;type:int;default:0;not null;comment:实名状态快照(0=未实名,1=已实名),任意绑定卡已实名则为1" json:"real_name_status"` ``` #### 快照更新时机 以下两处卡实名状态变化时,需同步更新所属设备的快照: **1. 轮询实名处理**(`internal/task/polling_realname_handler.go`) 卡状态变化后,已有 `triggerDeviceRealnameActivation` 查出 `deviceID`,在此同步更新设备快照: ```go // statusChanged 时,如果卡属于某设备,重新计算并写入设备快照 if statusChanged { if binding, err := h.deviceSimBindingStore.GetActiveBindingByCardID(ctx, cardID); err == nil { h.deviceStore.RefreshRealnameSnapshot(ctx, binding.DeviceID) } } ``` **2. 管理员手动修改卡实名状态**(`internal/service/iot_card/service.go:ManualUpdateRealnameStatus`) 更新卡状态成功后,查所属设备并更新快照(同上逻辑)。 #### 快照计算 `DeviceStore.RefreshRealnameSnapshot`: ```go // RefreshRealnameSnapshot 重新计算并写入设备实名状态快照 func (s *DeviceStore) RefreshRealnameSnapshot(ctx context.Context, deviceID uint) error { var count int64 s.db.WithContext(ctx).Raw(` SELECT COUNT(*) FROM tb_device_sim_binding dsb JOIN tb_iot_card ic ON ic.id = dsb.iot_card_id WHERE dsb.device_id = ? AND dsb.deleted_at IS NULL AND ic.real_name_status = 1 AND ic.deleted_at IS NULL `, deviceID).Scan(&count) status := 0 if count > 0 { status = 1 } return s.db.WithContext(ctx).Model(&model.Device{}). Where("id = ?", deviceID). Update("real_name_status", status).Error } ``` #### DTO 变更 **请求**(`internal/model/dto/device_dto.go` → `ListDeviceRequest` 新增): ```go RealNameStatus *int `json:"real_name_status" query:"real_name_status" validate:"omitempty,oneof=0 1" description:"实名状态 (0:未实名, 1:已实名)"` ``` **响应**(`DeviceResponse` 新增): ```go RealNameStatus int `json:"real_name_status" description:"实名状态 (0:未实名, 1:已实名)"` RealNameStatusName string `json:"real_name_status_name" description:"实名状态名称(中文)"` ``` **Store 过滤**(直接 WHERE,无需 EXISTS): ```go if req.RealNameStatus != nil { query = query.Where("real_name_status = ?", *req.RealNameStatus) } ``` ### 前端 IoT卡管理筛选栏新增"实名状态"下拉(全部/已实名/未实名)→ 传 `real_name_status=0|1`。 设备管理同上,列表展示 `real_name_status_name` 字段。 --- ## 需求11:资产详情-套餐到期时间字段 + 15天高亮 ### 后端 **无需改动。** 后台资产详情页实际调用的是: ``` GET /api/admin/assets/resolve/:identifier ``` 该接口的 `AssetResolveResponse`(`internal/model/dto/asset_dto.go`)已包含: ```go CurrentPackage string `json:"current_package"` // 当前套餐名称 CurrentPackageActivatedAt *time.Time `json:"current_package_activated_at"` // 开始时间 CurrentPackageExpiresAt *time.Time `json:"current_package_expires_at"` // 到期时间(无套餐为 null) ``` 到期时间字段已有,前端直接读 `current_package_expires_at` 即可,不需要新增后端字段。 ### 前端 资产详情页"套餐信息"板块展示到期时间,并在剩余 ≤15 天时高亮: - 读取 `resolve` 接口返回的 `current_package_expires_at` - 若为 `null`:展示"暂无套餐" - 剩余天数由前端计算:`Math.ceil((expiresAt - now) / 86400000)` - 剩余 ≤15 天:**红色/高亮**展示(建议红色文字 + 标签) --- ## 需求12:换货管理显示修复 ### 背景 换货单表 `tb_exchange_order`: - `old_asset_identifier` — 旧资产标识符快照 - `new_asset_identifier` — 新资产标识符快照 - `old_asset_id` / `new_asset_id` — 旧/新资产主键 ### EXC-001/EXC-002:旧/新资产标识显示不一致 **根本原因**:后端创建换货单时快照逻辑有误(`internal/service/exchange/service.go`)。 - 卡的旧资产:快照了 `card.VirtualNo`(虚拟号),**应为 `card.ICCID`** - 卡的新资产:快照了操作员输入的 identifier 原值,未规范化,**应统一为 `card.ICCID`** - 设备:快照 `VirtualNo` 优先,没有则 `IMEI`,**逻辑正确,无需改动** **修复**(`internal/service/exchange/service.go`): `resolveAssetByIdentifierWithTx` 及锁定资产路径中,卡的 `Identifier` 改为 `card.ICCID`: ```go // 修复前 return &resolvedExchangeAsset{..., Identifier: card.VirtualNo, ...} // 修复后 return &resolvedExchangeAsset{..., Identifier: card.ICCID, ...} ``` 历史数据不回填,仅修正后续新建换货单的快照行为。 ### EXC-003/EXC-004:旧/新资产搜索支持 ICCID/接入号/虚拟号 **方案**:拆分为独立的旧资产和新资产搜索,搜索逻辑用**两步查询**,不用 JOIN。 **DTO 变更**(`internal/model/dto/exchange_dto.go` → `ExchangeListRequest`): 废弃原有 `Identifier` 字段,改为: ```go OldAssetKeyword string `json:"old_asset_keyword" query:"old_asset_keyword" validate:"omitempty,max=100" description:"旧资产搜索(ICCID/接入号/虚拟号)"` NewAssetKeyword string `json:"new_asset_keyword" query:"new_asset_keyword" validate:"omitempty,max=100" description:"新资产搜索(ICCID/接入号/虚拟号)"` ``` **Store 修改**(`internal/store/postgres/exchange_order_store.go`): 两步查询——先在资产表搜出 ID,再过滤换货表: ```go // 步骤1:旧资产关键词搜索 if req.OldAssetKeyword != "" { kw := "%" + req.OldAssetKeyword + "%" var cardIDs []uint s.db.WithContext(ctx).Table("tb_iot_card"). Where("(iccid LIKE ? OR virtual_no LIKE ? OR msisdn LIKE ?) AND deleted_at IS NULL", kw, kw, kw). Pluck("id", &cardIDs) var deviceIDs []uint s.db.WithContext(ctx).Table("tb_device"). Where("(virtual_no LIKE ? OR imei LIKE ?) AND deleted_at IS NULL", kw, kw). Pluck("id", &deviceIDs) if len(cardIDs) == 0 && len(deviceIDs) == 0 { return &ExchangeListResult{}, nil // 无匹配,直接返回空 } query = query.Where( "(old_asset_type = 'iot_card' AND old_asset_id IN ?) OR (old_asset_type = 'device' AND old_asset_id IN ?)", cardIDs, deviceIDs, ) } // new_asset_keyword 同理,过滤 new_asset_id ``` ### 前端 - EXC-001/002:后端修复后,`old_asset_identifier` 和 `new_asset_identifier` 均为 ICCID(卡)或设备号(设备),展示直接读这两个字段即可 - EXC-003/004:搜索栏拆分为"旧资产"和"新资产"两个独立输入框,分别传 `old_asset_keyword` 和 `new_asset_keyword` --- ## 需求13:列表字段新增 ### 核心原则 提交人、审批人均在**写入时快照**到单据本身,读取时直接返回,不做额外查询。 --- ### COL-003:换货管理列表新增提交人(待建) > 需求文档原写"换号管理",确认为"换货管理"(系统无"换号"概念)。 **迁移**:`tb_exchange_order` 新增字段: ```sql ALTER TABLE tb_exchange_order ADD COLUMN submitter_name varchar(50) NOT NULL DEFAULT ''; ``` **Model**(`internal/model/exchange_order.go`): ```go SubmitterName string `gorm:"column:submitter_name;type:varchar(50);not null;default:'';comment:提交人账号名快照" json:"submitter_name"` ``` **创建换货单时**(`internal/service/exchange/service.go`)快照当前操作人 username: ```go SubmitterName: middleware.GetUsername(ctx), // 从 ctx 取当前登录账号的 username ``` **响应 DTO**(`internal/model/dto/exchange_dto.go` → `ExchangeOrderResponse` 新增): ```go SubmitterName string `json:"submitter_name" description:"提交人账号名"` ``` --- ### COL-001:退款管理列表新增提交人、审批人(依赖审批流) **迁移**:`tb_refund` 新增字段: ```sql ALTER TABLE tb_refund ADD COLUMN submitter_name varchar(50) NOT NULL DEFAULT ''; ALTER TABLE tb_refund ADD COLUMN dept_leader_name varchar(50) NOT NULL DEFAULT ''; ALTER TABLE tb_refund ADD COLUMN finance_approver_name varchar(50) NOT NULL DEFAULT ''; ``` - `submitter_name`:创建退款单时快照操作人 username - `dept_leader_name` / `finance_approver_name`:审批流各环节通过时快照对应审批人 username > **实施依赖**:`dept_leader_name` / `finance_approver_name` 在审批流(需求20)实现后才能写入,未实现前返回空字符串。`submitter_name` 本迭代即可实现。 **响应 DTO**(退款列表响应新增): ```go SubmitterName string `json:"submitter_name" description:"提交人账号名"` DeptLeaderName string `json:"dept_leader_name" description:"部门领导审批人账号名"` FinanceApproverName string `json:"finance_approver_name" description:"财务审批人账号名"` ``` --- ### COL-002:代理充值列表新增提交人、审批人(依赖审批流) 与 COL-001 同理,`tb_agent_recharge`(充值单表)新增同样三个字段,快照逻辑一致。 > **实施依赖**:`submitter_name` 本迭代可实现;审批人字段依赖需求21(充值审批流)。 --- ### 前端 各列表页新增对应字段列,展示即可,无额外交互。