sync: 同步 v1.0.4 并脱敏配置与镜像
This commit is contained in:
@@ -2,6 +2,7 @@ package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"strings"
|
||||
|
||||
@@ -26,6 +27,10 @@ type AsyncMessage struct {
|
||||
BatchNo string `json:"batchNo"`
|
||||
}
|
||||
|
||||
var errOutboundValidationRejected = errors.New("存在不满足出库条件的卡,本次未执行")
|
||||
|
||||
const outboundNotExecutedReason = "校验通过,但本批次存在失败明细,整批未执行出库"
|
||||
|
||||
func SubmitExcelAsync(ctx context.Context, file *ghttp.UploadFile, in *model.TSimCardOutboundExcelSubmitParam) (*batchimport.Result, error) {
|
||||
operator := batchimport.OperatorContext{TenantId: contexts.GetTenantId(ctx), DeptId: contexts.GetDeptId(ctx), UserId: contexts.GetUserId(ctx)}
|
||||
processor := NewProcessor(PreviewOptions{DeptId: operator.DeptId})
|
||||
@@ -119,8 +124,9 @@ func handleAsyncMessage(ctx context.Context, message AsyncMessage) error {
|
||||
requiredSource = strings.TrimSpace(main.BatchInfo.Get("inboundBatchNo").String())
|
||||
}
|
||||
var (
|
||||
claimed bool
|
||||
output *CompleteOutput
|
||||
claimed bool
|
||||
output *CompleteOutput
|
||||
validationRejected bool
|
||||
)
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
claimResult, claimErr := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
@@ -153,6 +159,7 @@ func handleAsyncMessage(ctx context.Context, message AsyncMessage) error {
|
||||
var executeErr error
|
||||
output, executeErr = processor.ExecutePending(ctx, tx, message.BatchNo, param, rows, operator, requiredSource)
|
||||
if executeErr != nil {
|
||||
validationRejected = errors.Is(executeErr, errOutboundValidationRejected)
|
||||
return executeErr
|
||||
}
|
||||
if _, updateErr := tx.Ctx(ctx).Model(dao.BatchMain.Table()).Where(dao.BatchMain.Columns().BatchNo, message.BatchNo).Data(do.BatchMain{
|
||||
@@ -169,6 +176,9 @@ func handleAsyncMessage(ctx context.Context, message AsyncMessage) error {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if validationRejected {
|
||||
return markAsyncValidationFailed(ctx, message.BatchNo, main.TenantId, rows, err)
|
||||
}
|
||||
return markAsyncFailed(ctx, message.BatchNo, err)
|
||||
}
|
||||
if !claimed {
|
||||
@@ -186,6 +196,43 @@ func handleAsyncMessage(ctx context.Context, message AsyncMessage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func markAsyncValidationFailed(ctx context.Context, batchNo, tenantID string, rows []*Row, cause error) error {
|
||||
validCount, failCount := 0, 0
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
if row.Reason == outboundNotExecutedReason {
|
||||
validCount++
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
status := consts.BatchDetailStatusFailed
|
||||
if row.Reason == outboundNotExecutedReason {
|
||||
status = consts.BatchDetailStatusNotExecuted
|
||||
}
|
||||
if _, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
|
||||
Where(dao.BatchDetail.Columns().TenantId, tenantID).
|
||||
Where(dao.BatchDetail.Columns().RowNo, row.RowNo).
|
||||
Data(do.BatchDetail{DetailStatus: status, ErrorReason: row.Reason}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "保存出库校验明细失败")
|
||||
}
|
||||
}
|
||||
_, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(dao.BatchMain.Columns().BatchNo, batchNo).
|
||||
Where(dao.BatchMain.Columns().TenantId, tenantID).
|
||||
Data(do.BatchMain{BatchAsyncStatus: consts.BatchAsyncStatusFailed, RunResult: consts.BatchRunResultFailed, ValidCount: validCount, SuccessCount: 0, FailCount: failCount, FailReason: asyncFailureReason(cause), EndTime: gtime.Now()}).Update()
|
||||
return gerror.Wrap(err, "保存出库校验结果失败")
|
||||
})
|
||||
}
|
||||
|
||||
func markAsyncFailed(ctx context.Context, batchNo string, err error) error {
|
||||
reason := asyncFailureReason(err)
|
||||
if _, updateErr := g.DB().Model(dao.BatchMain.Table()).Ctx(ctx).Where(dao.BatchMain.Columns().BatchNo, batchNo).Data(do.BatchMain{
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
)
|
||||
|
||||
func TestValidateOutboundRowCollectsIndependentFailures(t *testing.T) {
|
||||
row := &Row{Iccid: "card", Msisdn: "wrong", Imsi: "wrong"}
|
||||
stock := &entity.TSimCardStock{CreatedDept: 200, BatchNo: "other", Msisdn: "13800008000", Imsi: "460001234567890"}
|
||||
reason := validateOutboundRow(row, stock, true, batchimport.OperatorContext{DeptId: 100}, true, "required")
|
||||
for _, want := range []string{"不属于当前机构库存", "不属于指定库存批次", "已存在正式卡记录", "MSISDN与库存记录不一致", "IMSI与库存记录不一致"} {
|
||||
if !strings.Contains(reason, want) {
|
||||
t.Fatalf("missing %q in %q", want, reason)
|
||||
}
|
||||
}
|
||||
rows := []*Row{{Reason: reason}, {}, {Reason: "ICCID不存在库存记录,不能出库"}}
|
||||
markRemainingRowsSkipped(rows)
|
||||
if rows[0].Reason != reason || rows[1].Reason != outboundNotExecutedReason || rows[2].Reason != "ICCID不存在库存记录,不能出库" {
|
||||
t.Fatalf("per-card validation results were overwritten: %+v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboundChunkSizesSupportFiftyThousandCards(t *testing.T) {
|
||||
const totalCards = 50000
|
||||
if outboundDatabaseInsertChunkSize <= 0 || outboundDatabaseInsertChunkSize > 500 {
|
||||
t.Fatalf("outboundDatabaseInsertChunkSize = %d, want 1..500", outboundDatabaseInsertChunkSize)
|
||||
}
|
||||
if chunks := (totalCards + outboundDatabaseInsertChunkSize - 1) / outboundDatabaseInsertChunkSize; chunks != 100 {
|
||||
t.Fatalf("card insert chunks = %d, want 100", chunks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsyncFailureReasonSummarizesPlaceholderOverflow(t *testing.T) {
|
||||
reason := asyncFailureReason(errors.New("INSERT INTO t_sim_card_info (...) Error 1390: Prepared statement contains too many placeholders"))
|
||||
if reason != "新增正式卡失败:单次批量写入参数数量超过数据库限制" {
|
||||
t.Fatalf("unexpected reason: %q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsyncFailureReasonFitsBatchDetailColumn(t *testing.T) {
|
||||
reason := asyncFailureReason(errors.New(strings.Repeat("失败原因", 300)))
|
||||
if len([]rune(reason)) > maxAsyncFailureReasonLength {
|
||||
t.Fatalf("reason length = %d, want <= %d", len([]rune(reason)), maxAsyncFailureReasonLength)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOutboundIdentifiersReportsEveryDuplicate(t *testing.T) {
|
||||
rows := []*Row{
|
||||
{RowNo: 1, Iccid: " 8986000000000000001 "},
|
||||
{RowNo: 2, Iccid: "8986000000000000001"},
|
||||
{RowNo: 3, Iccid: ""},
|
||||
{RowNo: 4, Iccid: "8986000000000000002"},
|
||||
}
|
||||
iccids, err := validateOutboundIdentifiers(rows)
|
||||
if err != nil || len(iccids) != 1 || iccids[0] != rows[3].Iccid {
|
||||
t.Fatalf("valid identifiers = %v, err = %v", iccids, err)
|
||||
}
|
||||
for index := 0; index < 3; index++ {
|
||||
if rows[index].Reason == "" {
|
||||
t.Fatalf("row %d lost its validation reason", index+1)
|
||||
}
|
||||
}
|
||||
markRemainingRowsSkipped(rows)
|
||||
if rows[3].Reason != outboundNotExecutedReason {
|
||||
t.Fatalf("valid row reason = %q", rows[3].Reason)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
@@ -211,10 +212,13 @@ func (p *Processor) validateExecuteContext(ctx context.Context, tx gdb.TX, in *m
|
||||
if err := p.validateSelectablePackage(ctx, tx, in.PackageCode, operator.TenantId); err != nil {
|
||||
return err
|
||||
}
|
||||
iccids, err := strictRowsToIccids(rows)
|
||||
iccids, err := validateOutboundIdentifiers(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(iccids) == 0 {
|
||||
return errOutboundValidationRejected
|
||||
}
|
||||
stockByIccid, err := p.repo.LockStockCardsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "校验出库库存卡失败")
|
||||
@@ -237,11 +241,38 @@ func (p *Processor) validateExecuteContext(ctx context.Context, tx gdb.TX, in *m
|
||||
|
||||
if hasInvalidRows(rows) {
|
||||
markRemainingRowsSkipped(rows)
|
||||
return gerror.New("存在不满足出库条件的卡,本次未执行")
|
||||
return errOutboundValidationRejected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOutboundIdentifiers(rows []*Row) ([]string, error) {
|
||||
seen := make(map[string]*Row, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
return nil, gerror.New("出库行不能为空")
|
||||
}
|
||||
row.Iccid = strings.TrimSpace(row.Iccid)
|
||||
if row.Iccid == "" {
|
||||
row.AddReason("ICCID不能为空")
|
||||
continue
|
||||
}
|
||||
if first, exists := seen[row.Iccid]; exists {
|
||||
row.AddReason(fmt.Sprintf("ICCID与第%d行重复", first.RowNo))
|
||||
first.AddReason(fmt.Sprintf("ICCID与第%d行重复", row.RowNo))
|
||||
continue
|
||||
}
|
||||
seen[row.Iccid] = row
|
||||
}
|
||||
iccids := make([]string, 0, len(seen))
|
||||
for iccid, row := range seen {
|
||||
if row.Reason == "" {
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
}
|
||||
return iccids, nil
|
||||
}
|
||||
|
||||
func (p *Processor) validateSelectablePackage(ctx context.Context, tx gdb.TX, packageCode, tenantId string) error {
|
||||
pkg, err := p.repo.LoadPackage(ctx, tx, tenantId, packageCode)
|
||||
if err != nil {
|
||||
@@ -329,27 +360,28 @@ func (p *Processor) submitParams(params any) *model.TSimCardOutboundSubmitParam
|
||||
}
|
||||
|
||||
func validateOutboundRow(row *Row, stock *entity.TSimCardStock, formalExists bool, operator batchimport.OperatorContext, strictStockIdentity bool, requiredSourceBatchNo string) string {
|
||||
var reasons []string
|
||||
if stock == nil {
|
||||
return "ICCID不存在库存记录,不能出库"
|
||||
}
|
||||
if stock.CreatedDept != operator.DeptId {
|
||||
return "ICCID不属于当前机构库存,不能出库"
|
||||
reasons = append(reasons, "ICCID不属于当前机构库存,不能出库")
|
||||
}
|
||||
if requiredSourceBatchNo != "" && strings.TrimSpace(stock.BatchNo) != requiredSourceBatchNo {
|
||||
return "ICCID不属于指定库存批次,不能出库"
|
||||
reasons = append(reasons, "ICCID不属于指定库存批次,不能出库")
|
||||
}
|
||||
if formalExists {
|
||||
return "ICCID已存在正式卡记录,不能出库"
|
||||
reasons = append(reasons, "ICCID已存在正式卡记录,不能出库")
|
||||
}
|
||||
if strictStockIdentity {
|
||||
if strings.TrimSpace(row.Msisdn) != strings.TrimSpace(stock.Msisdn) {
|
||||
return "MSISDN与库存记录不一致,不能出库"
|
||||
reasons = append(reasons, "MSISDN与库存记录不一致,不能出库")
|
||||
}
|
||||
if strings.TrimSpace(row.Imsi) != strings.TrimSpace(stock.Imsi) {
|
||||
return "IMSI与库存记录不一致,不能出库"
|
||||
reasons = append(reasons, "IMSI与库存记录不一致,不能出库")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return strings.Join(reasons, ";")
|
||||
}
|
||||
|
||||
func hasInvalidRows(rows []*Row) bool {
|
||||
@@ -364,7 +396,7 @@ func hasInvalidRows(rows []*Row) bool {
|
||||
func markRemainingRowsSkipped(rows []*Row) {
|
||||
for _, row := range rows {
|
||||
if row != nil && row.Reason == "" {
|
||||
row.AddReason("同批存在不满足出库条件的卡,本行未执行")
|
||||
row.AddReason(outboundNotExecutedReason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user