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,34 @@
package lifecycle
import (
"context"
"fmt"
"github.com/gogf/gf/v2/frame/g"
)
type BatchExecutor interface {
Execute(ctx context.Context, batchNo string) error
MarkFailed(ctx context.Context, batchNo, reason string) error
}
func Dispatch(executor BatchExecutor, batchNo string) {
go func() {
ctx := context.Background()
defer func() {
if recovered := recover(); recovered != nil {
reason := fmt.Sprintf("后台执行异常: %v", recovered)
g.Log().Errorf(ctx, "SIM lifecycle batch panic, batchNo=%s, reason=%s", batchNo, reason)
if err := executor.MarkFailed(ctx, batchNo, reason); err != nil {
g.Log().Errorf(ctx, "mark SIM lifecycle batch failed after panic, batchNo=%s, error=%+v", batchNo, err)
}
}
}()
if err := executor.Execute(ctx, batchNo); err != nil {
g.Log().Errorf(ctx, "execute SIM lifecycle batch failed, batchNo=%s, error=%+v", batchNo, err)
if markErr := executor.MarkFailed(ctx, batchNo, err.Error()); markErr != nil {
g.Log().Errorf(ctx, "mark SIM lifecycle batch failed, batchNo=%s, error=%+v", batchNo, markErr)
}
}
}()
}
@@ -0,0 +1,246 @@
package lifecycle
import (
"context"
"sort"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"github.com/gogf/gf/v2/errors/gerror"
)
type executorRepository interface {
ClaimBatch(ctx context.Context, batchNo string) (bool, error)
LoadTask(ctx context.Context, batchNo string) (*Task, error)
LoadCards(ctx context.Context, task *Task) (map[string]*entity.TSimCardInfo, error)
SaveValidationFailure(ctx context.Context, task *Task) error
SaveOutcome(ctx context.Context, task *Task, row *Row, card *entity.TSimCardInfo, outcome Outcome) error
SaveDetailFailure(ctx context.Context, task *Task, row *Row) error
FinalizeBatch(ctx context.Context, task *Task) error
MarkBatchFailed(ctx context.Context, batchNo, reason string) error
}
type statusChanger interface {
ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) ([]model.SimBaseRes[model.SimCardStatusChangeItem], error)
}
type Executor struct {
repo executorRepository
upstream statusChanger
}
func NewExecutor() *Executor {
return &Executor{repo: NewRepository(), upstream: service.UpstreamChan()}
}
func newExecutor(repo executorRepository, upstream statusChanger) *Executor {
return &Executor{repo: repo, upstream: upstream}
}
func (e *Executor) Execute(ctx context.Context, batchNo string) error {
batchNo = strings.TrimSpace(batchNo)
if batchNo == "" {
return gerror.New("批次号不能为空")
}
claimed, err := e.repo.ClaimBatch(ctx, batchNo)
if err != nil {
return gerror.Wrap(err, "抢占停复机批次失败")
}
if !claimed {
return nil
}
task, err := e.repo.LoadTask(ctx, batchNo)
if err != nil {
return err
}
cards, err := e.repo.LoadCards(ctx, task)
if err != nil {
return gerror.Wrap(err, "读取SIM卡业务数据失败")
}
if !validateTask(task, cards) {
if err = e.repo.SaveValidationFailure(ctx, task); err != nil {
return err
}
return e.repo.FinalizeBatch(ctx, task)
}
groups := groupRowsByChannel(task.Rows)
channelKeys := make([]string, 0, len(groups))
for chanKey := range groups {
channelKeys = append(channelKeys, chanKey)
}
sort.Strings(channelKeys)
for _, chanKey := range channelKeys {
rows := groups[chanKey]
outcomes := e.changeGroup(ctx, task, rows)
for i, row := range rows {
outcome := outcomes[i]
card := cards[row.Iccid]
if err = e.repo.SaveOutcome(ctx, task, row, card, outcome); err == nil {
continue
}
row.OrderNum = outcome.OrderNum
row.UpstreamCode = outcome.Code
row.UpstreamReason = outcome.Message
row.Reason = "上游已返回,但本地状态落库失败: " + err.Error()
if saveErr := e.repo.SaveDetailFailure(ctx, task, row); saveErr != nil {
return gerror.Wrap(saveErr, "记录本地状态落库失败结果失败")
}
}
}
return e.repo.FinalizeBatch(ctx, task)
}
func (e *Executor) MarkFailed(ctx context.Context, batchNo, reason string) error {
return e.repo.MarkBatchFailed(ctx, strings.TrimSpace(batchNo), strings.TrimSpace(reason))
}
func validateTask(task *Task, cards map[string]*entity.TSimCardInfo) bool {
hasFailure := false
for _, row := range task.Rows {
if row == nil {
hasFailure = true
continue
}
if row.Reason != "" {
hasFailure = true
continue
}
card := cards[row.Iccid]
if card == nil {
row.AddErrorReason("ICCID不存在于正式卡记录")
} else if !row.Authorized {
row.AddErrorReason("ICCID不在当前操作人的数据权限范围内")
} else if strings.TrimSpace(card.ChanKey) == "" {
row.AddErrorReason("ICCID缺少通道标识chanKey")
} else if row.Msisdn != "" && row.Msisdn != strings.TrimSpace(card.Msisdn) {
row.AddErrorReason("MSISDN与正式卡记录不一致")
} else if row.Imsi != "" && row.Imsi != strings.TrimSpace(card.Imsi) {
row.AddErrorReason("IMSI与正式卡记录不一致")
} else {
validateCardStatus(task.Operation, row, card)
}
if row.Reason != "" {
hasFailure = true
continue
}
row.Msisdn = strings.TrimSpace(card.Msisdn)
row.Imsi = strings.TrimSpace(card.Imsi)
row.ChanKey = strings.TrimSpace(card.ChanKey)
row.StatusBefore = card.BossCardStatus
}
if hasFailure {
for _, row := range task.Rows {
if row != nil && row.Reason == "" {
row.AddErrorReason("同批次存在校验失败卡,本卡未请求上游")
}
}
task.FailureReason = "整批业务校验失败,未请求上游"
return false
}
task.ValidationOK = true
return true
}
func validateCardStatus(operation Operation, row *Row, card *entity.TSimCardInfo) {
// TODO: 正常系统开发完成后恢复卡状态限制
// 注释原因:开发环境经常出现卡状态不在预期可操作范围内的情况,先放行
_ = operation
_ = row
_ = card
// switch operation {
// case OperationSuspend:
// if card.BossCardStatus != consts.V3ApiSimBossCardStatusActivated {
// row.AddErrorReason("仅已激活状态的卡可以自主停机")
// }
// case OperationResume:
// if card.BossCardStatus != consts.V3ApiSimBossCardStatusStopped {
// row.AddErrorReason("仅停机状态的卡可以自主复机")
// }
// }
}
func groupRowsByChannel(rows []*Row) map[string][]*Row {
groups := make(map[string][]*Row)
for _, row := range rows {
groups[row.ChanKey] = append(groups[row.ChanKey], row)
}
return groups
}
func (e *Executor) changeGroup(ctx context.Context, task *Task, rows []*Row) []Outcome {
simKeys := make([]*model.SimKey, 0, len(rows))
for _, row := range rows {
simKeys = append(simKeys, &model.SimKey{
ChanKey: row.ChanKey,
ICCID: row.Iccid,
MSISDN: row.Msisdn,
IMSI: row.Imsi,
})
}
response, err := e.upstream.ChangeSimStatus(ctx, &model.SimCardStatusChangeInput{
SimKeys: simKeys,
ReqData: model.SimCardStatusChangeInputItem{OperType: task.Config.OperType},
})
if err != nil {
outcomes := make([]Outcome, len(rows))
for i := range outcomes {
outcomes[i] = Outcome{Message: "上游请求失败: " + err.Error()}
}
return outcomes
}
return mapOutcomes(rows, response)
}
func mapOutcomes(rows []*Row, response []model.SimBaseRes[model.SimCardStatusChangeItem]) []Outcome {
outcomes := make([]Outcome, len(rows))
used := make([]bool, len(response))
for rowIndex, row := range rows {
matched := -1
for responseIndex, item := range response {
if !used[responseIndex] && sameSim(row, item.SimKey) {
matched = responseIndex
break
}
}
if matched < 0 && rowIndex < len(response) && !used[rowIndex] && response[rowIndex].SimKey == nil {
matched = rowIndex
}
if matched < 0 {
outcomes[rowIndex] = Outcome{Message: "上游未返回该卡处理结果"}
continue
}
used[matched] = true
item := response[matched]
message := strings.TrimSpace(item.Message)
if item.Code != 0 && message == "" {
message = "上游处理失败"
}
outcomes[rowIndex] = Outcome{
Success: item.Code == 0,
Code: item.Code,
Message: message,
OrderNum: strings.TrimSpace(item.ResData.OrderNum),
}
}
return outcomes
}
func sameSim(row *Row, key *model.SimKey) bool {
if row == nil || key == nil {
return false
}
if value := strings.TrimSpace(key.ICCID); value != "" {
return value == row.Iccid
}
if value := strings.TrimSpace(key.MSISDN); value != "" {
return value == row.Msisdn
}
if value := strings.TrimSpace(key.IMSI); value != "" {
return value == row.Imsi
}
return false
}
@@ -0,0 +1,87 @@
package lifecycle
import (
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"github.com/gogf/gf/v2/errors/gerror"
)
type Operation int
const (
OperationSuspend Operation = iota + 1
OperationResume
OperationRestart
)
type OperationConfig struct {
Operation Operation
Code string
Name string
BatchType int
BatchPrefix string
OperType string
TargetStatus int
TaskName string
ExportScene int
ImportDir string
ResultDir string
}
func (operation Operation) Config() (OperationConfig, error) {
switch operation {
case OperationSuspend:
return OperationConfig{
Operation: operation,
Code: "stop",
Name: "自主停机",
BatchType: consts.BatchTypeStop,
BatchPrefix: consts.BatchNoPrefixStop,
OperType: consts.SimCardStatusOperTypeStop,
TargetStatus: consts.V3ApiSimBossCardStatusStopped,
TaskName: consts.ResourceBatchDownloadTaskSimCardStop,
ExportScene: consts.ResourceExportSceneStop,
ImportDir: consts.SimCardStopSubDirImportExcel,
ResultDir: consts.SimCardStopSubDirResultExcel,
}, nil
case OperationResume:
return OperationConfig{
Operation: operation,
Code: "start",
Name: "自主复机",
BatchType: consts.BatchTypeStart,
BatchPrefix: consts.BatchNoPrefixStart,
OperType: consts.SimCardStatusOperTypeStart,
TargetStatus: consts.V3ApiSimBossCardStatusActivated,
TaskName: consts.ResourceBatchDownloadTaskSimCardStart,
ExportScene: consts.ResourceExportSceneStart,
ImportDir: consts.SimCardStartSubDirImportExcel,
ResultDir: consts.SimCardStartSubDirResultExcel,
}, nil
case OperationRestart:
return OperationConfig{
Operation: operation,
Code: "restart",
Name: "自主重启",
BatchType: consts.BatchTypeRestart,
BatchPrefix: consts.BatchNoPrefixRestart,
TaskName: consts.ResourceBatchDownloadTaskSimCardRestart,
ExportScene: consts.ResourceExportSceneRestart,
ImportDir: consts.SimCardRestartSubDirImportExcel,
ResultDir: consts.SimCardRestartSubDirResultExcel,
}, nil
default:
return OperationConfig{}, gerror.New("不支持的SIM卡状态操作")
}
}
func OperationByBatchType(batchType int) (Operation, error) {
switch batchType {
case consts.BatchTypeStop:
return OperationSuspend, nil
case consts.BatchTypeStart:
return OperationResume, nil
default:
return 0, gerror.Newf("批次类型%d不是停复机执行批次", batchType)
}
}
@@ -0,0 +1,92 @@
package lifecycle
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/errors/gerror"
)
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,87 @@
package lifecycle
import (
"context"
"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/model"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
type Processor struct {
config OperationConfig
}
func NewProcessor(operation Operation) (*Processor, error) {
config, err := operation.Config()
if err != nil {
return nil, err
}
return &Processor{config: config}, nil
}
func (p *Processor) Config() batchimport.ProcessorConfig {
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: consts.SimCardLifecycleMaxFileSize,
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, resultOss *model.SysOssSaveContentModel, downloadID int64) *gjson.Json {
info := g.Map{
"operation": p.config.Code,
}
if p.config.OperType != "" {
info["operType"] = p.config.OperType
}
if p.config.Operation == OperationRestart {
info["stopOperType"] = consts.SimCardStatusOperTypeStop
info["resumeOperType"] = consts.SimCardStatusOperTypeStart
info["resumeDelaySeconds"] = 300
info["restart"] = g.Map{"phase": "created"}
}
if options, ok := req.BizParams.(*SubmitOptions); ok && options != nil {
if remark := strings.TrimSpace(options.Remark); remark != "" {
info["remark"] = remark
}
}
if result != nil && result.ImportFileOssId > 0 {
info["importFileOssId"] = result.ImportFileOssId
} else if importOss != nil && importOss.OssId > 0 {
info["importFileOssId"] = importOss.OssId
}
if resultOss != nil {
info["resultFileOssId"] = resultOss.OssId
}
if downloadID > 0 {
info["resultDownloadId"] = downloadID
}
return gjson.New(info)
}
@@ -0,0 +1,418 @@
package lifecycle
import (
"context"
"fmt"
"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/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type Repository struct{}
func NewRepository() *Repository {
return &Repository{}
}
func (r *Repository) ClaimBatch(ctx context.Context, batchNo string) (bool, error) {
claimed := false
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
result, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().BatchNo, batchNo).
WhereIn(dao.BatchMain.Columns().BatchType, []int{consts.BatchTypeStart, consts.BatchTypeStop}).
Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusPending).
Data(do.BatchMain{
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
RunResult: consts.BatchRunResultRunning,
StartTime: gtime.Now(),
}).Update()
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected != 1 {
return nil
}
claimed = true
_, err = tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusPending).
Data(do.BatchDetail{DetailStatus: consts.BatchDetailStatusRunning}).
Update()
return err
})
return claimed, err
}
func (r *Repository) LoadTask(ctx context.Context, batchNo string) (*Task, error) {
batch := &entity.BatchMain{}
if err := dao.BatchMain.Ctx(ctx).
Where(dao.BatchMain.Columns().BatchNo, batchNo).
Scan(batch); err != nil {
return nil, gerror.Wrap(err, "读取停复机批次失败")
}
if batch.Id == 0 {
return nil, gerror.New("停复机批次不存在")
}
operation, err := OperationByBatchType(batch.BatchType)
if err != nil {
return nil, err
}
config, err := operation.Config()
if err != nil {
return nil, err
}
var details []*entity.BatchDetail
if err = dao.BatchDetail.Ctx(ctx).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Where(dao.BatchDetail.Columns().BatchType, batch.BatchType).
OrderAsc(dao.BatchDetail.Columns().RowNo).
Scan(&details); err != nil {
return nil, gerror.Wrap(err, "读取停复机批次明细失败")
}
if len(details) == 0 {
return nil, gerror.New("停复机批次没有明细")
}
task := &Task{
Batch: batch,
Operation: operation,
Config: config,
Rows: make([]*Row, 0, len(details)),
Operator: batchimport.OperatorContext{TenantId: batch.TenantId, DeptId: batch.CreatedDept, UserId: batch.CreatedBy},
StartTime: batch.StartTime,
}
if batch.BatchInfo != nil {
task.Remark = strings.TrimSpace(batch.BatchInfo.Get("remark").String())
}
task.ImportOssID = sourceOssIDFromBatch(batch)
for _, detail := range details {
row := &Row{
DetailID: detail.Id,
RowNo: detail.RowNo,
Iccid: strings.TrimSpace(detail.ItemId),
Reason: strings.TrimSpace(detail.ErrorReason),
}
if detail.Snapshot != nil {
if value := strings.TrimSpace(detail.Snapshot.Get("ICCID").String()); value != "" {
row.Iccid = value
}
row.Msisdn = strings.TrimSpace(detail.Snapshot.Get("MSISDN").String())
row.Imsi = strings.TrimSpace(detail.Snapshot.Get("IMSI").String())
row.Authorized = detail.Snapshot.Get("authorized").Bool()
}
task.Rows = append(task.Rows, row)
}
return task, nil
}
func (r *Repository) LoadCards(ctx context.Context, task *Task) (map[string]*entity.TSimCardInfo, error) {
result := make(map[string]*entity.TSimCardInfo, len(task.Rows))
iccids := make([]string, 0, len(task.Rows))
for _, row := range task.Rows {
if row != nil && row.Iccid != "" {
iccids = append(iccids, row.Iccid)
}
}
if len(iccids) == 0 {
return result, nil
}
var cards []*entity.TSimCardInfo
err := dao.TSimCardInfo.Ctx(ctx).
Where(dao.TSimCardInfo.Columns().TenantId, task.Batch.TenantId).
WhereIn(dao.TSimCardInfo.Columns().Iccid, iccids).
Scan(&cards)
if err != nil {
return nil, err
}
for _, card := range cards {
if card != nil {
result[strings.TrimSpace(card.Iccid)] = card
}
}
return result, nil
}
func (r *Repository) SaveValidationFailure(ctx context.Context, task *Task) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
for _, row := range task.Rows {
if row == nil {
continue
}
if _, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().Id, row.DetailID).
Where(dao.BatchDetail.Columns().BatchNo, task.Batch.BatchNo).
Data(do.BatchDetail{
DetailStatus: consts.BatchDetailStatusFailed,
ErrorReason: row.Reason,
Snapshot: row.Snapshot(),
UpdatedBy: task.Operator.UserId,
}).Update(); err != nil {
return gerror.Wrap(err, "保存整批校验失败明细失败")
}
}
return nil
})
}
func (r *Repository) SaveOutcome(ctx context.Context, task *Task, row *Row, card *entity.TSimCardInfo, outcome Outcome) error {
row.OrderNum = outcome.OrderNum
row.UpstreamCode = outcome.Code
row.UpstreamReason = outcome.Message
if outcome.Success {
row.Reason = ""
} else if strings.TrimSpace(outcome.Message) != "" {
row.Reason = outcome.Message
} else {
row.Reason = "上游处理失败"
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if outcome.Success {
result, err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
Where(dao.TSimCardInfo.Columns().Id, card.Id).
Where(dao.TSimCardInfo.Columns().TenantId, task.Batch.TenantId).
Where(dao.TSimCardInfo.Columns().BossCardStatus, row.StatusBefore).
Data(do.TSimCardInfo{
BossCardStatus: task.Config.TargetStatus,
StatusSyncTime: gtime.Now(),
UpdatedBy: task.Operator.UserId,
}).Update()
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected != 1 {
return gerror.New("SIM卡本地状态已变化")
}
}
detailStatus := consts.BatchDetailStatusFailed
if outcome.Success {
detailStatus = consts.BatchDetailStatusSuccess
}
result, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().Id, row.DetailID).
Where(dao.BatchDetail.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning).
Data(do.BatchDetail{
DetailStatus: detailStatus,
ErrorReason: row.Reason,
Snapshot: row.Snapshot(),
UpdatedBy: task.Operator.UserId,
}).Update()
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected != 1 {
return gerror.New("批次明细状态已变化")
}
return nil
})
}
func (r *Repository) SaveDetailFailure(ctx context.Context, task *Task, row *Row) error {
_, err := dao.BatchDetail.Ctx(ctx).
Where(dao.BatchDetail.Columns().Id, row.DetailID).
Where(dao.BatchDetail.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning).
Data(do.BatchDetail{
DetailStatus: consts.BatchDetailStatusFailed,
ErrorReason: row.Reason,
Snapshot: row.Snapshot(),
UpdatedBy: task.Operator.UserId,
}).Update()
return err
}
func (r *Repository) FinalizeBatch(ctx context.Context, task *Task) error {
processor, err := NewProcessor(task.Operation)
if err != nil {
return err
}
rows := make([]batchimport.Row, 0, len(task.Rows))
successCount := 0
for _, row := range task.Rows {
rows = append(rows, row)
if row != nil && row.Reason == "" {
successCount++
}
}
failCount := len(task.Rows) - successCount
validCount := 0
if task.ValidationOK {
validCount = len(task.Rows)
}
importStatus := consts.ImportStatusSuccess
batchStatus := consts.BatchAsyncStatusSuccess
runResult := consts.BatchRunResultSuccess
if failCount > 0 {
importStatus = consts.ImportStatusFailed
batchStatus = consts.BatchAsyncStatusFailed
runResult = consts.BatchRunResultFailed
if task.FailureReason == "" {
task.FailureReason = fmt.Sprintf("%d张卡处理失败", failCount)
}
}
result := &batchimport.Result{
ImportStatus: importStatus,
BatchNo: task.Batch.BatchNo,
BatchType: task.Batch.BatchType,
FailReason: task.FailureReason,
ImportFileOssId: task.ImportOssID,
TotalRows: len(task.Rows),
ValidRows: validCount,
ErrorRows: failCount,
InsertedRows: successCount,
}
req := batchimport.SubmitRequest{
SourceChannel: batchmetadata.Channel(task.Batch.SourceChannel),
ExecutionMode: int(task.Batch.ExecutionMode),
BizParams: &SubmitOptions{Remark: task.Remark},
SourceType: task.Batch.SourceType,
Operator: task.Operator,
BatchNo: task.Batch.BatchNo,
BatchType: task.Batch.BatchType,
}
importOss, err := r.loadImportOss(ctx, task)
if err != nil {
return err
}
endTime := gtime.Now()
backgroundCtx := contexts.Set(ctx, &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
ID: task.Operator.UserId,
DeptId: task.Operator.DeptId,
TenantId: task.Operator.TenantId,
}}})
resultOss, downloadID, err := batchimport.SaveAsyncResultFiles(backgroundCtx, req, processor, result, rows, importOss, task.StartTime, endTime)
if err != nil {
return gerror.Wrap(err, "生成停复机结果Excel失败")
}
batchInfo := processor.BuildBatchInfo(backgroundCtx, req, result, importOss, resultOss, downloadID)
_, err = dao.BatchMain.Ctx(ctx).
Where(dao.BatchMain.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing).
Data(do.BatchMain{
BatchAsyncStatus: batchStatus,
ValidCount: validCount,
SuccessCount: successCount,
FailCount: failCount,
RunResult: runResult,
FailReason: task.FailureReason,
ResultOssId: lifecycleOssIDValue(resultOss.OssId),
BatchInfo: batchInfo,
EndTime: endTime,
UpdatedBy: task.Operator.UserId,
}).Update()
return gerror.Wrap(err, "汇总停复机批次失败")
}
func (r *Repository) loadImportOss(ctx context.Context, task *Task) (*model.SysOssSaveContentModel, error) {
if task.ImportOssID == 0 {
return &model.SysOssSaveContentModel{SysOss: &entity.SysOss{}}, nil
}
oss := &entity.SysOss{}
if err := dao.SysOss.Ctx(ctx).
Where(dao.SysOss.Columns().OssId, task.ImportOssID).
Where(dao.SysOss.Columns().TenantId, task.Batch.TenantId).
Scan(oss); err != nil {
return nil, gerror.Wrap(err, "读取原始导入文件失败")
}
return &model.SysOssSaveContentModel{SysOss: oss}, nil
}
func sourceOssIDFromBatch(batch *entity.BatchMain) int64 {
if batch == nil {
return 0
}
if batch.SourceOssId > 0 {
return batch.SourceOssId
}
if batch.BatchInfo == nil {
return 0
}
for _, path := range []string{"importFileOssId", "source.importFileOssId", "source.inputFileOssId"} {
if ossID := batch.BatchInfo.Get(path).Int64(); ossID > 0 {
return ossID
}
}
return 0
}
func lifecycleOssIDValue(ossID int64) any {
if ossID <= 0 {
return nil
}
return ossID
}
func (r *Repository) MarkBatchFailed(ctx context.Context, batchNo, reason string) error {
if batchNo == "" {
return gerror.New("批次号不能为空")
}
if reason == "" {
reason = "后台执行失败"
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
_, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
WhereIn(dao.BatchDetail.Columns().DetailStatus, []int{consts.BatchDetailStatusPending, consts.BatchDetailStatusRunning}).
Data(do.BatchDetail{
DetailStatus: consts.BatchDetailStatusFailed,
ErrorReason: reason,
}).Update()
if err != nil {
return err
}
successCount, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusSuccess).
Count()
if err != nil {
return err
}
failCount, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusFailed).
Count()
if err != nil {
return err
}
_, err = tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().BatchNo, batchNo).
WhereIn(dao.BatchMain.Columns().BatchAsyncStatus, []int{consts.BatchAsyncStatusPending, consts.BatchAsyncStatusProcessing}).
Data(do.BatchMain{
BatchAsyncStatus: consts.BatchAsyncStatusFailed,
SuccessCount: successCount,
FailCount: failCount,
RunResult: consts.BatchRunResultFailed,
FailReason: reason,
EndTime: gtime.Now(),
}).Update()
return err
})
}
@@ -0,0 +1,91 @@
package lifecycle
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 BatchExecuteMessage = common.BatchExecuteMessage
type SubmitResult = common.SubmitResult
type Submitter struct {
operation Operation
}
func NewSubmitter(operation Operation) *Submitter {
return &Submitter{operation: 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, "lifecycle")
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, "lifecycle")
return common.PendingSubmitResult(result, consts.BatchAsyncStatusPending), nil
}
@@ -0,0 +1,107 @@
package lifecycle
import (
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type Row struct {
DetailID int64
RowNo int
Iccid string
Msisdn string
Imsi string
Authorized bool
ChanKey string
StatusBefore int
OrderNum string
UpstreamCode int
UpstreamReason string
Reason 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{})
}
return gjson.New(g.Map{
"ICCID": r.Iccid,
"MSISDN": r.Msisdn,
"IMSI": r.Imsi,
"authorized": r.Authorized,
"chanKey": r.ChanKey,
"statusBefore": r.StatusBefore,
"orderNum": r.OrderNum,
"upstreamCode": r.UpstreamCode,
"upstreamMessage": r.UpstreamReason,
})
}
type SubmitOptions = common.SubmitOptions
type Task struct {
Batch *entity.BatchMain
Operation Operation
Config OperationConfig
Rows []*Row
Operator batchimport.OperatorContext
ImportOssID int64
Remark string
StartTime *gtime.Time
ValidationOK bool
FailureReason string
}
type Outcome struct {
Success bool
Code int
Message string
OrderNum string
}