feat: Excel 导入按列位置读取,IoT 卡导入支持卡业务类型
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m28s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m28s
- 重写 parseCardRows:移除列名识别,固定跳过第1行表头,按 col0=ICCID、 col1=MSISDN、col2=VirtualNo 取值,VirtualNo 升级为必填字段 - 重写 ParseDeviceExcel:移除 buildDeviceColumnIndex,IMEI 调整至第5列 新增 MaxSimSlots 范围校验 [1,4],整行为空时不计入 total - 删除 findCardColumns 和 buildDeviceColumnIndex 两个冗余函数 - ImportIotCardRequest 新增 card_category 字段(normal/industry,默认 normal) - IotCardImportTask model 新增 card_category 字段 - 迁移 000111:tb_iot_card_import_task 新增 card_category 列 - CreateImportTask 将 req.CardCategory 写入任务,空串兜底 normal - processBatch 改用 task.CardCategory 替代写死的 CardCategoryNormal - ImportTaskResponse 新增 card_category 字段返回 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -120,55 +120,50 @@ func ParseDeviceExcel(filePath string) (*DeviceParseResult, error) {
|
||||
return nil, ErrExcelNoData
|
||||
}
|
||||
|
||||
header := rows[0]
|
||||
colIndex := buildDeviceColumnIndex(header)
|
||||
|
||||
parseResult := &DeviceParseResult{
|
||||
Rows: make([]DeviceRow, 0),
|
||||
ParseErrors: make([]CSVParseError, 0),
|
||||
}
|
||||
|
||||
// 按固定列位置读取,第1行为表头跳过:
|
||||
// col0=虚拟号 col1=设备名称 col2=设备型号 col3=设备类型 col4=IMEI
|
||||
// col5=制造商 col6=最大SIM槽数 col7~10=卡1~4 ICCID
|
||||
for i := 1; i < len(rows); i++ {
|
||||
record := rows[i]
|
||||
lineNum := i + 1
|
||||
|
||||
row := DeviceRow{Line: lineNum}
|
||||
|
||||
if idx := colIndex["virtual_no"]; idx >= 0 && idx < len(record) {
|
||||
row.VirtualNo = strings.TrimSpace(record[idx])
|
||||
}
|
||||
if idx := colIndex["imei"]; idx >= 0 && idx < len(record) {
|
||||
row.IMEI = strings.TrimSpace(record[idx])
|
||||
}
|
||||
if idx := colIndex["device_name"]; idx >= 0 && idx < len(record) {
|
||||
row.DeviceName = strings.TrimSpace(record[idx])
|
||||
}
|
||||
if idx := colIndex["device_model"]; idx >= 0 && idx < len(record) {
|
||||
row.DeviceModel = strings.TrimSpace(record[idx])
|
||||
}
|
||||
if idx := colIndex["device_type"]; idx >= 0 && idx < len(record) {
|
||||
row.DeviceType = strings.TrimSpace(record[idx])
|
||||
}
|
||||
if idx := colIndex["max_sim_slots"]; idx >= 0 && idx < len(record) {
|
||||
if n, err := strconv.Atoi(strings.TrimSpace(record[idx])); err == nil {
|
||||
row.MaxSimSlots = n
|
||||
getCol := func(idx int) string {
|
||||
if idx < len(record) {
|
||||
return strings.TrimSpace(record[idx])
|
||||
}
|
||||
}
|
||||
if idx := colIndex["manufacturer"]; idx >= 0 && idx < len(record) {
|
||||
row.Manufacturer = strings.TrimSpace(record[idx])
|
||||
return ""
|
||||
}
|
||||
|
||||
row.VirtualNo = getCol(0)
|
||||
row.DeviceName = getCol(1)
|
||||
row.DeviceModel = getCol(2)
|
||||
row.DeviceType = getCol(3)
|
||||
row.IMEI = getCol(4)
|
||||
row.Manufacturer = getCol(5)
|
||||
maxSimSlotsStr := getCol(6)
|
||||
|
||||
row.ICCIDs = make([]string, 0, 4)
|
||||
for j := 1; j <= 4; j++ {
|
||||
colName := "iccid_" + strconv.Itoa(j)
|
||||
if idx := colIndex[colName]; idx >= 0 && idx < len(record) {
|
||||
iccid := strings.TrimSpace(record[idx])
|
||||
if iccid != "" {
|
||||
row.ICCIDs = append(row.ICCIDs, iccid)
|
||||
}
|
||||
for j := 7; j <= 10; j++ {
|
||||
iccid := getCol(j)
|
||||
if iccid != "" {
|
||||
row.ICCIDs = append(row.ICCIDs, iccid)
|
||||
}
|
||||
}
|
||||
|
||||
// 整行目标列皆为空,视为空行跳过,不计入 total
|
||||
if row.VirtualNo == "" && row.DeviceName == "" && row.DeviceModel == "" &&
|
||||
row.DeviceType == "" && row.IMEI == "" && row.Manufacturer == "" &&
|
||||
maxSimSlotsStr == "" && len(row.ICCIDs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
parseResult.TotalCount++
|
||||
|
||||
if row.VirtualNo == "" {
|
||||
@@ -179,8 +174,22 @@ func ParseDeviceExcel(filePath string) (*DeviceParseResult, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析最大SIM槽数
|
||||
if maxSimSlotsStr != "" {
|
||||
if n, err := strconv.Atoi(maxSimSlotsStr); err == nil {
|
||||
row.MaxSimSlots = n
|
||||
}
|
||||
}
|
||||
|
||||
// 范围校验:不在 [1,4] 则失败;为空/0 时回填默认值 4
|
||||
if row.MaxSimSlots == 0 {
|
||||
row.MaxSimSlots = 4
|
||||
} else if row.MaxSimSlots < 1 || row.MaxSimSlots > 4 {
|
||||
parseResult.ParseErrors = append(parseResult.ParseErrors, CSVParseError{
|
||||
Line: lineNum,
|
||||
Reason: "最大SIM槽数必须在1-4之间",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
parseResult.Rows = append(parseResult.Rows, row)
|
||||
@@ -209,70 +218,44 @@ func selectSheet(f *excelize.File) string {
|
||||
}
|
||||
|
||||
// parseCardRows 解析卡数据行
|
||||
// 自动检测表头并提取ICCID和MSISDN列
|
||||
// 第1行固定为表头跳过,按列位置取值:col0=ICCID、col1=MSISDN、col2=VirtualNo
|
||||
func parseCardRows(rows [][]string) (*CSVParseResult, error) {
|
||||
result := &CSVParseResult{
|
||||
Cards: make([]CardInfo, 0),
|
||||
ParseErrors: make([]CSVParseError, 0),
|
||||
}
|
||||
|
||||
headerSkipped := false
|
||||
iccidCol, msisdnCol, virtualNoCol := -1, -1, -1
|
||||
|
||||
if len(rows) > 0 {
|
||||
iccidCol, msisdnCol, virtualNoCol = findCardColumns(rows[0])
|
||||
if iccidCol >= 0 && msisdnCol >= 0 {
|
||||
headerSkipped = true
|
||||
}
|
||||
}
|
||||
|
||||
startLine := 0
|
||||
if headerSkipped {
|
||||
startLine = 1
|
||||
}
|
||||
|
||||
for i := startLine; i < len(rows); i++ {
|
||||
// 从第2行(索引1)开始,跳过第1行表头
|
||||
for i := 1; i < len(rows); i++ {
|
||||
row := rows[i]
|
||||
lineNum := i + 1
|
||||
|
||||
if len(row) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
iccid := ""
|
||||
msisdn := ""
|
||||
virtualNo := ""
|
||||
|
||||
if iccidCol >= 0 {
|
||||
if iccidCol < len(row) {
|
||||
iccid = strings.TrimSpace(row[iccidCol])
|
||||
}
|
||||
if msisdnCol < len(row) {
|
||||
msisdn = strings.TrimSpace(row[msisdnCol])
|
||||
}
|
||||
if virtualNoCol >= 0 && virtualNoCol < len(row) {
|
||||
virtualNo = strings.TrimSpace(row[virtualNoCol])
|
||||
}
|
||||
} else {
|
||||
if len(row) >= 1 {
|
||||
iccid = strings.TrimSpace(row[0])
|
||||
}
|
||||
if len(row) >= 2 {
|
||||
msisdn = strings.TrimSpace(row[1])
|
||||
}
|
||||
if len(row) >= 1 {
|
||||
iccid = strings.TrimSpace(row[0])
|
||||
}
|
||||
if len(row) >= 2 {
|
||||
msisdn = strings.TrimSpace(row[1])
|
||||
}
|
||||
if len(row) >= 3 {
|
||||
virtualNo = strings.TrimSpace(row[2])
|
||||
}
|
||||
|
||||
// 三列皆为空视为空行,跳过且不计入 total
|
||||
if iccid == "" && msisdn == "" && virtualNo == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
result.TotalCount++
|
||||
|
||||
if iccid == "" && msisdn == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if iccid == "" {
|
||||
result.ParseErrors = append(result.ParseErrors, CSVParseError{
|
||||
Line: lineNum,
|
||||
MSISDN: msisdn,
|
||||
Reason: "ICCID不能为空",
|
||||
Reason: "ICCID 不能为空",
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -281,7 +264,16 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) {
|
||||
result.ParseErrors = append(result.ParseErrors, CSVParseError{
|
||||
Line: lineNum,
|
||||
ICCID: iccid,
|
||||
Reason: "MSISDN不能为空",
|
||||
Reason: "MSISDN 不能为空",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if virtualNo == "" {
|
||||
result.ParseErrors = append(result.ParseErrors, CSVParseError{
|
||||
Line: lineNum,
|
||||
ICCID: iccid,
|
||||
Reason: "虚拟号(virtual_no)不能为空",
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -296,68 +288,3 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// findCardColumns 查找ICCID、MSISDN和VirtualNo列索引
|
||||
// 支持中英文列名识别
|
||||
func findCardColumns(header []string) (iccidCol, msisdnCol, virtualNoCol int) {
|
||||
iccidCol, msisdnCol, virtualNoCol = -1, -1, -1
|
||||
|
||||
for i, col := range header {
|
||||
colLower := strings.ToLower(strings.TrimSpace(col))
|
||||
|
||||
if colLower == "iccid" || colLower == "卡号" || colLower == "号码" {
|
||||
if iccidCol == -1 {
|
||||
iccidCol = i
|
||||
}
|
||||
}
|
||||
|
||||
if colLower == "msisdn" || colLower == "接入号" || colLower == "手机号" || colLower == "电话" {
|
||||
if msisdnCol == -1 {
|
||||
msisdnCol = i
|
||||
}
|
||||
}
|
||||
|
||||
if colLower == "virtual_no" || colLower == "virtualno" || colLower == "虚拟号" || colLower == "设备号" {
|
||||
if virtualNoCol == -1 {
|
||||
virtualNoCol = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return iccidCol, msisdnCol, virtualNoCol
|
||||
}
|
||||
|
||||
// buildDeviceColumnIndex 构建设备导入列索引
|
||||
// 识别表头中的列名,返回列名到列索引的映射
|
||||
// IMEI 列支持多种列名格式:imei/IMEI/设备IMEI/IMEI号
|
||||
func buildDeviceColumnIndex(header []string) map[string]int {
|
||||
index := map[string]int{
|
||||
"virtual_no": -1,
|
||||
"imei": -1,
|
||||
"device_name": -1,
|
||||
"device_model": -1,
|
||||
"device_type": -1,
|
||||
"max_sim_slots": -1,
|
||||
"manufacturer": -1,
|
||||
"iccid_1": -1,
|
||||
"iccid_2": -1,
|
||||
"iccid_3": -1,
|
||||
"iccid_4": -1,
|
||||
}
|
||||
|
||||
for i, col := range header {
|
||||
normalized := strings.ToLower(strings.TrimSpace(col))
|
||||
// 支持 IMEI 多种列名格式
|
||||
switch normalized {
|
||||
case "imei", "设备imei", "imei号":
|
||||
if index["imei"] == -1 {
|
||||
index["imei"] = i
|
||||
}
|
||||
default:
|
||||
if _, exists := index[normalized]; exists {
|
||||
index[normalized] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user