feat: 新增数据库迁移,重命名 device_no 为 virtual_no,新增 iot_card.virtual_no 和 package.virtual_ratio 字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-03-14 18:27:28 +08:00
parent b5147d1acb
commit b9c3875c08
77 changed files with 5832 additions and 2393 deletions

View File

@@ -9,10 +9,11 @@ import (
"github.com/xuri/excelize/v2"
)
// CardInfo 卡信息(ICCID + MSISDN)
// CardInfo 卡信息(ICCID + MSISDN + VirtualNo)
type CardInfo struct {
ICCID string
MSISDN string
ICCID string
MSISDN string
VirtualNo string
}
// CSVParseResult Excel/CSV 解析结果
@@ -33,7 +34,7 @@ type CSVParseError struct {
// DeviceRow 设备导入数据行
type DeviceRow struct {
Line int
DeviceNo string
VirtualNo string
DeviceName string
DeviceModel string
DeviceType string
@@ -128,8 +129,8 @@ func ParseDeviceExcel(filePath string) ([]DeviceRow, int, error) {
row := DeviceRow{Line: lineNum}
// 提取各字段
if idx := colIndex["device_no"]; idx >= 0 && idx < len(record) {
row.DeviceNo = strings.TrimSpace(record[idx])
if idx := colIndex["virtual_no"]; idx >= 0 && idx < len(record) {
row.VirtualNo = strings.TrimSpace(record[idx])
}
if idx := colIndex["device_name"]; idx >= 0 && idx < len(record) {
row.DeviceName = strings.TrimSpace(record[idx])
@@ -161,8 +162,8 @@ func ParseDeviceExcel(filePath string) ([]DeviceRow, int, error) {
}
}
// 跳过设备号为空的行
if row.DeviceNo == "" {
// 跳过虚拟号为空的行
if row.VirtualNo == "" {
continue
}
@@ -204,47 +205,44 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) {
ParseErrors: make([]CSVParseError, 0),
}
// 检测表头 (第1行)
headerSkipped := false
iccidCol, msisdnCol := -1, -1
iccidCol, msisdnCol, virtualNoCol := -1, -1, -1
if len(rows) > 0 {
iccidCol, msisdnCol = findCardColumns(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++ {
row := rows[i]
lineNum := i + 1 // Excel行号从1开始
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 {
// 无表头,假设第一列ICCID,第二列MSISDN
if len(row) >= 1 {
iccid = strings.TrimSpace(row[0])
}
@@ -253,11 +251,9 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) {
}
}
// 验证
result.TotalCount++
if iccid == "" && msisdn == "" {
// 空行,跳过
continue
}
@@ -280,45 +276,50 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) {
}
result.Cards = append(result.Cards, CardInfo{
ICCID: iccid,
MSISDN: msisdn,
ICCID: iccid,
MSISDN: msisdn,
VirtualNo: virtualNo,
})
}
return result, nil
}
// findCardColumns 查找ICCIDMSISDN列索引
// findCardColumns 查找ICCIDMSISDN和VirtualNo列索引
// 支持中英文列名识别
func findCardColumns(header []string) (iccidCol, msisdnCol int) {
iccidCol, msisdnCol = -1, -1
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))
// 识别ICCID列
if colLower == "iccid" || colLower == "卡号" || colLower == "号码" {
if iccidCol == -1 { // 只取第一个匹配
if iccidCol == -1 {
iccidCol = i
}
}
// 识别MSISDN列
if colLower == "msisdn" || colLower == "接入号" || colLower == "手机号" || colLower == "电话" {
if msisdnCol == -1 { // 只取第一个匹配
if msisdnCol == -1 {
msisdnCol = i
}
}
if colLower == "virtual_no" || colLower == "virtualno" || colLower == "虚拟号" || colLower == "设备号" {
if virtualNoCol == -1 {
virtualNoCol = i
}
}
}
return iccidCol, msisdnCol
return iccidCol, msisdnCol, virtualNoCol
}
// buildDeviceColumnIndex 构建设备导入列索引
// 识别表头中的列名,返回列名到列索引的映射
func buildDeviceColumnIndex(header []string) map[string]int {
index := map[string]int{
"device_no": -1,
"virtual_no": -1,
"device_name": -1,
"device_model": -1,
"device_type": -1,