Files
iotlcm/worker-svr/internal/service/lifecycle/executor.go

242 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lifecycle
import (
"context"
"sort"
"strings"
"sync"
"github.com/gogf/gf/v2/errors/gerror"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
)
type executor struct {
repository Repository
upstream Upstream
}
func NewExecutor(repository Repository, upstream Upstream) Executor {
return &executor{repository: repository, upstream: upstream}
}
func (e *executor) Execute(ctx context.Context, batchNo string) error {
task, err := e.repository.Load(ctx, strings.TrimSpace(batchNo))
if err != nil {
return gerror.Wrap(err, "load lifecycle batch")
}
if !validateTask(task) {
if err = e.repository.SaveValidationFailure(ctx, task); err != nil {
return gerror.Wrap(err, "save lifecycle validation failure")
}
return gerror.Wrap(e.repository.Finalize(ctx, task), "finalize lifecycle validation failure")
}
groups := groupByChanKey(task.Details)
keys := make([]string, 0, len(groups))
for chanKey := range groups {
keys = append(keys, chanKey)
}
sort.Strings(keys)
for _, chanKey := range keys {
for index, outcome := range e.changeGroup(ctx, task, groups[chanKey]) {
detail := groups[chanKey][index]
resolved, saveErr := e.repository.SaveResult(ctx, task, detail, outcome)
if saveErr != nil {
return gerror.Wrapf(saveErr, "save lifecycle detail %d result", detail.ID)
}
detail.ApplyOutcome(resolved)
}
}
return gerror.Wrap(e.repository.Finalize(ctx, task), "finalize lifecycle batch")
}
func validateTask(task *Task) bool {
if task == nil || len(task.Details) == 0 {
return false
}
hasFailure := false
for _, detail := range task.Details {
if detail == nil {
hasFailure = true
continue
}
validateDetail(task.Operation, detail)
if detail.Reason != "" {
hasFailure = true
}
}
if !hasFailure {
task.ValidationOK = true
return true
}
for _, detail := range task.Details {
if detail != nil && detail.Reason == "" {
detail.Reason = "同批次存在校验失败卡,本卡未请求上游"
}
}
task.FailureReason = "批次校验未通过,未发起上游请求"
return false
}
func validateDetail(operation Operation, detail *Detail) {
if detail.Reason != "" {
return
}
if detail.Card == nil {
detail.Reason = "ICCID不存在或不属于当前租户"
return
}
detail.MSISDN = strings.TrimSpace(detail.Card.MSISDN)
detail.IMSI = strings.TrimSpace(detail.Card.IMSI)
detail.ChanKey = strings.TrimSpace(detail.Card.ChanKey)
detail.StatusBefore = detail.Card.BossCardStatus
if !detail.Authorized {
detail.Reason = "ICCID不在当前操作员授权范围内"
return
}
var reasons []string
if detail.ChanKey == "" {
reasons = append(reasons, "ICCID缺少通道标识")
}
if detail.InputMSISDN != "" && detail.InputMSISDN != detail.MSISDN {
reasons = append(reasons, "MSISDN与卡档案不一致")
}
if detail.InputIMSI != "" && detail.InputIMSI != detail.IMSI {
reasons = append(reasons, "IMSI与卡档案不一致")
}
detail.Reason = strings.Join(reasons, ";")
// TODO: 正常系统开发完成后恢复卡状态限制
// 注释原因:开发环境经常出现卡状态不在预期可操作范围内的情况,先放行
// if detail.Card.BossCardStatus != operation.ExpectedStatus {
// detail.Reason = "SIM card status does not allow this lifecycle operation"
// return
// }
}
func groupByChanKey(details []*Detail) map[string][]*Detail {
groups := make(map[string][]*Detail)
for _, detail := range details {
groups[detail.ChanKey] = append(groups[detail.ChanKey], detail)
}
return groups
}
func (e *executor) changeGroup(ctx context.Context, task *Task, details []*Detail) []Outcome {
subBatches := splitDetails(details, consts.BatchSubBatchSize)
subOutcomes := make([][]Outcome, len(subBatches))
sem := make(chan struct{}, consts.BatchSubBatchConcurrency)
var wg sync.WaitGroup
for i, sub := range subBatches {
wg.Add(1)
sem <- struct{}{}
go func(index int, batch []*Detail) {
defer wg.Done()
defer func() { <-sem }()
subOutcomes[index] = e.changeSubBatch(ctx, task, batch)
}(i, sub)
}
wg.Wait()
outcomes := make([]Outcome, 0, len(details))
for _, subOutcome := range subOutcomes {
outcomes = append(outcomes, subOutcome...)
}
return outcomes
}
// changeSubBatch 处理单个子批次:构建请求并调用上游,返回与该子批次明细等长的 Outcome 切片。
func (e *executor) changeSubBatch(ctx context.Context, task *Task, details []*Detail) []Outcome {
input := &model.SimCardStatusChangeInput{
SimKeys: make([]*model.SimKey, 0, len(details)),
ReqData: model.SimCardStatusChangeInputItem{OperType: task.Operation.OperType},
}
for _, detail := range details {
input.SimKeys = append(input.SimKeys, &model.SimKey{ChanKey: detail.ChanKey, ICCID: detail.ICCID, MSISDN: detail.MSISDN, IMSI: detail.IMSI})
}
response, err := e.upstream.ChangeSimStatus(ctx, input)
if err != nil {
outcomes := make([]Outcome, len(details))
for index := range outcomes {
outcomes[index].Message = "请求上游失败:" + err.Error()
}
return outcomes
}
return mapOutcomes(details, response)
}
// splitDetails 按固定大小把明细切成有序的子批次;空输入返回空切片。
func splitDetails(details []*Detail, size int) [][]*Detail {
if size <= 0 {
size = 1
}
batches := make([][]*Detail, 0, (len(details)+size-1)/size)
for i := 0; i < len(details); i += size {
end := i + size
if end > len(details) {
end = len(details)
}
batches = append(batches, details[i:end])
}
return batches
}
func mapOutcomes(details []*Detail, response []model.SimBaseRes[model.SimCardStatusChangeOutputItem]) []Outcome {
outcomes := make([]Outcome, len(details))
used := make([]bool, len(response))
for detailIndex, detail := range details {
matched := -1
for responseIndex, item := range response {
if !used[responseIndex] && sameSim(detail, item.SimKey) {
matched = responseIndex
break
}
}
if matched < 0 && detailIndex < len(response) && !used[detailIndex] && response[detailIndex].SimKey == nil {
matched = detailIndex
}
if matched < 0 {
outcomes[detailIndex] = Outcome{Message: "upstream did not return a result for this SIM card"}
continue
}
used[matched] = true
item := response[matched]
message := strings.TrimSpace(item.Message)
if item.Code != 0 && message == "" {
message = "upstream processing failed"
}
outcomes[detailIndex] = Outcome{Success: item.Code == 0, Code: item.Code, Message: message, OrderNum: strings.TrimSpace(item.ResData.OrderNum)}
}
return outcomes
}
func sameSim(detail *Detail, key *model.SimKey) bool {
if detail == nil || key == nil {
return false
}
if value := strings.TrimSpace(key.ICCID); value != "" {
return value == detail.ICCID
}
if value := strings.TrimSpace(key.MSISDN); value != "" {
return value == detail.MSISDN
}
if value := strings.TrimSpace(key.IMSI); value != "" {
return value == detail.IMSI
}
return false
}
func OperationByBatchType(batchType int) (Operation, error) {
switch batchType {
case consts.BatchTypeSimCardStart:
return Operation{BatchType: batchType, OperType: consts.SimCardStatusOperTypeStart, ExpectedStatus: consts.SimBossCardStatusStopped, TargetStatus: consts.SimBossCardStatusActivated}, nil
case consts.BatchTypeSimCardStop:
return Operation{BatchType: batchType, OperType: consts.SimCardStatusOperTypeStop, ExpectedStatus: consts.SimBossCardStatusActivated, TargetStatus: consts.SimBossCardStatusStopped}, nil
default:
return Operation{}, gerror.Newf("unsupported lifecycle batch type %d", batchType)
}
}