Files
iotlcm/admin-svr/server/internal/logic/iot/simcardbatch/outbound/async.go
T

322 lines
12 KiB
Go

package outbound
import (
"context"
"errors"
"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/library/rabbitmq"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/packagecustomer"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"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"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
)
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})
result, err := batchimport.CreatePendingUpload(ctx, batchimport.SubmitRequest{
SourceChannel: batchmetadata.Console,
ExecutionMode: batchmetadata.Bulk,
File: file, BizParams: in, SourceType: consts.BatchSourceTypeFile, Operator: operator,
}, processor, batchimport.PendingOptions{})
if err != nil {
return nil, err
}
return result, publishAsync(ctx, result.BatchNo)
}
func SubmitRowsAsync(ctx context.Context, in any, rows []batchimport.Row, sourceType int, sourceBatchNo string, strict bool) (*batchimport.Result, error) {
operator := batchimport.OperatorContext{TenantId: contexts.GetTenantId(ctx), DeptId: contexts.GetDeptId(ctx), UserId: contexts.GetUserId(ctx)}
processor := NewProcessor(PreviewOptions{DeptId: operator.DeptId, StrictStockIdentity: strict})
result, err := batchimport.CreatePendingRows(ctx, batchimport.SubmitRequest{
SourceChannel: batchmetadata.Console,
ExecutionMode: batchmetadata.Bulk,
BizParams: in, SourceType: sourceType, SourceBatchNo: sourceBatchNo, Operator: operator,
}, processor, rows, batchimport.PendingOptions{})
if err != nil {
return nil, err
}
return result, publishAsync(ctx, result.BatchNo)
}
func publishAsync(ctx context.Context, batchNo string) error {
if strings.TrimSpace(batchNo) == "" {
return gerror.New("出库批次号不能为空")
}
if !rabbitmq.Enabled(ctx) {
err := gerror.New("RabbitMQ未启用,无法提交异步出库任务")
if markErr := markAsyncFailed(ctx, batchNo, err); markErr != nil {
return markErr
}
return err
}
if err := rabbitmq.Publish(ctx, consts.QueueBatchOutboundExecute, AsyncMessage{BatchNo: batchNo}); err != nil {
publishErr := gerror.Wrap(err, "发布异步出库任务失败")
if markErr := markAsyncFailed(ctx, batchNo, publishErr); markErr != nil {
return markErr
}
return publishErr
}
return nil
}
func ConsumeAsync(ctx context.Context) error {
return rabbitmq.ConsumeJSON[AsyncMessage](ctx, consts.QueueBatchOutboundExecute, "iotlcm-admin-outbound", handleAsyncMessage)
}
func handleAsyncMessage(ctx context.Context, message AsyncMessage) error {
if strings.TrimSpace(message.BatchNo) == "" {
return nil
}
main := &struct {
BatchNo string
BatchType int
BatchAsyncStatus int
BatchInfo *gjson.Json
SourceBatchNo string
TenantId string
CreatedDept int64
CreatedBy int64
TotalCount int
}{}
if err := dao.BatchMain.Ctx(ctx).Where(dao.BatchMain.Columns().BatchNo, message.BatchNo).Scan(main); err != nil {
return err
}
if main.BatchNo == "" || main.BatchType != consts.BatchTypeOutbound {
return nil
}
strictStockIdentity := false
if main.BatchInfo != nil {
strictStockIdentity = main.BatchInfo.Get("strictStockIdentity").Bool()
}
processor := NewProcessor(PreviewOptions{DeptId: main.CreatedDept, StrictStockIdentity: strictStockIdentity})
param, err := submitParamFromInfo(main.BatchInfo)
if err != nil {
return markAsyncFailed(ctx, message.BatchNo, err)
}
rows, err := loadAsyncRows(ctx, message.BatchNo)
if err != nil {
return markAsyncFailed(ctx, message.BatchNo, err)
}
operator := batchimport.OperatorContext{TenantId: main.TenantId, DeptId: main.CreatedDept, UserId: main.CreatedBy}
requiredSource := ""
if main.BatchInfo != nil {
requiredSource = strings.TrimSpace(main.BatchInfo.Get("inboundBatchNo").String())
}
var (
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()).
Where(dao.BatchMain.Columns().BatchNo, message.BatchNo).
Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusPending).
Data(do.BatchMain{
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
RunResult: consts.BatchRunResultRunning,
StartTime: gtime.Now(),
UpdatedBy: main.CreatedBy,
}).
Update()
if claimErr != nil {
return claimErr
}
claimCount, claimErr := claimResult.RowsAffected()
if claimErr != nil {
return gerror.Wrap(claimErr, "读取异步出库任务领取结果失败")
}
if claimCount != 1 {
return nil
}
claimed = true
if _, updateErr := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().BatchNo, message.BatchNo).
Data(do.BatchDetail{DetailStatus: consts.BatchDetailStatusRunning, UpdatedBy: main.CreatedBy}).
Update(); updateErr != nil {
return updateErr
}
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{
OrderSn: output.OrderSn, BatchAsyncStatus: consts.BatchAsyncStatusSuccess, RunResult: consts.BatchRunResultSuccess,
SuccessCount: output.Quantity, ValidCount: output.Quantity, FailCount: 0, EndTime: gtime.Now(), UpdatedBy: main.CreatedBy,
}).Update(); updateErr != nil {
return updateErr
}
if _, updateErr := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).Where(dao.BatchDetail.Columns().BatchNo, message.BatchNo).Data(do.BatchDetail{
DetailStatus: consts.BatchDetailStatusSuccess, ErrorCode: "", ErrorReason: "", UpdatedBy: main.CreatedBy,
}).Update(); updateErr != nil {
return updateErr
}
return nil
})
if err != nil {
if validationRejected {
return markAsyncValidationFailed(ctx, message.BatchNo, main.TenantId, rows, err)
}
return markAsyncFailed(ctx, message.BatchNo, err)
}
if !claimed {
return nil
}
if output != nil && output.Customer != nil && output.Customer.DeptId > 0 {
packagecustomer.PublishSyncSafe(ctx, model.PackageCustomerSyncMessage{
TenantId: operator.TenantId,
DeptId: output.Customer.DeptId,
UserId: operator.UserId,
Trigger: "outbound",
SourceBatchNo: output.BatchNo,
})
}
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{
BatchAsyncStatus: consts.BatchAsyncStatusFailed,
RunResult: consts.BatchRunResultFailed,
ValidCount: 0,
SuccessCount: 0,
FailCount: gdb.Raw(dao.BatchMain.Columns().TotalCount),
FailReason: reason,
EndTime: gtime.Now(),
}).Update(); updateErr != nil {
return gerror.Wrap(updateErr, reason)
}
if _, updateErr := g.DB().Model(dao.BatchDetail.Table()).Ctx(ctx).Where(dao.BatchDetail.Columns().BatchNo, batchNo).Data(do.BatchDetail{
DetailStatus: consts.BatchDetailStatusFailed,
ErrorReason: reason,
}).Update(); updateErr != nil {
return gerror.Wrap(updateErr, reason)
}
return nil
}
func loadAsyncRows(ctx context.Context, batchNo string) ([]*Row, error) {
var details []*struct {
RowNo int
ItemId string
Snapshot *gjson.Json
}
if err := dao.BatchDetail.Ctx(ctx).Fields(dao.BatchDetail.Columns().RowNo, dao.BatchDetail.Columns().ItemId, dao.BatchDetail.Columns().Snapshot).Where(dao.BatchDetail.Columns().BatchNo, batchNo).OrderAsc(dao.BatchDetail.Columns().RowNo).Scan(&details); err != nil {
return nil, err
}
rows := make([]*Row, 0, len(details))
for _, detail := range details {
if detail == nil {
continue
}
row := &Row{RowNo: detail.RowNo, Iccid: strings.TrimSpace(detail.ItemId)}
if detail.Snapshot != nil {
row.Msisdn = detail.Snapshot.Get("MSISDN").String()
row.Imsi = detail.Snapshot.Get("IMSI").String()
row.SourceBatchNo = detail.Snapshot.Get("batchNo").String()
}
rows = append(rows, row)
}
if len(rows) == 0 {
return nil, gerror.New("出库批次没有明细")
}
return rows, nil
}
func submitParamFromInfo(info *gjson.Json) (*model.TSimCardOutboundSubmitParam, error) {
if info == nil {
return nil, gerror.New("出库批次参数不存在")
}
param := &model.TSimCardOutboundSubmitParam{}
if err := info.Scan(param); err != nil {
return nil, err
}
if param.CustomerAgentId <= 0 || strings.TrimSpace(param.PackageCode) == "" || param.PackageBeginTime == nil {
return nil, gerror.New("出库批次参数不完整")
}
return param, nil
}
const maxAsyncFailureReasonLength = 512
func asyncFailureReason(err error) string {
if err == nil {
return "异步出库失败"
}
reason := strings.TrimSpace(err.Error())
if reason == "" {
return "异步出库失败"
}
if strings.Contains(reason, "Prepared statement contains too many placeholders") {
return "新增正式卡失败:单次批量写入参数数量超过数据库限制"
}
if strings.Contains(reason, "Data too long for column 'error_reason'") {
return "异步出库失败:失败原因超过数据库字段长度限制"
}
runes := []rune(reason)
if len(runes) > maxAsyncFailureReasonLength {
return string(runes[:maxAsyncFailureReasonLength])
}
return reason
}