All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
主要改动: - 新增交互式环境配置脚本 (scripts/setup-env.sh) - 新增本地启动快捷脚本 (scripts/run-local.sh) - 新增环境变量模板文件 (.env.example) - 部署模式改版:使用嵌入式配置 + 环境变量覆盖 - 添加对象存储功能支持 - 改进 IoT 卡片导入任务 - 优化 OpenAPI 文档生成 - 删除旧的配置文件,改用嵌入式默认配置
133 lines
2.8 KiB
Go
133 lines
2.8 KiB
Go
package utils
|
||
|
||
import (
|
||
"encoding/csv"
|
||
"errors"
|
||
"io"
|
||
"strings"
|
||
)
|
||
|
||
// CardInfo 卡信息(ICCID + MSISDN)
|
||
type CardInfo struct {
|
||
ICCID string
|
||
MSISDN string
|
||
}
|
||
|
||
// CSVParseResult CSV 解析结果
|
||
type CSVParseResult struct {
|
||
Cards []CardInfo
|
||
TotalCount int
|
||
ParseErrors []CSVParseError
|
||
}
|
||
|
||
// CSVParseError CSV 解析错误
|
||
type CSVParseError struct {
|
||
Line int
|
||
ICCID string
|
||
MSISDN string
|
||
Reason string
|
||
}
|
||
|
||
// ErrInvalidCSVFormat CSV 格式错误
|
||
var ErrInvalidCSVFormat = errors.New("CSV 文件格式错误:缺少 MSISDN 列,文件必须包含 ICCID 和 MSISDN 两列")
|
||
|
||
// ParseCardCSV 解析包含 ICCID 和 MSISDN 两列的 CSV 文件
|
||
func ParseCardCSV(reader io.Reader) (*CSVParseResult, error) {
|
||
csvReader := csv.NewReader(reader)
|
||
csvReader.FieldsPerRecord = -1
|
||
csvReader.TrimLeadingSpace = true
|
||
|
||
result := &CSVParseResult{
|
||
Cards: make([]CardInfo, 0),
|
||
ParseErrors: make([]CSVParseError, 0),
|
||
}
|
||
|
||
lineNum := 0
|
||
headerSkipped := false
|
||
|
||
for {
|
||
record, err := csvReader.Read()
|
||
if err == io.EOF {
|
||
break
|
||
}
|
||
lineNum++
|
||
|
||
if err != nil {
|
||
result.ParseErrors = append(result.ParseErrors, CSVParseError{
|
||
Line: lineNum,
|
||
Reason: "CSV 解析错误: " + err.Error(),
|
||
})
|
||
continue
|
||
}
|
||
|
||
if len(record) == 0 {
|
||
continue
|
||
}
|
||
|
||
if len(record) < 2 {
|
||
if lineNum == 1 && !headerSkipped {
|
||
firstCol := strings.TrimSpace(record[0])
|
||
if isICCIDHeader(firstCol) {
|
||
return nil, ErrInvalidCSVFormat
|
||
}
|
||
}
|
||
result.ParseErrors = append(result.ParseErrors, CSVParseError{
|
||
Line: lineNum,
|
||
ICCID: strings.TrimSpace(record[0]),
|
||
Reason: "列数不足:缺少 MSISDN 列",
|
||
})
|
||
result.TotalCount++
|
||
continue
|
||
}
|
||
|
||
iccid := strings.TrimSpace(record[0])
|
||
msisdn := strings.TrimSpace(record[1])
|
||
|
||
if lineNum == 1 && !headerSkipped && isHeader(iccid, msisdn) {
|
||
headerSkipped = true
|
||
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,
|
||
})
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
func isICCIDHeader(value string) bool {
|
||
lower := strings.ToLower(value)
|
||
return lower == "iccid" || lower == "卡号" || lower == "号码"
|
||
}
|
||
|
||
func isMSISDNHeader(value string) bool {
|
||
lower := strings.ToLower(value)
|
||
return lower == "msisdn" || lower == "接入号" || lower == "手机号" || lower == "电话" || lower == "号码"
|
||
}
|
||
|
||
func isHeader(col1, col2 string) bool {
|
||
return isICCIDHeader(col1) && isMSISDNHeader(col2)
|
||
}
|