feat: 添加环境变量管理工具和部署配置改版
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:
2026-01-26 10:28:29 +08:00
parent 194078674a
commit 45aa7deb87
94 changed files with 6532 additions and 1967 deletions

View File

@@ -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)
}