chore: 并入 iotlcm-worker-svr 历史到 worker-svr/(原提交 96f0f4e)

This commit is contained in:
MiFaZhan
2026-11-02 13:28:03 +08:00
422 changed files with 80993 additions and 0 deletions
@@ -0,0 +1,125 @@
package unsub
import (
"context"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/batchresult"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/packagecustomer"
)
const (
cascadeFailureReason = "同一批次存在无效退订明细,该行已跳过"
validationSkipReason = "批次校验未通过,已跳过退订请求"
emptyICCIDReason = "ICCID不能为空"
duplicateICCIDReason = "ICCID在批次内重复"
)
type executor struct {
repository Repository
}
func NewExecutor(repository Repository) Executor {
return &executor{repository: repository}
}
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 unsub batch failed")
}
if !validateTask(task) {
if err = e.repository.SaveValidationFailure(ctx, task); err != nil {
return gerror.Wrap(err, "save unsub validation failure failed")
}
e.saveResultFile(ctx, task)
return nil
}
if err = e.repository.CommitBusiness(ctx, task); err != nil {
return gerror.Wrap(err, "commit unsub business failed")
}
packagecustomer.PublishSyncForDeptsSafe(ctx, task.Batch.TenantID, task.Batch.CreatedBy, unsubPackageSyncDeptIDs(task), "unsub", task.Batch.BatchNo)
e.saveResultFile(ctx, task)
return nil
}
func unsubPackageSyncDeptIDs(task *Task) []int64 {
if task == nil {
return nil
}
deptIds := []int64{task.Batch.DeptID, task.Batch.CreatedDept}
for _, detail := range task.Details {
if detail == nil || strings.TrimSpace(detail.Reason) != "" {
continue
}
if detail.CardDeptID > 0 {
deptIds = append(deptIds, detail.CardDeptID)
}
}
return deptIds
}
func (e *executor) saveResultFile(ctx context.Context, task *Task) {
request := batchresult.Request{
BatchNo: task.Batch.BatchNo,
BatchType: task.Batch.BatchType,
TenantID: task.Batch.TenantID,
Rows: task.ResultRows(),
}
if err := e.repository.SaveResultFile(ctx, task, request); err != nil {
g.Log().Errorf(ctx, "save unsub result file status failed, batchNo:%s, err:%+v", task.Batch.BatchNo, err)
}
}
func validateTask(task *Task) bool {
if task == nil || len(task.Details) == 0 {
if task != nil {
task.FailureReason = "退订批次无有效明细"
}
return false
}
hasFailure := false
iccids := make(map[string]int, len(task.Details))
for _, detail := range task.Details {
if detail == nil {
hasFailure = true
continue
}
if detail.Reason != "" {
hasFailure = true
continue
}
iccid := strings.TrimSpace(detail.ICCID)
if iccid == "" {
detail.Reason = emptyICCIDReason
hasFailure = true
continue
}
iccids[iccid]++
}
for _, detail := range task.Details {
if detail == nil {
continue
}
if detail.Reason != "" {
continue
}
if iccids[strings.TrimSpace(detail.ICCID)] > 1 {
detail.Reason = duplicateICCIDReason
hasFailure = true
}
}
if !hasFailure {
return true
}
for _, detail := range task.Details {
if detail != nil && detail.Reason == "" {
detail.Reason = cascadeFailureReason
}
}
task.FailureReason = validationSkipReason
return false
}
@@ -0,0 +1,195 @@
package unsub
import (
"context"
"errors"
"testing"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/batchresult"
)
func TestValidateTaskFailsWhenAnyDetailHasPreExistingErrorReason(t *testing.T) {
task := &Task{
Batch: Batch{BatchNo: "B-1"},
Details: []*Detail{
{RowNo: 1, ICCID: "8986"},
{RowNo: 2, ICCID: "8987", Reason: "snapshot has reason"},
},
}
if validateTask(task) {
t.Fatal("expected validation to fail when any detail has a pre-existing reason")
}
if task.Details[0].Reason != cascadeFailureReason {
t.Fatalf("expected cascade reason on clean detail, got %q", task.Details[0].Reason)
}
if task.FailureReason == "" {
t.Fatal("expected task failure reason to be populated")
}
}
func TestValidateTaskFailsOnEmptyICCID(t *testing.T) {
task := &Task{
Batch: Batch{BatchNo: "B-2"},
Details: []*Detail{
{RowNo: 1, ICCID: ""},
},
}
if validateTask(task) {
t.Fatal("expected empty ICCID to fail validation")
}
if task.Details[0].Reason != emptyICCIDReason {
t.Fatalf("expected empty ICCID reason, got %q", task.Details[0].Reason)
}
}
func TestValidateTaskFailsOnDuplicateICCID(t *testing.T) {
task := &Task{
Batch: Batch{BatchNo: "B-3"},
Details: []*Detail{
{RowNo: 1, ICCID: "8986"},
{RowNo: 2, ICCID: "8986"},
},
}
if validateTask(task) {
t.Fatal("expected duplicate ICCID to fail validation")
}
if task.Details[0].Reason != duplicateICCIDReason || task.Details[1].Reason != duplicateICCIDReason {
t.Fatalf("expected both duplicates flagged, got %q / %q", task.Details[0].Reason, task.Details[1].Reason)
}
}
func TestValidateTaskPassesOnCleanDetails(t *testing.T) {
task := &Task{
Batch: Batch{BatchNo: "B-4"},
Details: []*Detail{
{RowNo: 1, ICCID: "8986"},
{RowNo: 2, ICCID: "8987"},
},
}
if !validateTask(task) {
t.Fatal("expected validation to pass for clean details")
}
}
func TestExecuteHappyPathCommitsAndSavesResult(t *testing.T) {
task := &Task{
Batch: Batch{BatchNo: "B-5", BatchType: 108, TenantID: "000000"},
Details: []*Detail{
{RowNo: 1, ICCID: "8986"},
},
}
repo := &fakeRepository{task: task}
if err := NewExecutor(repo).Execute(context.Background(), "B-5"); err != nil {
t.Fatalf("Execute() error = %v", err)
}
if !repo.committed {
t.Fatal("expected business to be committed")
}
if !repo.resultSaved {
t.Fatal("expected result file to be saved")
}
if repo.validationSaved {
t.Fatal("did not expect validation failure path on happy path")
}
}
func TestExecuteValidationFailureSkipsCommit(t *testing.T) {
task := &Task{
Batch: Batch{BatchNo: "B-6", BatchType: 108},
Details: []*Detail{
{RowNo: 1, ICCID: "8986"},
{RowNo: 2, ICCID: "8987", Reason: "snapshot error"},
},
}
repo := &fakeRepository{task: task}
if err := NewExecutor(repo).Execute(context.Background(), "B-6"); err != nil {
t.Fatalf("Execute() error = %v", err)
}
if !repo.validationSaved {
t.Fatal("expected validation failure to be persisted")
}
if repo.committed {
t.Fatal("did not expect business to be committed when validation fails")
}
if !repo.resultSaved {
t.Fatal("expected result file to be saved even when validation fails")
}
}
func TestExecuteCommitFailurePropagatesAndSkipsResult(t *testing.T) {
task := &Task{
Batch: Batch{BatchNo: "B-7", BatchType: 108},
Details: []*Detail{
{RowNo: 1, ICCID: "8986"},
},
}
commitErr := errors.New("transaction failed")
repo := &fakeRepository{task: task, commitErr: commitErr}
err := NewExecutor(repo).Execute(context.Background(), "B-7")
if err == nil || !errors.Is(err, commitErr) {
t.Fatalf("Execute() error = %v, want wrap of %v", err, commitErr)
}
if repo.validationSaved {
t.Fatal("validation path should not run when commit failure happens before it")
}
if repo.resultSaved {
t.Fatal("result file should not be saved when commit fails")
}
}
func TestExecuteResultFileFailureOnlyMarksBatchInfo(t *testing.T) {
task := &Task{
Batch: Batch{BatchNo: "B-8", BatchType: 108, TenantID: "000000"},
Details: []*Detail{
{RowNo: 1, ICCID: "8986"},
},
}
repo := &fakeRepository{task: task, resultErr: errors.New("disk full")}
if err := NewExecutor(repo).Execute(context.Background(), "B-8"); err != nil {
t.Fatalf("Execute() error = %v, want nil when only result file fails", err)
}
if !repo.committed {
t.Fatal("expected business to be committed before result file failure")
}
if !repo.resultSaved {
t.Fatal("expected result file save attempt to be recorded")
}
}
type fakeRepository struct {
task *Task
committed bool
commitErr error
validationSaved bool
resultSaved bool
resultErr error
loadErr error
loadCalls int
saveCalls int
resultRequest batchresult.Request
}
func (r *fakeRepository) Load(_ context.Context, _ string) (*Task, error) {
r.loadCalls++
if r.loadErr != nil {
return nil, r.loadErr
}
return r.task, nil
}
func (r *fakeRepository) SaveValidationFailure(_ context.Context, _ *Task) error {
r.validationSaved = true
return nil
}
func (r *fakeRepository) CommitBusiness(_ context.Context, _ *Task) error {
r.committed = true
return r.commitErr
}
func (r *fakeRepository) SaveResultFile(_ context.Context, _ *Task, request batchresult.Request) error {
r.saveCalls++
r.resultRequest = request
r.resultSaved = true
return r.resultErr
}
+142
View File
@@ -0,0 +1,142 @@
package unsub
import (
"context"
"strings"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gtime"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/batchresult"
)
type Batch struct {
ID int64
BatchNo string
BatchType int
TenantID string
DeptID int64
CreatedBy int64
CreatedDept int64
StartTime *gtime.Time
BatchInfo *gjson.Json
SourceBatchNo string
Remark string
}
type Detail struct {
ID int64
RowNo int
ICCID string
MSISDN string
IMSI string
Reason string
Authorized bool
Remark string
SourceBatchNo string
SubscriptionNo string
OrderCode string
PackageCode string
PackageType int
EffectiveTime string
ExpectedExpireTime string
ActualExpireTime string
ReturnedToStock bool
StockDeptID int64
CardDeptID int64
StockBatchNo string
}
type Task struct {
Batch Batch
Details []*Detail
FailureReason string
}
type OrderRefund struct {
Amount float64
SerialNo string
Status string
Reason string
}
type Repository interface {
Load(ctx context.Context, batchNo string) (*Task, error)
SaveValidationFailure(ctx context.Context, task *Task) error
CommitBusiness(ctx context.Context, task *Task) error
SaveResultFile(ctx context.Context, task *Task, request batchresult.Request) error
}
type Executor interface {
Execute(ctx context.Context, batchNo string) error
}
func (d *Detail) Snapshot() *gjson.Json {
if d == nil {
return gjson.New(nil)
}
return gjson.New(map[string]any{
"rowNo": d.RowNo,
"ICCID": d.ICCID,
"MSISDN": d.MSISDN,
"IMSI": d.IMSI,
"authorized": d.Authorized,
"remark": d.Remark,
"sourceBatchNo": d.SourceBatchNo,
"subscriptionNo": d.SubscriptionNo,
"orderCode": d.OrderCode,
"packageCode": d.PackageCode,
"packageType": d.PackageType,
"effectiveTime": d.EffectiveTime,
"expectedExpireTime": d.ExpectedExpireTime,
"actualExpireTime": d.ActualExpireTime,
"returnedToStock": d.ReturnedToStock,
"stockDeptId": d.StockDeptID,
"cardDeptId": d.CardDeptID,
"stockBatchNo": d.StockBatchNo,
"errorReason": d.Reason,
})
}
func (t *Task) ResultRows() []batchresult.Row {
rows := make([]batchresult.Row, 0, len(t.Details))
for _, detail := range t.Details {
if detail == nil {
continue
}
rows = append(rows, batchresult.Row{
RowNo: detail.RowNo,
ICCID: detail.ICCID,
MSISDN: detail.MSISDN,
IMSI: detail.IMSI,
Success: detail.Reason == "",
Reason: detail.Reason,
})
}
return rows
}
func trimSnapshotString(value any) string {
return strings.TrimSpace(toStringValue(value))
}
func toStringValue(value any) string {
if value == nil {
return ""
}
switch v := value.(type) {
case string:
return v
case *string:
if v == nil {
return ""
}
return *v
case []byte:
return string(v)
default:
return ""
}
}