chore: 初始开源发布

iotlcm-admin-svr:管理后台服务。

- Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件)
- 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、
  MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位;
  私网 IP → 文档地址段;库名 xiujie_admin → iotlcm
- 真实手机号/邮箱 → 合法 11 位编造号 / example.com
- 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、
  私有依赖 go get 行、明文 registry 登录口令
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- Dockerfile 改用公共基础镜像
- 拼错的 RADEME.md → README.md,README 重写
- 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用
- 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes

数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
This commit is contained in:
MiFaZhan
2026-10-23 14:46:05 +08:00
commit 59eb34976a
1132 changed files with 138197 additions and 0 deletions
@@ -0,0 +1,11 @@
package dataswitch
import "github.com/gogf/gf/v2/errors/gerror"
func invalidOperation(op Operation) error {
return gerror.Newf("不支持的数据服务操作: %s", op)
}
func errInvalidBatchType(batchType int) error {
return gerror.Newf("批次类型%d不是数据服务批次", batchType)
}
@@ -0,0 +1,195 @@
package dataswitch
import (
"context"
"errors"
"fmt"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/excel"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/text"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
type Row struct {
DetailID int64
RowNo int
Iccid string
Msisdn string
Imsi string
Reason string
Authorized bool
CardID int64
CutoffThreshold float64
UsedFlowMb float64
PackageTotalFlowMb float64
RemainFlowMb float64
DataServiceStatusBefore int
TrafficCutoffCrossMonthHold int
TriggeredAt string
TriggerReason string
}
func (r *Row) GetRowNo() int {
if r == nil {
return 0
}
return r.RowNo
}
func (r *Row) GetItemID() string {
if r == nil {
return ""
}
return r.Iccid
}
func (r *Row) GetErrorReasons() []string {
if r == nil || r.Reason == "" {
return nil
}
return []string{r.Reason}
}
func (r *Row) AddErrorReason(reason string) {
if r == nil || r.Reason != "" || reason == "" {
return
}
r.Reason = reason
}
func (r *Row) GetIccid() string {
if r == nil {
return ""
}
return r.Iccid
}
func (r *Row) SetAuthorized(v bool) {
if r == nil {
return
}
r.Authorized = v
}
func (r *Row) Snapshot() *gjson.Json {
if r == nil {
return gjson.New(g.Map{})
}
snapshot := g.Map{
"ICCID": r.Iccid,
"MSISDN": r.Msisdn,
"IMSI": r.Imsi,
"authorized": r.Authorized,
}
if r.CardID > 0 {
snapshot["cardId"] = r.CardID
}
if r.CutoffThreshold > 0 {
snapshot["cutoffThreshold"] = r.CutoffThreshold
}
if r.UsedFlowMb > 0 {
snapshot["usedFlowMb"] = r.UsedFlowMb
}
if r.PackageTotalFlowMb > 0 {
snapshot["packageTotalFlowMb"] = r.PackageTotalFlowMb
}
if r.RemainFlowMb >= 0 && r.CutoffThreshold > 0 {
snapshot["remainFlowMb"] = r.RemainFlowMb
}
if r.DataServiceStatusBefore != 0 {
snapshot["dataServiceStatusBefore"] = r.DataServiceStatusBefore
}
if r.TrafficCutoffCrossMonthHold != 0 {
snapshot["trafficCutoffCrossMonthHold"] = r.TrafficCutoffCrossMonthHold
}
if r.TriggeredAt != "" {
snapshot["triggeredAt"] = r.TriggeredAt
}
if r.TriggerReason != "" {
snapshot["triggerReason"] = r.TriggerReason
}
return gjson.New(snapshot)
}
func parseExcelFileRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
parsed, err := excel.ParseFile(ctx, filePath, excel.ParseOptions{RequiredHeaders: []string{"ICCID"}, MaxDataRows: consts.SimCardBatchMaxRows})
if err != nil {
return nil, err
}
rows := make([]batchimport.Row, 0, len(parsed))
for _, item := range parsed {
rows = append(rows, &Row{
RowNo: item.RowNo,
Iccid: excel.Field(item, "ICCID"),
Msisdn: excel.Field(item, "MSISDN"),
Imsi: excel.Field(item, "IMSI"),
})
}
return rows, nil
}
func parseInputRows(ctx context.Context, input string) ([]batchimport.Row, error) {
lines, err := text.ParseWhitespaceFields(ctx, input)
if err != nil {
if errors.Is(err, text.ErrEmptyInput) {
return nil, gerror.New("批量输入内容不能为空")
}
return nil, err
}
rows := make([]batchimport.Row, 0, len(lines))
for _, line := range lines {
row := &Row{RowNo: line.RowNo}
switch len(line.Fields) {
case 1:
row.Iccid = line.Fields[0]
case 3:
row.Iccid = line.Fields[0]
row.Msisdn = line.Fields[1]
row.Imsi = line.Fields[2]
default:
row.AddErrorReason("格式错误,应为 ICCID 或 ICCID MSISDN IMSI")
}
rows = append(rows, row)
}
return rows, nil
}
func validateBasicRows(rows []batchimport.Row) error {
firstByIccid := make(map[string]*Row, len(rows))
issues := make([]string, 0)
for _, item := range rows {
row, ok := item.(*Row)
if !ok || row == nil {
return gerror.New("数据服务批次行类型错误")
}
row.Iccid = strings.TrimSpace(row.Iccid)
row.Msisdn = strings.TrimSpace(row.Msisdn)
row.Imsi = strings.TrimSpace(row.Imsi)
if row.Reason != "" {
issues = append(issues, fmt.Sprintf("第%d行:%s", row.RowNo, row.Reason))
continue
}
if row.Iccid == "" {
row.AddErrorReason("ICCID不能为空")
issues = append(issues, fmt.Sprintf("第%d行:ICCID不能为空", row.RowNo))
continue
}
if first, exists := firstByIccid[row.Iccid]; exists {
row.AddErrorReason(fmt.Sprintf("ICCID与第%d行重复", first.RowNo))
first.AddErrorReason("ICCID重复")
issues = append(issues, fmt.Sprintf("第%d行:ICCID与第%d行重复", row.RowNo, first.RowNo))
continue
}
firstByIccid[row.Iccid] = row
}
if len(issues) > 0 {
return gerror.New("数据服务批次基础格式校验失败:" + strings.Join(issues, ";"))
}
return nil
}
@@ -0,0 +1,70 @@
package dataswitch
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
)
type Processor struct {
config Config
}
func NewProcessor(operation Operation) (*Processor, error) {
cfg, err := operation.Config()
if err != nil {
return nil, err
}
return &Processor{config: cfg}, nil
}
func (p *Processor) Config() batchimport.ProcessorConfig {
maxFile := consts.SimCardDataStartMaxFileSize
if p.config.BatchType == consts.BatchTypeDataStop {
maxFile = consts.SimCardDataStopMaxFileSize
}
if p.config.BatchType == consts.BatchTypeDataCut {
maxFile = consts.SimCardDataCutMaxFileSize
}
return batchimport.ProcessorConfig{
Code: "sim_card_" + p.config.Code,
BatchType: p.config.BatchType,
BatchNoPrefix: p.config.BatchPrefix,
TaskName: p.config.TaskName,
ExportScene: p.config.ExportScene,
ImportDir: p.config.ImportDir,
ResultDir: p.config.ResultDir,
MaxFileSize: int64(maxFile),
MaxRows: consts.SimCardBatchMaxRows,
TemplateHeaders: []string{"ICCID", "MSISDN", "IMSI"},
RequiredHeaders: []string{"ICCID"},
}
}
func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
return parseExcelFileRows(ctx, filePath)
}
func (p *Processor) ValidateRows(_ context.Context, rows []batchimport.Row) error {
return validateBasicRows(rows)
}
func (p *Processor) Execute(context.Context, gdb.TX, *batchimport.SubmitRequest, []batchimport.Row) error {
return gerror.New("数据服务批次只能由异步执行器执行")
}
func (p *Processor) BuildBatchInfo(_ context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, _ *model.SysOssSaveContentModel, _ int64) *gjson.Json {
info := buildBatchInfo(req, p.config)
if result != nil && result.ImportFileOssId > 0 {
info.Set("importFileOssId", result.ImportFileOssId)
} else if importOss != nil && importOss.OssId > 0 {
info.Set("importFileOssId", importOss.OssId)
}
return info
}
@@ -0,0 +1,91 @@
package dataswitch
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/net/ghttp"
)
type Submitter struct {
operation Operation
}
func NewSubmitter(operation Operation) *Submitter {
return &Submitter{operation: operation}
}
func (s *Submitter) Operation() Operation {
return s.operation
}
func (s *Submitter) SubmitExcel(ctx context.Context, file *ghttp.UploadFile, remark string) (*SubmitResult, error) {
processor, err := NewProcessor(s.operation)
if err != nil {
return nil, err
}
req := batchimport.SubmitRequest{
SourceChannel: batchmetadata.Console,
ExecutionMode: batchmetadata.Bulk,
File: file,
BizParams: &common.SubmitOptions{Remark: strings.TrimSpace(remark)},
SourceType: consts.BatchSourceTypeFile,
Operator: common.CurrentOperator(ctx),
}
result, err := batchimport.CreatePendingUpload(ctx, req, processor, batchimport.PendingOptions{
PrepareRows: func(ctx context.Context, rows []batchimport.Row) error {
return common.PrepareAuthorizedRows(ctx, rows, func(item batchimport.Row) (*Row, error) {
row, ok := item.(*Row)
if !ok || row == nil {
return nil, gerror.New("数据服务批次行类型错误")
}
return row, nil
}, "数据服务批次行类型错误")
},
})
if err != nil {
return nil, err
}
common.PublishBatchMessage(ctx, result, consts.QueueBatchExecute, "dataswitch")
return common.PendingSubmitResult(result, consts.BatchAsyncStatusPending), nil
}
func (s *Submitter) SubmitInput(ctx context.Context, input, remark string) (*SubmitResult, error) {
rows, err := parseInputRows(ctx, input)
if err != nil {
return nil, err
}
processor, err := NewProcessor(s.operation)
if err != nil {
return nil, err
}
req := batchimport.SubmitRequest{
SourceChannel: batchmetadata.Console,
ExecutionMode: batchmetadata.Bulk,
BizParams: &common.SubmitOptions{Remark: strings.TrimSpace(remark)},
SourceType: consts.BatchSourceTypeBatchInput,
Operator: common.CurrentOperator(ctx),
}
result, err := batchimport.CreatePendingRows(ctx, req, processor, rows, batchimport.PendingOptions{
PrepareRows: func(ctx context.Context, rows []batchimport.Row) error {
return common.PrepareAuthorizedRows(ctx, rows, func(item batchimport.Row) (*Row, error) {
row, ok := item.(*Row)
if !ok || row == nil {
return nil, gerror.New("数据服务批次行类型错误")
}
return row, nil
}, "数据服务批次行类型错误")
},
})
if err != nil {
return nil, err
}
common.PublishBatchMessage(ctx, result, consts.QueueBatchExecute, "dataswitch")
return common.PendingSubmitResult(result, consts.BatchAsyncStatusPending), nil
}
@@ -0,0 +1,112 @@
package dataswitch
import (
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
)
const (
OperationDataStart Operation = "dataStart"
OperationDataStop Operation = "dataStop"
OperationDataCut Operation = "dataCut"
)
type Operation string
type Config struct {
Operation Operation
Code string
BatchType int
BatchPrefix string
OperType int
ApnName string
TaskName string
ExportScene int
ImportDir string
ResultDir string
}
func (op Operation) Config() (Config, error) {
switch op {
case OperationDataStart:
return Config{
Operation: op,
Code: "dataStart",
BatchType: consts.BatchTypeDataStart,
BatchPrefix: consts.BatchNoPrefixDataStart,
OperType: consts.SimCardDataOperTypeStart,
ApnName: consts.SimCardDataApnName,
TaskName: consts.ResourceBatchDownloadTaskSimCardDataStart,
ExportScene: consts.ResourceExportSceneDataStart,
ImportDir: consts.SimCardDataStartSubDirImportExcel,
ResultDir: consts.SimCardDataStartSubDirResultExcel,
}, nil
case OperationDataStop:
return Config{
Operation: op,
Code: "dataStop",
BatchType: consts.BatchTypeDataStop,
BatchPrefix: consts.BatchNoPrefixDataStop,
OperType: consts.SimCardDataOperTypeStop,
ApnName: consts.SimCardDataApnName,
TaskName: consts.ResourceBatchDownloadTaskSimCardDataStop,
ExportScene: consts.ResourceExportSceneDataStop,
ImportDir: consts.SimCardDataStopSubDirImportExcel,
ResultDir: consts.SimCardDataStopSubDirResultExcel,
}, nil
case OperationDataCut:
return Config{
Operation: op,
Code: "dataCut",
BatchType: consts.BatchTypeDataCut,
BatchPrefix: consts.BatchNoPrefixDataCut,
OperType: consts.SimCardDataOperTypeStop,
ApnName: consts.SimCardDataApnName,
TaskName: consts.ResourceBatchDownloadTaskSimCardDataCut,
ExportScene: consts.ResourceExportSceneDataCut,
ImportDir: consts.SimCardDataCutSubDirImportExcel,
ResultDir: consts.SimCardDataCutSubDirResultExcel,
}, nil
default:
return Config{}, invalidOperation(op)
}
}
func OperationByBatchType(batchType int) (Operation, error) {
switch batchType {
case consts.BatchTypeDataStart:
return OperationDataStart, nil
case consts.BatchTypeDataStop:
return OperationDataStop, nil
case consts.BatchTypeDataCut:
return OperationDataCut, nil
default:
return "", errInvalidBatchType(batchType)
}
}
type SubmitOptions = common.SubmitOptions
type BatchExecuteMessage = common.BatchExecuteMessage
type SubmitResult = common.SubmitResult
func buildBatchInfo(req batchimport.SubmitRequest, cfg Config) *gjson.Json {
info := g.Map{
"operation": cfg.Code,
"operType": cfg.OperType,
"apnName": cfg.ApnName,
"batchType": cfg.BatchType,
"resultFile": g.Map{"status": "pending"},
}
if options, ok := req.BizParams.(*SubmitOptions); ok && options != nil {
if remark := trimSpace(options.Remark); remark != "" {
info["remark"] = remark
}
}
return gjson.New(info)
}
@@ -0,0 +1,7 @@
package dataswitch
import "strings"
func trimSpace(s string) string {
return strings.TrimSpace(s)
}