chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)
This commit is contained in:
@@ -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,247 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"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(batchimport.NotExecutedReasonPrefix + "本卡校验通过")
|
||||
}
|
||||
}
|
||||
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,290 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
)
|
||||
|
||||
type fakeExecutorRepository struct {
|
||||
mu sync.Mutex
|
||||
claims []bool
|
||||
claimCalls int
|
||||
loadedBatchNos []string
|
||||
task *Task
|
||||
cards map[string]*entity.TSimCardInfo
|
||||
validationFailureCalls int
|
||||
outcomes []Outcome
|
||||
finalizeCalls int
|
||||
markFailedCalls int
|
||||
}
|
||||
|
||||
func (r *fakeExecutorRepository) ClaimBatch(context.Context, string) (bool, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
index := r.claimCalls
|
||||
r.claimCalls++
|
||||
if index >= len(r.claims) {
|
||||
return false, nil
|
||||
}
|
||||
return r.claims[index], nil
|
||||
}
|
||||
|
||||
func (r *fakeExecutorRepository) LoadTask(_ context.Context, batchNo string) (*Task, error) {
|
||||
r.loadedBatchNos = append(r.loadedBatchNos, batchNo)
|
||||
return r.task, nil
|
||||
}
|
||||
|
||||
func (r *fakeExecutorRepository) LoadCards(context.Context, *Task) (map[string]*entity.TSimCardInfo, error) {
|
||||
return r.cards, nil
|
||||
}
|
||||
|
||||
func (r *fakeExecutorRepository) SaveValidationFailure(context.Context, *Task) error {
|
||||
r.validationFailureCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeExecutorRepository) SaveOutcome(_ context.Context, _ *Task, _ *Row, _ *entity.TSimCardInfo, outcome Outcome) error {
|
||||
r.outcomes = append(r.outcomes, outcome)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeExecutorRepository) SaveDetailFailure(context.Context, *Task, *Row) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeExecutorRepository) FinalizeBatch(context.Context, *Task) error {
|
||||
r.finalizeCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeExecutorRepository) MarkBatchFailed(context.Context, string, string) error {
|
||||
r.markFailedCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeStatusChanger struct {
|
||||
calls int
|
||||
requests []*model.SimCardStatusChangeInput
|
||||
response []model.SimBaseRes[model.SimCardStatusChangeItem]
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *fakeStatusChanger) ChangeSimStatus(_ context.Context, input *model.SimCardStatusChangeInput) ([]model.SimBaseRes[model.SimCardStatusChangeItem], error) {
|
||||
c.calls++
|
||||
c.requests = append(c.requests, input)
|
||||
return c.response, c.err
|
||||
}
|
||||
|
||||
func TestActionOperTypeMapping(t *testing.T) {
|
||||
stop, err := OperationSuspend.Config()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
start, err := OperationResume.Config()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stop.OperType != "0" || stop.BatchType != consts.BatchTypeStop {
|
||||
t.Fatalf("unexpected stop mapping: %+v", stop)
|
||||
}
|
||||
if start.OperType != "1" || start.BatchType != consts.BatchTypeStart {
|
||||
t.Fatalf("unexpected start mapping: %+v", start)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteValidationFailureSkipsUpstream(t *testing.T) {
|
||||
task := newTestTask(t, OperationSuspend, []*Row{
|
||||
{RowNo: 1, Iccid: "missing", Authorized: true},
|
||||
{RowNo: 2, Iccid: "valid", Authorized: true},
|
||||
})
|
||||
repo := &fakeExecutorRepository{
|
||||
claims: []bool{true},
|
||||
task: task,
|
||||
cards: map[string]*entity.TSimCardInfo{
|
||||
"valid": {Id: 2, Iccid: "valid", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusActivated},
|
||||
},
|
||||
}
|
||||
upstream := &fakeStatusChanger{}
|
||||
if err := newExecutor(repo, upstream).Execute(context.Background(), "STP-test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if upstream.calls != 0 {
|
||||
t.Fatalf("upstream called %d times for invalid batch", upstream.calls)
|
||||
}
|
||||
if repo.validationFailureCalls != 1 || repo.finalizeCalls != 1 {
|
||||
t.Fatalf("validation/finalize calls = %d/%d", repo.validationFailureCalls, repo.finalizeCalls)
|
||||
}
|
||||
if task.Rows[0].Reason == "" || task.Rows[1].Reason == "" {
|
||||
t.Fatalf("expected all rows to retain failure reasons: %+v", task.Rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateStopStatus(t *testing.T) {
|
||||
t.Skip("TODO: 恢复卡状态限制后启用此测试")
|
||||
tests := []struct {
|
||||
name string
|
||||
status int
|
||||
valid bool
|
||||
}{
|
||||
{name: "activated", status: consts.V3ApiSimBossCardStatusActivated, valid: true},
|
||||
{name: "pending", status: consts.V3ApiSimBossCardStatusPendingActivate},
|
||||
{name: "testable", status: consts.V3ApiSimBossCardStatusTestable},
|
||||
{name: "stock", status: consts.V3ApiSimBossCardStatusStock},
|
||||
{name: "stopped", status: consts.V3ApiSimBossCardStatusStopped},
|
||||
{name: "pre-cancel", status: consts.V3ApiSimBossCardStatusPreCancel},
|
||||
{name: "canceled", status: consts.V3ApiSimBossCardStatusCanceled},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
row := &Row{Iccid: "card-1"}
|
||||
validateCardStatus(OperationSuspend, row, &entity.TSimCardInfo{BossCardStatus: tt.status})
|
||||
if got := row.Reason == ""; got != tt.valid {
|
||||
t.Fatalf("status %d valid=%v, reason=%q", tt.status, got, row.Reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteMixedStopStatusSkipsUpstream(t *testing.T) {
|
||||
t.Skip("TODO: 恢复卡状态限制后启用此测试")
|
||||
task := newTestTask(t, OperationSuspend, []*Row{
|
||||
{RowNo: 1, Iccid: "activated", Authorized: true},
|
||||
{RowNo: 2, Iccid: "stock", Authorized: true},
|
||||
})
|
||||
repo := &fakeExecutorRepository{
|
||||
claims: []bool{true},
|
||||
task: task,
|
||||
cards: map[string]*entity.TSimCardInfo{
|
||||
"activated": {Id: 1, Iccid: "activated", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusActivated},
|
||||
"stock": {Id: 2, Iccid: "stock", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusStock},
|
||||
},
|
||||
}
|
||||
upstream := &fakeStatusChanger{}
|
||||
if err := newExecutor(repo, upstream).Execute(context.Background(), "STP-mixed"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if upstream.calls != 0 {
|
||||
t.Fatalf("upstream called %d times for mixed status batch", upstream.calls)
|
||||
}
|
||||
if repo.validationFailureCalls != 1 || repo.finalizeCalls != 1 {
|
||||
t.Fatalf("validation/finalize calls = %d/%d", repo.validationFailureCalls, repo.finalizeCalls)
|
||||
}
|
||||
if task.Rows[0].Reason == "" || task.Rows[1].Reason == "" {
|
||||
t.Fatalf("expected both rows to be failed without upstream call: %+v", task.Rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteRestoresByBatchNoAndMapsPerCardResults(t *testing.T) {
|
||||
task := newTestTask(t, OperationSuspend, []*Row{
|
||||
{RowNo: 1, Iccid: "card-1", Authorized: true},
|
||||
{RowNo: 2, Iccid: "card-2", Authorized: true},
|
||||
})
|
||||
repo := &fakeExecutorRepository{
|
||||
claims: []bool{true},
|
||||
task: task,
|
||||
cards: map[string]*entity.TSimCardInfo{
|
||||
"card-1": {Id: 1, Iccid: "card-1", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusActivated},
|
||||
"card-2": {Id: 2, Iccid: "card-2", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusActivated},
|
||||
},
|
||||
}
|
||||
upstream := &fakeStatusChanger{response: []model.SimBaseRes[model.SimCardStatusChangeItem]{
|
||||
{Code: 0, SimKey: &model.SimKey{ICCID: "card-1"}, ResData: model.SimCardStatusChangeItem{OrderNum: "order-1"}},
|
||||
{Code: 500, Message: "operator rejected", SimKey: &model.SimKey{ICCID: "card-2"}},
|
||||
}}
|
||||
if err := newExecutor(repo, upstream).Execute(context.Background(), "STP-restore"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(repo.loadedBatchNos) != 1 || repo.loadedBatchNos[0] != "STP-restore" {
|
||||
t.Fatalf("executor did not restore by batchNo: %+v", repo.loadedBatchNos)
|
||||
}
|
||||
if upstream.calls != 1 || len(upstream.requests) != 1 {
|
||||
t.Fatalf("unexpected upstream calls: %d", upstream.calls)
|
||||
}
|
||||
if upstream.requests[0].ReqData.OperType != "0" {
|
||||
t.Fatalf("unexpected operType: %s", upstream.requests[0].ReqData.OperType)
|
||||
}
|
||||
if len(repo.outcomes) != 2 || !repo.outcomes[0].Success || repo.outcomes[1].Success {
|
||||
t.Fatalf("unexpected per-card outcomes: %+v", repo.outcomes)
|
||||
}
|
||||
if repo.outcomes[0].OrderNum != "order-1" || repo.outcomes[1].Message != "operator rejected" {
|
||||
t.Fatalf("upstream details were not preserved: %+v", repo.outcomes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteDuplicateClaimRunsOnlyOnce(t *testing.T) {
|
||||
task := newTestTask(t, OperationResume, []*Row{{RowNo: 1, Iccid: "card-1", Authorized: true}})
|
||||
repo := &fakeExecutorRepository{
|
||||
claims: []bool{true, false},
|
||||
task: task,
|
||||
cards: map[string]*entity.TSimCardInfo{
|
||||
"card-1": {Id: 1, Iccid: "card-1", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusStopped},
|
||||
},
|
||||
}
|
||||
upstream := &fakeStatusChanger{response: []model.SimBaseRes[model.SimCardStatusChangeItem]{
|
||||
{Code: 0, SimKey: &model.SimKey{ICCID: "card-1"}},
|
||||
}}
|
||||
executor := newExecutor(repo, upstream)
|
||||
if err := executor.Execute(context.Background(), "STR-once"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := executor.Execute(context.Background(), "STR-once"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if upstream.calls != 1 || len(repo.loadedBatchNos) != 1 || repo.finalizeCalls != 1 {
|
||||
t.Fatalf("duplicate claim executed twice: upstream=%d load=%d finalize=%d", upstream.calls, len(repo.loadedBatchNos), repo.finalizeCalls)
|
||||
}
|
||||
if upstream.requests[0].ReqData.OperType != "1" {
|
||||
t.Fatalf("unexpected start operType: %s", upstream.requests[0].ReqData.OperType)
|
||||
}
|
||||
}
|
||||
|
||||
type blockingExecutor struct {
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
func (e *blockingExecutor) Execute(context.Context, string) error {
|
||||
close(e.started)
|
||||
<-e.release
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *blockingExecutor) MarkFailed(context.Context, string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDispatchReturnsBeforeExecutorCompletes(t *testing.T) {
|
||||
executor := &blockingExecutor{started: make(chan struct{}), release: make(chan struct{})}
|
||||
Dispatch(executor, "STP-async")
|
||||
select {
|
||||
case <-executor.started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("background executor did not start")
|
||||
}
|
||||
select {
|
||||
case <-executor.release:
|
||||
t.Fatal("release channel should still be blocked")
|
||||
default:
|
||||
}
|
||||
close(executor.release)
|
||||
}
|
||||
|
||||
func newTestTask(t *testing.T, operation Operation, rows []*Row) *Task {
|
||||
t.Helper()
|
||||
config, err := operation.Config()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &Task{
|
||||
Batch: &entity.BatchMain{BatchNo: "batch-test", BatchType: config.BatchType, TenantId: "tenant-1"},
|
||||
Operation: operation,
|
||||
Config: config,
|
||||
Rows: rows,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatchExecuteMessageJSONContract(t *testing.T) {
|
||||
body, err := json.Marshal(BatchExecuteMessage{
|
||||
BatchNo: "STP202607170001",
|
||||
BatchType: 104,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal batch execute message: %v", err)
|
||||
}
|
||||
const expected = `{"batchNo":"STP202607170001","batchType":104}`
|
||||
if string(body) != expected {
|
||||
t.Fatalf("unexpected batch execute message: %s", body)
|
||||
}
|
||||
}
|
||||
@@ -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,92 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
)
|
||||
|
||||
func TestValidateBasicRowsRejectsDuplicateIccid(t *testing.T) {
|
||||
rows := []batchimport.Row{
|
||||
&Row{RowNo: 2, Iccid: " 8986001 "},
|
||||
&Row{RowNo: 3, Iccid: "8986001"},
|
||||
}
|
||||
err := validateBasicRows(rows)
|
||||
if err == nil || !strings.Contains(err.Error(), "ICCID") || !strings.Contains(err.Error(), "重复") {
|
||||
t.Fatalf("expected readable duplicate ICCID error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBasicRowsRejectsEveryEmptyIccid(t *testing.T) {
|
||||
rows := []batchimport.Row{
|
||||
&Row{RowNo: 2, Iccid: ""},
|
||||
&Row{RowNo: 5, Iccid: " "},
|
||||
}
|
||||
err := validateBasicRows(rows)
|
||||
if err == nil {
|
||||
t.Fatal("expected empty ICCID validation error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "第2行") || !strings.Contains(err.Error(), "第5行") {
|
||||
t.Fatalf("expected all empty ICCID rows in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBasicRowsRejectsWrongFieldCount(t *testing.T) {
|
||||
rows, err := ParseInputRows(context.Background(), "8986001 13800008000")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = validateBasicRows(rows)
|
||||
if err == nil || !strings.Contains(err.Error(), "格式") {
|
||||
t.Fatalf("expected wrong field count error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidBasicRowsStopBeforePendingPreparation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rows []batchimport.Row
|
||||
}{
|
||||
{
|
||||
name: "duplicate",
|
||||
rows: []batchimport.Row{
|
||||
&Row{RowNo: 2, Iccid: "8986001"},
|
||||
&Row{RowNo: 3, Iccid: "8986001"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple empty iccid",
|
||||
rows: []batchimport.Row{
|
||||
&Row{RowNo: 2},
|
||||
&Row{RowNo: 3, Iccid: " "},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
processor, err := NewProcessor(OperationSuspend)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prepared := false
|
||||
result, err := batchimport.CreatePendingRows(
|
||||
context.Background(),
|
||||
batchimport.SubmitRequest{},
|
||||
processor,
|
||||
tt.rows,
|
||||
batchimport.PendingOptions{PrepareRows: func(context.Context, []batchimport.Row) error {
|
||||
prepared = true
|
||||
return nil
|
||||
}},
|
||||
)
|
||||
if err == nil || result != nil {
|
||||
t.Fatalf("expected pre-persistence validation error, result=%+v err=%v", result, err)
|
||||
}
|
||||
if prepared {
|
||||
t.Fatal("invalid rows reached pending preparation")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,424 @@
|
||||
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
|
||||
}
|
||||
status := consts.BatchDetailStatusFailed
|
||||
if batchimport.IsNotExecuted(row) {
|
||||
status = consts.BatchDetailStatusNotExecuted
|
||||
}
|
||||
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: status,
|
||||
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
|
||||
failCount := 0
|
||||
for _, row := range task.Rows {
|
||||
rows = append(rows, row)
|
||||
if row != nil && row.Reason == "" && task.ValidationOK {
|
||||
successCount++
|
||||
} else if row == nil || !batchimport.IsNotExecuted(row) {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
validCount := len(task.Rows) - failCount
|
||||
if task.ValidationOK {
|
||||
validCount = len(task.Rows)
|
||||
}
|
||||
importStatus := consts.ImportStatusSuccess
|
||||
batchStatus := consts.BatchAsyncStatusSuccess
|
||||
runResult := consts.BatchRunResultSuccess
|
||||
if failCount > 0 || !task.ValidationOK {
|
||||
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,101 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"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/entity"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestRepositorySaveOutcomeUpdatesOnlySuccessfulCard(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if err := gdb.SetConfig(gdb.Config{
|
||||
"default": gdb.ConfigGroup{{
|
||||
Type: "sqlite",
|
||||
Link: "sqlite::@file(:memory:)",
|
||||
MaxOpenConnCount: 1,
|
||||
}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer g.DB().Close(ctx)
|
||||
|
||||
statements := []string{
|
||||
`CREATE TABLE t_sim_card_info (
|
||||
id INTEGER PRIMARY KEY,
|
||||
iccid TEXT NOT NULL,
|
||||
boss_card_status INTEGER NOT NULL,
|
||||
status_sync_time DATETIME,
|
||||
tenant_id TEXT NOT NULL,
|
||||
updated_by INTEGER,
|
||||
updated_at DATETIME
|
||||
)`,
|
||||
`CREATE TABLE batch_detail (
|
||||
id INTEGER PRIMARY KEY,
|
||||
batch_no TEXT NOT NULL,
|
||||
detail_status INTEGER NOT NULL,
|
||||
error_reason TEXT,
|
||||
snapshot TEXT,
|
||||
updated_by INTEGER,
|
||||
updated_at DATETIME
|
||||
)`,
|
||||
`INSERT INTO t_sim_card_info(id, iccid, boss_card_status, tenant_id) VALUES
|
||||
(1, 'card-success', 2, 'tenant-1'),
|
||||
(2, 'card-failed', 2, 'tenant-1')`,
|
||||
`INSERT INTO batch_detail(id, batch_no, detail_status) VALUES
|
||||
(1, 'STP-test', 1),
|
||||
(2, 'STP-test', 1)`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
config, err := OperationSuspend.Config()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
task := &Task{
|
||||
Batch: &entity.BatchMain{BatchNo: "STP-test", TenantId: "tenant-1"},
|
||||
Config: config,
|
||||
Operator: batchimport.OperatorContext{TenantId: "tenant-1", UserId: 10},
|
||||
}
|
||||
repository := NewRepository()
|
||||
successRow := &Row{DetailID: 1, Iccid: "card-success", StatusBefore: consts.V3ApiSimBossCardStatusActivated}
|
||||
failedRow := &Row{DetailID: 2, Iccid: "card-failed", StatusBefore: consts.V3ApiSimBossCardStatusActivated}
|
||||
if err = repository.SaveOutcome(ctx, task, successRow, &entity.TSimCardInfo{Id: 1}, Outcome{Success: true, OrderNum: "order-1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = repository.SaveOutcome(ctx, task, failedRow, &entity.TSimCardInfo{Id: 2}, Outcome{Code: 500, Message: "rejected"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var cards []struct {
|
||||
Iccid string `orm:"iccid"`
|
||||
BossCardStatus int `orm:"boss_card_status"`
|
||||
}
|
||||
if err = g.DB().Model("t_sim_card_info").OrderAsc("id").Scan(&cards); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(cards) != 2 || cards[0].BossCardStatus != consts.V3ApiSimBossCardStatusStopped || cards[1].BossCardStatus != consts.V3ApiSimBossCardStatusActivated {
|
||||
t.Fatalf("unexpected local card statuses: %+v", cards)
|
||||
}
|
||||
|
||||
var details []struct {
|
||||
DetailStatus int `orm:"detail_status"`
|
||||
ErrorReason string `orm:"error_reason"`
|
||||
}
|
||||
if err = g.DB().Model("batch_detail").OrderAsc("id").Scan(&details); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(details) != 2 || details[0].DetailStatus != consts.BatchDetailStatusSuccess || details[1].DetailStatus != consts.BatchDetailStatusFailed || details[1].ErrorReason != "rejected" {
|
||||
t.Fatalf("unexpected detail results: %+v", details)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user