Files
junhong_cmp_fiber/pkg/utils/excel.go
huang 21b73a750d
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
c端当前套餐开始时间过期时间,以及excel导入字段
2026-05-06 09:57:58 +08:00

305 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}