feat: 添加环境变量管理工具和部署配置改版
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m33s
主要改动: - 新增交互式环境配置脚本 (scripts/setup-env.sh) - 新增本地启动快捷脚本 (scripts/run-local.sh) - 新增环境变量模板文件 (.env.example) - 部署模式改版:使用嵌入式配置 + 环境变量覆盖 - 添加对象存储功能支持 - 改进 IoT 卡片导入任务 - 优化 OpenAPI 文档生成 - 删除旧的配置文件,改用嵌入式默认配置
This commit is contained in:
@@ -2,33 +2,49 @@ package utils
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CardInfo 卡信息(ICCID + MSISDN)
|
||||
type CardInfo struct {
|
||||
ICCID string
|
||||
MSISDN string
|
||||
}
|
||||
|
||||
// CSVParseResult CSV 解析结果
|
||||
type CSVParseResult struct {
|
||||
ICCIDs []string
|
||||
Cards []CardInfo
|
||||
TotalCount int
|
||||
ParseErrors []CSVParseError
|
||||
}
|
||||
|
||||
// CSVParseError CSV 解析错误
|
||||
type CSVParseError struct {
|
||||
Line int
|
||||
ICCID string
|
||||
MSISDN string
|
||||
Reason string
|
||||
}
|
||||
|
||||
func ParseICCIDFromCSV(reader io.Reader) (*CSVParseResult, error) {
|
||||
// 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{
|
||||
ICCIDs: make([]string, 0),
|
||||
Cards: make([]CardInfo, 0),
|
||||
ParseErrors: make([]CSVParseError, 0),
|
||||
}
|
||||
|
||||
lineNum := 0
|
||||
headerSkipped := false
|
||||
|
||||
for {
|
||||
record, err := csvReader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -48,23 +64,69 @@ func ParseICCIDFromCSV(reader io.Reader) (*CSVParseResult, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
iccid := strings.TrimSpace(record[0])
|
||||
if iccid == "" {
|
||||
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
|
||||
}
|
||||
|
||||
if lineNum == 1 && isHeader(iccid) {
|
||||
iccid := strings.TrimSpace(record[0])
|
||||
msisdn := strings.TrimSpace(record[1])
|
||||
|
||||
if lineNum == 1 && !headerSkipped && isHeader(iccid, msisdn) {
|
||||
headerSkipped = true
|
||||
continue
|
||||
}
|
||||
|
||||
result.TotalCount++
|
||||
result.ICCIDs = append(result.ICCIDs, iccid)
|
||||
|
||||
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 isHeader(value string) bool {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -8,89 +8,133 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseICCIDFromCSV(t *testing.T) {
|
||||
func TestParseCardCSV(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
csvContent string
|
||||
wantICCIDs []string
|
||||
wantCards []CardInfo
|
||||
wantTotalCount int
|
||||
wantErrorCount int
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
name: "单列ICCID无表头",
|
||||
csvContent: "89860012345678901234\n89860012345678901235\n89860012345678901236",
|
||||
wantICCIDs: []string{"89860012345678901234", "89860012345678901235", "89860012345678901236"},
|
||||
wantTotalCount: 3,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
{
|
||||
name: "单列ICCID有表头-iccid",
|
||||
csvContent: "iccid\n89860012345678901234\n89860012345678901235",
|
||||
wantICCIDs: []string{"89860012345678901234", "89860012345678901235"},
|
||||
name: "标准双列无表头",
|
||||
csvContent: "89860012345678901234,13800000001\n89860012345678901235,13800000002",
|
||||
wantCards: []CardInfo{
|
||||
{ICCID: "89860012345678901234", MSISDN: "13800000001"},
|
||||
{ICCID: "89860012345678901235", MSISDN: "13800000002"},
|
||||
},
|
||||
wantTotalCount: 2,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
{
|
||||
name: "单列ICCID有表头-ICCID大写",
|
||||
csvContent: "ICCID\n89860012345678901234",
|
||||
wantICCIDs: []string{"89860012345678901234"},
|
||||
name: "标准双列有表头-英文",
|
||||
csvContent: "iccid,msisdn\n89860012345678901234,13800000001\n89860012345678901235,13800000002",
|
||||
wantCards: []CardInfo{
|
||||
{ICCID: "89860012345678901234", MSISDN: "13800000001"},
|
||||
{ICCID: "89860012345678901235", MSISDN: "13800000002"},
|
||||
},
|
||||
wantTotalCount: 2,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
{
|
||||
name: "标准双列有表头-中文",
|
||||
csvContent: "卡号,接入号\n89860012345678901234,13800000001",
|
||||
wantCards: []CardInfo{
|
||||
{ICCID: "89860012345678901234", MSISDN: "13800000001"},
|
||||
},
|
||||
wantTotalCount: 1,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
{
|
||||
name: "单列ICCID有表头-卡号",
|
||||
csvContent: "卡号\n89860012345678901234",
|
||||
wantICCIDs: []string{"89860012345678901234"},
|
||||
name: "标准双列有表头-手机号",
|
||||
csvContent: "ICCID,手机号\n89860012345678901234,13800000001",
|
||||
wantCards: []CardInfo{
|
||||
{ICCID: "89860012345678901234", MSISDN: "13800000001"},
|
||||
},
|
||||
wantTotalCount: 1,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
{
|
||||
name: "单列ICCID有表头-号码",
|
||||
csvContent: "号码\n89860012345678901234",
|
||||
wantICCIDs: []string{"89860012345678901234"},
|
||||
wantTotalCount: 1,
|
||||
name: "单列CSV格式拒绝-有表头",
|
||||
csvContent: "iccid\n89860012345678901234",
|
||||
wantCards: nil,
|
||||
wantTotalCount: 0,
|
||||
wantErrorCount: 0,
|
||||
wantError: ErrInvalidCSVFormat,
|
||||
},
|
||||
{
|
||||
name: "单列CSV格式-无表头记录错误",
|
||||
csvContent: "89860012345678901234\n89860012345678901235",
|
||||
wantCards: []CardInfo{},
|
||||
wantTotalCount: 2,
|
||||
wantErrorCount: 2,
|
||||
},
|
||||
{
|
||||
name: "MSISDN为空记录失败",
|
||||
csvContent: "iccid,msisdn\n89860012345678901234,13800000001\n89860012345678901235,",
|
||||
wantCards: []CardInfo{{ICCID: "89860012345678901234", MSISDN: "13800000001"}},
|
||||
wantTotalCount: 2,
|
||||
wantErrorCount: 1,
|
||||
},
|
||||
{
|
||||
name: "ICCID为空记录失败",
|
||||
csvContent: "iccid,msisdn\n89860012345678901234,13800000001\n,13800000002",
|
||||
wantCards: []CardInfo{{ICCID: "89860012345678901234", MSISDN: "13800000001"}},
|
||||
wantTotalCount: 2,
|
||||
wantErrorCount: 1,
|
||||
},
|
||||
{
|
||||
name: "空文件",
|
||||
csvContent: "",
|
||||
wantICCIDs: []string{},
|
||||
wantCards: []CardInfo{},
|
||||
wantTotalCount: 0,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
{
|
||||
name: "只有表头",
|
||||
csvContent: "iccid",
|
||||
wantICCIDs: []string{},
|
||||
csvContent: "iccid,msisdn",
|
||||
wantCards: []CardInfo{},
|
||||
wantTotalCount: 0,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
{
|
||||
name: "包含空行",
|
||||
csvContent: "89860012345678901234\n\n89860012345678901235\n \n89860012345678901236",
|
||||
wantICCIDs: []string{"89860012345678901234", "89860012345678901235", "89860012345678901236"},
|
||||
wantTotalCount: 3,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
{
|
||||
name: "ICCID前后有空格",
|
||||
csvContent: " 89860012345678901234 \n89860012345678901235",
|
||||
wantICCIDs: []string{"89860012345678901234", "89860012345678901235"},
|
||||
name: "包含空行",
|
||||
csvContent: "89860012345678901234,13800000001\n\n89860012345678901235,13800000002",
|
||||
wantCards: []CardInfo{
|
||||
{ICCID: "89860012345678901234", MSISDN: "13800000001"},
|
||||
{ICCID: "89860012345678901235", MSISDN: "13800000002"},
|
||||
},
|
||||
wantTotalCount: 2,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
{
|
||||
name: "多列CSV只取第一列",
|
||||
csvContent: "89860012345678901234,额外数据,更多数据\n89860012345678901235,忽略,忽略",
|
||||
wantICCIDs: []string{"89860012345678901234", "89860012345678901235"},
|
||||
name: "ICCID和MSISDN前后有空格",
|
||||
csvContent: " 89860012345678901234 , 13800000001 ",
|
||||
wantCards: []CardInfo{
|
||||
{ICCID: "89860012345678901234", MSISDN: "13800000001"},
|
||||
},
|
||||
wantTotalCount: 1,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
{
|
||||
name: "多于两列只取前两列",
|
||||
csvContent: "89860012345678901234,13800000001,额外数据\n89860012345678901235,13800000002,忽略",
|
||||
wantCards: []CardInfo{
|
||||
{ICCID: "89860012345678901234", MSISDN: "13800000001"},
|
||||
{ICCID: "89860012345678901235", MSISDN: "13800000002"},
|
||||
},
|
||||
wantTotalCount: 2,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
{
|
||||
name: "Windows换行符CRLF",
|
||||
csvContent: "89860012345678901234\r\n89860012345678901235\r\n89860012345678901236",
|
||||
wantICCIDs: []string{"89860012345678901234", "89860012345678901235", "89860012345678901236"},
|
||||
wantTotalCount: 3,
|
||||
name: "Windows换行符CRLF",
|
||||
csvContent: "89860012345678901234,13800000001\r\n89860012345678901235,13800000002",
|
||||
wantCards: []CardInfo{
|
||||
{ICCID: "89860012345678901234", MSISDN: "13800000001"},
|
||||
{ICCID: "89860012345678901235", MSISDN: "13800000002"},
|
||||
},
|
||||
wantTotalCount: 2,
|
||||
wantErrorCount: 0,
|
||||
},
|
||||
}
|
||||
@@ -98,34 +142,78 @@ func TestParseICCIDFromCSV(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := strings.NewReader(tt.csvContent)
|
||||
result, err := ParseICCIDFromCSV(reader)
|
||||
result, err := ParseCardCSV(reader)
|
||||
|
||||
if tt.wantError != nil {
|
||||
require.ErrorIs(t, err, tt.wantError)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantICCIDs, result.ICCIDs, "ICCIDs 不匹配")
|
||||
assert.Equal(t, tt.wantCards, result.Cards, "Cards 不匹配")
|
||||
assert.Equal(t, tt.wantTotalCount, result.TotalCount, "TotalCount 不匹配")
|
||||
assert.Equal(t, tt.wantErrorCount, len(result.ParseErrors), "ParseErrors 数量不匹配")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCardCSV_ErrorDetails(t *testing.T) {
|
||||
t.Run("MSISDN为空时记录详细错误", func(t *testing.T) {
|
||||
csvContent := "iccid,msisdn\n89860012345678901234,"
|
||||
reader := strings.NewReader(csvContent)
|
||||
result, err := ParseCardCSV(reader)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.ParseErrors, 1)
|
||||
assert.Equal(t, 2, result.ParseErrors[0].Line)
|
||||
assert.Equal(t, "89860012345678901234", result.ParseErrors[0].ICCID)
|
||||
assert.Equal(t, "MSISDN 不能为空", result.ParseErrors[0].Reason)
|
||||
})
|
||||
|
||||
t.Run("ICCID为空时记录详细错误", func(t *testing.T) {
|
||||
csvContent := "iccid,msisdn\n,13800000001"
|
||||
reader := strings.NewReader(csvContent)
|
||||
result, err := ParseCardCSV(reader)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.ParseErrors, 1)
|
||||
assert.Equal(t, 2, result.ParseErrors[0].Line)
|
||||
assert.Equal(t, "13800000001", result.ParseErrors[0].MSISDN)
|
||||
assert.Equal(t, "ICCID 不能为空", result.ParseErrors[0].Reason)
|
||||
})
|
||||
|
||||
t.Run("列数不足时记录详细错误", func(t *testing.T) {
|
||||
csvContent := "89860012345678901234"
|
||||
reader := strings.NewReader(csvContent)
|
||||
result, err := ParseCardCSV(reader)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.ParseErrors, 1)
|
||||
assert.Equal(t, 1, result.ParseErrors[0].Line)
|
||||
assert.Equal(t, "89860012345678901234", result.ParseErrors[0].ICCID)
|
||||
assert.Contains(t, result.ParseErrors[0].Reason, "列数不足")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
col1 string
|
||||
col2 string
|
||||
expected bool
|
||||
}{
|
||||
{"iccid", true},
|
||||
{"ICCID", true},
|
||||
{"Iccid", true},
|
||||
{"卡号", true},
|
||||
{"号码", true},
|
||||
{"89860012345678901234", false},
|
||||
{"", false},
|
||||
{"id", false},
|
||||
{"card", false},
|
||||
{"iccid", "msisdn", true},
|
||||
{"ICCID", "MSISDN", true},
|
||||
{"卡号", "接入号", true},
|
||||
{"号码", "手机号", true},
|
||||
{"iccid", "电话", true},
|
||||
{"89860012345678901234", "13800000001", false},
|
||||
{"iccid", "", false},
|
||||
{"", "msisdn", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.value, func(t *testing.T) {
|
||||
result := isHeader(tt.value)
|
||||
t.Run(tt.col1+"_"+tt.col2, func(t *testing.T) {
|
||||
result := isHeader(tt.col1, tt.col2)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user