337 lines
13 KiB
Markdown
337 lines
13 KiB
Markdown
# 需求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_device` 新增字段:
|
||
|
||
```sql
|
||
ALTER TABLE tb_device
|
||
ADD COLUMN real_name_status INT NOT NULL DEFAULT 0;
|
||
|
||
COMMENT ON COLUMN tb_device.real_name_status
|
||
IS '实名状态快照(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:列表字段新增
|
||
|
||
### 核心原则
|
||
|
||
- 提交人账号名在业务单创建时快照到业务表。
|
||
- 审批节点、候选审批人和实际操作人快照统一保存在审批流任务表,不在业务表写死具体节点字段。
|
||
- 列表查询审批信息时,根据本页全部 `approval_instance_id` 批量查询并在内存分组,禁止逐条查询造成 N+1。
|
||
|
||
---
|
||
|
||
### 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_request` 新增提交人快照字段;`approval_instance_id` 由需求18/20统一增加:
|
||
```sql
|
||
ALTER TABLE tb_refund_request
|
||
ADD COLUMN submitter_name varchar(50) NOT NULL DEFAULT '';
|
||
```
|
||
|
||
- `submitter_name`:创建退款单时快照操作人 username
|
||
- 审批状态、当前节点和审批记录:从审批实例、任务和任务审批人快照批量读取
|
||
|
||
> **实施依赖**:动态审批摘要依赖审批流(需求20);`submitter_name` 可独立实现。
|
||
|
||
**响应 DTO**(退款列表响应新增):
|
||
```go
|
||
SubmitterName string `json:"submitter_name" description:"提交人账号名"`
|
||
ApprovalSource string `json:"approval_source" description:"审批来源 (none:无需审批, workflow:通用审批流, legacy:历史业务审批)"`
|
||
ApprovalStatus int `json:"approval_status" description:"审批状态 (1:审批中, 2:已通过, 3:已驳回, 4:已退回)"`
|
||
ApprovalStatusName string `json:"approval_status_name" description:"审批状态名称(中文)"`
|
||
CurrentApprovalNode string `json:"current_approval_node" description:"当前审批节点名称"`
|
||
ApprovalRecords []ApprovalRecordSummary `json:"approval_records" description:"审批节点和审批人摘要"`
|
||
ProcessingStatus int `json:"processing_status" description:"审批通过后的业务处理状态"`
|
||
ProcessingStatusName string `json:"processing_status_name" description:"业务处理状态名称(中文)"`
|
||
```
|
||
|
||
`ApprovalRecordSummary` 动态返回 `node_name`、`approval_mode`、`status` 和审批人列表;每位已操作审批人包含动作、审批意见和 `attachment_count`,但列表接口不返回完整附件元数据。不假设固定存在“部门领导”或“财务”节点。
|
||
|
||
停机发布前已经结束且没有流程实例的退款记录返回 `approval_source=legacy`。这类记录可以使用原 `processor_id`、`processed_at` 和审计日志组成只读历史摘要,但不得伪造多节点审批时间线;发布时仍待审批的记录必须先回填通用审批实例。
|
||
|
||
---
|
||
|
||
### COL-002:代理充值列表新增提交人、审批人(依赖审批流)
|
||
|
||
与 COL-001 同理,`tb_agent_recharge_record` 仅新增 `submitter_name` 快照字段;`approval_instance_id` 由需求18/21统一增加。审批摘要从审批流批量读取。历史终态充值返回 `approval_source=legacy` 并只读展示原状态和审计信息。
|
||
|
||
> **实施依赖**:`submitter_name` 本迭代可实现;动态审批摘要依赖需求21(充值审批流)。
|
||
|
||
---
|
||
|
||
### 前端
|
||
|
||
退款和充值列表增加“审批状态 / 当前节点 / 业务处理状态 / 审批记录”展示。审批记录按节点动态渲染,不能固定绑定两个审批人字段;审批已通过后的代理钱包退款可显示“回退处理中”,其他支付方式显示“待人工退款”,都不能显示成“待审批”。`approval_source=legacy` 时显示“历史审批”标识且不提供操作按钮。
|