chore: 初始开源发布
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 已说明。
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,777 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"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/logic/iot/cardpackage"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"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/os/gtime"
|
||||
)
|
||||
|
||||
const (
|
||||
outboundPaymentModeOffline = "offline"
|
||||
outboundPaymentModeBalance = "balance"
|
||||
outboundDetailUpdateChunkSize = 200
|
||||
|
||||
outboundFinanceIncomeExpenseExpense = 1
|
||||
outboundFinanceTransactionTypeConsume = 1
|
||||
outboundFinanceTransactionChannelBalance = 3
|
||||
outboundFinanceTransactionStatusPaid = 1
|
||||
outboundFinanceAccountStatusNormal = 0
|
||||
)
|
||||
|
||||
func (p *Processor) Complete(ctx context.Context, tx gdb.TX, in *CompleteInput) (*CompleteOutput, error) {
|
||||
if in == nil || in.SubmitParam == nil {
|
||||
return nil, gerror.New("出库参数不能为空")
|
||||
}
|
||||
if in.BatchNo == "" {
|
||||
return nil, gerror.New("出库批次号不能为空")
|
||||
}
|
||||
if len(in.Rows) == 0 {
|
||||
return nil, gerror.New("出库卡不能为空")
|
||||
}
|
||||
normalizeSubmitParam(in.SubmitParam)
|
||||
if err := validateSubmitParam(in.SubmitParam); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iccids, err := strictRowsToIccids(in.Rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stockByIccid, err := p.repo.LockStockCardsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定库存卡失败")
|
||||
}
|
||||
if len(stockByIccid) != len(iccids) {
|
||||
return nil, gerror.New("部分库存卡已变化,请刷新后重试")
|
||||
}
|
||||
|
||||
for _, iccid := range iccids {
|
||||
stock := stockByIccid[iccid]
|
||||
if stock == nil {
|
||||
return nil, gerror.Newf("ICCID不存在库存记录:%s", iccid)
|
||||
}
|
||||
if stock.CreatedDept != in.Operator.DeptId {
|
||||
return nil, gerror.Newf("ICCID不属于当前机构库存:%s", iccid)
|
||||
}
|
||||
if in.RequiredSourceBatchNo != "" && strings.TrimSpace(stock.BatchNo) != in.RequiredSourceBatchNo {
|
||||
return nil, gerror.Newf("ICCID 不属于指定批次:%s", iccid)
|
||||
}
|
||||
}
|
||||
if in.StrictStockIdentity {
|
||||
for _, row := range in.Rows {
|
||||
stock := stockByIccid[row.Iccid]
|
||||
if stock == nil {
|
||||
return nil, gerror.Newf("ICCID不存在库存记录:%s", row.Iccid)
|
||||
}
|
||||
if strings.TrimSpace(row.Msisdn) != strings.TrimSpace(stock.Msisdn) {
|
||||
return nil, gerror.Newf("MSISDN与库存记录不一致:%s", row.Iccid)
|
||||
}
|
||||
if strings.TrimSpace(row.Imsi) != strings.TrimSpace(stock.Imsi) {
|
||||
return nil, gerror.Newf("IMSI与库存记录不一致:%s", row.Iccid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formalByIccid, err := p.repo.GetExistingFormalIccids(ctx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询正式卡失败")
|
||||
}
|
||||
for _, iccid := range iccids {
|
||||
if formalByIccid[iccid] {
|
||||
return nil, gerror.Newf("ICCID已存在正式卡记录:%s", iccid)
|
||||
}
|
||||
}
|
||||
|
||||
quantity := len(iccids)
|
||||
subscribeCards := make([]cardpackage.SubscribeCard, 0, quantity)
|
||||
cardRows := make([]do.TSimCardInfo, 0, quantity)
|
||||
detailUpdates := make(map[string]*gjsonJsonHolder, quantity)
|
||||
sourceBatchNos := make([]string, 0, quantity)
|
||||
sourceBatchSet := make(map[string]struct{}, quantity)
|
||||
|
||||
for _, row := range in.Rows {
|
||||
stock := stockByIccid[row.Iccid]
|
||||
if stock == nil {
|
||||
return nil, gerror.Newf("ICCID不存在库存记录:%s", row.Iccid)
|
||||
}
|
||||
if strings.TrimSpace(stock.Iccid) != strings.TrimSpace(row.Iccid) {
|
||||
return nil, gerror.Newf("库存卡映射异常:第%d行%s匹配到%s", row.RowNo, row.Iccid, stock.Iccid)
|
||||
}
|
||||
row.SourceBatchNo = strings.TrimSpace(stock.BatchNo)
|
||||
row.Msisdn = chooseString(row.Msisdn, stock.Msisdn)
|
||||
row.Imsi = chooseString(row.Imsi, stock.Imsi)
|
||||
if row.SourceBatchNo != "" {
|
||||
if _, ok := sourceBatchSet[row.SourceBatchNo]; !ok {
|
||||
sourceBatchSet[row.SourceBatchNo] = struct{}{}
|
||||
sourceBatchNos = append(sourceBatchNos, row.SourceBatchNo)
|
||||
}
|
||||
}
|
||||
subscribeCards = append(subscribeCards, cardpackage.SubscribeCard{
|
||||
Iccid: stock.Iccid,
|
||||
Msisdn: stock.Msisdn,
|
||||
Imsi: stock.Imsi,
|
||||
})
|
||||
}
|
||||
inboundBatchInfos, err := p.repo.GetInboundBatchInfos(ctx, tx, sourceBatchNos)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询入库批次信息失败")
|
||||
}
|
||||
payment, err := prepareOutboundPayment(ctx, tx, in.BatchNo, subscriptionAmount(in.SubmitParam), in.Operator, in.SubmitParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packageSvc := cardpackage.NewService()
|
||||
subscription, err := packageSvc.CreatePackageSubscription(ctx, tx, &cardpackage.CreatePackageSubscriptionInput{
|
||||
BatchNo: in.BatchNo,
|
||||
Cards: subscribeCards,
|
||||
CustomerAgentId: in.SubmitParam.CustomerAgentId,
|
||||
PackageCode: strings.TrimSpace(in.SubmitParam.PackageCode),
|
||||
PackageBeginTime: in.SubmitParam.PackageBeginTime,
|
||||
DurationMonths: in.SubmitParam.DurationMonths,
|
||||
GiftDurationMonths: in.SubmitParam.GiftDurationMonths,
|
||||
DurationDisplayValue: displayDurationValue(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonths),
|
||||
GiftDurationDisplayValue: displayDurationValue(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonths),
|
||||
DurationDisplayLabel: displayDurationLabel(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonthPart, in.SubmitParam.DurationMonths),
|
||||
GiftDurationDisplayLabel: displayDurationLabel(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonthPart, in.SubmitParam.GiftDurationMonths),
|
||||
DurationUnit: consts.RenewDurationUnitYear,
|
||||
EndAtPreviousMonthEnd: true,
|
||||
Amount: in.SubmitParam.Amount,
|
||||
PayType: payment.PayType,
|
||||
PayStatus: payment.PayStatus,
|
||||
BalanceAmount: payment.BalanceAmount,
|
||||
OfflinePaidAmount: payment.OfflinePaidAmount,
|
||||
PayTime: payment.PaymentTime,
|
||||
OfflineContractCompleted: in.SubmitParam.OfflineContractCompleted,
|
||||
BuyerNote: normalizedRemark(in.SubmitParam),
|
||||
AdminNote: normalizedRemark(in.SubmitParam),
|
||||
BindRemark: in.BatchNo,
|
||||
SkipBindInsert: true,
|
||||
Operator: in.Operator,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(sourceBatchNos)
|
||||
if err = updateBatchMainAfterComplete(
|
||||
ctx,
|
||||
tx,
|
||||
in,
|
||||
subscription.OrderSn,
|
||||
subscription.Customer,
|
||||
subscription.Package,
|
||||
subscription.Amount,
|
||||
subscription.PackageBeginAt,
|
||||
subscription.PackageEndAt,
|
||||
sourceBatchNos,
|
||||
subscription.PackageStatus,
|
||||
subscription.EffectiveStatus,
|
||||
payment,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := subscription.SubscribeTime
|
||||
for _, row := range in.Rows {
|
||||
stock := stockByIccid[row.Iccid]
|
||||
bind := subscription.BindsByIccid[row.Iccid]
|
||||
if bind == nil {
|
||||
return nil, gerror.Newf("ICCID订购绑定生成失败:%s", row.Iccid)
|
||||
}
|
||||
cardType := inboundCardTypeValue(inboundBatchInfos[stock.BatchNo])
|
||||
cardRows = append(cardRows, do.TSimCardInfo{
|
||||
Iccid: stock.Iccid,
|
||||
Msisdn: stock.Msisdn,
|
||||
Imsi: stock.Imsi,
|
||||
Imei: stock.Imei,
|
||||
DeptId: in.SubmitParam.CustomerAgentId,
|
||||
CurrentPackageCode: subscription.Package.PackageCode,
|
||||
PackageStatus: subscription.PackageStatus,
|
||||
PackageTotalFlowMb: packageTotalFlowMB(subscription.Package),
|
||||
BossCardStatus: consts.V3ApiSimBossCardStatusPendingActivate,
|
||||
CardType: cardType,
|
||||
InboundBatchNo: stock.BatchNo,
|
||||
OutboundBatchNo: in.BatchNo,
|
||||
OutStockTime: now,
|
||||
FlowPkgBeginAt: subscription.PackageBeginAt,
|
||||
FlowPkgEndAt: subscription.PackageEndAt,
|
||||
ChanKey: stock.ChanKey,
|
||||
TenantId: in.Operator.TenantId,
|
||||
CreatedDept: in.Operator.DeptId,
|
||||
CreatedBy: in.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
|
||||
detailUpdates[row.Iccid] = &gjsonJsonHolder{Data: buildDetailSnapshot(row, stock, cardType, subscription.Customer, subscription.Package, in, subscription.OrderSn, bind.SubscriptionNo, subscription.Amount, subscription.PackageBeginAt, subscription.PackageEndAt)}
|
||||
}
|
||||
|
||||
for start := 0; start < len(cardRows); start += outboundDatabaseInsertChunkSize {
|
||||
end := start + outboundDatabaseInsertChunkSize
|
||||
if end > len(cardRows) {
|
||||
end = len(cardRows)
|
||||
}
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).Data(cardRows[start:end]).Insert(); err != nil {
|
||||
return nil, gerror.Wrap(err, "新增正式卡失败")
|
||||
}
|
||||
}
|
||||
if err = packageSvc.InsertCardPackageBinds(ctx, tx, subscription.BindRows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = updateBatchDetailSnapshots(ctx, tx, in.BatchNo, detailUpdates, in.Operator.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var affected int64
|
||||
for start := 0; start < len(iccids); start += outboundDatabaseChunkSize {
|
||||
end := start + outboundDatabaseChunkSize
|
||||
if end > len(iccids) {
|
||||
end = len(iccids)
|
||||
}
|
||||
result, deleteErr := tx.Ctx(ctx).Model(dao.TSimCardStock.Table()).
|
||||
WhereIn(dao.TSimCardStock.Columns().Iccid, iccids[start:end]).
|
||||
Where(dao.TSimCardStock.Columns().CreatedDept, in.Operator.DeptId).
|
||||
Delete()
|
||||
if deleteErr != nil {
|
||||
return nil, gerror.Wrap(deleteErr, "删除库存卡失败")
|
||||
}
|
||||
chunkAffected, rowsErr := result.RowsAffected()
|
||||
if rowsErr != nil {
|
||||
return nil, gerror.Wrap(rowsErr, "读取库存删除数量失败")
|
||||
}
|
||||
affected += chunkAffected
|
||||
}
|
||||
if int(affected) != quantity {
|
||||
return nil, gerror.New("部分库存卡已变化,请刷新后重试")
|
||||
}
|
||||
|
||||
return &CompleteOutput{
|
||||
OrderId: subscription.OrderId,
|
||||
OrderSn: subscription.OrderSn,
|
||||
BatchNo: in.BatchNo,
|
||||
BatchType: consts.BatchTypeOutbound,
|
||||
Quantity: quantity,
|
||||
SourceBatchNos: sourceBatchNos,
|
||||
Customer: subscription.Customer,
|
||||
Package: subscription.Package,
|
||||
PackageBeginAt: subscription.PackageBeginAt,
|
||||
PackageEndAt: subscription.PackageEndAt,
|
||||
PackageStatus: subscription.PackageStatus,
|
||||
EffectiveStatus: subscription.EffectiveStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type gjsonJsonHolder struct {
|
||||
Data any
|
||||
}
|
||||
|
||||
func validateSubmitParam(in *model.TSimCardOutboundSubmitParam) error {
|
||||
normalizeSubmitParam(in)
|
||||
if in.CustomerAgentId <= 0 {
|
||||
return gerror.New("客户不能为空")
|
||||
}
|
||||
if strings.TrimSpace(in.PackageCode) == "" {
|
||||
return gerror.New("套餐不能为空")
|
||||
}
|
||||
if in.PackageBeginTime == nil {
|
||||
return gerror.New("套餐开始时间不能为空")
|
||||
}
|
||||
if in.DurationMonths <= 0 {
|
||||
return gerror.New("套餐时长必须大于0")
|
||||
}
|
||||
if in.DurationMonthPart < 0 || in.DurationMonthPart > 11 {
|
||||
return gerror.New("套餐购买月份必须在0到11之间")
|
||||
}
|
||||
if in.GiftDurationMonthPart < 0 || in.GiftDurationMonthPart > 11 {
|
||||
return gerror.New("赠送月份必须在0到11之间")
|
||||
}
|
||||
if in.GiftDurationMonths < 0 {
|
||||
return gerror.New("赠送时长不能小于0")
|
||||
}
|
||||
amount, err := parseOutboundAmount(in.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if amount <= 0 {
|
||||
return gerror.New("订单金额必须大于0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rowsToIccids(rows []*Row) []string {
|
||||
iccids := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
iccid := strings.TrimSpace(row.Iccid)
|
||||
if iccid != "" {
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
}
|
||||
return uniqueStrings(iccids)
|
||||
}
|
||||
|
||||
func strictRowsToIccids(rows []*Row) ([]string, error) {
|
||||
seen := make(map[string]int, len(rows))
|
||||
iccids := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
return nil, gerror.New("出库行不能为空")
|
||||
}
|
||||
iccid := strings.TrimSpace(row.Iccid)
|
||||
if iccid == "" {
|
||||
return nil, gerror.Newf("第%d行ICCID为空,不能出库", row.RowNo)
|
||||
}
|
||||
if firstRowNo, ok := seen[iccid]; ok {
|
||||
return nil, gerror.Newf("第%d行ICCID与第%d行重复:%s", row.RowNo, firstRowNo, iccid)
|
||||
}
|
||||
seen[iccid] = row.RowNo
|
||||
row.Iccid = iccid
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
return iccids, nil
|
||||
}
|
||||
|
||||
func updateBatchMainAfterComplete(
|
||||
ctx context.Context,
|
||||
tx gdb.TX,
|
||||
in *CompleteInput,
|
||||
orderSn string,
|
||||
customer *entity.SysDept,
|
||||
pkg *entity.TSimFlowPackageInfo,
|
||||
amount float64,
|
||||
beginAt, endAt *gtime.Time,
|
||||
sourceBatchNos []string,
|
||||
packageStatus, effectiveStatus int,
|
||||
payment *outboundPaymentSnapshot,
|
||||
) error {
|
||||
current := &struct {
|
||||
BatchInfo *gjson.Json
|
||||
}{}
|
||||
if err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Fields(dao.BatchMain.Columns().BatchInfo).
|
||||
Where(dao.BatchMain.Columns().BatchNo, in.BatchNo).
|
||||
Scan(current); err != nil {
|
||||
return gerror.Wrap(err, "读取出库批次信息失败")
|
||||
}
|
||||
info := g.Map{}
|
||||
if current.BatchInfo != nil {
|
||||
for key, value := range current.BatchInfo.Map() {
|
||||
info[key] = value
|
||||
}
|
||||
}
|
||||
completedInfo := g.Map{
|
||||
"orderSn": orderSn,
|
||||
"customerAgentId": in.SubmitParam.CustomerAgentId,
|
||||
"customerName": customer.DeptName,
|
||||
"packageCode": pkg.PackageCode,
|
||||
"packageName": pkg.PackageName,
|
||||
"packageType": pkg.PackageType,
|
||||
"packageBeginTime": formatTime(beginAt),
|
||||
"packageEndTime": formatTime(endAt),
|
||||
"durationYears": in.SubmitParam.DurationYears,
|
||||
"durationMonthPart": in.SubmitParam.DurationMonthPart,
|
||||
"durationMonths": in.SubmitParam.DurationMonths,
|
||||
"giftDurationYears": in.SubmitParam.GiftDurationYears,
|
||||
"giftDurationMonthPart": in.SubmitParam.GiftDurationMonthPart,
|
||||
"giftDurationMonths": in.SubmitParam.GiftDurationMonths,
|
||||
"durationLabel": displayDurationLabel(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonthPart, in.SubmitParam.DurationMonths),
|
||||
"giftDurationLabel": displayDurationLabel(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonthPart, in.SubmitParam.GiftDurationMonths),
|
||||
"amount": amount,
|
||||
"quantity": len(in.Rows),
|
||||
"offlineContractCompleted": in.SubmitParam.OfflineContractCompleted,
|
||||
"remark": normalizedRemark(in.SubmitParam),
|
||||
"buyerNote": normalizedRemark(in.SubmitParam),
|
||||
"adminNote": normalizedRemark(in.SubmitParam),
|
||||
"sourceBatchNos": sourceBatchNos,
|
||||
"packageStatus": packageStatus,
|
||||
"effectiveStatus": effectiveStatus,
|
||||
}
|
||||
for key, value := range completedInfo {
|
||||
info[key] = value
|
||||
}
|
||||
if payment != nil {
|
||||
info["paymentMode"] = payment.PaymentMode
|
||||
info["payType"] = payment.PayType
|
||||
info["payStatus"] = payment.PayStatus
|
||||
info["balanceAmount"] = payment.BalanceAmount
|
||||
info["offlinePaidAmount"] = payment.OfflinePaidAmount
|
||||
info["balanceBefore"] = payment.BalanceBefore
|
||||
info["balanceAfter"] = payment.BalanceAfter
|
||||
info["balanceDetailSerialNo"] = payment.BalanceDetailSerialNo
|
||||
info["payerDeptId"] = payment.PayerDeptId
|
||||
}
|
||||
_, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(dao.BatchMain.Columns().BatchNo, in.BatchNo).
|
||||
Data(do.BatchMain{
|
||||
OrderSn: orderSn,
|
||||
TargetDeptId: in.SubmitParam.CustomerAgentId,
|
||||
BatchInfo: gjson.New(info),
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新出库批次失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateBatchDetailSnapshots(ctx context.Context, tx gdb.TX, batchNo string, snapshots map[string]*gjsonJsonHolder, userId int64) error {
|
||||
if len(snapshots) == 0 {
|
||||
return nil
|
||||
}
|
||||
iccids := make([]string, 0, len(snapshots))
|
||||
for iccid := range snapshots {
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
sort.Strings(iccids)
|
||||
|
||||
columns := dao.BatchDetail.Columns()
|
||||
for start := 0; start < len(iccids); start += outboundDetailUpdateChunkSize {
|
||||
end := start + outboundDetailUpdateChunkSize
|
||||
if end > len(iccids) {
|
||||
end = len(iccids)
|
||||
}
|
||||
chunk := iccids[start:end]
|
||||
var sqlBuilder strings.Builder
|
||||
args := make([]any, 0, len(chunk)*3+2)
|
||||
sqlBuilder.WriteString("UPDATE `")
|
||||
sqlBuilder.WriteString(dao.BatchDetail.Table())
|
||||
sqlBuilder.WriteString("` SET `")
|
||||
sqlBuilder.WriteString(columns.Snapshot)
|
||||
sqlBuilder.WriteString("` = CASE `")
|
||||
sqlBuilder.WriteString(columns.ItemId)
|
||||
sqlBuilder.WriteString("`")
|
||||
for _, iccid := range chunk {
|
||||
sqlBuilder.WriteString(" WHEN ? THEN ?")
|
||||
args = append(args, iccid, gjson.New(snapshots[iccid].Data).String())
|
||||
}
|
||||
sqlBuilder.WriteString(" ELSE `")
|
||||
sqlBuilder.WriteString(columns.Snapshot)
|
||||
sqlBuilder.WriteString("` END, `")
|
||||
sqlBuilder.WriteString(columns.UpdatedBy)
|
||||
sqlBuilder.WriteString("` = ? WHERE `")
|
||||
sqlBuilder.WriteString(columns.BatchNo)
|
||||
sqlBuilder.WriteString("` = ? AND `")
|
||||
sqlBuilder.WriteString(columns.ItemId)
|
||||
sqlBuilder.WriteString("` IN (")
|
||||
args = append(args, userId, batchNo)
|
||||
for index, iccid := range chunk {
|
||||
if index > 0 {
|
||||
sqlBuilder.WriteByte(',')
|
||||
}
|
||||
sqlBuilder.WriteByte('?')
|
||||
args = append(args, iccid)
|
||||
}
|
||||
sqlBuilder.WriteByte(')')
|
||||
if _, err := tx.Exec(sqlBuilder.String(), args...); err != nil {
|
||||
return gerror.Wrapf(err, "更新批次明细快照失败,范围:%d-%d", start+1, end)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildDetailSnapshot(row *Row, stock *entity.TSimCardStock, cardType any, customer *entity.SysDept, pkg *entity.TSimFlowPackageInfo, in *CompleteInput, orderSn, subscriptionNo string, amount float64, beginAt, endAt *gtime.Time) g.Map {
|
||||
return g.Map{
|
||||
"ICCID": stock.Iccid,
|
||||
"MSISDN": stock.Msisdn,
|
||||
"IMSI": stock.Imsi,
|
||||
"IMEI": stock.Imei,
|
||||
"cardType": cardType,
|
||||
"inboundBatchNo": stock.BatchNo,
|
||||
"outboundBatchNo": in.BatchNo,
|
||||
"orderSn": orderSn,
|
||||
"subscriptionNo": subscriptionNo,
|
||||
"customerAgentId": in.SubmitParam.CustomerAgentId,
|
||||
"customerName": customer.DeptName,
|
||||
"packageCode": pkg.PackageCode,
|
||||
"packageName": pkg.PackageName,
|
||||
"packageType": pkg.PackageType,
|
||||
"packageBeginTime": formatTime(beginAt),
|
||||
"packageEndTime": formatTime(endAt),
|
||||
"durationYears": in.SubmitParam.DurationYears,
|
||||
"durationMonthPart": in.SubmitParam.DurationMonthPart,
|
||||
"durationMonths": in.SubmitParam.DurationMonths,
|
||||
"giftDurationYears": in.SubmitParam.GiftDurationYears,
|
||||
"giftDurationMonthPart": in.SubmitParam.GiftDurationMonthPart,
|
||||
"giftDurationMonths": in.SubmitParam.GiftDurationMonths,
|
||||
"durationLabel": displayDurationLabel(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonthPart, in.SubmitParam.DurationMonths),
|
||||
"giftDurationLabel": displayDurationLabel(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonthPart, in.SubmitParam.GiftDurationMonths),
|
||||
"packageTotalFlowMb": packageTotalFlowMB(pkg),
|
||||
"amount": amount,
|
||||
"remark": normalizedRemark(in.SubmitParam),
|
||||
"rowNo": row.RowNo,
|
||||
"stock": g.Map{
|
||||
"chanKey": stock.ChanKey,
|
||||
"sourceInboundBatchNo": stock.BatchNo,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func inboundCardTypeValue(info *gjson.Json) any {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
value := strings.TrimSpace(info.Get("inboundOptions.cardType").String())
|
||||
if value == "" {
|
||||
value = strings.TrimSpace(info.Get("cardType").String())
|
||||
}
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
cardType, err := strconv.Atoi(value)
|
||||
if err != nil || cardType <= 0 {
|
||||
return nil
|
||||
}
|
||||
return cardType
|
||||
}
|
||||
|
||||
func formatTime(t *gtime.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Format("Y-m-d H:i:s")
|
||||
}
|
||||
|
||||
func chooseString(primary, fallback string) string {
|
||||
primary = strings.TrimSpace(primary)
|
||||
if primary != "" {
|
||||
return primary
|
||||
}
|
||||
return strings.TrimSpace(fallback)
|
||||
}
|
||||
|
||||
type outboundPaymentSnapshot struct {
|
||||
PaymentMode string
|
||||
PayType int
|
||||
PayStatus int
|
||||
PayAmount float64
|
||||
BalanceAmount float64
|
||||
OfflinePaidAmount float64
|
||||
BalanceBefore float64
|
||||
BalanceAfter float64
|
||||
BalanceDetailSerialNo string
|
||||
PaymentTime *gtime.Time
|
||||
PayerDeptId int64
|
||||
}
|
||||
|
||||
func prepareOutboundPayment(ctx context.Context, tx gdb.TX, batchNo string, amount float64, operator batchimport.OperatorContext, in *model.TSimCardOutboundSubmitParam) (*outboundPaymentSnapshot, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("出库付款参数不能为空")
|
||||
}
|
||||
if amount < 0 {
|
||||
return nil, gerror.New("订单金额不能小于0")
|
||||
}
|
||||
now := gtime.Now()
|
||||
if in.OfflineContractCompleted {
|
||||
return &outboundPaymentSnapshot{
|
||||
PaymentMode: outboundPaymentModeOffline,
|
||||
PayType: consts.PayTypeOffline,
|
||||
PayStatus: consts.PayStatusPaid,
|
||||
PayAmount: amount,
|
||||
OfflinePaidAmount: amount,
|
||||
PaymentTime: now,
|
||||
PayerDeptId: in.CustomerAgentId,
|
||||
}, nil
|
||||
}
|
||||
return deductOutboundBalanceTx(ctx, tx, batchNo, amount, now, operator, in.CustomerAgentId)
|
||||
}
|
||||
|
||||
func deductOutboundBalanceTx(ctx context.Context, tx gdb.TX, batchNo string, amount float64, paymentTime *gtime.Time, operator batchimport.OperatorContext, payerDeptID int64) (*outboundPaymentSnapshot, error) {
|
||||
if tx == nil {
|
||||
return nil, gerror.New("出库余额支付必须在事务中执行")
|
||||
}
|
||||
if operator.DeptId <= 0 || operator.TenantId == "" || operator.UserId <= 0 {
|
||||
return nil, gerror.New("当前用户未绑定组织、租户或用户信息")
|
||||
}
|
||||
if payerDeptID <= 0 {
|
||||
return nil, gerror.New("出库客户不能为空")
|
||||
}
|
||||
if paymentTime == nil {
|
||||
paymentTime = gtime.Now()
|
||||
}
|
||||
serialNo := outboundBalanceSerialNo(batchNo)
|
||||
if serialNo == "" {
|
||||
return nil, gerror.New("出库余额流水号生成失败")
|
||||
}
|
||||
|
||||
account := &entity.TAgentBalance{}
|
||||
if err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
|
||||
Where(dao.TAgentBalance.Columns().DeptId, payerDeptID).
|
||||
Where(dao.TAgentBalance.Columns().TenantId, operator.TenantId).
|
||||
LockUpdate().
|
||||
Scan(account); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, gerror.Wrap(err, "查询账户余额失败")
|
||||
}
|
||||
if account.Id == 0 {
|
||||
return nil, gerror.New("客户暂无余额账户,请先充值")
|
||||
}
|
||||
if account.Status != outboundFinanceAccountStatusNormal {
|
||||
return nil, gerror.New("账户余额已冻结,不能扣余额")
|
||||
}
|
||||
record, err := tx.Ctx(ctx).Model(dao.TBalanceDetail.Table()).
|
||||
Where(dao.TBalanceDetail.Columns().TenantId, operator.TenantId).
|
||||
Where(dao.TBalanceDetail.Columns().SerialNo, serialNo).
|
||||
LockUpdate().
|
||||
One()
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, gerror.Wrap(err, "查询出库余额流水失败")
|
||||
}
|
||||
if record != nil && !record.IsEmpty() {
|
||||
return nil, gerror.New("当前出库批次已扣款,请勿重复提交")
|
||||
}
|
||||
|
||||
before := roundOutboundAmount(account.Balance)
|
||||
if before+0.0001 < amount {
|
||||
return nil, gerror.Newf("账户余额不足,需支付%.2f元,当前余额%.2f元", amount, before)
|
||||
}
|
||||
after := roundOutboundAmount(before - amount)
|
||||
now := gtime.Now()
|
||||
result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
|
||||
WherePri(account.Id).
|
||||
Data(g.Map{
|
||||
dao.TAgentBalance.Columns().Balance: after,
|
||||
dao.TAgentBalance.Columns().UpdatedBy: operator.UserId,
|
||||
dao.TAgentBalance.Columns().UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "扣减账户余额失败")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取账户余额更新结果失败")
|
||||
}
|
||||
if affected != 1 {
|
||||
return nil, gerror.New("账户余额已变化,请重试")
|
||||
}
|
||||
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TBalanceDetail.Table()).Data(g.Map{
|
||||
dao.TBalanceDetail.Columns().DeptId: payerDeptID,
|
||||
dao.TBalanceDetail.Columns().SerialNo: serialNo,
|
||||
dao.TBalanceDetail.Columns().TransactionTime: paymentTime,
|
||||
dao.TBalanceDetail.Columns().IncomeExpenseType: outboundFinanceIncomeExpenseExpense,
|
||||
dao.TBalanceDetail.Columns().TransactionType: outboundFinanceTransactionTypeConsume,
|
||||
dao.TBalanceDetail.Columns().TransactionChannel: outboundFinanceTransactionChannelBalance,
|
||||
dao.TBalanceDetail.Columns().TransactionStatus: outboundFinanceTransactionStatusPaid,
|
||||
dao.TBalanceDetail.Columns().Amount: amount,
|
||||
dao.TBalanceDetail.Columns().BeforeBalance: before,
|
||||
dao.TBalanceDetail.Columns().BalanceAfter: after,
|
||||
dao.TBalanceDetail.Columns().OrderSn: batchNo,
|
||||
dao.TBalanceDetail.Columns().Remark: fmt.Sprintf("SIM卡出库余额支付,批次号:%s", batchNo),
|
||||
dao.TBalanceDetail.Columns().TenantId: operator.TenantId,
|
||||
dao.TBalanceDetail.Columns().CreatedDept: operator.DeptId,
|
||||
dao.TBalanceDetail.Columns().CreatedBy: operator.UserId,
|
||||
dao.TBalanceDetail.Columns().CreatedAt: now,
|
||||
}).Insert(); err != nil {
|
||||
return nil, gerror.Wrap(err, "写入出库余额流水失败")
|
||||
}
|
||||
|
||||
return &outboundPaymentSnapshot{
|
||||
PaymentMode: outboundPaymentModeBalance,
|
||||
PayType: consts.PayTypeBalance,
|
||||
PayStatus: consts.PayStatusPaid,
|
||||
PayAmount: amount,
|
||||
BalanceAmount: amount,
|
||||
BalanceBefore: before,
|
||||
BalanceAfter: after,
|
||||
BalanceDetailSerialNo: serialNo,
|
||||
PaymentTime: paymentTime,
|
||||
PayerDeptId: payerDeptID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func subscriptionAmount(in *model.TSimCardOutboundSubmitParam) float64 {
|
||||
amount, _ := parseOutboundAmount(in.Amount)
|
||||
return amount
|
||||
}
|
||||
|
||||
func parseOutboundAmount(value string) (float64, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, gerror.New("订单金额不能为空")
|
||||
}
|
||||
amount, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return 0, gerror.New("订单金额格式不正确")
|
||||
}
|
||||
if amount < 0 {
|
||||
return 0, gerror.New("订单金额不能小于0")
|
||||
}
|
||||
return roundOutboundAmount(amount), nil
|
||||
}
|
||||
|
||||
func roundOutboundAmount(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
func outboundBalanceSerialNo(batchNo string) string {
|
||||
batchNo = strings.TrimSpace(batchNo)
|
||||
if batchNo == "" {
|
||||
return ""
|
||||
}
|
||||
return "OUTPAY" + batchNo
|
||||
}
|
||||
|
||||
func displayDurationValue(years, months int) int {
|
||||
if years > 0 {
|
||||
return years
|
||||
}
|
||||
if months > 0 && months%12 == 0 {
|
||||
return months / 12
|
||||
}
|
||||
return months
|
||||
}
|
||||
|
||||
func displayDurationLabel(years, monthPart, totalMonths int) string {
|
||||
if years <= 0 && monthPart <= 0 && totalMonths > 0 {
|
||||
years = totalMonths / 12
|
||||
monthPart = totalMonths % 12
|
||||
}
|
||||
parts := make([]string, 0, 2)
|
||||
if years > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d年", years))
|
||||
}
|
||||
if monthPart > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d个月", monthPart))
|
||||
}
|
||||
if len(parts) == 0 && totalMonths > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d个月", totalMonths))
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
func packageTotalFlowMB(pkg *entity.TSimFlowPackageInfo) float64 {
|
||||
if pkg == nil {
|
||||
return 0
|
||||
}
|
||||
if pkg.FlowMb > 0 {
|
||||
return pkg.FlowMb
|
||||
}
|
||||
if pkg.FlowGb > 0 {
|
||||
return pkg.FlowGb * 1024
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common/cardsource"
|
||||
)
|
||||
|
||||
func ParseInputRows(ctx context.Context, input string) ([]batchimport.Row, error) {
|
||||
return cardsource.BuildIccidRows(ctx, input, "批量输入出库数据为空", func(line cardsource.IccidLine) *Row {
|
||||
return &Row{
|
||||
RowNo: line.RowNo,
|
||||
Iccid: line.Iccid,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/excel"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type ExcelParseResult struct {
|
||||
Rows []*Row
|
||||
SubmitParam *model.TSimCardOutboundSubmitParam
|
||||
}
|
||||
|
||||
type consistencyValue struct {
|
||||
rowNo int
|
||||
normalized string
|
||||
}
|
||||
|
||||
func excelHeaders() []string {
|
||||
return []string{
|
||||
"ICCID",
|
||||
"MSISDN",
|
||||
"IMSI",
|
||||
"客户/机构",
|
||||
"套餐",
|
||||
"套餐开始时间",
|
||||
"购买时长(年)",
|
||||
"赠送时长(月)",
|
||||
"订单金额",
|
||||
"线下合同/销售/走账已完成",
|
||||
"备注",
|
||||
}
|
||||
}
|
||||
|
||||
func excelRequiredHeaders() []string {
|
||||
return []string{"ICCID"}
|
||||
}
|
||||
|
||||
func ParseExcelFile(ctx context.Context, filePath string) (*ExcelParseResult, error) {
|
||||
parsedRows, err := excel.ParseFile(ctx, filePath, excel.ParseOptions{
|
||||
RequiredHeaders: excelRequiredHeaders(),
|
||||
SingleSheet: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildExcelParseResult(parsedRows), nil
|
||||
}
|
||||
|
||||
func ParseExcelReader(ctx context.Context, reader io.Reader) (*ExcelParseResult, error) {
|
||||
parsedRows, err := excel.ParseReader(ctx, reader, excel.ParseOptions{
|
||||
RequiredHeaders: excelRequiredHeaders(),
|
||||
SingleSheet: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildExcelParseResult(parsedRows), nil
|
||||
}
|
||||
|
||||
func buildExcelParseResult(parsedRows []excel.ParsedRow) *ExcelParseResult {
|
||||
rows := make([]*Row, 0, len(parsedRows))
|
||||
consistentCustomerAgentId := consistencyValue{}
|
||||
consistentPackageCode := consistencyValue{}
|
||||
consistentPackageBeginTime := consistencyValue{}
|
||||
consistentDurationYears := consistencyValue{}
|
||||
consistentDurationMonthPart := consistencyValue{}
|
||||
consistentGiftDurationYears := consistencyValue{}
|
||||
consistentGiftDurationMonthPart := consistencyValue{}
|
||||
consistentAmount := consistencyValue{}
|
||||
consistentOfflineContractCompleted := consistencyValue{}
|
||||
consistentRemark := consistencyValue{}
|
||||
|
||||
for _, parsed := range parsedRows {
|
||||
row := &Row{
|
||||
RowNo: parsed.RowNo,
|
||||
Iccid: excel.Field(parsed, "ICCID"),
|
||||
Msisdn: excel.Field(parsed, "MSISDN"),
|
||||
Imsi: excel.Field(parsed, "IMSI"),
|
||||
// 出库不再配置测试期、沉默期;1 表示否。
|
||||
HasTestPeriod: 1,
|
||||
HasSilentPeriod: 1,
|
||||
}
|
||||
|
||||
parseRequiredPositiveInt(row, excelField(parsed, "customerAgentId"), "客户/机构", func(value int) {
|
||||
row.CustomerAgentId = int64(value)
|
||||
checkConsistency(row, "客户/机构", strconv.FormatInt(row.CustomerAgentId, 10), &consistentCustomerAgentId)
|
||||
})
|
||||
|
||||
row.PackageCode = strings.TrimSpace(excelField(parsed, "packageCode"))
|
||||
if row.PackageCode == "" {
|
||||
row.AddReason("套餐不能为空")
|
||||
} else {
|
||||
checkConsistency(row, "套餐", row.PackageCode, &consistentPackageCode)
|
||||
}
|
||||
|
||||
parseRequiredDateTime(row, excelField(parsed, "packageBeginTime"), "套餐开始时间", func(value *gtime.Time) {
|
||||
row.PackageBeginTime = value
|
||||
checkConsistency(row, "套餐开始时间", formatTime(value), &consistentPackageBeginTime)
|
||||
})
|
||||
|
||||
parseExcelDuration(row, parsed, &consistentDurationYears, &consistentDurationMonthPart)
|
||||
|
||||
parseExcelGiftDuration(row, parsed, &consistentGiftDurationYears, &consistentGiftDurationMonthPart)
|
||||
|
||||
parseRequiredAmount(row, excelField(parsed, "amount"), func(value string) {
|
||||
row.Amount = value
|
||||
checkConsistency(row, "订单金额", value, &consistentAmount)
|
||||
})
|
||||
|
||||
parseRequiredBool(row, excelField(parsed, "offlineContractCompleted"), "线下合同/销售/走账已完成", func(value bool) {
|
||||
row.OfflineContractCompleted = value
|
||||
checkConsistency(row, "线下合同/销售/走账已完成", strconv.FormatBool(value), &consistentOfflineContractCompleted)
|
||||
})
|
||||
|
||||
row.Remark = strings.TrimSpace(excelField(parsed, "remark"))
|
||||
checkConsistency(row, "备注", row.Remark, &consistentRemark)
|
||||
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
result := &ExcelParseResult{Rows: rows}
|
||||
if hasInvalidRows(rows) {
|
||||
return result
|
||||
}
|
||||
result.SubmitParam = &model.TSimCardOutboundSubmitParam{
|
||||
CustomerAgentId: rows[0].CustomerAgentId,
|
||||
PackageCode: rows[0].PackageCode,
|
||||
PackageBeginTime: rows[0].PackageBeginTime,
|
||||
DurationYears: rows[0].DurationYears,
|
||||
DurationMonthPart: rows[0].DurationMonthPart,
|
||||
DurationMonths: rows[0].DurationMonths,
|
||||
GiftDurationYears: rows[0].GiftDurationYears,
|
||||
GiftDurationMonthPart: rows[0].GiftDurationMonthPart,
|
||||
GiftDurationMonths: rows[0].GiftDurationMonths,
|
||||
HasTestPeriod: 1,
|
||||
HasSilentPeriod: 1,
|
||||
Amount: rows[0].Amount,
|
||||
OfflineContractCompleted: rows[0].OfflineContractCompleted,
|
||||
Remark: rows[0].Remark,
|
||||
BuyerNote: rows[0].Remark,
|
||||
AdminNote: rows[0].Remark,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func excelField(row excel.ParsedRow, name string) string {
|
||||
for _, alias := range excelFieldAliases(name) {
|
||||
if value := strings.TrimSpace(excel.Field(row, alias)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func excelFieldAliases(name string) []string {
|
||||
switch name {
|
||||
case "customerAgentId":
|
||||
return []string{"客户/机构", "客户/机构ID", "客户ID", "customerAgentId"}
|
||||
case "packageCode":
|
||||
return []string{"套餐", "套餐编码", "packageCode"}
|
||||
case "packageBeginTime":
|
||||
return []string{"套餐开始时间", "packageBeginTime"}
|
||||
case "durationYears":
|
||||
return []string{"购买时长(年)", "套餐购买时长(年)", "durationYears"}
|
||||
case "durationMonthPart":
|
||||
return []string{"购买时长(月)", "套餐购买时长(月)", "durationMonthPart"}
|
||||
case "durationMonths":
|
||||
return []string{"durationMonths"}
|
||||
case "giftDurationYears":
|
||||
return []string{"赠送时长(年)", "giftDurationYears"}
|
||||
case "giftDurationMonthPart":
|
||||
return []string{"赠送时长(月)", "giftDurationMonthPart"}
|
||||
case "giftDurationMonths":
|
||||
return []string{"giftDurationMonths"}
|
||||
case "amount":
|
||||
return []string{"订单金额", "amount"}
|
||||
case "offlineContractCompleted":
|
||||
return []string{"线下合同/销售/走账已完成", "是否走线下", "offlineContractCompleted"}
|
||||
case "remark":
|
||||
return []string{"备注", "remark", "buyerNote", "adminNote"}
|
||||
default:
|
||||
return []string{name}
|
||||
}
|
||||
}
|
||||
|
||||
func parseExcelDuration(row *Row, parsed excel.ParsedRow, consistentYears, consistentMonthPart *consistencyValue) {
|
||||
yearsRaw := excelField(parsed, "durationYears")
|
||||
monthPartRaw := excelField(parsed, "durationMonthPart")
|
||||
if strings.TrimSpace(yearsRaw) != "" || strings.TrimSpace(monthPartRaw) != "" {
|
||||
parseOptionalNonNegativeInt(row, yearsRaw, "购买时长(年)", 0, func(value int) {
|
||||
row.DurationYears = value
|
||||
checkConsistency(row, "购买时长(年)", strconv.Itoa(value), consistentYears)
|
||||
})
|
||||
parseOptionalMonthPart(row, monthPartRaw, "购买时长(月)", func(value int) {
|
||||
row.DurationMonthPart = value
|
||||
checkConsistency(row, "购买时长(月)", strconv.Itoa(value), consistentMonthPart)
|
||||
})
|
||||
row.DurationMonths = row.DurationYears*12 + row.DurationMonthPart
|
||||
if row.DurationMonths <= 0 {
|
||||
row.AddReason("购买时长必须大于0")
|
||||
}
|
||||
return
|
||||
}
|
||||
parseRequiredPositiveInt(row, excelField(parsed, "durationMonths"), "套餐购买时长(月)", func(value int) {
|
||||
row.DurationMonths = value
|
||||
row.DurationYears = value / 12
|
||||
row.DurationMonthPart = value % 12
|
||||
checkConsistency(row, "套餐购买时长(月)", strconv.Itoa(value), consistentMonthPart)
|
||||
})
|
||||
}
|
||||
|
||||
func parseExcelGiftDuration(row *Row, parsed excel.ParsedRow, consistentYears, consistentMonthPart *consistencyValue) {
|
||||
yearsRaw := excelField(parsed, "giftDurationYears")
|
||||
monthPartRaw := excelField(parsed, "giftDurationMonthPart")
|
||||
if strings.TrimSpace(yearsRaw) != "" || strings.TrimSpace(monthPartRaw) != "" {
|
||||
parseOptionalNonNegativeInt(row, yearsRaw, "赠送时长(年)", 0, func(value int) {
|
||||
row.GiftDurationYears = value
|
||||
checkConsistency(row, "赠送时长(年)", strconv.Itoa(value), consistentYears)
|
||||
})
|
||||
parseOptionalMonthPart(row, monthPartRaw, "赠送时长(月)", func(value int) {
|
||||
row.GiftDurationMonthPart = value
|
||||
checkConsistency(row, "赠送时长(月)", strconv.Itoa(value), consistentMonthPart)
|
||||
})
|
||||
row.GiftDurationMonths = row.GiftDurationYears*12 + row.GiftDurationMonthPart
|
||||
return
|
||||
}
|
||||
parseOptionalNonNegativeInt(row, excelField(parsed, "giftDurationMonths"), "赠送时长(月)", 0, func(value int) {
|
||||
row.GiftDurationMonths = value
|
||||
row.GiftDurationYears = value / 12
|
||||
row.GiftDurationMonthPart = value % 12
|
||||
checkConsistency(row, "赠送时长(月)", strconv.Itoa(value), consistentMonthPart)
|
||||
})
|
||||
}
|
||||
|
||||
func checkConsistency(row *Row, field, normalized string, base *consistencyValue) {
|
||||
if row == nil || !row.Valid() {
|
||||
return
|
||||
}
|
||||
if base.rowNo == 0 {
|
||||
base.rowNo = row.RowNo
|
||||
base.normalized = normalized
|
||||
return
|
||||
}
|
||||
if base.normalized != normalized {
|
||||
row.AddReason(fmt.Sprintf("%s与第%d行不一致", field, base.rowNo))
|
||||
}
|
||||
}
|
||||
|
||||
func parseRequiredPositiveInt(row *Row, raw, field string, apply func(value int)) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
row.AddReason(field + "不能为空")
|
||||
return
|
||||
}
|
||||
value, err := excel.ParsePositiveInt(raw)
|
||||
if err != nil {
|
||||
row.AddReason(field + "格式不正确")
|
||||
return
|
||||
}
|
||||
apply(value)
|
||||
}
|
||||
|
||||
func parseOptionalNonNegativeInt(row *Row, raw, field string, defaultValue int, apply func(value int)) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
apply(defaultValue)
|
||||
return
|
||||
}
|
||||
value, err := excel.ParseNonNegativeInt(raw)
|
||||
if err != nil {
|
||||
row.AddReason(field + "格式不正确")
|
||||
return
|
||||
}
|
||||
apply(value)
|
||||
}
|
||||
|
||||
func parseOptionalMonthPart(row *Row, raw, field string, apply func(value int)) {
|
||||
parseOptionalNonNegativeInt(row, raw, field, 0, func(value int) {
|
||||
if value > 11 {
|
||||
row.AddReason(field + "必须在0到11之间")
|
||||
return
|
||||
}
|
||||
apply(value)
|
||||
})
|
||||
}
|
||||
|
||||
func parseOptionalAmount(row *Row, raw string, apply func(value string)) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
apply("")
|
||||
return
|
||||
}
|
||||
value, err := excel.ParseAmount(raw)
|
||||
if err != nil {
|
||||
row.AddReason("订单金额格式不正确")
|
||||
return
|
||||
}
|
||||
apply(fmt.Sprintf("%.2f", value))
|
||||
}
|
||||
|
||||
func parseRequiredAmount(row *Row, raw string, apply func(value string)) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
row.AddReason("订单金额不能为空")
|
||||
return
|
||||
}
|
||||
parseOptionalAmount(row, raw, apply)
|
||||
}
|
||||
|
||||
func parseRequiredBool(row *Row, raw, field string, apply func(value bool)) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
row.AddReason(field + "不能为空")
|
||||
return
|
||||
}
|
||||
value, err := excel.ParseBool(raw)
|
||||
if err != nil {
|
||||
row.AddReason(field + "格式不正确")
|
||||
return
|
||||
}
|
||||
apply(value)
|
||||
}
|
||||
|
||||
func parseRequiredDateTime(row *Row, raw, field string, apply func(value *gtime.Time)) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
row.AddReason(field + "不能为空")
|
||||
return
|
||||
}
|
||||
value, err := excel.ParseDateTime(raw)
|
||||
if err != nil {
|
||||
row.AddReason(field + "格式不正确")
|
||||
return
|
||||
}
|
||||
apply(value)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func validateRows(ctx context.Context, rows []*Row, opts PreviewOptions, repo *Repository) error {
|
||||
identityFirstRow := make(map[string]*Row, len(rows))
|
||||
iccids := make([]string, 0, len(rows))
|
||||
msisdns := make([]string, 0, len(rows))
|
||||
imsis := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
row.Iccid = strings.TrimSpace(row.Iccid)
|
||||
row.Msisdn = strings.TrimSpace(row.Msisdn)
|
||||
row.Imsi = strings.TrimSpace(row.Imsi)
|
||||
row.RangeCodeType = rowIdentityCodeType(row)
|
||||
key := rowIdentityKey(row)
|
||||
if key == "" {
|
||||
row.AddReason(rowIdentityLabel(row.RangeCodeType) + "不能为空")
|
||||
continue
|
||||
}
|
||||
identityKey := row.RangeCodeType + ":" + key
|
||||
if firstRow, ok := identityFirstRow[identityKey]; ok {
|
||||
label := rowIdentityLabel(row.RangeCodeType)
|
||||
row.AddReason(fmt.Sprintf("%s与第%d行重复", label, firstRow.RowNo))
|
||||
firstRow.AddReason(fmt.Sprintf("%s与第%d行重复", label, row.RowNo))
|
||||
continue
|
||||
}
|
||||
identityFirstRow[identityKey] = row
|
||||
switch row.RangeCodeType {
|
||||
case "msisdn":
|
||||
msisdns = append(msisdns, key)
|
||||
case "imsi":
|
||||
imsis = append(imsis, key)
|
||||
default:
|
||||
iccids = append(iccids, key)
|
||||
}
|
||||
}
|
||||
|
||||
stockByIccid, err := repo.GetStockCardsByIccids(ctx, iccids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stockByMsisdn, err := repo.GetStockCardsByMsisdns(ctx, msisdns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stockByImsi, err := repo.GetStockCardsByImsis(ctx, imsis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowStock := make(map[*Row]*StockCard, len(rows))
|
||||
resolvedIccidFirstRow := make(map[string]*Row, len(rows))
|
||||
resolvedIccids := make([]string, 0, len(rows))
|
||||
|
||||
for _, row := range rows {
|
||||
if !row.Valid() {
|
||||
continue
|
||||
}
|
||||
stock := stockForRow(row, stockByIccid, stockByMsisdn, stockByImsi)
|
||||
if stock == nil {
|
||||
row.AddReason(rowIdentityLabel(row.RangeCodeType) + "不存在库存记录")
|
||||
continue
|
||||
}
|
||||
providedIccid := row.Iccid
|
||||
providedMsisdn := row.Msisdn
|
||||
providedImsi := row.Imsi
|
||||
row.Iccid = strings.TrimSpace(stock.Iccid)
|
||||
row.Msisdn = chooseString(row.Msisdn, stock.Msisdn)
|
||||
row.Imsi = chooseString(row.Imsi, stock.Imsi)
|
||||
if row.Iccid == "" {
|
||||
row.AddReason("库存卡ICCID为空")
|
||||
continue
|
||||
}
|
||||
if firstRow, ok := resolvedIccidFirstRow[row.Iccid]; ok {
|
||||
row.AddReason(fmt.Sprintf("ICCID与第%d行指向同一库存卡", firstRow.RowNo))
|
||||
firstRow.AddReason(fmt.Sprintf("ICCID与第%d行指向同一库存卡", row.RowNo))
|
||||
continue
|
||||
}
|
||||
resolvedIccidFirstRow[row.Iccid] = row
|
||||
if stock.CreatedDept != opts.DeptId {
|
||||
row.AddReason("ICCID不属于当前机构库存")
|
||||
continue
|
||||
}
|
||||
if opts.StrictStockIdentity {
|
||||
if providedIccid != "" && providedIccid != strings.TrimSpace(stock.Iccid) {
|
||||
row.AddReason("ICCID与库存记录不一致")
|
||||
continue
|
||||
}
|
||||
if providedMsisdn != "" && providedMsisdn != strings.TrimSpace(stock.Msisdn) {
|
||||
row.AddReason("MSISDN与库存记录不一致")
|
||||
continue
|
||||
}
|
||||
if providedImsi != "" && providedImsi != strings.TrimSpace(stock.Imsi) {
|
||||
row.AddReason("IMSI与库存记录不一致")
|
||||
continue
|
||||
}
|
||||
}
|
||||
row.SourceBatchNo = strings.TrimSpace(stock.BatchNo)
|
||||
rowStock[row] = stock
|
||||
resolvedIccids = append(resolvedIccids, row.Iccid)
|
||||
}
|
||||
|
||||
formalByIccid, err := repo.GetExistingFormalIccids(ctx, resolvedIccids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
if !row.Valid() || rowStock[row] == nil {
|
||||
continue
|
||||
}
|
||||
if formalByIccid[row.Iccid] {
|
||||
row.AddReason("ICCID已存在正式卡记录")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rowIdentityCodeType(row *Row) string {
|
||||
if row == nil {
|
||||
return "iccid"
|
||||
}
|
||||
if codeType := normalizeRangeCodeType(row.RangeCodeType); codeType != "" {
|
||||
return codeType
|
||||
}
|
||||
switch {
|
||||
case strings.TrimSpace(row.Iccid) != "":
|
||||
return "iccid"
|
||||
case strings.TrimSpace(row.Msisdn) != "":
|
||||
return "msisdn"
|
||||
case strings.TrimSpace(row.Imsi) != "":
|
||||
return "imsi"
|
||||
default:
|
||||
return "iccid"
|
||||
}
|
||||
}
|
||||
|
||||
func rowIdentityKey(row *Row) string {
|
||||
if row == nil {
|
||||
return ""
|
||||
}
|
||||
switch row.RangeCodeType {
|
||||
case "msisdn":
|
||||
return strings.TrimSpace(row.Msisdn)
|
||||
case "imsi":
|
||||
return strings.TrimSpace(row.Imsi)
|
||||
default:
|
||||
return strings.TrimSpace(row.Iccid)
|
||||
}
|
||||
}
|
||||
|
||||
func rowIdentityLabel(codeType string) string {
|
||||
switch codeType {
|
||||
case "msisdn":
|
||||
return "MSISDN"
|
||||
case "imsi":
|
||||
return "IMSI"
|
||||
default:
|
||||
return "ICCID"
|
||||
}
|
||||
}
|
||||
|
||||
func stockForRow(row *Row, stockByIccid, stockByMsisdn, stockByImsi map[string]*StockCard) *StockCard {
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
switch row.RangeCodeType {
|
||||
case "msisdn":
|
||||
return stockByMsisdn[strings.TrimSpace(row.Msisdn)]
|
||||
case "imsi":
|
||||
return stockByImsi[strings.TrimSpace(row.Imsi)]
|
||||
default:
|
||||
return stockByIccid[strings.TrimSpace(row.Iccid)]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"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/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
repo *Repository
|
||||
opts PreviewOptions
|
||||
config batchimport.ProcessorConfig
|
||||
excelSubmitParam *model.TSimCardOutboundSubmitParam
|
||||
}
|
||||
|
||||
func (p *Processor) ExecutePending(ctx context.Context, tx gdb.TX, batchNo string, in *model.TSimCardOutboundSubmitParam, rows []*Row, operator batchimport.OperatorContext, requiredSourceBatchNo string) (*CompleteOutput, error) {
|
||||
if strings.TrimSpace(batchNo) == "" {
|
||||
return nil, gerror.New("出库批次号不能为空")
|
||||
}
|
||||
if in == nil {
|
||||
return nil, gerror.New("出库参数不能为空")
|
||||
}
|
||||
if err := p.validateExecuteContext(ctx, tx, in, operator, rows, requiredSourceBatchNo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Complete(ctx, tx, &CompleteInput{
|
||||
SubmitParam: in,
|
||||
BatchNo: batchNo,
|
||||
Rows: rows,
|
||||
Operator: operator,
|
||||
StrictStockIdentity: p.opts.StrictStockIdentity,
|
||||
RequiredSourceBatchNo: requiredSourceBatchNo,
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
outboundCardPackageType = 101
|
||||
outboundPoolPackageType = 102
|
||||
)
|
||||
|
||||
func NewProcessor(opts PreviewOptions) *Processor {
|
||||
return &Processor{
|
||||
repo: NewRepository(),
|
||||
opts: opts,
|
||||
config: batchimport.ProcessorConfig{
|
||||
Code: "sim_card_outbound",
|
||||
BatchType: consts.BatchTypeOutbound,
|
||||
BatchNoPrefix: consts.BatchNoPrefixOutbound,
|
||||
TaskName: consts.ResourceBatchDownloadTaskSimCardOutbound,
|
||||
ExportScene: consts.ResourceExportSceneOutbound,
|
||||
ImportDir: consts.SimCardOutboundSubDirImportExcel,
|
||||
ResultDir: consts.SimCardOutboundSubDirResultExcel,
|
||||
MaxFileSize: consts.SimCardOutboundMaxFileSize,
|
||||
TemplateHeaders: excelHeaders(),
|
||||
RequiredHeaders: excelRequiredHeaders(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) Config() batchimport.ProcessorConfig {
|
||||
return p.config
|
||||
}
|
||||
|
||||
func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
parsed, err := ParseExcelFile(ctx, filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.excelSubmitParam = nil
|
||||
if parsed != nil {
|
||||
p.excelSubmitParam = parsed.SubmitParam
|
||||
}
|
||||
result := make([]batchimport.Row, 0, len(parsed.Rows))
|
||||
for _, row := range parsed.Rows {
|
||||
result = append(result, row)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *Processor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
outboundRows := make([]*Row, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok {
|
||||
item.AddErrorReason("导入行类型错误")
|
||||
continue
|
||||
}
|
||||
outboundRows = append(outboundRows, row)
|
||||
}
|
||||
|
||||
opts := p.opts
|
||||
if opts.DeptId == 0 {
|
||||
opts.DeptId = contexts.GetDeptId(ctx)
|
||||
}
|
||||
return validateRows(ctx, outboundRows, opts, p.repo)
|
||||
}
|
||||
|
||||
func (p *Processor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
|
||||
if req == nil {
|
||||
return gerror.New("出库请求不能为空")
|
||||
}
|
||||
in := p.submitParams(req.BizParams)
|
||||
if in == nil {
|
||||
return gerror.New("出库参数类型错误")
|
||||
}
|
||||
|
||||
outboundRows := make([]*Row, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok {
|
||||
return gerror.New("导入行类型错误")
|
||||
}
|
||||
outboundRows = append(outboundRows, row)
|
||||
}
|
||||
if err := p.validateExecuteContext(ctx, tx, in, req.Operator, outboundRows, requiredSourceBatchNo(req.BizParams)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
output, err := p.Complete(ctx, tx, &CompleteInput{
|
||||
SubmitParam: in,
|
||||
BatchNo: req.BatchNo,
|
||||
Rows: outboundRows,
|
||||
Operator: req.Operator,
|
||||
StrictStockIdentity: p.opts.StrictStockIdentity,
|
||||
RequiredSourceBatchNo: requiredSourceBatchNo(req.BizParams),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.ExecutionResult = output
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
|
||||
info := g.Map{}
|
||||
info["strictStockIdentity"] = p.opts.StrictStockIdentity
|
||||
if result != nil && result.ImportFileOssId > 0 {
|
||||
info["importFileOssId"] = result.ImportFileOssId
|
||||
} else if importOss != nil {
|
||||
info["importFileOssId"] = importOss.OssId
|
||||
}
|
||||
if resultOss != nil {
|
||||
info["resultFileOssId"] = resultOss.OssId
|
||||
}
|
||||
if downloadId > 0 {
|
||||
info["resultDownloadId"] = downloadId
|
||||
}
|
||||
if in := p.submitParams(req.BizParams); in != nil {
|
||||
normalizeSubmitParam(in)
|
||||
info["customerAgentId"] = in.CustomerAgentId
|
||||
info["packageCode"] = strings.TrimSpace(in.PackageCode)
|
||||
info["packageBeginTime"] = in.PackageBeginTime
|
||||
info["durationYears"] = in.DurationYears
|
||||
info["durationMonthPart"] = in.DurationMonthPart
|
||||
info["durationMonths"] = in.DurationMonths
|
||||
info["giftDurationYears"] = in.GiftDurationYears
|
||||
info["giftDurationMonthPart"] = in.GiftDurationMonthPart
|
||||
info["giftDurationMonths"] = in.GiftDurationMonths
|
||||
info["amount"] = strings.TrimSpace(in.Amount)
|
||||
info["offlineContractCompleted"] = in.OfflineContractCompleted
|
||||
info["remark"] = normalizedRemark(in)
|
||||
info["buyerNote"] = normalizedRemark(in)
|
||||
info["adminNote"] = normalizedRemark(in)
|
||||
}
|
||||
if in, ok := req.BizParams.(*model.TSimCardOutboundRangeSubmitParam); ok && in != nil {
|
||||
info["rangeCodeType"] = normalizeRangeCodeType(in.RangeCodeType)
|
||||
info["startIccid"] = strings.TrimSpace(in.StartIccid)
|
||||
info["endIccid"] = strings.TrimSpace(in.EndIccid)
|
||||
info["startMsisdn"] = strings.TrimSpace(in.StartMsisdn)
|
||||
info["endMsisdn"] = strings.TrimSpace(in.EndMsisdn)
|
||||
info["startImsi"] = strings.TrimSpace(in.StartImsi)
|
||||
info["endImsi"] = strings.TrimSpace(in.EndImsi)
|
||||
}
|
||||
if in, ok := req.BizParams.(*model.TSimCardOutboundBatchSubmitParam); ok && in != nil {
|
||||
info["inboundBatchNo"] = strings.TrimSpace(in.BatchNo)
|
||||
}
|
||||
if output, ok := req.ExecutionResult.(*CompleteOutput); ok && output != nil {
|
||||
info["orderSn"] = output.OrderSn
|
||||
info["quantity"] = output.Quantity
|
||||
info["sourceBatchNos"] = output.SourceBatchNos
|
||||
info["packageBeginTime"] = output.PackageBeginAt
|
||||
info["packageEndTime"] = output.PackageEndAt
|
||||
info["packageStatus"] = output.PackageStatus
|
||||
info["effectiveStatus"] = output.EffectiveStatus
|
||||
if output.Customer != nil {
|
||||
info["customerName"] = output.Customer.DeptName
|
||||
}
|
||||
if output.Package != nil {
|
||||
info["packageName"] = output.Package.PackageName
|
||||
info["packageType"] = output.Package.PackageType
|
||||
}
|
||||
}
|
||||
return gjson.New(info)
|
||||
}
|
||||
|
||||
func (p *Processor) validateExecuteContext(ctx context.Context, tx gdb.TX, in *model.TSimCardOutboundSubmitParam, operator batchimport.OperatorContext, rows []*Row, requiredSourceBatchNo string) error {
|
||||
normalizeSubmitParam(in)
|
||||
if err := validateSubmitParam(in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.validateSelectablePackage(ctx, tx, in.PackageCode, operator.TenantId); err != nil {
|
||||
return err
|
||||
}
|
||||
iccids, err := strictRowsToIccids(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stockByIccid, err := p.repo.LockStockCardsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "校验出库库存卡失败")
|
||||
}
|
||||
formalByIccid, err := p.repo.GetExistingFormalIccids(ctx, iccids)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "校验正式卡失败")
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if row == nil || row.Reason != "" {
|
||||
continue
|
||||
}
|
||||
stock := stockByIccid[row.Iccid]
|
||||
reason := validateOutboundRow(row, stock, formalByIccid[row.Iccid], operator, p.opts.StrictStockIdentity, requiredSourceBatchNo)
|
||||
if reason != "" {
|
||||
row.AddReason(reason)
|
||||
}
|
||||
}
|
||||
|
||||
if hasInvalidRows(rows) {
|
||||
markRemainingRowsSkipped(rows)
|
||||
return gerror.New("存在不满足出库条件的卡,本次未执行")
|
||||
}
|
||||
return 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 {
|
||||
return gerror.Wrap(err, "校验出库套餐失败")
|
||||
}
|
||||
if pkg == nil {
|
||||
return gerror.New("出库套餐不存在")
|
||||
}
|
||||
if !isOutboundSelectablePackageType(pkg.PackageType) {
|
||||
return gerror.New("出库套餐必须是卡套餐或池套餐")
|
||||
}
|
||||
if pkg.Enabled != 1 {
|
||||
return gerror.New("出库套餐未启用,不能出库")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isOutboundSelectablePackageType(packageType int) bool {
|
||||
return packageType == outboundCardPackageType || packageType == outboundPoolPackageType
|
||||
}
|
||||
|
||||
func normalizeSubmitParam(in *model.TSimCardOutboundSubmitParam) {
|
||||
if in == nil {
|
||||
return
|
||||
}
|
||||
if in.DurationMonths <= 0 && in.DurationYears > 0 {
|
||||
in.DurationMonths = in.DurationYears*12 + in.DurationMonthPart
|
||||
}
|
||||
if in.DurationMonths <= 0 && in.DurationMonthPart > 0 {
|
||||
in.DurationMonths = in.DurationMonthPart
|
||||
}
|
||||
if in.DurationYears <= 0 && in.DurationMonthPart <= 0 && in.DurationMonths > 0 {
|
||||
in.DurationYears = in.DurationMonths / 12
|
||||
in.DurationMonthPart = in.DurationMonths % 12
|
||||
}
|
||||
if in.GiftDurationMonths <= 0 && in.GiftDurationYears > 0 {
|
||||
in.GiftDurationMonths = in.GiftDurationYears*12 + in.GiftDurationMonthPart
|
||||
}
|
||||
if in.GiftDurationMonths <= 0 && in.GiftDurationMonthPart > 0 {
|
||||
in.GiftDurationMonths = in.GiftDurationMonthPart
|
||||
}
|
||||
if in.GiftDurationYears <= 0 && in.GiftDurationMonthPart <= 0 && in.GiftDurationMonths > 0 {
|
||||
in.GiftDurationYears = in.GiftDurationMonths / 12
|
||||
in.GiftDurationMonthPart = in.GiftDurationMonths % 12
|
||||
}
|
||||
disablePeriodOptions(in)
|
||||
if remark := normalizedRemark(in); remark != "" {
|
||||
in.Remark = remark
|
||||
in.BuyerNote = remark
|
||||
in.AdminNote = remark
|
||||
}
|
||||
}
|
||||
|
||||
func disablePeriodOptions(in *model.TSimCardOutboundSubmitParam) {
|
||||
if in == nil {
|
||||
return
|
||||
}
|
||||
// 1 表示否。出库不再暴露测试期、沉默期配置,统一按无测试期/无沉默期保存。
|
||||
in.HasTestPeriod = 1
|
||||
in.TestPeriodDays = 0
|
||||
in.TestPeriodLatestExpiryDate = nil
|
||||
in.HasSilentPeriod = 1
|
||||
in.SilentPeriodDays = 0
|
||||
in.SilentPeriodLatestExpiryDate = nil
|
||||
}
|
||||
|
||||
func normalizedRemark(in *model.TSimCardOutboundSubmitParam) string {
|
||||
if in == nil {
|
||||
return ""
|
||||
}
|
||||
if value := strings.TrimSpace(in.Remark); value != "" {
|
||||
return value
|
||||
}
|
||||
if value := strings.TrimSpace(in.BuyerNote); value != "" {
|
||||
return value
|
||||
}
|
||||
return strings.TrimSpace(in.AdminNote)
|
||||
}
|
||||
|
||||
func (p *Processor) submitParams(params any) *model.TSimCardOutboundSubmitParam {
|
||||
if in := outboundSubmitParams(params); in != nil {
|
||||
return in
|
||||
}
|
||||
return p.excelSubmitParam
|
||||
}
|
||||
|
||||
func validateOutboundRow(row *Row, stock *entity.TSimCardStock, formalExists bool, operator batchimport.OperatorContext, strictStockIdentity bool, requiredSourceBatchNo string) string {
|
||||
if stock == nil {
|
||||
return "ICCID不存在库存记录,不能出库"
|
||||
}
|
||||
if stock.CreatedDept != operator.DeptId {
|
||||
return "ICCID不属于当前机构库存,不能出库"
|
||||
}
|
||||
if requiredSourceBatchNo != "" && strings.TrimSpace(stock.BatchNo) != requiredSourceBatchNo {
|
||||
return "ICCID不属于指定库存批次,不能出库"
|
||||
}
|
||||
if formalExists {
|
||||
return "ICCID已存在正式卡记录,不能出库"
|
||||
}
|
||||
if strictStockIdentity {
|
||||
if strings.TrimSpace(row.Msisdn) != strings.TrimSpace(stock.Msisdn) {
|
||||
return "MSISDN与库存记录不一致,不能出库"
|
||||
}
|
||||
if strings.TrimSpace(row.Imsi) != strings.TrimSpace(stock.Imsi) {
|
||||
return "IMSI与库存记录不一致,不能出库"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func hasInvalidRows(rows []*Row) bool {
|
||||
for _, row := range rows {
|
||||
if row != nil && row.Reason != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func markRemainingRowsSkipped(rows []*Row) {
|
||||
for _, row := range rows {
|
||||
if row != nil && row.Reason == "" {
|
||||
row.AddReason("同批存在不满足出库条件的卡,本行未执行")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func outboundSubmitParams(params any) *model.TSimCardOutboundSubmitParam {
|
||||
switch in := params.(type) {
|
||||
case *model.TSimCardOutboundSubmitParam:
|
||||
return in
|
||||
case *model.TSimCardOutboundRangeSubmitParam:
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
return &in.TSimCardOutboundSubmitParam
|
||||
case *model.TSimCardOutboundInputSubmitParam:
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
return &in.TSimCardOutboundSubmitParam
|
||||
case *model.TSimCardOutboundBatchSubmitParam:
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
return &in.TSimCardOutboundSubmitParam
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func requiredSourceBatchNo(params any) string {
|
||||
if in, ok := params.(*model.TSimCardOutboundBatchSubmitParam); ok && in != nil {
|
||||
return strings.TrimSpace(in.BatchNo)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/cmd/inithttp"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func init() {
|
||||
inithttp.RegisterHttpInitFunc("outboundBatchQueue", func(ctx context.Context) error {
|
||||
if !rabbitmq.Enabled(ctx) {
|
||||
g.Log().Warning(ctx, "outbound batch queue is disabled because RabbitMQ is not configured")
|
||||
return nil
|
||||
}
|
||||
return ConsumeAsync(ctx)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/numrange"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common/cardsource"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
const maxRangeRows = 100000
|
||||
|
||||
func BuildRangeRows(ctx context.Context, rangeCodeType, startIccid, endIccid, startMsisdn, endMsisdn, startImsi, endImsi string) ([]batchimport.Row, error) {
|
||||
if isLegacyFullIdentityRange(rangeCodeType, startIccid, endIccid, startMsisdn, endMsisdn, startImsi, endImsi) {
|
||||
return buildFullIdentityRangeRows(ctx, startIccid, endIccid, startMsisdn, endMsisdn, startImsi, endImsi)
|
||||
}
|
||||
|
||||
codeType, start, end, err := selectedRange(ctx, rangeCodeType, startIccid, endIccid, startMsisdn, endMsisdn, startImsi, endImsi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := numrange.Build(ctx, start, end, strings.ToUpper(codeType), maxRangeRows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return nil, gerror.New("连号出库数据为空")
|
||||
}
|
||||
rows := make([]batchimport.Row, 0, len(values))
|
||||
for i, value := range values {
|
||||
row := &Row{RowNo: i + 1, RangeCodeType: codeType}
|
||||
switch codeType {
|
||||
case "msisdn":
|
||||
row.Msisdn = value
|
||||
case "imsi":
|
||||
row.Imsi = value
|
||||
default:
|
||||
row.Iccid = value
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func buildFullIdentityRangeRows(ctx context.Context, startIccid, endIccid, startMsisdn, endMsisdn, startImsi, endImsi string) ([]batchimport.Row, error) {
|
||||
return cardsource.BuildRangeRows(ctx, cardsource.RangeConfig{
|
||||
StartIccid: startIccid,
|
||||
EndIccid: endIccid,
|
||||
StartMsisdn: startMsisdn,
|
||||
EndMsisdn: endMsisdn,
|
||||
StartImsi: startImsi,
|
||||
EndImsi: endImsi,
|
||||
MaxRows: maxRangeRows,
|
||||
EmptyMessage: "连号出库数据为空",
|
||||
OverLimitMessage: "连号出库数量不能超过%d条",
|
||||
CountMismatchMessage: "ICCID号段数量(%d)、MSISDN号段数量(%d)、IMSI号段数量(%d)必须一致",
|
||||
}, func(rowNo int, iccid, msisdn, imsi string) *Row {
|
||||
return &Row{
|
||||
RowNo: rowNo,
|
||||
Iccid: iccid,
|
||||
Msisdn: msisdn,
|
||||
Imsi: imsi,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func isLegacyFullIdentityRange(rangeCodeType, startIccid, endIccid, startMsisdn, endMsisdn, startImsi, endImsi string) bool {
|
||||
return strings.TrimSpace(rangeCodeType) == "" &&
|
||||
strings.TrimSpace(startIccid) != "" && strings.TrimSpace(endIccid) != "" &&
|
||||
strings.TrimSpace(startMsisdn) != "" && strings.TrimSpace(endMsisdn) != "" &&
|
||||
strings.TrimSpace(startImsi) != "" && strings.TrimSpace(endImsi) != ""
|
||||
}
|
||||
|
||||
func selectedRange(ctx context.Context, rangeCodeType, startIccid, endIccid, startMsisdn, endMsisdn, startImsi, endImsi string) (codeType, start, end string, err error) {
|
||||
_ = ctx
|
||||
codeType = normalizeRangeCodeType(rangeCodeType)
|
||||
if codeType == "" {
|
||||
codeType = inferRangeCodeType(startIccid, endIccid, startMsisdn, endMsisdn, startImsi, endImsi)
|
||||
}
|
||||
switch codeType {
|
||||
case "iccid":
|
||||
start, end = strings.TrimSpace(startIccid), strings.TrimSpace(endIccid)
|
||||
case "msisdn":
|
||||
start, end = strings.TrimSpace(startMsisdn), strings.TrimSpace(endMsisdn)
|
||||
case "imsi":
|
||||
start, end = strings.TrimSpace(startImsi), strings.TrimSpace(endImsi)
|
||||
default:
|
||||
return "", "", "", gerror.New("请选择连号号码类型")
|
||||
}
|
||||
if start == "" || end == "" {
|
||||
return "", "", "", gerror.Newf("%s起止号不能为空", strings.ToUpper(codeType))
|
||||
}
|
||||
return codeType, start, end, nil
|
||||
}
|
||||
|
||||
func normalizeRangeCodeType(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "iccid", "iccids":
|
||||
return "iccid"
|
||||
case "msisdn", "msisdns":
|
||||
return "msisdn"
|
||||
case "imsi", "imsis":
|
||||
return "imsi"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func inferRangeCodeType(startIccid, endIccid, startMsisdn, endMsisdn, startImsi, endImsi string) string {
|
||||
complete := func(start, end string) bool {
|
||||
return strings.TrimSpace(start) != "" && strings.TrimSpace(end) != ""
|
||||
}
|
||||
switch {
|
||||
case complete(startIccid, endIccid):
|
||||
return "iccid"
|
||||
case complete(startMsisdn, endMsisdn):
|
||||
return "msisdn"
|
||||
case complete(startImsi, endImsi):
|
||||
return "imsi"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
outboundDatabaseChunkSize = 5000
|
||||
outboundDatabaseInsertChunkSize = 500
|
||||
)
|
||||
|
||||
type Repository struct{}
|
||||
|
||||
type StockCard struct {
|
||||
Iccid string `orm:"iccid"`
|
||||
Msisdn string `orm:"msisdn"`
|
||||
Imsi string `orm:"imsi"`
|
||||
Imei string `orm:"imei"`
|
||||
ChanKey string `orm:"chan_key"`
|
||||
CreatedDept int64 `orm:"created_dept"`
|
||||
BatchNo string `orm:"batch_no"`
|
||||
}
|
||||
|
||||
func NewRepository() *Repository {
|
||||
return &Repository{}
|
||||
}
|
||||
|
||||
func (r *Repository) GetStockCardsByIccids(ctx context.Context, iccids []string) (map[string]*StockCard, error) {
|
||||
columns := dao.TSimCardStock.Columns()
|
||||
return r.getStockCardsByColumn(ctx, columns.Iccid, iccids)
|
||||
|
||||
}
|
||||
|
||||
func (r *Repository) GetStockCardsByMsisdns(ctx context.Context, msisdns []string) (map[string]*StockCard, error) {
|
||||
columns := dao.TSimCardStock.Columns()
|
||||
return r.getStockCardsByColumn(ctx, columns.Msisdn, msisdns)
|
||||
}
|
||||
|
||||
func (r *Repository) GetStockCardsByImsis(ctx context.Context, imsis []string) (map[string]*StockCard, error) {
|
||||
columns := dao.TSimCardStock.Columns()
|
||||
return r.getStockCardsByColumn(ctx, columns.Imsi, imsis)
|
||||
}
|
||||
|
||||
func (r *Repository) getStockCardsByColumn(ctx context.Context, column string, values []string) (map[string]*StockCard, error) {
|
||||
result := make(map[string]*StockCard)
|
||||
uniqueValues := uniqueStrings(values)
|
||||
if len(uniqueValues) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
columns := dao.TSimCardStock.Columns()
|
||||
for start := 0; start < len(uniqueValues); start += outboundDatabaseChunkSize {
|
||||
end := start + outboundDatabaseChunkSize
|
||||
if end > len(uniqueValues) {
|
||||
end = len(uniqueValues)
|
||||
}
|
||||
|
||||
var list []*StockCard
|
||||
if err := dao.TSimCardStock.Ctx(ctx).
|
||||
Fields(columns.Iccid, columns.Msisdn, columns.Imsi, columns.Imei, columns.ChanKey, columns.CreatedDept, columns.BatchNo).
|
||||
WhereIn(column, uniqueValues[start:end]).
|
||||
Scan(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
key := ""
|
||||
switch column {
|
||||
case columns.Msisdn:
|
||||
key = item.Msisdn
|
||||
case columns.Imsi:
|
||||
key = item.Imsi
|
||||
default:
|
||||
key = item.Iccid
|
||||
}
|
||||
if key != "" {
|
||||
result[key] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) LockStockCardsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*entity.TSimCardStock, error) {
|
||||
result := make(map[string]*entity.TSimCardStock)
|
||||
uniqueIccids := uniqueStrings(iccids)
|
||||
if len(uniqueIccids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
for start := 0; start < len(uniqueIccids); start += outboundDatabaseChunkSize {
|
||||
end := start + outboundDatabaseChunkSize
|
||||
if end > len(uniqueIccids) {
|
||||
end = len(uniqueIccids)
|
||||
}
|
||||
|
||||
var list []*entity.TSimCardStock
|
||||
if err := tx.Ctx(ctx).Model(dao.TSimCardStock.Table()).
|
||||
WhereIn(dao.TSimCardStock.Columns().Iccid, uniqueIccids[start:end]).
|
||||
LockUpdate().
|
||||
Scan(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
if item != nil && item.Iccid != "" {
|
||||
result[item.Iccid] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetExistingFormalIccids(ctx context.Context, iccids []string) (map[string]bool, error) {
|
||||
result := make(map[string]bool)
|
||||
uniqueIccids := uniqueStrings(iccids)
|
||||
if len(uniqueIccids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
columns := dao.TSimCardInfo.Columns()
|
||||
for start := 0; start < len(uniqueIccids); start += outboundDatabaseChunkSize {
|
||||
end := start + outboundDatabaseChunkSize
|
||||
if end > len(uniqueIccids) {
|
||||
end = len(uniqueIccids)
|
||||
}
|
||||
|
||||
var list []struct {
|
||||
Iccid string `orm:"iccid"`
|
||||
}
|
||||
if err := dao.TSimCardInfo.Ctx(ctx).
|
||||
Fields(columns.Iccid).
|
||||
WhereIn(columns.Iccid, uniqueIccids[start:end]).
|
||||
Scan(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
if item.Iccid != "" {
|
||||
result[item.Iccid] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetInboundBatchInfos(ctx context.Context, tx gdb.TX, batchNos []string) (map[string]*gjson.Json, error) {
|
||||
result := make(map[string]*gjson.Json)
|
||||
uniqueBatchNos := uniqueStrings(batchNos)
|
||||
if len(uniqueBatchNos) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var list []struct {
|
||||
BatchNo string `orm:"batch_no"`
|
||||
BatchInfo *gjson.Json `orm:"batch_info"`
|
||||
}
|
||||
dbModel := dao.BatchMain.Ctx(ctx)
|
||||
if tx != nil {
|
||||
dbModel = tx.Ctx(ctx).Model(dao.BatchMain.Table()).Safe()
|
||||
}
|
||||
columns := dao.BatchMain.Columns()
|
||||
if err := dbModel.
|
||||
Fields(columns.BatchNo, columns.BatchInfo).
|
||||
WhereIn(columns.BatchNo, uniqueBatchNos).
|
||||
Scan(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
if item.BatchNo != "" {
|
||||
result[item.BatchNo] = item.BatchInfo
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) LoadPackage(ctx context.Context, tx gdb.TX, tenantId, packageCode string) (*entity.TSimFlowPackageInfo, error) {
|
||||
packageCode = strings.TrimSpace(packageCode)
|
||||
if packageCode == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
mod := dao.TSimFlowPackageInfo.Ctx(ctx)
|
||||
if tx != nil {
|
||||
mod = tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()).Safe()
|
||||
}
|
||||
mod = mod.Where(dao.TSimFlowPackageInfo.Columns().PackageCode, packageCode)
|
||||
if strings.TrimSpace(tenantId) != "" {
|
||||
mod = mod.Where(dao.TSimFlowPackageInfo.Columns().TenantId, strings.TrimSpace(tenantId))
|
||||
}
|
||||
|
||||
var pkg *entity.TSimFlowPackageInfo
|
||||
if err := mod.Scan(&pkg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type Row struct {
|
||||
RowNo int
|
||||
Iccid string
|
||||
Msisdn string
|
||||
Imsi string
|
||||
RangeCodeType string
|
||||
CustomerAgentId int64
|
||||
PackageCode string
|
||||
PackageBeginTime *gtime.Time
|
||||
DurationYears int
|
||||
DurationMonthPart int
|
||||
DurationMonths int
|
||||
GiftDurationYears int
|
||||
GiftDurationMonthPart int
|
||||
GiftDurationMonths int
|
||||
HasTestPeriod int
|
||||
TestPeriodDays int
|
||||
TestPeriodLatestExpiryDate *gtime.Time
|
||||
HasSilentPeriod int
|
||||
SilentPeriodDays int
|
||||
SilentPeriodLatestExpiryDate *gtime.Time
|
||||
Amount string
|
||||
OfflineContractCompleted bool
|
||||
Remark string
|
||||
SourceBatchNo string
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (r *Row) Valid() bool {
|
||||
return r != nil && r.Reason == ""
|
||||
}
|
||||
|
||||
func (r *Row) AddReason(reason string) {
|
||||
if r == nil || r.Reason != "" || reason == "" {
|
||||
return
|
||||
}
|
||||
r.Reason = reason
|
||||
}
|
||||
|
||||
func (r *Row) GetRowNo() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
return r.RowNo
|
||||
}
|
||||
|
||||
func (r *Row) GetItemID() string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return r.Iccid
|
||||
}
|
||||
|
||||
func (r *Row) GetErrorReasons() []string {
|
||||
if r == nil || r.Reason == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{r.Reason}
|
||||
}
|
||||
|
||||
func (r *Row) AddErrorReason(reason string) {
|
||||
r.AddReason(reason)
|
||||
}
|
||||
|
||||
func (r *Row) Snapshot() *gjson.Json {
|
||||
if r == nil {
|
||||
return gjson.New(g.Map{})
|
||||
}
|
||||
return gjson.New(g.Map{
|
||||
"ICCID": r.Iccid,
|
||||
"MSISDN": r.Msisdn,
|
||||
"IMSI": r.Imsi,
|
||||
"rangeCodeType": r.RangeCodeType,
|
||||
"customerAgentId": r.CustomerAgentId,
|
||||
"packageCode": r.PackageCode,
|
||||
"packageBeginTime": formatTime(r.PackageBeginTime),
|
||||
"durationYears": r.DurationYears,
|
||||
"durationMonthPart": r.DurationMonthPart,
|
||||
"durationMonths": r.DurationMonths,
|
||||
"giftDurationYears": r.GiftDurationYears,
|
||||
"giftDurationMonthPart": r.GiftDurationMonthPart,
|
||||
"giftDurationMonths": r.GiftDurationMonths,
|
||||
"amount": r.Amount,
|
||||
"offlineContractCompleted": r.OfflineContractCompleted,
|
||||
"remark": r.Remark,
|
||||
"batchNo": r.SourceBatchNo,
|
||||
})
|
||||
}
|
||||
|
||||
type PreviewOptions struct {
|
||||
DeptId int64
|
||||
StrictStockIdentity bool
|
||||
}
|
||||
|
||||
type CompleteInput struct {
|
||||
SubmitParam *model.TSimCardOutboundSubmitParam
|
||||
BatchNo string
|
||||
Rows []*Row
|
||||
Operator batchimport.OperatorContext
|
||||
StrictStockIdentity bool
|
||||
RequiredSourceBatchNo string
|
||||
}
|
||||
|
||||
type CompleteOutput struct {
|
||||
OrderId int64
|
||||
OrderSn string
|
||||
BatchNo string
|
||||
BatchType int
|
||||
Quantity int
|
||||
SourceBatchNos []string
|
||||
Customer *entity.SysDept
|
||||
Package *entity.TSimFlowPackageInfo
|
||||
PackageBeginAt *gtime.Time
|
||||
PackageEndAt *gtime.Time
|
||||
PackageStatus int
|
||||
EffectiveStatus int
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func uniqueStrings(list []string) []string {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(list))
|
||||
result := make([]string, 0, len(list))
|
||||
for _, item := range list {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user