任务列表返回目标名称
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m1s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m1s
This commit is contained in:
@@ -73,7 +73,7 @@
|
|||||||
| 钱包流水与代理充值业务导出 | N/A:只读投影,不改变钱包、充值或审批状态;任务创建操作者与店铺权限快照沿用现有导出任务记录 | N/A:只读取主钱包流水、充值记录和本地通用审批实例;金额及余额使用既有权威事实,不写入领域账本 | N/A:不实时调用支付渠道或企业微信;明文业务凭证 Key 只进入导出结果,不复制到 Integration Log,且仍禁止记录 Secret、access_token、media_id 和附件正文 | 仅沿用现有 `export:dispatch` → `export:shard` → `export:finalize` Asynq 链路,不新增业务 Outbox |
|
| 钱包流水与代理充值业务导出 | N/A:只读投影,不改变钱包、充值或审批状态;任务创建操作者与店铺权限快照沿用现有导出任务记录 | N/A:只读取主钱包流水、充值记录和本地通用审批实例;金额及余额使用既有权威事实,不写入领域账本 | N/A:不实时调用支付渠道或企业微信;明文业务凭证 Key 只进入导出结果,不复制到 Integration Log,且仍禁止记录 Secret、access_token、media_id 和附件正文 | 仅沿用现有 `export:dispatch` → `export:shard` → `export:finalize` Asynq 链路,不新增业务 Outbox |
|
||||||
| 退款与换货业务导出 | N/A:只读投影,不改变退款、换货、资产或审批状态;任务创建操作者与店铺权限快照沿用现有导出任务记录 | N/A:只读取退款、订单、套餐使用、换货资产快照和本地审批事实;金额及处理标记沿用既有权威事实,不写入领域账本 | N/A:不实时调用企业微信、支付或 Gateway;明文业务凭证和收货资料只进入有权导出结果,不复制到 Integration Log,且仍禁止记录 Secret、access_token、media_id 和附件正文 | 仅沿用现有 `export:dispatch` → `export:shard` → `export:finalize` Asynq 链路,不新增业务 Outbox |
|
| 退款与换货业务导出 | N/A:只读投影,不改变退款、换货、资产或审批状态;任务创建操作者与店铺权限快照沿用现有导出任务记录 | N/A:只读取退款、订单、套餐使用、换货资产快照和本地审批事实;金额及处理标记沿用既有权威事实,不写入领域账本 | N/A:不实时调用企业微信、支付或 Gateway;明文业务凭证和收货资料只进入有权导出结果,不复制到 Integration Log,且仍禁止记录 Secret、access_token、media_id 和附件正文 | 仅沿用现有 `export:dispatch` → `export:shard` → `export:finalize` Asynq 链路,不新增业务 Outbox |
|
||||||
| IoT 卡固定档位限速 | 复用资产操作审计 `card_speed_tier`,记录后台操作者、卡 ICCID、固定档位、Integration Log ID 和 success/failed 结果;设备无入口且不通过绑定卡间接限速 | N/A:不在本地保存或修改卡当前限速状态,Gateway 是外部执行方 | 每次实际 Gateway 调用前写 pending,按 success/failed/unknown 终结;超时 unknown 保存按 ICCID 人工核对策略,摘要不含 Secret、access_token 或完整响应正文 | N/A:单次外部命令无后续可靠副作用,结果未知禁止盲目重发,不创建自动补偿 Outbox |
|
| IoT 卡固定档位限速 | 复用资产操作审计 `card_speed_tier`,记录后台操作者、卡 ICCID、固定档位、Integration Log ID 和 success/failed 结果;设备无入口且不通过绑定卡间接限速 | N/A:不在本地保存或修改卡当前限速状态,Gateway 是外部执行方 | 每次实际 Gateway 调用前写 pending,按 success/failed/unknown 终结;超时 unknown 保存按 ICCID 人工核对策略,摘要不含 Secret、access_token 或完整响应正文 | N/A:单次外部命令无后续可靠副作用,结果未知禁止盲目重发,不创建自动补偿 Outbox |
|
||||||
| 设备 CSV 批量分配、设置套餐系列或回收 | 任务记录冻结操作者和可选目标;逐批复用 `device_allocate`、`device_series_binding` 或 `device_recall` 资产操作审计,记录设备前后值、目标、成功/失败数和失败原因 | N/A:设备归属、绑定卡归属、分配记录及 `series_id` 是权威业务事实,不另建领域账本 | N/A:CSV 解析、分配和回收均为本地数据库操作,不调用 Gateway、支付或企微;对象存储沿用现有存储日志 | 复用 `device:import` Asynq 任务;状态条件阻止完成任务重复执行,处理中断恢复时已达到目标关系的设备按成功处理,不新增业务 Outbox |
|
| 设备 CSV 批量分配、设置套餐系列或回收 | 任务记录冻结操作者和可选目标;逐批复用 `device_allocate`、`device_series_binding` 或 `device_recall` 资产操作审计,记录设备前后值、目标、成功/失败数和失败原因;任务查询的 `target_name` 仅批量投影当前店铺或套餐系列名称 | N/A:设备归属、绑定卡归属、分配记录及 `series_id` 是权威业务事实,不另建领域账本 | N/A:CSV 解析、分配和回收均为本地数据库操作,不调用 Gateway、支付或企微;对象存储沿用现有存储日志 | 复用 `device:import` Asynq 任务;状态条件阻止完成任务重复执行,处理中断恢复时已达到目标关系的设备按成功处理,不新增业务 Outbox |
|
||||||
|
|
||||||
### `deliver-july-iteration-confirmed-scope` 任务覆盖映射
|
### `deliver-july-iteration-confirmed-scope` 任务覆盖映射
|
||||||
|
|
||||||
|
|||||||
@@ -5087,6 +5087,9 @@ components:
|
|||||||
minimum: 0
|
minimum: 0
|
||||||
nullable: true
|
nullable: true
|
||||||
type: integer
|
type: integer
|
||||||
|
target_name:
|
||||||
|
description: 批量分配目标店铺名称或套餐系列名称,回收和导入任务为空
|
||||||
|
type: string
|
||||||
task_no:
|
task_no:
|
||||||
description: 任务编号
|
description: 任务编号
|
||||||
type: string
|
type: string
|
||||||
@@ -5167,6 +5170,9 @@ components:
|
|||||||
minimum: 0
|
minimum: 0
|
||||||
nullable: true
|
nullable: true
|
||||||
type: integer
|
type: integer
|
||||||
|
target_name:
|
||||||
|
description: 批量分配目标店铺名称或套餐系列名称,回收和导入任务为空
|
||||||
|
type: string
|
||||||
task_no:
|
task_no:
|
||||||
description: 任务编号
|
description: 任务编号
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@@ -48,6 +48,6 @@ GET /api/admin/devices/import/tasks
|
|||||||
GET /api/admin/devices/import/tasks/:id
|
GET /api/admin/devices/import/tasks/:id
|
||||||
```
|
```
|
||||||
|
|
||||||
响应新增 `operation_type`、`operation_name` 和 `target_id`,逐行失败原因继续通过原详情结构返回。
|
响应新增 `operation_type`、`operation_name`、`target_id` 和 `target_name`;`target_name` 根据操作类型返回目标店铺名称或套餐系列名称,逐行失败原因继续通过原详情结构返回。
|
||||||
|
|
||||||
回滚应用版本前应先停止新任务入口并等待处理中的任务结束。迁移新增列保持兼容,不删除既有任务;已经完成的设备归属、套餐系列关系、分配记录和资产审计不得通过清表回滚,应使用现有回收或系列绑定接口前向修正。
|
回滚应用版本前应先停止新任务入口并等待处理中的任务结束。迁移新增列保持兼容,不删除既有任务;已经完成的设备归属、套餐系列关系、分配记录和资产审计不得通过清表回滚,应使用现有回收或系列绑定接口前向修正。
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type DeviceImportTaskResponse struct {
|
|||||||
OperationType string `json:"operation_type" description:"任务业务类型 (import:导入设备, assign_shop:分配目标代理, assign_series:设置套餐系列, recall:回收设备)"`
|
OperationType string `json:"operation_type" description:"任务业务类型 (import:导入设备, assign_shop:分配目标代理, assign_series:设置套餐系列, recall:回收设备)"`
|
||||||
OperationName string `json:"operation_name" description:"任务业务类型名称(中文)"`
|
OperationName string `json:"operation_name" description:"任务业务类型名称(中文)"`
|
||||||
TargetID *uint `json:"target_id,omitempty" description:"批量分配目标店铺或套餐系列ID,回收任务为空"`
|
TargetID *uint `json:"target_id,omitempty" description:"批量分配目标店铺或套餐系列ID,回收任务为空"`
|
||||||
|
TargetName string `json:"target_name,omitempty" description:"批量分配目标店铺名称或套餐系列名称,回收和导入任务为空"`
|
||||||
Status int `json:"status" description:"任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)"`
|
Status int `json:"status" description:"任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)"`
|
||||||
StatusName string `json:"status_name" description:"任务状态名称(中文)"`
|
StatusName string `json:"status_name" description:"任务状态名称(中文)"`
|
||||||
StatusText string `json:"status_text" description:"任务状态文本"`
|
StatusText string `json:"status_text" description:"任务状态文本"`
|
||||||
|
|||||||
@@ -191,10 +191,14 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceImportTaskRequest
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
targetNames, err := s.loadTargetNames(ctx, tasks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
list := make([]*dto.DeviceImportTaskResponse, 0, len(tasks))
|
list := make([]*dto.DeviceImportTaskResponse, 0, len(tasks))
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
list = append(list, s.toTaskResponse(task))
|
list = append(list, s.toTaskResponse(task, targetNames))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &dto.ListDeviceImportTaskResponse{
|
return &dto.ListDeviceImportTaskResponse{
|
||||||
@@ -210,9 +214,13 @@ func (s *Service) GetByID(ctx context.Context, id uint) (*dto.DeviceImportTaskDe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New(errors.CodeNotFound, "导入任务不存在")
|
return nil, errors.New(errors.CodeNotFound, "导入任务不存在")
|
||||||
}
|
}
|
||||||
|
targetNames, err := s.loadTargetNames(ctx, []*model.DeviceImportTask{task})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
resp := &dto.DeviceImportTaskDetailResponse{
|
resp := &dto.DeviceImportTaskDetailResponse{
|
||||||
DeviceImportTaskResponse: *s.toTaskResponse(task),
|
DeviceImportTaskResponse: *s.toTaskResponse(task, targetNames),
|
||||||
SkippedItems: make([]*dto.DeviceImportResultItemDTO, 0),
|
SkippedItems: make([]*dto.DeviceImportResultItemDTO, 0),
|
||||||
FailedItems: make([]*dto.DeviceImportResultItemDTO, 0),
|
FailedItems: make([]*dto.DeviceImportResultItemDTO, 0),
|
||||||
WarningItems: make([]*dto.DeviceImportResultItemDTO, 0),
|
WarningItems: make([]*dto.DeviceImportResultItemDTO, 0),
|
||||||
@@ -239,7 +247,49 @@ func (s *Service) GetByID(ctx context.Context, id uint) (*dto.DeviceImportTaskDe
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) toTaskResponse(task *model.DeviceImportTask) *dto.DeviceImportTaskResponse {
|
type deviceImportTargetNames struct {
|
||||||
|
shops map[uint]string
|
||||||
|
series map[uint]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadTargetNames 批量解析任务目标名称,避免列表逐条查询。
|
||||||
|
func (s *Service) loadTargetNames(ctx context.Context, tasks []*model.DeviceImportTask) (deviceImportTargetNames, error) {
|
||||||
|
shopIDs := make([]uint, 0)
|
||||||
|
seriesIDs := make([]uint, 0)
|
||||||
|
for _, task := range tasks {
|
||||||
|
if task.TargetID == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch task.OperationType {
|
||||||
|
case constants.DeviceImportOperationAssignShop:
|
||||||
|
shopIDs = append(shopIDs, *task.TargetID)
|
||||||
|
case constants.DeviceImportOperationAssignSeries:
|
||||||
|
seriesIDs = append(seriesIDs, *task.TargetID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names := deviceImportTargetNames{shops: make(map[uint]string), series: make(map[uint]string)}
|
||||||
|
if len(shopIDs) > 0 {
|
||||||
|
var shops []model.Shop
|
||||||
|
if err := s.db.WithContext(ctx).Select("id, shop_name").Where("id IN ?", shopIDs).Find(&shops).Error; err != nil {
|
||||||
|
return names, errors.Wrap(errors.CodeDatabaseError, err, "查询设备批量任务目标店铺失败")
|
||||||
|
}
|
||||||
|
for _, shop := range shops {
|
||||||
|
names.shops[shop.ID] = shop.ShopName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(seriesIDs) > 0 {
|
||||||
|
var series []model.PackageSeries
|
||||||
|
if err := s.db.WithContext(ctx).Select("id, series_name").Where("id IN ?", seriesIDs).Find(&series).Error; err != nil {
|
||||||
|
return names, errors.Wrap(errors.CodeDatabaseError, err, "查询设备批量任务目标套餐系列失败")
|
||||||
|
}
|
||||||
|
for _, item := range series {
|
||||||
|
names.series[item.ID] = item.SeriesName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) toTaskResponse(task *model.DeviceImportTask, targetNames deviceImportTargetNames) *dto.DeviceImportTaskResponse {
|
||||||
var startedAt, completedAt *time.Time
|
var startedAt, completedAt *time.Time
|
||||||
if task.StartedAt != nil {
|
if task.StartedAt != nil {
|
||||||
startedAt = task.StartedAt
|
startedAt = task.StartedAt
|
||||||
@@ -254,6 +304,7 @@ func (s *Service) toTaskResponse(task *model.DeviceImportTask) *dto.DeviceImport
|
|||||||
OperationType: task.OperationType,
|
OperationType: task.OperationType,
|
||||||
OperationName: constants.GetDeviceImportOperationName(task.OperationType),
|
OperationName: constants.GetDeviceImportOperationName(task.OperationType),
|
||||||
TargetID: task.TargetID,
|
TargetID: task.TargetID,
|
||||||
|
TargetName: targetNames.resolve(task),
|
||||||
Status: task.Status,
|
Status: task.Status,
|
||||||
StatusName: getStatusText(task.Status),
|
StatusName: getStatusText(task.Status),
|
||||||
StatusText: getStatusText(task.Status),
|
StatusText: getStatusText(task.Status),
|
||||||
@@ -273,6 +324,19 @@ func (s *Service) toTaskResponse(task *model.DeviceImportTask) *dto.DeviceImport
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n deviceImportTargetNames) resolve(task *model.DeviceImportTask) string {
|
||||||
|
if task.TargetID == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if task.OperationType == constants.DeviceImportOperationAssignShop {
|
||||||
|
return n.shops[*task.TargetID]
|
||||||
|
}
|
||||||
|
if task.OperationType == constants.DeviceImportOperationAssignSeries {
|
||||||
|
return n.series[*task.TargetID]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func getStatusText(status int) string {
|
func getStatusText(status int) string {
|
||||||
switch status {
|
switch status {
|
||||||
case model.ImportTaskStatusPending:
|
case model.ImportTaskStatusPending:
|
||||||
|
|||||||
Reference in New Issue
Block a user