Compare commits
12 Commits
505b30aa8f
...
0d2baabcf1
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d2baabcf1 | |||
| 9d8e3926ff | |||
| c5ed040359 | |||
| 363e0efb34 | |||
| 1363797378 | |||
| 71498883b5 | |||
| 1d75a4c31a | |||
| 89eec1406a | |||
| 9a625f51ef | |||
| 2d6456c4a3 | |||
| 9bd1d60e85 | |||
| 908c5fa1de |
@@ -711,6 +711,9 @@ components:
|
||||
real_name_status_name:
|
||||
description: 实名状态名称(中文)
|
||||
type: string
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
sn:
|
||||
description: 设备序列号
|
||||
type: string
|
||||
@@ -941,6 +944,9 @@ components:
|
||||
real_name_status:
|
||||
description: 实名状态(asset_type=card时有效):0未实名 1已实名
|
||||
type: integer
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
type: object
|
||||
DtoAssetRefreshRequest:
|
||||
properties:
|
||||
@@ -1092,6 +1098,9 @@ components:
|
||||
real_name_status:
|
||||
description: 实名状态:0未实名 1已实名
|
||||
type: integer
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
series_id:
|
||||
description: 套餐系列ID
|
||||
minimum: 0
|
||||
@@ -1587,6 +1596,9 @@ components:
|
||||
real_name_status:
|
||||
description: 实名状态:0未实名 1已实名
|
||||
type: integer
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
slot_position:
|
||||
description: 插槽位置
|
||||
type: integer
|
||||
@@ -3204,6 +3216,9 @@ components:
|
||||
msisdn:
|
||||
description: 接入号
|
||||
type: string
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
slot_position:
|
||||
description: 插槽位置 (1-4)
|
||||
type: integer
|
||||
@@ -3241,6 +3256,9 @@ components:
|
||||
real_name_status:
|
||||
description: 实名状态 (0:未实名, 1:已实名)
|
||||
type: integer
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
slot_position:
|
||||
description: 插槽位置
|
||||
type: integer
|
||||
@@ -3448,6 +3466,9 @@ components:
|
||||
description: 任务ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
skip_count:
|
||||
description: 跳过数
|
||||
type: integer
|
||||
@@ -3514,6 +3535,9 @@ components:
|
||||
description: 任务ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
skip_count:
|
||||
description: 跳过数
|
||||
type: integer
|
||||
@@ -3759,6 +3783,9 @@ components:
|
||||
online_status:
|
||||
description: 在线状态:0未知 1在线 2离线
|
||||
type: integer
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
series_id:
|
||||
description: 套餐系列ID
|
||||
minimum: 0
|
||||
@@ -4308,6 +4335,9 @@ components:
|
||||
maxLength: 500
|
||||
minLength: 1
|
||||
type: string
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名),默认 none
|
||||
type: string
|
||||
required:
|
||||
- file_key
|
||||
type: object
|
||||
@@ -4342,6 +4372,9 @@ components:
|
||||
maxLength: 500
|
||||
minLength: 1
|
||||
type: string
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名),默认 none
|
||||
type: string
|
||||
required:
|
||||
- carrier_id
|
||||
- file_key
|
||||
@@ -4420,6 +4453,9 @@ components:
|
||||
description: 任务ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
skip_count:
|
||||
description: 跳过数
|
||||
type: integer
|
||||
@@ -4490,6 +4526,9 @@ components:
|
||||
description: 任务ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
skip_count:
|
||||
description: 跳过数
|
||||
type: integer
|
||||
@@ -6819,6 +6858,9 @@ components:
|
||||
real_name_status_name:
|
||||
description: 实名状态名称(中文)
|
||||
type: string
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
series_id:
|
||||
description: 套餐系列ID
|
||||
minimum: 0
|
||||
@@ -6975,6 +7017,31 @@ components:
|
||||
description: 更新后的轮询状态 (true:已启用, false:已禁用)
|
||||
type: boolean
|
||||
type: object
|
||||
DtoUpdateAssetRealnamePolicyRequest:
|
||||
properties:
|
||||
realname_policy:
|
||||
description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
enum:
|
||||
- none
|
||||
- before_order
|
||||
- after_order
|
||||
type: string
|
||||
required:
|
||||
- realname_policy
|
||||
type: object
|
||||
DtoUpdateAssetRealnamePolicyResponse:
|
||||
properties:
|
||||
asset_id:
|
||||
description: 资产ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
asset_type:
|
||||
description: 资产类型 (card:IoT卡, device:设备)
|
||||
type: string
|
||||
realname_policy:
|
||||
description: 更新后的实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
type: string
|
||||
type: object
|
||||
DtoUpdateAuthorizationRemarkReq:
|
||||
properties:
|
||||
remark:
|
||||
@@ -9618,6 +9685,78 @@ paths:
|
||||
summary: 更新资产轮询状态
|
||||
tags:
|
||||
- 资产管理
|
||||
/api/admin/assets/{identifier}/realname-mode:
|
||||
patch:
|
||||
description: 更新指定资产(IoT卡或设备)的实名认证策略,支持 none/before_order/after_order 三种模式。
|
||||
parameters:
|
||||
- description: 资产标识符(ICCID 或 VirtualNo)
|
||||
in: path
|
||||
name: identifier
|
||||
required: true
|
||||
schema:
|
||||
description: 资产标识符(ICCID 或 VirtualNo)
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoUpdateAssetRealnamePolicyRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
code:
|
||||
description: 响应码
|
||||
example: 0
|
||||
type: integer
|
||||
data:
|
||||
$ref: '#/components/schemas/DtoUpdateAssetRealnamePolicyResponse'
|
||||
msg:
|
||||
description: 响应消息
|
||||
example: success
|
||||
type: string
|
||||
timestamp:
|
||||
description: 时间戳
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- code
|
||||
- msg
|
||||
- data
|
||||
- timestamp
|
||||
type: object
|
||||
description: 成功
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 更新资产实名认证策略
|
||||
tags:
|
||||
- 资产管理
|
||||
/api/admin/assets/{identifier}/realtime-status:
|
||||
get:
|
||||
description: 查询资产实时状态。卡类型:读取 DB/Redis 持久化状态;设备类型:实时调用 Gateway sync-info 接口获取最新数据,Gateway 失败不阻断主流程(DeviceRealtime.gateway_msg 返回失败原因)。
|
||||
|
||||
@@ -119,7 +119,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
Asset: func() *admin.AssetHandler {
|
||||
pollingQueueMgr := pollingPkg.NewPollingQueueManager(deps.Redis, constants.PollingShardCount, deps.Logger)
|
||||
assetPollingSvc := pollingSvcPkg.NewAssetPollingService(deviceStore, deviceSimBindingStore, svc.IotCard, pollingQueueMgr, deps.Logger)
|
||||
h := admin.NewAssetHandler(svc.Asset, svc.Device, svc.StopResumeService, assetPollingSvc)
|
||||
h := admin.NewAssetHandler(svc.Asset, svc.Device, svc.IotCard, svc.StopResumeService, assetPollingSvc)
|
||||
h.SetLifecycleService(svc.AssetLifecycle)
|
||||
return h
|
||||
}(),
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
type AssetHandler struct {
|
||||
assetService *assetService.Service
|
||||
deviceService *deviceService.Service
|
||||
iotCardService *iotCardService.Service
|
||||
iotCardStopResume *iotCardService.StopResumeService
|
||||
assetPolling *pollingSvc.AssetPollingService
|
||||
assetLifecycleService AssetLifecycleService
|
||||
@@ -28,12 +29,14 @@ type AssetHandler struct {
|
||||
func NewAssetHandler(
|
||||
assetSvc *assetService.Service,
|
||||
deviceSvc *deviceService.Service,
|
||||
iotCardSvc *iotCardService.Service,
|
||||
iotCardStopResume *iotCardService.StopResumeService,
|
||||
assetPolling *pollingSvc.AssetPollingService,
|
||||
) *AssetHandler {
|
||||
return &AssetHandler{
|
||||
assetService: assetSvc,
|
||||
deviceService: deviceSvc,
|
||||
iotCardService: iotCardSvc,
|
||||
iotCardStopResume: iotCardStopResume,
|
||||
assetPolling: assetPolling,
|
||||
}
|
||||
@@ -283,3 +286,47 @@ func (h *AssetHandler) UpdatePollingStatus(c *fiber.Ctx) error {
|
||||
EnablePolling: req.EnablePolling,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRealnamePolicy 更新资产实名认证策略
|
||||
// PATCH /api/admin/assets/:identifier/realname-mode
|
||||
func (h *AssetHandler) UpdateRealnamePolicy(c *fiber.Ctx) error {
|
||||
identifier := c.Params("identifier")
|
||||
if identifier == "" {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
var req dto.UpdateAssetRealnamePolicyRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if req.RealnamePolicy != constants.RealnamePolicyNone &&
|
||||
req.RealnamePolicy != constants.RealnamePolicyBeforeOrder &&
|
||||
req.RealnamePolicy != constants.RealnamePolicyAfterOrder {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的实名认证策略")
|
||||
}
|
||||
|
||||
asset, err := h.assetService.Resolve(c.UserContext(), identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch asset.AssetType {
|
||||
case "card":
|
||||
if err := h.iotCardService.UpdateRealnamePolicy(c.UserContext(), asset.AssetID, req.RealnamePolicy); err != nil {
|
||||
return err
|
||||
}
|
||||
case "device":
|
||||
if err := h.deviceService.UpdateRealnamePolicy(c.UserContext(), asset.AssetID, req.RealnamePolicy); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New(errors.CodeInvalidParam, "不支持的资产类型")
|
||||
}
|
||||
|
||||
return response.Success(c, &dto.UpdateAssetRealnamePolicyResponse{
|
||||
AssetType: asset.AssetType,
|
||||
AssetID: asset.AssetID,
|
||||
RealnamePolicy: req.RealnamePolicy,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,6 +107,17 @@ func (h *ClientRealnameHandler) GetRealnameLink(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// after_order 策略:必须先有充值或订单才能实名
|
||||
if asset.RealnamePolicy == constants.RealnamePolicyAfterOrder {
|
||||
hasValid, checkErr := h.assetService.HasValidRechargeOrPaidOrder(ctx, asset.AssetType, asset.AssetID)
|
||||
if checkErr != nil {
|
||||
logger.GetAppLogger().Warn("检查资产有效充值或订单失败", zap.Error(checkErr))
|
||||
}
|
||||
if !hasValid {
|
||||
return errors.New(errors.CodeRealnameNotAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 检查实名状态
|
||||
if targetCard.RealNameStatus == constants.RealNameStatusVerified {
|
||||
return errors.New(errors.CodeInvalidStatus, "该卡已完成实名")
|
||||
|
||||
@@ -230,6 +230,11 @@ func (h *ClientWalletHandler) GetRechargeCheck(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// before_order 策略:必须先实名才能充值
|
||||
if resolved.Asset.RealnamePolicy == constants.RealnamePolicyBeforeOrder && resolved.Asset.RealNameStatus != 1 {
|
||||
return errors.New(errors.CodeNeedRealname)
|
||||
}
|
||||
|
||||
check, err := h.rechargeService.GetRechargeCheck(resolved.SkipPermissionCtx, resolved.ResourceType, resolved.Asset.AssetID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -264,6 +269,11 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// before_order 策略:必须先实名才能充值
|
||||
if resolved.Asset.RealnamePolicy == constants.RealnamePolicyBeforeOrder && resolved.Asset.RealNameStatus != 1 {
|
||||
return errors.New(errors.CodeNeedRealname)
|
||||
}
|
||||
|
||||
wallet, err := h.getOrCreateWallet(resolved)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -43,6 +43,7 @@ type Device struct {
|
||||
SoftwareVersion string `gorm:"column:software_version;type:varchar(100);not null;default:'';comment:固件版本号" json:"software_version"`
|
||||
SwitchMode string `gorm:"column:switch_mode;type:varchar(10);not null;default:'0';comment:切卡模式:0=自动 1=手动" json:"switch_mode"`
|
||||
LastGatewaySyncAt *time.Time `gorm:"column:last_gateway_sync_at;comment:最后一次 sync-info 同步时间" json:"last_gateway_sync_at,omitempty"`
|
||||
RealnamePolicy string `gorm:"column:realname_policy;type:varchar(20);default:'none';not null;comment:实名认证策略(none=无需实名,before_order=先实名后充值/购买,after_order=先充值/购买后实名)" json:"realname_policy"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -11,23 +11,24 @@ import (
|
||||
// 通过异步任务处理 CSV 文件导入设备并绑定卡
|
||||
type DeviceImportTask struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
TaskNo string `gorm:"column:task_no;type:varchar(50);uniqueIndex:idx_device_import_task_no,where:deleted_at IS NULL;not null;comment:任务编号(唯一)" json:"task_no"`
|
||||
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||
StorageKey string `gorm:"column:storage_key;type:varchar(500);comment:对象存储文件路径" json:"storage_key"`
|
||||
FileName string `gorm:"column:file_name;type:varchar(255);comment:原始文件名" json:"file_name"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:任务状态 1-待处理 2-处理中 3-已完成 4-失败" json:"status"`
|
||||
TotalCount int `gorm:"column:total_count;type:int;default:0;comment:总记录数" json:"total_count"`
|
||||
SuccessCount int `gorm:"column:success_count;type:int;default:0;comment:成功数" json:"success_count"`
|
||||
SkipCount int `gorm:"column:skip_count;type:int;default:0;comment:跳过数" json:"skip_count"`
|
||||
FailCount int `gorm:"column:fail_count;type:int;default:0;comment:失败数" json:"fail_count"`
|
||||
SkippedItems ImportResultItems `gorm:"column:skipped_items;type:jsonb;comment:跳过记录详情" json:"skipped_items"`
|
||||
FailedItems ImportResultItems `gorm:"column:failed_items;type:jsonb;comment:失败记录详情" json:"failed_items"`
|
||||
WarningCount int `gorm:"column:warning_count;default:0;comment:警告数量(部分成功的设备)" json:"warning_count"`
|
||||
WarningItems ImportResultItems `gorm:"column:warning_items;type:jsonb;comment:警告记录详情" json:"warning_items"`
|
||||
ErrorMessage string `gorm:"column:error_message;type:text;comment:错误信息" json:"error_message"`
|
||||
StartedAt *time.Time `gorm:"column:started_at;comment:开始处理时间" json:"started_at"`
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at"`
|
||||
BaseModel `gorm:"embedded"`
|
||||
TaskNo string `gorm:"column:task_no;type:varchar(50);uniqueIndex:idx_device_import_task_no,where:deleted_at IS NULL;not null;comment:任务编号(唯一)" json:"task_no"`
|
||||
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||
StorageKey string `gorm:"column:storage_key;type:varchar(500);comment:对象存储文件路径" json:"storage_key"`
|
||||
FileName string `gorm:"column:file_name;type:varchar(255);comment:原始文件名" json:"file_name"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:任务状态 1-待处理 2-处理中 3-已完成 4-失败" json:"status"`
|
||||
TotalCount int `gorm:"column:total_count;type:int;default:0;comment:总记录数" json:"total_count"`
|
||||
SuccessCount int `gorm:"column:success_count;type:int;default:0;comment:成功数" json:"success_count"`
|
||||
SkipCount int `gorm:"column:skip_count;type:int;default:0;comment:跳过数" json:"skip_count"`
|
||||
FailCount int `gorm:"column:fail_count;type:int;default:0;comment:失败数" json:"fail_count"`
|
||||
SkippedItems ImportResultItems `gorm:"column:skipped_items;type:jsonb;comment:跳过记录详情" json:"skipped_items"`
|
||||
FailedItems ImportResultItems `gorm:"column:failed_items;type:jsonb;comment:失败记录详情" json:"failed_items"`
|
||||
WarningCount int `gorm:"column:warning_count;default:0;comment:警告数量(部分成功的设备)" json:"warning_count"`
|
||||
WarningItems ImportResultItems `gorm:"column:warning_items;type:jsonb;comment:警告记录详情" json:"warning_items"`
|
||||
ErrorMessage string `gorm:"column:error_message;type:text;comment:错误信息" json:"error_message"`
|
||||
StartedAt *time.Time `gorm:"column:started_at;comment:开始处理时间" json:"started_at"`
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at"`
|
||||
RealnamePolicy string `gorm:"column:realname_policy;type:varchar(20);default:'none';not null;comment:导入批次实名策略(none=无需实名,before_order=先实名后充值/购买,after_order=先充值/购买后实名)" json:"realname_policy"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -21,6 +21,7 @@ type AssetResolveResponse struct {
|
||||
// 状态聚合字段
|
||||
RealNameStatus int `json:"real_name_status" description:"实名状态:0未实名 1已实名"`
|
||||
RealNameAt *time.Time `json:"real_name_at" description:"最近一次完成实名的时间,未实名时为 null"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
CurrentPackage string `json:"current_package" description:"当前套餐名称(无套餐时为空)"`
|
||||
PackageTotalMB int64 `json:"package_total_mb" description:"当前套餐总虚流量(MB),已按virtual_ratio换算"`
|
||||
PackageUsedMB float64 `json:"package_used_mb" description:"当前已用虚流量(MB),已按virtual_ratio换算"`
|
||||
@@ -73,6 +74,7 @@ type BoundCardInfo struct {
|
||||
CarrierID uint `json:"carrier_id,omitempty" description:"运营商ID"`
|
||||
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型"`
|
||||
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
}
|
||||
|
||||
// AssetRealtimeStatusResponse 资产实时状态(只读DB/Redis)
|
||||
@@ -81,6 +83,7 @@ type AssetRealtimeStatusResponse struct {
|
||||
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||
NetworkStatus int `json:"network_status,omitempty" description:"网络状态(asset_type=card时有效):0停机 1开机"`
|
||||
RealNameStatus int `json:"real_name_status,omitempty" description:"实名状态(asset_type=card时有效):0未实名 1已实名"`
|
||||
RealnamePolicy string `json:"realname_policy,omitempty" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
CurrentMonthUsageMB float64 `json:"current_month_usage_mb,omitempty" description:"系统累计的自然月流量MB(asset_type=card时有效)"`
|
||||
LastSyncTime *time.Time `json:"last_sync_time,omitempty" description:"最后同步时间(asset_type=card时有效)"`
|
||||
DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"保护期状态(asset_type=device时有效):none/stop/start"`
|
||||
@@ -279,3 +282,16 @@ type UpdateAssetPollingStatusResponse struct {
|
||||
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||
EnablePolling bool `json:"enable_polling" description:"更新后的轮询状态 (true:已启用, false:已禁用)"`
|
||||
}
|
||||
|
||||
// UpdateAssetRealnamePolicyRequest 更新资产实名认证策略请求
|
||||
type UpdateAssetRealnamePolicyRequest struct {
|
||||
Identifier string `path:"identifier" description:"资产标识符(ICCID 或 VirtualNo)" required:"true"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)" required:"true" enum:"none,before_order,after_order"`
|
||||
}
|
||||
|
||||
// UpdateAssetRealnamePolicyResponse 更新资产实名认证策略响应
|
||||
type UpdateAssetRealnamePolicyResponse struct {
|
||||
AssetType string `json:"asset_type" description:"资产类型 (card:IoT卡, device:设备)"`
|
||||
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"更新后的实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type AssetInfoResponse struct {
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
RealNameStatus int `json:"real_name_status" description:"实名状态(0:未实名, 1:已实名)"`
|
||||
RealNameStatusName string `json:"real_name_status_name" description:"实名状态名称(中文)"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
||||
Generation string `json:"generation" description:"世代"`
|
||||
WalletBalance int64 `json:"wallet_balance" description:"钱包余额(分)"`
|
||||
|
||||
@@ -45,6 +45,7 @@ type DeviceCardItem struct {
|
||||
RealNameStatus int `json:"real_name_status" description:"实名状态 (0:未实名, 1:已实名)"`
|
||||
SlotPosition int `json:"slot_position" description:"插槽位置"`
|
||||
IsActive bool `json:"is_active" description:"是否当前激活卡"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
}
|
||||
|
||||
// DeviceCardListResponse F1 设备卡列表响应
|
||||
|
||||
@@ -36,6 +36,7 @@ type DeviceResponse struct {
|
||||
SeriesID *uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesName string `json:"series_name" description:"套餐系列名称"`
|
||||
ActivatedAt *time.Time `json:"activated_at" description:"激活时间"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
// 设备实时同步字段(由 sync-info 接口写入)
|
||||
@@ -71,16 +72,17 @@ type ListDeviceCardsRequest struct {
|
||||
}
|
||||
|
||||
type DeviceCardBindingResponse struct {
|
||||
ID uint `json:"id" description:"绑定记录ID"`
|
||||
SlotPosition int `json:"slot_position" description:"插槽位置 (1-4)"`
|
||||
IotCardID uint `json:"iot_card_id" description:"IoT卡ID"`
|
||||
ICCID string `json:"iccid" description:"ICCID"`
|
||||
MSISDN string `json:"msisdn" description:"接入号"`
|
||||
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
||||
Status int `json:"status" description:"卡状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
StatusName string `json:"status_name" description:"卡状态名称(中文)"`
|
||||
BindTime *time.Time `json:"bind_time" description:"绑定时间"`
|
||||
IsCurrent bool `json:"is_current" description:"是否为当前使用的卡"`
|
||||
ID uint `json:"id" description:"绑定记录ID"`
|
||||
SlotPosition int `json:"slot_position" description:"插槽位置 (1-4)"`
|
||||
IotCardID uint `json:"iot_card_id" description:"IoT卡ID"`
|
||||
ICCID string `json:"iccid" description:"ICCID"`
|
||||
MSISDN string `json:"msisdn" description:"接入号"`
|
||||
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
||||
Status int `json:"status" description:"卡状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
StatusName string `json:"status_name" description:"卡状态名称(中文)"`
|
||||
BindTime *time.Time `json:"bind_time" description:"绑定时间"`
|
||||
IsCurrent bool `json:"is_current" description:"是否为当前使用的卡"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
}
|
||||
|
||||
type ListDeviceCardsResponse struct {
|
||||
|
||||
@@ -3,8 +3,9 @@ package dto
|
||||
import "time"
|
||||
|
||||
type ImportDeviceRequest struct {
|
||||
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"`
|
||||
FileKey string `json:"file_key" validate:"required,min=1,max=500" required:"true" minLength:"1" maxLength:"500" description:"对象存储文件路径(通过 /storage/upload-url 获取)"`
|
||||
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"`
|
||||
FileKey string `json:"file_key" validate:"required,min=1,max=500" required:"true" minLength:"1" maxLength:"500" description:"对象存储文件路径(通过 /storage/upload-url 获取)"`
|
||||
RealnamePolicy string `json:"realname_policy" validate:"omitempty,oneof=none before_order after_order" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名),默认 none"`
|
||||
}
|
||||
|
||||
type ImportDeviceResponse struct {
|
||||
@@ -23,21 +24,22 @@ type ListDeviceImportTaskRequest struct {
|
||||
}
|
||||
|
||||
type DeviceImportTaskResponse struct {
|
||||
ID uint `json:"id" description:"任务ID"`
|
||||
TaskNo string `json:"task_no" description:"任务编号"`
|
||||
Status int `json:"status" description:"任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)"`
|
||||
StatusText string `json:"status_text" description:"任务状态文本"`
|
||||
BatchNo string `json:"batch_no,omitempty" description:"批次号"`
|
||||
FileName string `json:"file_name,omitempty" description:"文件名"`
|
||||
TotalCount int `json:"total_count" description:"总数"`
|
||||
SuccessCount int `json:"success_count" description:"成功数"`
|
||||
SkipCount int `json:"skip_count" description:"跳过数"`
|
||||
FailCount int `json:"fail_count" description:"失败数"`
|
||||
WarningCount int `json:"warning_count" description:"警告数(部分成功的设备数量)"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty" description:"开始处理时间"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" description:"完成时间"`
|
||||
ErrorMessage string `json:"error_message,omitempty" description:"错误信息"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
ID uint `json:"id" description:"任务ID"`
|
||||
TaskNo string `json:"task_no" description:"任务编号"`
|
||||
Status int `json:"status" description:"任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)"`
|
||||
StatusText string `json:"status_text" description:"任务状态文本"`
|
||||
BatchNo string `json:"batch_no,omitempty" description:"批次号"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
FileName string `json:"file_name,omitempty" description:"文件名"`
|
||||
TotalCount int `json:"total_count" description:"总数"`
|
||||
SuccessCount int `json:"success_count" description:"成功数"`
|
||||
SkipCount int `json:"skip_count" description:"跳过数"`
|
||||
FailCount int `json:"fail_count" description:"失败数"`
|
||||
WarningCount int `json:"warning_count" description:"警告数(部分成功的设备数量)"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty" description:"开始处理时间"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" description:"完成时间"`
|
||||
ErrorMessage string `json:"error_message,omitempty" description:"错误信息"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
}
|
||||
|
||||
type ListDeviceImportTaskResponse struct {
|
||||
|
||||
@@ -53,6 +53,7 @@ type StandaloneIotCardResponse struct {
|
||||
SeriesID *uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesName string `json:"series_name" description:"套餐系列名称"`
|
||||
DeviceVirtualNo string `json:"device_virtual_no" description:"绑定设备的虚拟号(未绑定时为空字符串)"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
@@ -65,10 +66,11 @@ type ListStandaloneIotCardResponse struct {
|
||||
}
|
||||
|
||||
type ImportIotCardRequest struct {
|
||||
CarrierID uint `json:"carrier_id" validate:"required,min=1" required:"true" minimum:"1" description:"运营商ID"`
|
||||
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"`
|
||||
FileKey string `json:"file_key" validate:"required,min=1,max=500" required:"true" minLength:"1" maxLength:"500" description:"对象存储文件路径(通过 /storage/upload-url 获取)"`
|
||||
CardCategory string `json:"card_category" validate:"omitempty,oneof=normal industry" description:"卡业务类型 (normal:普通卡, industry:行业卡),默认 normal"`
|
||||
CarrierID uint `json:"carrier_id" validate:"required,min=1" required:"true" minimum:"1" description:"运营商ID"`
|
||||
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"`
|
||||
FileKey string `json:"file_key" validate:"required,min=1,max=500" required:"true" minLength:"1" maxLength:"500" description:"对象存储文件路径(通过 /storage/upload-url 获取)"`
|
||||
CardCategory string `json:"card_category" validate:"omitempty,oneof=normal industry" description:"卡业务类型 (normal:普通卡, industry:行业卡),默认 normal"`
|
||||
RealnamePolicy string `json:"realname_policy" validate:"omitempty,oneof=none before_order after_order" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名),默认 none"`
|
||||
}
|
||||
|
||||
type ImportIotCardResponse struct {
|
||||
@@ -88,24 +90,25 @@ type ListImportTaskRequest struct {
|
||||
}
|
||||
|
||||
type ImportTaskResponse struct {
|
||||
ID uint `json:"id" description:"任务ID"`
|
||||
TaskNo string `json:"task_no" description:"任务编号"`
|
||||
Status int `json:"status" description:"任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)"`
|
||||
StatusText string `json:"status_text" description:"任务状态文本"`
|
||||
CarrierID uint `json:"carrier_id" description:"运营商ID"`
|
||||
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
|
||||
BatchNo string `json:"batch_no,omitempty" description:"批次号"`
|
||||
CardCategory string `json:"card_category" description:"卡业务类型 (normal:普通卡, industry:行业卡)"`
|
||||
FileName string `json:"file_name,omitempty" description:"文件名"`
|
||||
TotalCount int `json:"total_count" description:"总数"`
|
||||
SuccessCount int `json:"success_count" description:"成功数"`
|
||||
SkipCount int `json:"skip_count" description:"跳过数"`
|
||||
FailCount int `json:"fail_count" description:"失败数"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty" description:"开始处理时间"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" description:"完成时间"`
|
||||
ErrorMessage string `json:"error_message,omitempty" description:"错误信息"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
ID uint `json:"id" description:"任务ID"`
|
||||
TaskNo string `json:"task_no" description:"任务编号"`
|
||||
Status int `json:"status" description:"任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)"`
|
||||
StatusText string `json:"status_text" description:"任务状态文本"`
|
||||
CarrierID uint `json:"carrier_id" description:"运营商ID"`
|
||||
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
|
||||
BatchNo string `json:"batch_no,omitempty" description:"批次号"`
|
||||
CardCategory string `json:"card_category" description:"卡业务类型 (normal:普通卡, industry:行业卡)"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
FileName string `json:"file_name,omitempty" description:"文件名"`
|
||||
TotalCount int `json:"total_count" description:"总数"`
|
||||
SuccessCount int `json:"success_count" description:"成功数"`
|
||||
SkipCount int `json:"skip_count" description:"跳过数"`
|
||||
FailCount int `json:"fail_count" description:"失败数"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty" description:"开始处理时间"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" description:"完成时间"`
|
||||
ErrorMessage string `json:"error_message,omitempty" description:"错误信息"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
}
|
||||
|
||||
type ListImportTaskResponse struct {
|
||||
|
||||
@@ -54,6 +54,7 @@ type IotCard struct {
|
||||
VirtualNo string `gorm:"column:virtual_no;type:varchar(50);uniqueIndex:idx_iot_card_virtual_no,where:deleted_at IS NULL AND virtual_no IS NOT NULL AND virtual_no <> '';comment:虚拟号(可选,非空时全局唯一)" json:"virtual_no,omitempty"`
|
||||
DeviceVirtualNo string `gorm:"column:device_virtual_no;type:varchar(100);not null;default:'';comment:绑定设备的虚拟号快照(未绑定时为空字符串)" json:"device_virtual_no"`
|
||||
LastGatewayReadingMB float64 `gorm:"column:last_gateway_reading_mb;type:float;not null;default:0;comment:上次轮询时网关返回的流量读数(MB)" json:"last_gateway_reading_mb"`
|
||||
RealnamePolicy string `gorm:"column:realname_policy;type:varchar(20);default:'none';not null;comment:实名认证策略(none=无需实名,before_order=先实名后充值/购买,after_order=先充值/购买后实名)" json:"realname_policy"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -10,28 +10,29 @@ import (
|
||||
|
||||
type IotCardImportTask struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
TaskNo string `gorm:"column:task_no;type:varchar(50);uniqueIndex:idx_import_task_no,where:deleted_at IS NULL;not null;comment:任务编号(IMP-YYYYMMDD-XXXXXX)" json:"task_no"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:任务状态 1-待处理 2-处理中 3-已完成 4-失败" json:"status"`
|
||||
CarrierID uint `gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"`
|
||||
CarrierType string `gorm:"column:carrier_type;type:varchar(20);not null;comment:运营商类型(CMCC/CUCC/CTCC/CBN)" json:"carrier_type"`
|
||||
CarrierName string `gorm:"column:carrier_name;type:varchar(100);comment:运营商名称,创建任务时快照" json:"carrier_name"`
|
||||
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||
FileName string `gorm:"column:file_name;type:varchar(255);comment:原始文件名" json:"file_name"`
|
||||
TotalCount int `gorm:"column:total_count;type:int;default:0;not null;comment:总数" json:"total_count"`
|
||||
SuccessCount int `gorm:"column:success_count;type:int;default:0;not null;comment:成功数" json:"success_count"`
|
||||
SkipCount int `gorm:"column:skip_count;type:int;default:0;not null;comment:跳过数" json:"skip_count"`
|
||||
FailCount int `gorm:"column:fail_count;type:int;default:0;not null;comment:失败数" json:"fail_count"`
|
||||
SkippedItems ImportResultItems `gorm:"column:skipped_items;type:jsonb;comment:跳过记录详情" json:"skipped_items"`
|
||||
FailedItems ImportResultItems `gorm:"column:failed_items;type:jsonb;comment:失败记录详情" json:"failed_items"`
|
||||
StartedAt *time.Time `gorm:"column:started_at;comment:开始处理时间" json:"started_at"`
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at"`
|
||||
ErrorMessage string `gorm:"column:error_message;type:text;comment:任务级错误信息" json:"error_message"`
|
||||
ShopID *uint `gorm:"column:shop_id;index;comment:店铺ID(发起导入的店铺)" json:"shop_id,omitempty"`
|
||||
CardList CardListJSON `gorm:"column:card_list;type:jsonb;comment:待导入卡列表[{iccid,msisdn}]" json:"-"`
|
||||
StorageBucket string `gorm:"column:storage_bucket;type:varchar(100);comment:对象存储桶名" json:"storage_bucket,omitempty"`
|
||||
StorageKey string `gorm:"column:storage_key;type:varchar(500);comment:对象存储文件路径" json:"storage_key,omitempty"`
|
||||
CardCategory string `gorm:"column:card_category;type:varchar(20);default:normal;not null;comment:卡业务类型(normal/industry)" json:"card_category"`
|
||||
BaseModel `gorm:"embedded"`
|
||||
TaskNo string `gorm:"column:task_no;type:varchar(50);uniqueIndex:idx_import_task_no,where:deleted_at IS NULL;not null;comment:任务编号(IMP-YYYYMMDD-XXXXXX)" json:"task_no"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:任务状态 1-待处理 2-处理中 3-已完成 4-失败" json:"status"`
|
||||
CarrierID uint `gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"`
|
||||
CarrierType string `gorm:"column:carrier_type;type:varchar(20);not null;comment:运营商类型(CMCC/CUCC/CTCC/CBN)" json:"carrier_type"`
|
||||
CarrierName string `gorm:"column:carrier_name;type:varchar(100);comment:运营商名称,创建任务时快照" json:"carrier_name"`
|
||||
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||
FileName string `gorm:"column:file_name;type:varchar(255);comment:原始文件名" json:"file_name"`
|
||||
TotalCount int `gorm:"column:total_count;type:int;default:0;not null;comment:总数" json:"total_count"`
|
||||
SuccessCount int `gorm:"column:success_count;type:int;default:0;not null;comment:成功数" json:"success_count"`
|
||||
SkipCount int `gorm:"column:skip_count;type:int;default:0;not null;comment:跳过数" json:"skip_count"`
|
||||
FailCount int `gorm:"column:fail_count;type:int;default:0;not null;comment:失败数" json:"fail_count"`
|
||||
SkippedItems ImportResultItems `gorm:"column:skipped_items;type:jsonb;comment:跳过记录详情" json:"skipped_items"`
|
||||
FailedItems ImportResultItems `gorm:"column:failed_items;type:jsonb;comment:失败记录详情" json:"failed_items"`
|
||||
StartedAt *time.Time `gorm:"column:started_at;comment:开始处理时间" json:"started_at"`
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at"`
|
||||
ErrorMessage string `gorm:"column:error_message;type:text;comment:任务级错误信息" json:"error_message"`
|
||||
ShopID *uint `gorm:"column:shop_id;index;comment:店铺ID(发起导入的店铺)" json:"shop_id,omitempty"`
|
||||
CardList CardListJSON `gorm:"column:card_list;type:jsonb;comment:待导入卡列表[{iccid,msisdn}]" json:"-"`
|
||||
StorageBucket string `gorm:"column:storage_bucket;type:varchar(100);comment:对象存储桶名" json:"storage_bucket,omitempty"`
|
||||
StorageKey string `gorm:"column:storage_key;type:varchar(500);comment:对象存储文件路径" json:"storage_key,omitempty"`
|
||||
CardCategory string `gorm:"column:card_category;type:varchar(20);default:normal;not null;comment:卡业务类型(normal/industry)" json:"card_category"`
|
||||
RealnamePolicy string `gorm:"column:realname_policy;type:varchar(20);default:'none';not null;comment:导入批次实名策略(none=无需实名,before_order=先实名后充值/购买,after_order=先充值/购买后实名)" json:"realname_policy"`
|
||||
}
|
||||
|
||||
// CardItem 卡信息(ICCID + MSISDN + VirtualNo)
|
||||
|
||||
@@ -118,4 +118,13 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
|
||||
Output: new(dto.UpdateAssetPollingStatusResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "PATCH", "/:identifier/realname-mode", handler.UpdateRealnamePolicy, RouteSpec{
|
||||
Summary: "更新资产实名认证策略",
|
||||
Description: "更新指定资产(IoT卡或设备)的实名认证策略,支持 none/before_order/after_order 三种模式。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.UpdateAssetRealnamePolicyRequest),
|
||||
Output: new(dto.UpdateAssetRealnamePolicyResponse),
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -164,6 +164,7 @@ func (s *Service) buildDeviceResolveResponse(ctx context.Context, device *model.
|
||||
SwitchMode: device.SwitchMode,
|
||||
LastGatewaySyncAt: device.LastGatewaySyncAt,
|
||||
EnablePolling: device.EnablePolling,
|
||||
RealnamePolicy: device.RealnamePolicy,
|
||||
}
|
||||
|
||||
// 查绑定卡
|
||||
@@ -250,6 +251,7 @@ func (s *Service) buildCardResolveResponse(ctx context.Context, card *model.IotC
|
||||
UpdatedAt: card.UpdatedAt,
|
||||
RealNameStatus: card.RealNameStatus,
|
||||
RealNameAt: card.FirstRealnameAt,
|
||||
RealnamePolicy: card.RealnamePolicy,
|
||||
NetworkStatus: card.NetworkStatus,
|
||||
ICCID: card.ICCID,
|
||||
CarrierID: card.CarrierID,
|
||||
@@ -999,6 +1001,72 @@ func paymentStatusText(status int) string {
|
||||
}
|
||||
}
|
||||
|
||||
// GetEffectiveRealnamePolicy 获取资产的有效实名认证策略
|
||||
// 设备卡使用设备策略,单独卡使用卡策略
|
||||
func GetEffectiveRealnamePolicy(card *model.IotCard, device *model.Device) string {
|
||||
if device != nil {
|
||||
return device.RealnamePolicy
|
||||
}
|
||||
if card != nil {
|
||||
return card.RealnamePolicy
|
||||
}
|
||||
return constants.RealnamePolicyNone
|
||||
}
|
||||
|
||||
// HasValidRechargeOrPaidOrder 检查资产在当前世代是否有有效充值或已支付订单
|
||||
// 用于 after_order 策略的生效条件判断
|
||||
func (s *Service) HasValidRechargeOrPaidOrder(ctx context.Context, assetType string, assetID uint) (bool, error) {
|
||||
generation := s.resolveAssetGeneration(ctx, assetType, assetID)
|
||||
|
||||
if assetType == "card" {
|
||||
var rechargeCount int64
|
||||
err := s.db.WithContext(ctx).Model(&model.RechargeOrder{}).
|
||||
Where("iot_card_id = ? AND generation = ? AND status = ?", assetID, generation, model.RechargeOrderStatusPaid).
|
||||
Count(&rechargeCount).Error
|
||||
if err != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询卡充值订单失败")
|
||||
}
|
||||
if rechargeCount > 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var orderCount int64
|
||||
err = s.db.WithContext(ctx).Model(&model.Order{}).
|
||||
Where("iot_card_id = ? AND generation = ? AND payment_status = ?", assetID, generation, model.PaymentStatusPaid).
|
||||
Count(&orderCount).Error
|
||||
if err != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询卡订单失败")
|
||||
}
|
||||
if orderCount > 0 {
|
||||
return true, nil
|
||||
}
|
||||
} else if assetType == "device" {
|
||||
var rechargeCount int64
|
||||
err := s.db.WithContext(ctx).Model(&model.RechargeOrder{}).
|
||||
Where("device_id = ? AND generation = ? AND status = ?", assetID, generation, model.RechargeOrderStatusPaid).
|
||||
Count(&rechargeCount).Error
|
||||
if err != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询设备充值订单失败")
|
||||
}
|
||||
if rechargeCount > 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var orderCount int64
|
||||
err = s.db.WithContext(ctx).Model(&model.Order{}).
|
||||
Where("device_id = ? AND generation = ? AND payment_status = ?", assetID, generation, model.PaymentStatusPaid).
|
||||
Count(&orderCount).Error
|
||||
if err != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询设备订单失败")
|
||||
}
|
||||
if orderCount > 0 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// packageStatusName 套餐状态码转中文名称
|
||||
func packageStatusName(status int) string {
|
||||
switch status {
|
||||
|
||||
@@ -140,11 +140,10 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 暂不做实名认证拦截,由网关侧处理
|
||||
// REALNAME-03: 普通卡需要实名,行业卡不需要,待后续业务明确后按卡类型分支启用
|
||||
// if assetInfo.CardCategory == "normal" && assetInfo.RealNameStatus != constants.RealNameStatusVerified {
|
||||
// return nil, errors.New(errors.CodeNeedRealname)
|
||||
// }
|
||||
// before_order 策略:必须先实名才能购买套餐
|
||||
if assetInfo.RealnamePolicy == constants.RealnamePolicyBeforeOrder && assetInfo.RealNameStatus != 1 {
|
||||
return nil, errors.New(errors.CodeNeedRealname)
|
||||
}
|
||||
|
||||
// 先判断是否需要强充,避免普通下单时不必要地校验微信配置
|
||||
forceRecharge := s.checkForceRechargeRequirement(skipCtx, validationResult)
|
||||
@@ -945,6 +944,30 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
|
||||
}
|
||||
|
||||
// before_order 策略:必须先实名才能支付订单
|
||||
var assetRealnamePolicy string
|
||||
var assetRealnameStatus int
|
||||
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
|
||||
card, err := s.iotCardStore.GetByID(skipCtx, *order.IotCardID)
|
||||
if err == nil && card != nil {
|
||||
assetRealnamePolicy = card.RealnamePolicy
|
||||
assetRealnameStatus = card.RealNameStatus
|
||||
}
|
||||
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
|
||||
device, err := s.deviceStore.GetByID(skipCtx, *order.DeviceID)
|
||||
if err == nil && device != nil {
|
||||
assetRealnamePolicy = device.RealnamePolicy
|
||||
// 设备的 RealNameStatus 需要通过 Resolve 获取(从当前卡派生)
|
||||
resolved, resolveErr := s.assetService.Resolve(skipCtx, device.VirtualNo)
|
||||
if resolveErr == nil && resolved != nil {
|
||||
assetRealnameStatus = resolved.RealNameStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
if assetRealnamePolicy == constants.RealnamePolicyBeforeOrder && assetRealnameStatus != 1 {
|
||||
return nil, errors.New(errors.CodeNeedRealname)
|
||||
}
|
||||
|
||||
switch req.PaymentMethod {
|
||||
case model.PaymentMethodWallet:
|
||||
if err := s.orderPaymentService.WalletPay(skipCtx, orderID, model.BuyerTypePersonal, customerID); err != nil {
|
||||
|
||||
@@ -1018,6 +1018,30 @@ func (s *Service) StartDevice(ctx context.Context, deviceID uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRealnamePolicy 更新设备的实名认证策略
|
||||
func (s *Service) UpdateRealnamePolicy(ctx context.Context, deviceID uint, realnamePolicy string) error {
|
||||
// 检查设备是否存在
|
||||
device, err := s.deviceStore.GetByID(ctx, deviceID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "设备不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||||
}
|
||||
|
||||
// 幂等检查
|
||||
if device.RealnamePolicy == realnamePolicy {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
if err := s.deviceStore.UpdateRealnamePolicy(ctx, deviceID, realnamePolicy); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新实名认证策略失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) invalidatePollingCardCache(cardID uint) {
|
||||
if s.redis == nil {
|
||||
return
|
||||
|
||||
@@ -44,11 +44,12 @@ func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportDeviceReq
|
||||
fileName := filepath.Base(req.FileKey)
|
||||
|
||||
task := &model.DeviceImportTask{
|
||||
TaskNo: taskNo,
|
||||
Status: model.ImportTaskStatusPending,
|
||||
BatchNo: req.BatchNo,
|
||||
FileName: fileName,
|
||||
StorageKey: req.FileKey,
|
||||
TaskNo: taskNo,
|
||||
Status: model.ImportTaskStatusPending,
|
||||
BatchNo: req.BatchNo,
|
||||
FileName: fileName,
|
||||
StorageKey: req.FileKey,
|
||||
RealnamePolicy: req.RealnamePolicy,
|
||||
}
|
||||
task.Creator = userID
|
||||
task.Updater = userID
|
||||
|
||||
@@ -1124,3 +1124,32 @@ func (s *Service) BatchDeleteCards(ctx context.Context, cardIDs []uint) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRealnamePolicy 更新卡的实名认证策略
|
||||
func (s *Service) UpdateRealnamePolicy(ctx context.Context, cardID uint, realnamePolicy string) error {
|
||||
// 检查卡是否存在
|
||||
card, err := s.iotCardStore.GetByID(ctx, cardID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "IoT卡不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||||
}
|
||||
|
||||
// 幂等检查
|
||||
if card.RealnamePolicy == realnamePolicy {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
if err := s.iotCardStore.UpdateRealnamePolicy(ctx, cardID, realnamePolicy); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新实名认证策略失败")
|
||||
}
|
||||
|
||||
s.logger.Info("更新卡实名认证策略",
|
||||
zap.Uint("card_id", cardID),
|
||||
zap.String("realname_policy", realnamePolicy),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,15 +76,16 @@ func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportIotCardRe
|
||||
}
|
||||
|
||||
task := &model.IotCardImportTask{
|
||||
TaskNo: taskNo,
|
||||
Status: model.ImportTaskStatusPending,
|
||||
CarrierID: req.CarrierID,
|
||||
CarrierType: carrier.CarrierType,
|
||||
CarrierName: carrier.CarrierName,
|
||||
BatchNo: req.BatchNo,
|
||||
FileName: fileName,
|
||||
StorageKey: req.FileKey,
|
||||
CardCategory: cardCategory,
|
||||
TaskNo: taskNo,
|
||||
Status: model.ImportTaskStatusPending,
|
||||
CarrierID: req.CarrierID,
|
||||
CarrierType: carrier.CarrierType,
|
||||
CarrierName: carrier.CarrierName,
|
||||
BatchNo: req.BatchNo,
|
||||
FileName: fileName,
|
||||
StorageKey: req.FileKey,
|
||||
CardCategory: cardCategory,
|
||||
RealnamePolicy: req.RealnamePolicy,
|
||||
}
|
||||
task.Creator = userID
|
||||
task.Updater = userID
|
||||
|
||||
@@ -250,3 +250,10 @@ func (s *DeviceStore) UpdatePollingStatus(ctx context.Context, deviceID uint, en
|
||||
Where("id = ?", deviceID).
|
||||
Update("enable_polling", enablePolling).Error
|
||||
}
|
||||
|
||||
// UpdateRealnamePolicy 更新设备的实名认证策略
|
||||
func (s *DeviceStore) UpdateRealnamePolicy(ctx context.Context, deviceID uint, realnamePolicy string) error {
|
||||
return s.db.WithContext(ctx).Model(&model.Device{}).
|
||||
Where("id = ?", deviceID).
|
||||
Update("realname_policy", realnamePolicy).Error
|
||||
}
|
||||
|
||||
@@ -124,6 +124,13 @@ func (s *IotCardStore) UpdatePollingStatus(ctx context.Context, cardID uint, ena
|
||||
Update("enable_polling", enablePolling).Error
|
||||
}
|
||||
|
||||
// UpdateRealnamePolicy 更新卡的实名认证策略
|
||||
func (s *IotCardStore) UpdateRealnamePolicy(ctx context.Context, cardID uint, realnamePolicy string) error {
|
||||
return s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("id = ?", cardID).
|
||||
Update("realname_policy", realnamePolicy).Error
|
||||
}
|
||||
|
||||
func (s *IotCardStore) Delete(ctx context.Context, id uint) error {
|
||||
return s.db.WithContext(ctx).Delete(&model.IotCard{}, id).Error
|
||||
}
|
||||
|
||||
@@ -279,15 +279,16 @@ func (h *DeviceImportHandler) processBatch(ctx context.Context, task *model.Devi
|
||||
txIotCardStore := postgres.NewIotCardStore(tx, nil)
|
||||
|
||||
device := &model.Device{
|
||||
VirtualNo: row.VirtualNo,
|
||||
IMEI: row.IMEI,
|
||||
DeviceName: row.DeviceName,
|
||||
DeviceModel: row.DeviceModel,
|
||||
DeviceType: row.DeviceType,
|
||||
MaxSimSlots: row.MaxSimSlots,
|
||||
Manufacturer: row.Manufacturer,
|
||||
BatchNo: task.BatchNo,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
VirtualNo: row.VirtualNo,
|
||||
IMEI: row.IMEI,
|
||||
DeviceName: row.DeviceName,
|
||||
DeviceModel: row.DeviceModel,
|
||||
DeviceType: row.DeviceType,
|
||||
MaxSimSlots: row.MaxSimSlots,
|
||||
Manufacturer: row.Manufacturer,
|
||||
BatchNo: task.BatchNo,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
RealnamePolicy: task.RealnamePolicy,
|
||||
}
|
||||
device.Creator = task.Creator
|
||||
device.Updater = task.Creator
|
||||
|
||||
@@ -358,6 +358,7 @@ func (h *IotCardImportHandler) processBatch(ctx context.Context, task *model.Iot
|
||||
ActivationStatus: constants.ActivationStatusInactive,
|
||||
RealNameStatus: constants.RealNameStatusNotVerified,
|
||||
NetworkStatus: constants.NetworkStatusOffline,
|
||||
RealnamePolicy: task.RealnamePolicy,
|
||||
}
|
||||
iotCard.BaseModel.Creator = task.Creator
|
||||
iotCard.BaseModel.Updater = task.Creator
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
## 1. 基础设施(常量 + 迁移 + Model)
|
||||
|
||||
- [ ] 1.1 在 `pkg/constants/` 中定义 `RealnamePolicy` 枚举常量(none / before_order / after_order)及新增错误码 `CodeRealnameNotAvailable`(`pkg/errors/codes.go`)
|
||||
- [ ] 1.2 创建数据库迁移文件,为 `tb_iot_card` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
|
||||
- [ ] 1.3 创建数据库迁移文件,为 `tb_device` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
|
||||
- [ ] 1.4 创建数据库迁移文件,为 `tb_iot_card_import_task` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
|
||||
- [ ] 1.5 创建数据库迁移文件,为 `tb_device_import_task` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
|
||||
- [ ] 1.6 在 `internal/model/iot_card.go` 的 `IotCard` 结构体新增 `RealnamePolicy` 字段(含 gorm 标签和中文注释)
|
||||
- [ ] 1.7 在 `internal/model/device.go` 的 `Device` 结构体新增 `RealnamePolicy` 字段(含 gorm 标签和中文注释)
|
||||
- [ ] 1.8 在 `internal/model/iot_card_import_task.go` 的 `IotCardImportTask` 结构体新增 `RealnamePolicy` 字段
|
||||
- [ ] 1.9 在 `internal/model/device_import_task.go` 的 `DeviceImportTask` 结构体新增 `RealnamePolicy` 字段
|
||||
- [ ] 1.10 执行迁移并验证四张表字段已正确添加
|
||||
- [x] 1.1 在 `pkg/constants/` 中定义 `RealnamePolicy` 枚举常量(none / before_order / after_order)及新增错误码 `CodeRealnameNotAvailable`(`pkg/errors/codes.go`)
|
||||
- [x] 1.2 创建数据库迁移文件,为 `tb_iot_card` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
|
||||
- [x] 1.3 创建数据库迁移文件,为 `tb_device` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
|
||||
- [x] 1.4 创建数据库迁移文件,为 `tb_iot_card_import_task` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
|
||||
- [x] 1.5 创建数据库迁移文件,为 `tb_device_import_task` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
|
||||
- [x] 1.6 在 `internal/model/iot_card.go` 的 `IotCard` 结构体新增 `RealnamePolicy` 字段(含 gorm 标签和中文注释)
|
||||
- [x] 1.7 在 `internal/model/device.go` 的 `Device` 结构体新增 `RealnamePolicy` 字段(含 gorm 标签和中文注释)
|
||||
- [x] 1.8 在 `internal/model/iot_card_import_task.go` 的 `IotCardImportTask` 结构体新增 `RealnamePolicy` 字段
|
||||
- [x] 1.9 在 `internal/model/device_import_task.go` 的 `DeviceImportTask` 结构体新增 `RealnamePolicy` 字段
|
||||
- [x] 1.10 执行迁移并验证四张表字段已正确添加
|
||||
|
||||
## 2. 公共策略判断逻辑(Service 层)
|
||||
|
||||
- [ ] 2.1 在合适的公共 Service 或工具包中实现 `GetEffectiveRealnamePolicy(card *model.IotCard, device *model.Device) string`:设备卡取 device.RealnamePolicy,单卡取 card.RealnamePolicy
|
||||
- [ ] 2.2 实现检查 `after_order` 生效条件的方法:查询该资产当前 generation 内是否存在有效充值(status=2)或已支付订单(payment_status=2),返回 bool
|
||||
- [x] 2.1 在合适的公共 Service 或工具包中实现 `GetEffectiveRealnamePolicy(card *model.IotCard, device *model.Device) string`:设备卡取 device.RealnamePolicy,单卡取 card.RealnamePolicy
|
||||
- [x] 2.2 实现检查 `after_order` 生效条件的方法:查询该资产当前 generation 内是否存在有效充值(status=2)或已支付订单(payment_status=2),返回 bool
|
||||
|
||||
## 3. IoT 卡导入模块改造
|
||||
|
||||
- [ ] 3.1 在 `ImportIotCardRequest` DTO 中新增 `realname_policy` 字段(含 validate、description 标签)
|
||||
- [ ] 3.2 更新 `iot_card_import` Service 的 CreateImportTask 方法:将请求中的 `realname_policy` 保存到 `IotCardImportTask.realname_policy`
|
||||
- [ ] 3.3 更新 IoT 卡导入 Worker 处理逻辑:创建 `IotCard` 记录时从任务的 `realname_policy` 写入卡的 `realname_policy`
|
||||
- [ ] 3.4 在 `ImportTaskResponse`(IoT 卡)中新增 `realname_policy` 字段(含 description 标签)
|
||||
- [x] 3.1 在 `ImportIotCardRequest` DTO 中新增 `realname_policy` 字段(含 validate、description 标签)
|
||||
- [x] 3.2 更新 `iot_card_import` Service 的 CreateImportTask 方法:将请求中的 `realname_policy` 保存到 `IotCardImportTask.realname_policy`
|
||||
- [x] 3.3 更新 IoT 卡导入 Worker 处理逻辑:创建 `IotCard` 记录时从任务的 `realname_policy` 写入卡的 `realname_policy`
|
||||
- [x] 3.4 在 `ImportTaskResponse`(IoT 卡)中新增 `realname_policy` 字段(含 description 标签)
|
||||
|
||||
## 4. 设备导入模块改造
|
||||
|
||||
- [ ] 4.1 在 `ImportDeviceRequest` DTO 中新增 `realname_policy` 字段(含 validate、description 标签)
|
||||
- [ ] 4.2 更新 `device_import` Service 的 CreateImportTask 方法:将请求中的 `realname_policy` 保存到 `DeviceImportTask.realname_policy`
|
||||
- [ ] 4.3 更新设备导入 Worker 处理逻辑:创建 `Device` 记录时从任务的 `realname_policy` 写入设备的 `realname_policy`
|
||||
- [ ] 4.4 在 `DeviceImportTaskResponse` 中新增 `realname_policy` 字段(含 description 标签)
|
||||
- [x] 4.1 在 `ImportDeviceRequest` DTO 中新增 `realname_policy` 字段(含 validate、description 标签)
|
||||
- [x] 4.2 更新 `device_import` Service 的 CreateImportTask 方法:将请求中的 `realname_policy` 保存到 `DeviceImportTask.realname_policy`
|
||||
- [x] 4.3 更新设备导入 Worker 处理逻辑:创建 `Device` 记录时从任务的 `realname_policy` 写入设备的 `realname_policy`
|
||||
- [x] 4.4 在 `DeviceImportTaskResponse` 中新增 `realname_policy` 字段(含 description 标签)
|
||||
|
||||
## 5. 后台管理查询接口 DTO 添加字段
|
||||
|
||||
- [ ] 5.1 在 `StandaloneIotCardResponse`(`iot_card_dto.go`)中新增 `realname_policy` 字段,更新 Store/Service 中的 DTO 填充逻辑
|
||||
- [ ] 5.2 在 `DeviceResponse`(`device_dto.go`)中新增 `realname_policy` 字段,更新填充逻辑
|
||||
- [ ] 5.3 在 `AssetResolveResponse`(`asset_dto.go`)中新增 `realname_policy` 字段,更新 `asset` Service 的 Resolve 方法填充逻辑
|
||||
- [ ] 5.4 在 `AssetRealtimeStatusResponse`(`asset_dto.go`)中新增 `realname_policy` 字段,更新填充逻辑
|
||||
- [ ] 5.5 在 `DeviceCardBindingResponse`(`device_dto.go`)中新增 `realname_policy` 字段,更新填充逻辑
|
||||
- [ ] 5.6 在 `BoundCardInfo`(`asset_dto.go`,后台管理端公共子结构)中新增 `realname_policy` 字段,更新填充逻辑
|
||||
- [x] 5.1 在 `StandaloneIotCardResponse`(`iot_card_dto.go`)中新增 `realname_policy` 字段,更新 Store/Service 中的 DTO 填充逻辑
|
||||
- [x] 5.2 在 `DeviceResponse`(`device_dto.go`)中新增 `realname_policy` 字段,更新填充逻辑
|
||||
- [x] 5.3 在 `AssetResolveResponse`(`asset_dto.go`)中新增 `realname_policy` 字段,更新 `asset` Service 的 Resolve 方法填充逻辑
|
||||
- [x] 5.4 在 `AssetRealtimeStatusResponse`(`asset_dto.go`)中新增 `realname_policy` 字段,更新填充逻辑
|
||||
- [x] 5.5 在 `DeviceCardBindingResponse`(`device_dto.go`)中新增 `realname_policy` 字段,更新填充逻辑
|
||||
- [x] 5.6 在 `BoundCardInfo`(`asset_dto.go`,后台管理端公共子结构)中新增 `realname_policy` 字段,更新填充逻辑
|
||||
|
||||
## 6. C 端查询接口 DTO 添加字段
|
||||
|
||||
- [ ] 6.1 在 `AssetInfoResponse`(`client_asset_dto.go`)中新增 `realname_policy` 字段,更新 `client_asset` Service 的填充逻辑
|
||||
- [ ] 6.2 在 C 端 `BoundCardInfo`(`client_asset_dto.go` 内嵌子结构)中新增 `realname_policy` 字段,更新填充逻辑
|
||||
- [ ] 6.3 在 `DeviceCardItem`(`client_realname_device_dto.go`)中新增 `realname_policy` 字段,更新 `GetDeviceCards` 接口填充逻辑
|
||||
- [x] 6.1 在 `AssetInfoResponse`(`client_asset_dto.go`)中新增 `realname_policy` 字段,更新 `client_asset` Service 的填充逻辑
|
||||
- [x] 6.2 在 C 端 `BoundCardInfo`(`client_asset_dto.go` 内嵌子结构)中新增 `realname_policy` 字段,更新填充逻辑
|
||||
- [x] 6.3 在 `DeviceCardItem`(`client_realname_device_dto.go`)中新增 `realname_policy` 字段,更新 `GetDeviceCards` 接口填充逻辑
|
||||
|
||||
## 7. C 端充值接口拦截(C3/C4)
|
||||
|
||||
- [ ] 7.1 在 `client_wallet` Service 的充值预检方法(C3 对应逻辑)中,在归属校验后插入实名策略检查:before_order + 未实名 → 返回 CodeNeedRealname
|
||||
- [ ] 7.2 在 `client_wallet` Service 的充值下单方法(C4 对应逻辑)中,在归属校验后、OpenID 查询前插入实名策略检查:before_order + 未实名 → 返回 CodeNeedRealname
|
||||
- [x] 7.1 在 `client_wallet` Service 的充值预检方法(C3 对应逻辑)中,在归属校验后插入实名策略检查:before_order + 未实名 → 返回 CodeNeedRealname
|
||||
- [x] 7.2 在 `client_wallet` Service 的充值下单方法(C4 对应逻辑)中,在归属校验后、OpenID 查询前插入实名策略检查:before_order + 未实名 → 返回 CodeNeedRealname
|
||||
|
||||
## 8. C 端购买套餐/订单接口拦截(D1/D4)
|
||||
|
||||
- [ ] 8.1 删除 `client_order` Service 中的 `REALNAME-03` 注释代码
|
||||
- [ ] 8.2 在 `client_order` Service 的 CreateOrder 方法(D1)中,套餐校验之后、OpenID 查询之前插入实名策略检查
|
||||
- [ ] 8.3 在 `client_order` Service 的 PayOrder 方法(D4)中,在执行支付前插入实名策略检查
|
||||
- [x] 8.1 删除 `client_order` Service 中的 `REALNAME-03` 注释代码
|
||||
- [x] 8.2 在 `client_order` Service 的 CreateOrder 方法(D1)中,套餐校验之后、OpenID 查询之前插入实名策略检查
|
||||
- [x] 8.3 在 `client_order` Service 的 PayOrder 方法(D4)中,在执行支付前插入实名策略检查
|
||||
|
||||
## 9. C 端实名链接接口改造(E1)
|
||||
|
||||
- [ ] 9.1 在 `client_realname` Service 的 GetRealnameLink 方法中,目标卡定位后,`real_name_status=1` 检查前,插入策略检查:after_order + 无有效充值/订单 → 返回 CodeRealnameNotAvailable
|
||||
- [x] 9.1 在 `client_realname` Service 的 GetRealnameLink 方法中,目标卡定位后,`real_name_status=1` 检查前,插入策略检查:after_order + 无有效充值/订单 → 返回 CodeRealnameNotAvailable
|
||||
- [ ] 9.2 验证 before_order 和 none 模式下实名链接接口行为不受影响
|
||||
|
||||
## 10. 后台管理更新接口(UpdateRealnameMode)
|
||||
|
||||
- [ ] 10.1 在 `iot_card` Store 中新增 `UpdateRealnamePolicy(ctx, cardID uint, policy string) error` 方法
|
||||
- [ ] 10.2 在 `device` Store 中新增 `UpdateRealnamePolicy(ctx, deviceID uint, policy string) error` 方法
|
||||
- [ ] 10.3 在 `iot_card` Service 中新增 `UpdateRealnamePolicy(ctx, cardID uint, policy string) error` 方法(含幂等检查)
|
||||
- [ ] 10.4 在 `device` Service 中新增 `UpdateRealnamePolicy(ctx, deviceID uint, policy string) error` 方法(含幂等检查)
|
||||
- [ ] 10.5 在 `asset_dto.go` 中新增 `UpdateAssetRealnameModRequest` 和 `UpdateAssetRealnameModResponse` DTO
|
||||
- [ ] 10.6 在 `asset` Handler(`internal/handler/admin/asset.go`)中新增 `UpdateRealnameMode` 方法,遵循 `UpdatePollingStatus` 模式
|
||||
- [ ] 10.7 在路由注册文件中注册新接口 `PATCH /api/admin/assets/:identifier/realname-mode`
|
||||
- [ ] 10.8 更新文档生成器 `cmd/api/docs.go` 和 `cmd/gendocs/main.go`,添加新 Handler
|
||||
- [x] 10.1 在 `iot_card` Store 中新增 `UpdateRealnamePolicy(ctx, cardID uint, policy string) error` 方法
|
||||
- [x] 10.2 在 `device` Store 中新增 `UpdateRealnamePolicy(ctx, deviceID uint, policy string) error` 方法
|
||||
- [x] 10.3 在 `iot_card` Service 中新增 `UpdateRealnamePolicy(ctx, cardID uint, policy string) error` 方法(含幂等检查)
|
||||
- [x] 10.4 在 `device` Service 中新增 `UpdateRealnamePolicy(ctx, deviceID uint, policy string) error` 方法(含幂等检查)
|
||||
- [x] 10.5 在 `asset_dto.go` 中新增 `UpdateAssetRealnameModRequest` 和 `UpdateAssetRealnameModResponse` DTO
|
||||
- [x] 10.6 在 `asset` Handler(`internal/handler/admin/asset.go`)中新增 `UpdateRealnameMode` 方法,遵循 `UpdatePollingStatus` 模式
|
||||
- [x] 10.7 在路由注册文件中注册新接口 `PATCH /api/admin/assets/:identifier/realname-mode`
|
||||
- [x] 10.8 更新文档生成器 `cmd/api/docs.go` 和 `cmd/gendocs/main.go`,添加新 Handler
|
||||
|
||||
## 11. 验证与收尾
|
||||
|
||||
- [ ] 11.1 运行 `lsp_diagnostics` 检查所有修改文件,确保无编译错误
|
||||
- [ ] 11.2 使用 PostgreSQL MCP 验证四张表的字段已正确添加,存量数据 realname_policy 均为 'none'
|
||||
- [x] 11.2 使用 PostgreSQL MCP 验证四张表的字段已正确添加,存量数据 realname_policy 均为 'none'
|
||||
- [ ] 11.3 手动验证 before_order 拦截:充值/购买接口在未实名时返回 CodeNeedRealname
|
||||
- [ ] 11.4 手动验证 after_order 拦截:实名链接接口在无充值/订单时被拦截,有充值后放行
|
||||
- [ ] 11.5 手动验证 none 模式:充值/购买/实名链接均正常放行
|
||||
|
||||
@@ -122,6 +122,13 @@ const (
|
||||
AssetTypeDevice = "device" // 设备
|
||||
)
|
||||
|
||||
// 实名认证策略
|
||||
const (
|
||||
RealnamePolicyNone = "none" // 无需实名
|
||||
RealnamePolicyBeforeOrder = "before_order" // 先实名后充值/购买
|
||||
RealnamePolicyAfterOrder = "after_order" // 先充值/购买后实名
|
||||
)
|
||||
|
||||
// 放款类型
|
||||
const (
|
||||
PaymentTypeManual = "manual" // 人工打款
|
||||
|
||||
@@ -151,6 +151,7 @@ const (
|
||||
CodeOldPhoneMismatch = 1186 // 旧手机号与当前绑定不匹配
|
||||
CodeNeedRealname = 1187 // 该套餐需实名认证后购买
|
||||
CodeOpenIDNotFound = 1188 // 未找到微信授权信息,请先完成授权
|
||||
CodeRealnameNotAvailable = 1189 // 请先完成充值或购买套餐后再进行实名认证
|
||||
|
||||
// 换货相关错误 (1200-1209)
|
||||
CodeExchangeOrderNotFound = 1200 // 换货单不存在
|
||||
@@ -287,6 +288,7 @@ var allErrorCodes = []int{
|
||||
CodeOldPhoneMismatch,
|
||||
CodeNeedRealname,
|
||||
CodeOpenIDNotFound,
|
||||
CodeRealnameNotAvailable,
|
||||
CodeExchangeOrderNotFound,
|
||||
CodeExchangeInProgress,
|
||||
CodeExchangeStatusInvalid,
|
||||
@@ -418,6 +420,7 @@ var errorMessages = map[int]string{
|
||||
CodeOldPhoneMismatch: "旧手机号与当前绑定不匹配",
|
||||
CodeNeedRealname: "该套餐需实名认证后购买",
|
||||
CodeOpenIDNotFound: "未找到微信授权信息,请先完成授权",
|
||||
CodeRealnameNotAvailable: "请先完成充值或购买套餐后再进行实名认证",
|
||||
CodeExchangeOrderNotFound: "换货单不存在或无权限",
|
||||
CodeExchangeInProgress: "该资产存在进行中的换货单",
|
||||
CodeExchangeStatusInvalid: "换货单当前状态不允许此操作",
|
||||
|
||||
@@ -56,7 +56,7 @@ func BuildDocHandlers() *bootstrap.Handlers {
|
||||
PollingAlert: admin.NewPollingAlertHandler(nil),
|
||||
PollingCleanup: admin.NewPollingCleanupHandler(nil),
|
||||
PollingManualTrigger: admin.NewPollingManualTriggerHandler(nil),
|
||||
Asset: admin.NewAssetHandler(nil, nil, nil, nil),
|
||||
Asset: admin.NewAssetHandler(nil, nil, nil, nil, nil),
|
||||
AssetLifecycle: admin.NewAssetLifecycleHandler(nil),
|
||||
AssetWallet: admin.NewAssetWalletHandler(nil),
|
||||
WechatConfig: admin.NewWechatConfigHandler(nil),
|
||||
|
||||
Reference in New Issue
Block a user