59eb34976a
iotlcm-admin-svr:管理后台服务。 - Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件) - 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、 MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位; 私网 IP → 文档地址段;库名 xiujie_admin → iotlcm - 真实手机号/邮箱 → 合法 11 位编造号 / example.com - 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、 私有依赖 go get 行、明文 registry 登录口令 - 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名 - Dockerfile 改用公共基础镜像 - 拼错的 RADEME.md → README.md,README 重写 - 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用 - 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
275 lines
10 KiB
Go
275 lines
10 KiB
Go
package outbound
|
|
|
|
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/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"`
|
|
}
|
|
|
|
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
|
|
)
|
|
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 {
|
|
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 {
|
|
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 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
|
|
}
|