Compare commits
2 Commits
303bffbe84
...
14a8ea5a2c
| Author | SHA1 | Date | |
|---|---|---|---|
| 14a8ea5a2c | |||
| 38ef73fd7f |
@@ -2201,6 +2201,30 @@ components:
|
|||||||
- password
|
- password
|
||||||
- user_type
|
- user_type
|
||||||
type: object
|
type: object
|
||||||
|
DtoCreateAdminOrderRequest:
|
||||||
|
properties:
|
||||||
|
identifier:
|
||||||
|
description: 资产标识符(ICCID 或 VirtualNo)
|
||||||
|
maxLength: 100
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
package_ids:
|
||||||
|
description: 套餐ID列表
|
||||||
|
items:
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
maxItems: 10
|
||||||
|
minItems: 1
|
||||||
|
nullable: true
|
||||||
|
type: array
|
||||||
|
payment_method:
|
||||||
|
description: 支付方式 (wallet:钱包支付, offline:线下支付)
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- identifier
|
||||||
|
- package_ids
|
||||||
|
- payment_method
|
||||||
|
type: object
|
||||||
DtoCreateAgentRechargeRequest:
|
DtoCreateAgentRechargeRequest:
|
||||||
properties:
|
properties:
|
||||||
amount:
|
amount:
|
||||||
@@ -2427,38 +2451,6 @@ components:
|
|||||||
description: 提现单号
|
description: 提现单号
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
DtoCreateOrderRequest:
|
|
||||||
properties:
|
|
||||||
device_id:
|
|
||||||
description: 设备ID(设备购买时必填)
|
|
||||||
minimum: 0
|
|
||||||
nullable: true
|
|
||||||
type: integer
|
|
||||||
iot_card_id:
|
|
||||||
description: IoT卡ID(单卡购买时必填)
|
|
||||||
minimum: 0
|
|
||||||
nullable: true
|
|
||||||
type: integer
|
|
||||||
order_type:
|
|
||||||
description: 订单类型 (single_card:单卡购买, device:设备购买)
|
|
||||||
type: string
|
|
||||||
package_ids:
|
|
||||||
description: 套餐ID列表
|
|
||||||
items:
|
|
||||||
minimum: 0
|
|
||||||
type: integer
|
|
||||||
maxItems: 10
|
|
||||||
minItems: 1
|
|
||||||
nullable: true
|
|
||||||
type: array
|
|
||||||
payment_method:
|
|
||||||
description: 支付方式 (wallet:钱包支付, wechat:微信支付, alipay:支付宝支付)
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- order_type
|
|
||||||
- package_ids
|
|
||||||
- payment_method
|
|
||||||
type: object
|
|
||||||
DtoCreatePackageRequest:
|
DtoCreatePackageRequest:
|
||||||
properties:
|
properties:
|
||||||
calendar_type:
|
calendar_type:
|
||||||
@@ -15396,7 +15388,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/DtoCreateOrderRequest'
|
$ref: '#/components/schemas/DtoCreateAdminOrderRequest'
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
content:
|
content:
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ type IotCard struct {
|
|||||||
AssetStatus int `gorm:"column:asset_status;type:int;not null;default:1;comment:业务状态 1-在库 2-已销售 3-已换货 4-已停用" json:"asset_status"`
|
AssetStatus int `gorm:"column:asset_status;type:int;not null;default:1;comment:业务状态 1-在库 2-已销售 3-已换货 4-已停用" json:"asset_status"`
|
||||||
Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"`
|
Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"`
|
||||||
IsStandalone bool `gorm:"column:is_standalone;type:boolean;default:true;not null;comment:是否为独立卡(未绑定设备) 由触发器自动维护" json:"is_standalone"`
|
IsStandalone bool `gorm:"column:is_standalone;type:boolean;default:true;not null;comment:是否为独立卡(未绑定设备) 由触发器自动维护" json:"is_standalone"`
|
||||||
VirtualNo string `gorm:"column:virtual_no;type:varchar(50);not null;uniqueIndex:idx_iot_card_virtual_no,where:deleted_at IS NULL;comment:虚拟号(必填,全局唯一)" json:"virtual_no"`
|
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"`
|
||||||
LastGatewayReadingMB float64 `gorm:"column:last_gateway_reading_mb;type:float;not null;default:0;comment:上次轮询时网关返回的流量读数(MB)" json:"last_gateway_reading_mb"`
|
LastGatewayReadingMB float64 `gorm:"column:last_gateway_reading_mb;type:float;not null;default:0;comment:上次轮询时网关返回的流量读数(MB)" json:"last_gateway_reading_mb"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -305,8 +305,10 @@ func (h *IotCardImportHandler) processBatch(ctx context.Context, task *model.Iot
|
|||||||
|
|
||||||
virtualNos := make([]string, 0, len(newCards))
|
virtualNos := make([]string, 0, len(newCards))
|
||||||
for _, card := range newCards {
|
for _, card := range newCards {
|
||||||
|
if card.VirtualNo != "" {
|
||||||
virtualNos = append(virtualNos, card.VirtualNo)
|
virtualNos = append(virtualNos, card.VirtualNo)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
existingVirtualNos := make(map[string]bool)
|
existingVirtualNos := make(map[string]bool)
|
||||||
existingVirtualNos, err = h.iotCardStore.ExistsByVirtualNoBatch(ctx, virtualNos)
|
existingVirtualNos, err = h.iotCardStore.ExistsByVirtualNoBatch(ctx, virtualNos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -320,17 +322,7 @@ func (h *IotCardImportHandler) processBatch(ctx context.Context, task *model.Iot
|
|||||||
finalCards := make([]model.CardItem, 0, len(newCards))
|
finalCards := make([]model.CardItem, 0, len(newCards))
|
||||||
for _, card := range newCards {
|
for _, card := range newCards {
|
||||||
meta := cardMetaMap[card.ICCID]
|
meta := cardMetaMap[card.ICCID]
|
||||||
if card.VirtualNo == "" {
|
if card.VirtualNo != "" && (existingVirtualNos[card.VirtualNo] || batchUsedVirtualNos[card.VirtualNo]) {
|
||||||
result.failedItems = append(result.failedItems, model.ImportResultItem{
|
|
||||||
Line: meta.line,
|
|
||||||
ICCID: card.ICCID,
|
|
||||||
MSISDN: meta.msisdn,
|
|
||||||
Reason: "虚拟号(virtual_no)不能为空",
|
|
||||||
})
|
|
||||||
result.failCount++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if existingVirtualNos[card.VirtualNo] || batchUsedVirtualNos[card.VirtualNo] {
|
|
||||||
result.failedItems = append(result.failedItems, model.ImportResultItem{
|
result.failedItems = append(result.failedItems, model.ImportResultItem{
|
||||||
Line: meta.line,
|
Line: meta.line,
|
||||||
ICCID: card.ICCID,
|
ICCID: card.ICCID,
|
||||||
@@ -378,9 +370,11 @@ func (h *IotCardImportHandler) processBatch(ctx context.Context, task *model.Iot
|
|||||||
if err := txIdentifierStore.Register(ctx, card.ICCID, model.AssetTypeIotCard, iotCard.ID); err != nil {
|
if err := txIdentifierStore.Register(ctx, card.ICCID, model.AssetTypeIotCard, iotCard.ID); err != nil {
|
||||||
return fmt.Errorf("ICCID 已被占用: %s", card.ICCID)
|
return fmt.Errorf("ICCID 已被占用: %s", card.ICCID)
|
||||||
}
|
}
|
||||||
|
if card.VirtualNo != "" {
|
||||||
if err := txIdentifierStore.Register(ctx, card.VirtualNo, model.AssetTypeIotCard, iotCard.ID); err != nil {
|
if err := txIdentifierStore.Register(ctx, card.VirtualNo, model.AssetTypeIotCard, iotCard.ID); err != nil {
|
||||||
return fmt.Errorf("虚拟号已被占用: %s", card.VirtualNo)
|
return fmt.Errorf("虚拟号已被占用: %s", card.VirtualNo)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
|
|||||||
13
migrations/000108_iot_card_virtual_no_optional.down.sql
Normal file
13
migrations/000108_iot_card_virtual_no_optional.down.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-- 回滚:将 tb_iot_card.virtual_no 恢复为 NOT NULL,并重建无条件唯一索引
|
||||||
|
|
||||||
|
-- 清理空值记录(如有)
|
||||||
|
DELETE FROM tb_iot_card WHERE virtual_no IS NULL OR virtual_no = '';
|
||||||
|
|
||||||
|
-- 删除条件唯一索引
|
||||||
|
DROP INDEX IF EXISTS idx_iot_card_virtual_no;
|
||||||
|
|
||||||
|
-- 将 virtual_no 列改为 NOT NULL
|
||||||
|
ALTER TABLE tb_iot_card ALTER COLUMN virtual_no SET NOT NULL;
|
||||||
|
|
||||||
|
-- 重建无条件唯一索引
|
||||||
|
CREATE UNIQUE INDEX idx_iot_card_virtual_no ON tb_iot_card(virtual_no) WHERE deleted_at IS NULL;
|
||||||
13
migrations/000108_iot_card_virtual_no_optional.up.sql
Normal file
13
migrations/000108_iot_card_virtual_no_optional.up.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-- 将 tb_iot_card.virtual_no 恢复为可选字段(可为空)
|
||||||
|
-- 背景:业务确认单卡导入时虚拟号非必填,不能强制要求
|
||||||
|
-- 步骤:1. 删除无条件唯一索引,2. 移除 NOT NULL 约束,3. 重建条件唯一索引(允许多条空值共存)
|
||||||
|
|
||||||
|
-- 删除当前无条件唯一索引
|
||||||
|
DROP INDEX IF EXISTS idx_iot_card_virtual_no;
|
||||||
|
|
||||||
|
-- 将 virtual_no 列改回允许 NULL
|
||||||
|
ALTER TABLE tb_iot_card ALTER COLUMN virtual_no DROP NOT NULL;
|
||||||
|
|
||||||
|
-- 重建条件唯一索引(仅对非空、非空字符串的值保证唯一;允许多条空值/NULL 共存)
|
||||||
|
CREATE UNIQUE INDEX idx_iot_card_virtual_no ON tb_iot_card(virtual_no)
|
||||||
|
WHERE deleted_at IS NULL AND virtual_no IS NOT NULL AND virtual_no <> '';
|
||||||
@@ -7,8 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/xuri/excelize/v2"
|
"github.com/xuri/excelize/v2"
|
||||||
|
|
||||||
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CardInfo 卡信息(ICCID + MSISDN + VirtualNo)
|
// CardInfo 卡信息(ICCID + MSISDN + VirtualNo)
|
||||||
@@ -228,10 +226,6 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if virtualNoCol == -1 {
|
|
||||||
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "Excel 文件缺少 virtual_no 列,请使用最新模板")
|
|
||||||
}
|
|
||||||
|
|
||||||
startLine := 0
|
startLine := 0
|
||||||
if headerSkipped {
|
if headerSkipped {
|
||||||
startLine = 1
|
startLine = 1
|
||||||
@@ -292,16 +286,6 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if virtualNo == "" {
|
|
||||||
result.ParseErrors = append(result.ParseErrors, CSVParseError{
|
|
||||||
Line: lineNum,
|
|
||||||
ICCID: iccid,
|
|
||||||
MSISDN: msisdn,
|
|
||||||
Reason: "虚拟号(virtual_no)不能为空",
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Cards = append(result.Cards, CardInfo{
|
result.Cards = append(result.Cards, CardInfo{
|
||||||
ICCID: iccid,
|
ICCID: iccid,
|
||||||
MSISDN: msisdn,
|
MSISDN: msisdn,
|
||||||
|
|||||||
Reference in New Issue
Block a user