All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
305 lines
7.0 KiB
Go
305 lines
7.0 KiB
Go
package utils
|
||
|
||
import (
|
||
stderrors "errors"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/xuri/excelize/v2"
|
||
)
|
||
|
||
// CardInfo 卡信息(ICCID + MSISDN + VirtualNo)
|
||
type CardInfo struct {
|
||
ICCID string
|
||
MSISDN string
|
||
VirtualNo string
|
||
}
|
||
|
||
// CSVParseResult Excel/CSV 解析结果
|
||
type CSVParseResult struct {
|
||
Cards []CardInfo
|
||
TotalCount int
|
||
ParseErrors []CSVParseError
|
||
}
|
||
|
||
// CSVParseError Excel/CSV 解析错误
|
||
type CSVParseError struct {
|
||
Line int
|
||
ICCID string
|
||
MSISDN string
|
||
Reason string
|
||
}
|
||
|
||
// DeviceParseResult 设备导入 Excel 解析结果
|
||
type DeviceParseResult struct {
|
||
Rows []DeviceRow
|
||
TotalCount int
|
||
ParseErrors []CSVParseError
|
||
}
|
||
|
||
// DeviceRow 设备导入数据行
|
||
type DeviceRow struct {
|
||
Line int
|
||
VirtualNo string
|
||
SN string
|
||
IMEI string // 设备IMEI,对应 Excel IMEI 列,用于 Gateway API 调用
|
||
DeviceName string
|
||
DeviceModel string
|
||
DeviceType string
|
||
MaxSimSlots int
|
||
Manufacturer string
|
||
SlotICCIDs []DeviceSlotICCID
|
||
}
|
||
|
||
// DeviceSlotICCID 设备槽位与 ICCID 的映射
|
||
type DeviceSlotICCID struct {
|
||
SlotPosition int
|
||
ICCID string
|
||
}
|
||
|
||
var (
|
||
// ErrExcelNoSheets Excel文件无工作表
|
||
ErrExcelNoSheets = stderrors.New("Excel文件无工作表")
|
||
// ErrExcelNoData Excel文件无数据行
|
||
ErrExcelNoData = stderrors.New("Excel文件无数据行(至少需要表头+1行数据)")
|
||
)
|
||
|
||
// ParseCardExcel 解析包含 ICCID 和 MSISDN 两列的 Excel 文件
|
||
// filePath: Excel文件路径(.xlsx格式)
|
||
// 返回: 解析结果 (与CSV解析器返回相同的数据结构)
|
||
func ParseCardExcel(filePath string) (*CSVParseResult, error) {
|
||
// 1. 打开Excel文件
|
||
f, err := excelize.OpenFile(filePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("打开Excel失败: %w", err)
|
||
}
|
||
defer func() {
|
||
if err := f.Close(); err != nil {
|
||
// 日志记录关闭错误,但不影响解析结果
|
||
}
|
||
}()
|
||
|
||
// 2. 选择sheet (优先"导入数据",否则第一个)
|
||
sheetName := selectSheet(f)
|
||
if sheetName == "" {
|
||
return nil, ErrExcelNoSheets
|
||
}
|
||
|
||
// 3. 读取所有行
|
||
rows, err := f.GetRows(sheetName)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取sheet失败: %w", err)
|
||
}
|
||
|
||
if len(rows) < 2 {
|
||
return nil, ErrExcelNoData
|
||
}
|
||
|
||
// 4. 解析表头 + 数据行
|
||
return parseCardRows(rows)
|
||
}
|
||
|
||
// ParseDeviceExcel 解析设备导入 Excel 文件
|
||
// filePath: Excel文件路径(.xlsx格式)
|
||
// 返回: 解析结果(包含有效行和失败行)、错误
|
||
func ParseDeviceExcel(filePath string) (*DeviceParseResult, error) {
|
||
f, err := excelize.OpenFile(filePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("打开Excel失败: %w", err)
|
||
}
|
||
defer func() {
|
||
if err := f.Close(); err != nil {
|
||
}
|
||
}()
|
||
|
||
sheetName := selectSheet(f)
|
||
if sheetName == "" {
|
||
return nil, ErrExcelNoSheets
|
||
}
|
||
|
||
rows, err := f.GetRows(sheetName)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取sheet失败: %w", err)
|
||
}
|
||
|
||
if len(rows) < 2 {
|
||
return nil, ErrExcelNoData
|
||
}
|
||
|
||
parseResult := &DeviceParseResult{
|
||
Rows: make([]DeviceRow, 0),
|
||
ParseErrors: make([]CSVParseError, 0),
|
||
}
|
||
|
||
// 按固定列位置读取,第1行为表头跳过:
|
||
// col0=虚拟号 col1=SN col2=设备名称 col3=设备型号 col4=设备类型
|
||
// col5=IMEI col6=制造商 col7=最大SIM槽数 col8~11=卡1~4 ICCID
|
||
for i := 1; i < len(rows); i++ {
|
||
record := rows[i]
|
||
lineNum := i + 1
|
||
|
||
row := DeviceRow{Line: lineNum}
|
||
|
||
getCol := func(idx int) string {
|
||
if idx < len(record) {
|
||
return strings.TrimSpace(record[idx])
|
||
}
|
||
return ""
|
||
}
|
||
|
||
row.VirtualNo = getCol(0)
|
||
row.SN = getCol(1)
|
||
row.DeviceName = getCol(2)
|
||
row.DeviceModel = getCol(3)
|
||
row.DeviceType = getCol(4)
|
||
row.IMEI = getCol(5)
|
||
row.Manufacturer = getCol(6)
|
||
maxSimSlotsStr := getCol(7)
|
||
|
||
row.SlotICCIDs = make([]DeviceSlotICCID, 0, 4)
|
||
for j := 8; j <= 11; j++ {
|
||
iccid := getCol(j)
|
||
if iccid != "" {
|
||
row.SlotICCIDs = append(row.SlotICCIDs, DeviceSlotICCID{
|
||
SlotPosition: j - 7,
|
||
ICCID: iccid,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 整行目标列皆为空,视为空行跳过,不计入 total
|
||
if row.VirtualNo == "" && row.SN == "" && row.DeviceName == "" && row.DeviceModel == "" &&
|
||
row.DeviceType == "" && row.IMEI == "" && row.Manufacturer == "" &&
|
||
maxSimSlotsStr == "" && len(row.SlotICCIDs) == 0 {
|
||
continue
|
||
}
|
||
|
||
parseResult.TotalCount++
|
||
|
||
if row.VirtualNo == "" {
|
||
parseResult.ParseErrors = append(parseResult.ParseErrors, CSVParseError{
|
||
Line: lineNum,
|
||
Reason: "设备虚拟号(virtual_no)不能为空",
|
||
})
|
||
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
|
||
}
|
||
|
||
for _, slotICCID := range row.SlotICCIDs {
|
||
if slotICCID.SlotPosition > row.MaxSimSlots {
|
||
parseResult.ParseErrors = append(parseResult.ParseErrors, CSVParseError{
|
||
Line: lineNum,
|
||
ICCID: slotICCID.ICCID,
|
||
Reason: "卡槽填写超出最大SIM槽数",
|
||
})
|
||
goto nextDeviceRow
|
||
}
|
||
}
|
||
|
||
parseResult.Rows = append(parseResult.Rows, row)
|
||
|
||
nextDeviceRow:
|
||
}
|
||
|
||
return parseResult, nil
|
||
}
|
||
|
||
// selectSheet 选择要读取的sheet
|
||
// 优先返回名为"导入数据"的sheet,否则返回第一个sheet
|
||
func selectSheet(f *excelize.File) string {
|
||
sheets := f.GetSheetList()
|
||
if len(sheets) == 0 {
|
||
return ""
|
||
}
|
||
|
||
// 优先查找"导入数据"sheet
|
||
for _, sheet := range sheets {
|
||
if sheet == "导入数据" {
|
||
return sheet
|
||
}
|
||
}
|
||
|
||
// 返回第一个sheet
|
||
return sheets[0]
|
||
}
|
||
|
||
// parseCardRows 解析卡数据行
|
||
// 第1行固定为表头跳过,按列位置取值:col0=ICCID、col1=MSISDN、col2=VirtualNo
|
||
func parseCardRows(rows [][]string) (*CSVParseResult, error) {
|
||
result := &CSVParseResult{
|
||
Cards: make([]CardInfo, 0),
|
||
ParseErrors: make([]CSVParseError, 0),
|
||
}
|
||
|
||
// 从第2行(索引1)开始,跳过第1行表头
|
||
for i := 1; i < len(rows); i++ {
|
||
row := rows[i]
|
||
lineNum := i + 1
|
||
|
||
iccid := ""
|
||
msisdn := ""
|
||
virtualNo := ""
|
||
|
||
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 == "" {
|
||
result.ParseErrors = append(result.ParseErrors, CSVParseError{
|
||
Line: lineNum,
|
||
MSISDN: msisdn,
|
||
Reason: "ICCID 不能为空",
|
||
})
|
||
continue
|
||
}
|
||
|
||
if msisdn == "" {
|
||
result.ParseErrors = append(result.ParseErrors, CSVParseError{
|
||
Line: lineNum,
|
||
ICCID: iccid,
|
||
Reason: "MSISDN 不能为空",
|
||
})
|
||
continue
|
||
}
|
||
|
||
result.Cards = append(result.Cards, CardInfo{
|
||
ICCID: iccid,
|
||
MSISDN: msisdn,
|
||
VirtualNo: virtualNo,
|
||
})
|
||
}
|
||
|
||
return result, nil
|
||
}
|