chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
package renew
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/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"
|
||||
)
|
||||
|
||||
type AsyncConfirmInput struct {
|
||||
BatchNo string
|
||||
TargetBatchType int
|
||||
DurationYears int
|
||||
DurationMonths int
|
||||
GiftDurationMonths int
|
||||
RenewDate string
|
||||
PackageBeginTime string
|
||||
PackageEndTime string
|
||||
OfflineContract bool
|
||||
BuyerNote string
|
||||
AdminNote string
|
||||
Remark string
|
||||
PriceSnapshot *model.TSimCardRenewPriceSnapshot
|
||||
PaymentSnapshot *model.TSimCardRenewPaymentSnapshot
|
||||
SourceBatchNo string
|
||||
Operator batchimport.OperatorContext
|
||||
}
|
||||
|
||||
type AsyncConfirmResult struct {
|
||||
BatchNo string
|
||||
BatchType int
|
||||
BatchAsyncStatus int
|
||||
}
|
||||
|
||||
// ConvertPrepareToAsync 以 CAS 方式把 batch_type=4 + batch_async_status=Success + run_result=Success 的预处理批次
|
||||
// 转换为 batch_type=101 的待处理续费批次:重置主表正式执行相关字段并把明细的 BatchType 切换到 101、DetailStatus 重置为 Pending。
|
||||
func ConvertPrepareToAsync(ctx context.Context, tx gdb.TX, in *AsyncConfirmInput) (*AsyncConfirmResult, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("续费确认参数不能为空")
|
||||
}
|
||||
batchNo := strings.TrimSpace(in.BatchNo)
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("续费批次号不能为空")
|
||||
}
|
||||
targetBatchType := in.TargetBatchType
|
||||
if targetBatchType == 0 {
|
||||
targetBatchType = consts.BatchTypeRenew
|
||||
}
|
||||
columns := dao.BatchMain.Columns()
|
||||
main := &entity.BatchMain{}
|
||||
err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(columns.BatchNo, batchNo).
|
||||
Where(columns.TenantId, contexts.GetTenantId(ctx)).
|
||||
Where(columns.CreatedDept, contexts.GetDeptId(ctx)).
|
||||
LockUpdate().
|
||||
Scan(main)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询续费预处理批次失败")
|
||||
}
|
||||
if main.Id == 0 {
|
||||
return nil, gerror.New("续费预处理批次不存在")
|
||||
}
|
||||
if main.BatchType != consts.BatchTypePrepare {
|
||||
return nil, gerror.New("续费批次不是预处理类型")
|
||||
}
|
||||
if main.BatchAsyncStatus != consts.BatchAsyncStatusSuccess {
|
||||
return nil, gerror.New("续费预处理批次尚未完成")
|
||||
}
|
||||
if main.RunResult != consts.BatchRunResultSuccess {
|
||||
return nil, gerror.New("续费预处理批次未成功")
|
||||
}
|
||||
if main.BatchInfo == nil {
|
||||
return nil, gerror.New("续费预处理批次缺少快照")
|
||||
}
|
||||
if main.BatchInfo.Get("targetBatchType").Int() != targetBatchType {
|
||||
return nil, gerror.New("续费预处理批次类型不匹配")
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
info := mergeConfirmBatchInfo(main.BatchInfo, in)
|
||||
data := do.BatchMain{
|
||||
BatchType: targetBatchType,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
RunResult: consts.BatchRunResultPending,
|
||||
StartTime: nil,
|
||||
EndTime: nil,
|
||||
FailReason: "",
|
||||
SuccessCount: 0,
|
||||
FailCount: 0,
|
||||
BatchInfo: gjson.New(info),
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
res, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(columns.Id, main.Id).
|
||||
Where(columns.BatchType, consts.BatchTypePrepare).
|
||||
Where(columns.BatchAsyncStatus, consts.BatchAsyncStatusSuccess).
|
||||
Where(columns.RunResult, consts.BatchRunResultSuccess).
|
||||
Data(data).
|
||||
Update()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "更新续费批次状态失败")
|
||||
}
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取续费批次更新结果失败")
|
||||
}
|
||||
if affected != 1 {
|
||||
return nil, gerror.New("续费批次状态已变化,请重试")
|
||||
}
|
||||
|
||||
detailColumns := dao.BatchDetail.Columns()
|
||||
if _, err = tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Where(detailColumns.BatchNo, batchNo).
|
||||
Where(detailColumns.BatchType, consts.BatchTypePrepare).
|
||||
Data(do.BatchDetail{
|
||||
BatchType: targetBatchType,
|
||||
DetailStatus: consts.BatchDetailStatusPending,
|
||||
ErrorCode: "",
|
||||
ErrorReason: "",
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return nil, gerror.Wrap(err, "更新续费批次明细类型失败")
|
||||
}
|
||||
|
||||
return &AsyncConfirmResult{
|
||||
BatchNo: batchNo,
|
||||
BatchType: targetBatchType,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mergeConfirmBatchInfo(base *gjson.Json, in *AsyncConfirmInput) g.Map {
|
||||
data := g.Map{}
|
||||
if base != nil {
|
||||
for key, value := range base.Map() {
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
if in == nil {
|
||||
return data
|
||||
}
|
||||
durationValue := in.DurationYears
|
||||
if durationValue <= 0 {
|
||||
durationValue = renewDurationYearsFromLegacy(in.DurationMonths, 0, in.PriceSnapshot)
|
||||
}
|
||||
if durationValue > 0 {
|
||||
data["durationValue"] = durationValue
|
||||
data["durationYears"] = durationValue
|
||||
actualDurationMonths := renewActualDurationMonths(in.PriceSnapshot, durationValue)
|
||||
data["durationMonths"] = actualDurationMonths
|
||||
data["actualDurationMonths"] = actualDurationMonths
|
||||
}
|
||||
if in.GiftDurationMonths >= 0 {
|
||||
data["giftDurationMonths"] = in.GiftDurationMonths
|
||||
}
|
||||
if strings.TrimSpace(in.RenewDate) != "" {
|
||||
data["renewDate"] = strings.TrimSpace(in.RenewDate)
|
||||
}
|
||||
if strings.TrimSpace(in.PackageBeginTime) != "" {
|
||||
data["packageBeginTime"] = strings.TrimSpace(in.PackageBeginTime)
|
||||
}
|
||||
if strings.TrimSpace(in.PackageEndTime) != "" {
|
||||
data["packageEndTime"] = strings.TrimSpace(in.PackageEndTime)
|
||||
}
|
||||
data["offlineContractCompleted"] = in.OfflineContract
|
||||
if note := strings.TrimSpace(in.BuyerNote); note != "" {
|
||||
data["buyerNote"] = note
|
||||
}
|
||||
if note := strings.TrimSpace(in.AdminNote); note != "" {
|
||||
data["adminNote"] = note
|
||||
}
|
||||
if note := strings.TrimSpace(in.Remark); note != "" {
|
||||
data["remark"] = note
|
||||
}
|
||||
if in.PriceSnapshot != nil {
|
||||
data["priceSnapshot"] = in.PriceSnapshot
|
||||
data["packageCode"] = in.PriceSnapshot.PackageCode
|
||||
data["packageName"] = in.PriceSnapshot.PackageName
|
||||
data["renewUnitPrice"] = in.PriceSnapshot.RenewUnitPrice
|
||||
data["billingMethod"] = in.PriceSnapshot.BillingMethod
|
||||
data["renewDurationUnit"] = in.PriceSnapshot.RenewDurationUnit
|
||||
data["renewDurationUnitText"] = in.PriceSnapshot.RenewDurationUnitText
|
||||
data["priceSource"] = in.PriceSnapshot.PriceSource
|
||||
data["priceOwnerDeptId"] = in.PriceSnapshot.PriceOwnerDeptId
|
||||
data["agentDefaultPriceId"] = in.PriceSnapshot.AgentDefaultPriceId
|
||||
data["targetPriceId"] = in.PriceSnapshot.TargetPriceId
|
||||
data["renewableDurationOptions"] = in.PriceSnapshot.RenewableDurationOptions
|
||||
data["renewablePackageCodes"] = in.PriceSnapshot.RenewablePackageCodes
|
||||
data["renewablePackages"] = in.PriceSnapshot.RenewablePackages
|
||||
if durationValue > 0 {
|
||||
data["actualDurationMonths"] = renewActualDurationMonths(in.PriceSnapshot, durationValue)
|
||||
}
|
||||
}
|
||||
if in.PaymentSnapshot != nil {
|
||||
data["paymentMode"] = strings.TrimSpace(in.PaymentSnapshot.PaymentMode)
|
||||
data["payType"] = in.PaymentSnapshot.PayType
|
||||
data["payStatus"] = in.PaymentSnapshot.PayStatus
|
||||
data["payAmount"] = in.PaymentSnapshot.PayAmount
|
||||
data["balanceAmount"] = in.PaymentSnapshot.BalanceAmount
|
||||
data["offlinePaidAmount"] = in.PaymentSnapshot.OfflinePaidAmount
|
||||
data["balanceBefore"] = in.PaymentSnapshot.BalanceBefore
|
||||
data["balanceAfter"] = in.PaymentSnapshot.BalanceAfter
|
||||
data["balanceDetailSerialNo"] = strings.TrimSpace(in.PaymentSnapshot.BalanceDetailSerialNo)
|
||||
data["payerDeptId"] = in.PaymentSnapshot.PayerDeptId
|
||||
data["payerUserId"] = in.PaymentSnapshot.PayerUserId
|
||||
data["isTopDept"] = in.PaymentSnapshot.IsTopDept
|
||||
if in.PaymentSnapshot.PaymentTime != nil {
|
||||
data["paymentTime"] = in.PaymentSnapshot.PaymentTime.Format("Y-m-d H:i:s")
|
||||
}
|
||||
data["paymentSnapshot"] = in.PaymentSnapshot
|
||||
}
|
||||
if strings.TrimSpace(in.SourceBatchNo) != "" {
|
||||
data["sourceBatchNo"] = strings.TrimSpace(in.SourceBatchNo)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package renew
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/logic/iot/cardpackage"
|
||||
"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"
|
||||
)
|
||||
|
||||
func (p *Processor) Complete(ctx context.Context, tx gdb.TX, in *CompleteInput) (*CompleteOutput, error) {
|
||||
if in == nil || in.SubmitParam == nil {
|
||||
return nil, gerror.New("续费参数不能为空")
|
||||
}
|
||||
sourceBatchNo := strings.TrimSpace(in.SubmitParam.SourceBatchNo)
|
||||
if in.BatchNo == "" {
|
||||
return nil, gerror.New("续费批次号不能为空")
|
||||
}
|
||||
if len(in.Rows) == 0 {
|
||||
return nil, gerror.New("续费卡不能为空")
|
||||
}
|
||||
|
||||
iccids := rowsToIccids(in.Rows)
|
||||
paymentSnapshot, err := executeRenewPayment(ctx, tx, paymentExecutionInput{
|
||||
BatchNo: in.BatchNo,
|
||||
PriceSnapshot: in.PriceSnapshot,
|
||||
Quantity: len(iccids),
|
||||
SubmitParam: in.SubmitParam,
|
||||
Operator: in.Operator,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in.SubmitParam.PaymentSnapshot = paymentSnapshot
|
||||
in.SubmitParam.PaymentMode = paymentSnapshot.PaymentMode
|
||||
in.SubmitParam.Amount = formatRenewAmount(paymentSnapshot.PayAmount)
|
||||
|
||||
packageSvc := cardpackage.NewService()
|
||||
durationValue := renewDisplayDurationValue(in.SubmitParam)
|
||||
durationUnit := renewDurationUnitFromSnapshot(in.PriceSnapshot)
|
||||
actualDurationMonths := renewActualDurationMonths(in.PriceSnapshot, durationValue)
|
||||
if actualDurationMonths <= 0 {
|
||||
return nil, gerror.New("续费时长必须大于0")
|
||||
}
|
||||
packageBeginTime := strings.TrimSpace(in.SubmitParam.PackageBeginTime)
|
||||
var packageBeginAt *gtime.Time
|
||||
if strings.TrimSpace(packageBeginTime) != "" {
|
||||
packageBeginAt, err = gtime.StrToTime(packageBeginTime)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "续费开始日期格式不正确")
|
||||
}
|
||||
packageBeginAt = normalizeRenewDayEndTime(packageBeginAt)
|
||||
}
|
||||
packageEndTime := strings.TrimSpace(in.SubmitParam.PackageEndTime)
|
||||
var packageEndAt *gtime.Time
|
||||
if strings.TrimSpace(packageEndTime) != "" {
|
||||
packageEndAt, err = gtime.StrToTime(packageEndTime)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "续费结束日期格式不正确")
|
||||
}
|
||||
packageEndAt = normalizeRenewDayEndTime(packageEndAt)
|
||||
}
|
||||
renew, err := packageSvc.RenewCurrentPackage(ctx, tx, &cardpackage.RenewCurrentPackageInput{
|
||||
BatchNo: in.BatchNo,
|
||||
SourceBatchNo: sourceBatchNo,
|
||||
PackageCode: strings.TrimSpace(in.SubmitParam.PackageCode),
|
||||
Iccids: iccids,
|
||||
DurationMonths: actualDurationMonths,
|
||||
GiftDurationMonths: in.SubmitParam.GiftDurationMonths,
|
||||
DurationDisplayValue: durationValue,
|
||||
DurationUnit: durationUnit,
|
||||
PackageBeginTime: packageBeginAt,
|
||||
PackageEndTime: packageEndAt,
|
||||
Amount: in.SubmitParam.Amount,
|
||||
PayType: paymentSnapshot.PayType,
|
||||
PayStatus: paymentSnapshot.PayStatus,
|
||||
BalanceAmount: paymentSnapshot.BalanceAmount,
|
||||
OfflinePaidAmount: paymentSnapshot.OfflinePaidAmount,
|
||||
PayTime: paymentSnapshot.PaymentTime,
|
||||
OfflineContractCompleted: in.SubmitParam.OfflineContractCompleted,
|
||||
BuyerNote: in.SubmitParam.BuyerNote,
|
||||
AdminNote: in.SubmitParam.AdminNote,
|
||||
OrderRemark: "卡续费业务订单载体",
|
||||
BindRemark: in.BatchNo,
|
||||
Operator: in.Operator,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
detailUpdates := make(map[string]*gjsonJsonHolder, len(iccids))
|
||||
for _, row := range in.Rows {
|
||||
card := renew.CardsByIccid[row.Iccid]
|
||||
latestBind := renew.LatestBindsByIccid[row.Iccid]
|
||||
newBind := renew.NewBindsByIccid[row.Iccid]
|
||||
detailUpdates[row.Iccid] = &gjsonJsonHolder{Data: buildDetailSnapshot(row, card, latestBind, newBind, in, renew)}
|
||||
}
|
||||
if err = updateBatchDetailSnapshots(ctx, tx, in.BatchNo, detailUpdates, in.Operator.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = convertBatchDetailsAfterComplete(ctx, tx, in.BatchNo, in.Operator.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = updateBatchMainAfterComplete(ctx, tx, in, renew); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
renewUnitPrice := renew.UnitPrice
|
||||
if in.PriceSnapshot != nil {
|
||||
renewUnitPrice = in.PriceSnapshot.RenewUnitPrice
|
||||
}
|
||||
return &CompleteOutput{
|
||||
BatchNo: in.BatchNo,
|
||||
BatchType: consts.BatchTypeRenew,
|
||||
SourceBatchNo: sourceBatchNo,
|
||||
Quantity: len(iccids),
|
||||
PackageCode: renew.PackageCode,
|
||||
PackageName: renew.Package.PackageName,
|
||||
OrderSn: renew.OrderSn,
|
||||
Amount: renew.Amount,
|
||||
RenewUnitPrice: renewUnitPrice,
|
||||
RenewDate: in.RenewDate,
|
||||
RenewTime: renew.RenewTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeRenewDayEndTime(t *gtime.Time) *gtime.Time {
|
||||
if t == nil || t.IsZero() {
|
||||
return t
|
||||
}
|
||||
tt := t.Time
|
||||
return gtime.NewFromTime(time.Date(tt.Year(), tt.Month(), tt.Day(), 23, 59, 59, 0, tt.Location()))
|
||||
}
|
||||
|
||||
type gjsonJsonHolder struct {
|
||||
Data any
|
||||
}
|
||||
|
||||
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 updateBatchDetailSnapshots(ctx context.Context, tx gdb.TX, batchNo string, snapshots map[string]*gjsonJsonHolder, userId int64) error {
|
||||
for iccid, snapshot := range snapshots {
|
||||
_, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
|
||||
Where(dao.BatchDetail.Columns().ItemId, iccid).
|
||||
Data(do.BatchDetail{
|
||||
Snapshot: gjson.New(snapshot.Data),
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新续费批次明细快照失败")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateBatchMainAfterComplete(ctx context.Context, tx gdb.TX, in *CompleteInput, renew *cardpackage.RenewCurrentPackageOutput) error {
|
||||
durationValue := renewDisplayDurationValue(in.SubmitParam)
|
||||
actualDurationMonths := renewActualDurationMonths(in.PriceSnapshot, durationValue)
|
||||
info := g.Map{
|
||||
"orderSn": renew.OrderSn,
|
||||
"packageCode": renew.PackageCode,
|
||||
"packageName": renew.Package.PackageName,
|
||||
"packageType": renew.Package.PackageType,
|
||||
"durationYears": in.SubmitParam.DurationYears,
|
||||
"durationMonths": actualDurationMonths,
|
||||
"durationValue": durationValue,
|
||||
"actualDurationMonths": actualDurationMonths,
|
||||
"giftDurationMonths": in.SubmitParam.GiftDurationMonths,
|
||||
"amount": renew.Amount,
|
||||
"quantity": renew.Quantity,
|
||||
"renewDate": in.RenewDate,
|
||||
"packageBeginTime": in.SubmitParam.PackageBeginTime,
|
||||
"packageEndTime": in.SubmitParam.PackageEndTime,
|
||||
"offlineContractCompleted": in.SubmitParam.OfflineContractCompleted,
|
||||
"renewTime": renew.RenewTime,
|
||||
}
|
||||
if sourceBatchNo := strings.TrimSpace(in.SubmitParam.SourceBatchNo); sourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = sourceBatchNo
|
||||
}
|
||||
if buyerNote := strings.TrimSpace(in.SubmitParam.BuyerNote); buyerNote != "" {
|
||||
info["buyerNote"] = buyerNote
|
||||
}
|
||||
if adminNote := strings.TrimSpace(in.SubmitParam.AdminNote); adminNote != "" {
|
||||
info["adminNote"] = adminNote
|
||||
}
|
||||
if remark := strings.TrimSpace(in.SubmitParam.Remark); remark != "" {
|
||||
info["remark"] = remark
|
||||
}
|
||||
if in.PriceSnapshot != nil {
|
||||
info["customerAgentId"] = in.PriceSnapshot.CustomerAgentId
|
||||
info["billingMethod"] = in.PriceSnapshot.BillingMethod
|
||||
info["renewDurationUnit"] = in.PriceSnapshot.RenewDurationUnit
|
||||
info["renewDurationUnitText"] = in.PriceSnapshot.RenewDurationUnitText
|
||||
info["priceOwnerDeptId"] = in.PriceSnapshot.PriceOwnerDeptId
|
||||
info["priceSource"] = in.PriceSnapshot.PriceSource
|
||||
info["agentDefaultPriceId"] = in.PriceSnapshot.AgentDefaultPriceId
|
||||
info["targetPriceId"] = in.PriceSnapshot.TargetPriceId
|
||||
info["renewUnitPrice"] = in.PriceSnapshot.RenewUnitPrice
|
||||
info["renewableDurationOptions"] = in.PriceSnapshot.RenewableDurationOptions
|
||||
info["renewablePackageCodes"] = in.PriceSnapshot.RenewablePackageCodes
|
||||
info["renewablePackages"] = in.PriceSnapshot.RenewablePackages
|
||||
}
|
||||
if in.SubmitParam.PaymentSnapshot != nil {
|
||||
appendRenewPaymentSnapshot(info, in.SubmitParam.PaymentSnapshot)
|
||||
}
|
||||
_, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(dao.BatchMain.Columns().BatchNo, in.BatchNo).
|
||||
Data(do.BatchMain{
|
||||
BatchType: consts.BatchTypeRenew,
|
||||
OrderSn: renew.OrderSn,
|
||||
TargetDeptId: renew.Customer.DeptId,
|
||||
BatchInfo: gjson.New(info),
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: renew.RenewTime,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新续费批次失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertBatchDetailsAfterComplete(ctx context.Context, tx gdb.TX, batchNo string, userId int64) error {
|
||||
_, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
|
||||
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare).
|
||||
Data(do.BatchDetail{
|
||||
BatchType: consts.BatchTypeRenew,
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新续费批次明细类型失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildDetailSnapshot(row *Row, card *entity.TSimCardInfo, latestBind, newBind *entity.TSimCardBindPkg, in *CompleteInput, renew *cardpackage.RenewCurrentPackageOutput) g.Map {
|
||||
durationValue := renewDisplayDurationValue(in.SubmitParam)
|
||||
actualDurationMonths := renewActualDurationMonths(in.PriceSnapshot, durationValue)
|
||||
data := g.Map{
|
||||
"ICCID": card.Iccid,
|
||||
"MSISDN": card.Msisdn,
|
||||
"IMSI": card.Imsi,
|
||||
"renewBatchNo": in.BatchNo,
|
||||
"packageCode": renew.PackageCode,
|
||||
"packageName": renew.Package.PackageName,
|
||||
"oldSubscriptionNo": latestBind.SubscriptionNo,
|
||||
"newSubscriptionNo": newBind.SubscriptionNo,
|
||||
"orderSn": renew.OrderSn,
|
||||
"oldExpectedExpireTime": formatTime(latestBind.ExpectedExpireTime),
|
||||
"renewEffectiveTime": formatTime(newBind.EffectiveTime),
|
||||
"renewExpectedExpireTime": formatTime(newBind.ExpectedExpireTime),
|
||||
"cardFlowPkgEndAt": formatTime(newBind.ExpectedExpireTime),
|
||||
"durationYears": in.SubmitParam.DurationYears,
|
||||
"durationMonths": actualDurationMonths,
|
||||
"durationValue": durationValue,
|
||||
"actualDurationMonths": actualDurationMonths,
|
||||
"giftDurationMonths": in.SubmitParam.GiftDurationMonths,
|
||||
"renewDate": in.RenewDate,
|
||||
"packageBeginTime": in.SubmitParam.PackageBeginTime,
|
||||
"packageEndTime": in.SubmitParam.PackageEndTime,
|
||||
"amount": renew.Amount,
|
||||
"rowNo": row.RowNo,
|
||||
}
|
||||
if sourceBatchNo := strings.TrimSpace(in.SubmitParam.SourceBatchNo); sourceBatchNo != "" {
|
||||
data["sourceBatchNo"] = sourceBatchNo
|
||||
}
|
||||
if in.PriceSnapshot != nil {
|
||||
data["renewUnitPrice"] = in.PriceSnapshot.RenewUnitPrice
|
||||
data["billingMethod"] = in.PriceSnapshot.BillingMethod
|
||||
data["renewDurationUnit"] = in.PriceSnapshot.RenewDurationUnit
|
||||
data["renewDurationUnitText"] = in.PriceSnapshot.RenewDurationUnitText
|
||||
data["priceOwnerDeptId"] = in.PriceSnapshot.PriceOwnerDeptId
|
||||
data["priceSource"] = in.PriceSnapshot.PriceSource
|
||||
}
|
||||
if snapshot := in.SubmitParam.PaymentSnapshot; snapshot != nil {
|
||||
data["paymentMode"] = snapshot.PaymentMode
|
||||
data["payType"] = snapshot.PayType
|
||||
data["payAmount"] = snapshot.PayAmount
|
||||
data["balanceAmount"] = snapshot.BalanceAmount
|
||||
data["balanceBefore"] = snapshot.BalanceBefore
|
||||
data["balanceAfter"] = snapshot.BalanceAfter
|
||||
data["balanceDetailSerialNo"] = snapshot.BalanceDetailSerialNo
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func formatTime(t *gtime.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Format("Y-m-d H:i:s")
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package renew
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
)
|
||||
|
||||
type ExcelProcessor struct {
|
||||
*Processor
|
||||
options *ExcelSubmitOptions
|
||||
}
|
||||
|
||||
func NewExcelProcessor() *ExcelProcessor {
|
||||
return &ExcelProcessor{
|
||||
Processor: NewProcessor(),
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareExcelProcessor parses the customer-facing Excel format but persists
|
||||
// an internal prepare batch. The worker performs the database validation before
|
||||
// the user can confirm the formal renew batch.
|
||||
type PrepareExcelProcessor struct {
|
||||
parser *ExcelProcessor
|
||||
prepare *PrepareProcessor
|
||||
}
|
||||
|
||||
func NewPrepareExcelProcessor() *PrepareExcelProcessor {
|
||||
return &PrepareExcelProcessor{
|
||||
parser: NewExcelProcessor(),
|
||||
prepare: NewPrepareProcessor(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ExcelProcessor) Config() batchimport.ProcessorConfig {
|
||||
cfg := p.Processor.Config()
|
||||
cfg.TemplateHeaders = ExcelTemplateHeaders()
|
||||
cfg.RequiredHeaders = []string{"ICCID"}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (p *ExcelProcessor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
rows, options, err := ParseExcelFileRows(ctx, filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.options = options
|
||||
|
||||
out := make([]batchimport.Row, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, row)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p *ExcelProcessor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
|
||||
return p.Processor.Execute(ctx, tx, req, rows)
|
||||
}
|
||||
|
||||
func (p *ExcelProcessor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
|
||||
return p.Processor.BuildBatchInfo(ctx, req, result, importOss, resultOss, downloadId)
|
||||
}
|
||||
|
||||
func (p *ExcelProcessor) ExcelSubmitOptions() *ExcelSubmitOptions {
|
||||
return p.options
|
||||
}
|
||||
|
||||
func (p *PrepareExcelProcessor) Config() batchimport.ProcessorConfig {
|
||||
cfg := p.prepare.Config()
|
||||
cfg.TemplateHeaders = ExcelTemplateHeaders()
|
||||
cfg.RequiredHeaders = []string{"ICCID"}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (p *PrepareExcelProcessor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
return p.parser.ParseRows(ctx, filePath)
|
||||
}
|
||||
|
||||
func (p *PrepareExcelProcessor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
// Keep submit non-blocking; the worker owns prepare validation.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PrepareExcelProcessor) ExcelSubmitOptions() *ExcelSubmitOptions {
|
||||
return p.parser.ExcelSubmitOptions()
|
||||
}
|
||||
|
||||
func (p *PrepareExcelProcessor) BuildBatchInfo(
|
||||
ctx context.Context,
|
||||
req batchimport.SubmitRequest,
|
||||
result *batchimport.Result,
|
||||
importOss *model.SysOssSaveContentModel,
|
||||
resultOss *model.SysOssSaveContentModel,
|
||||
downloadId int64,
|
||||
) *gjson.Json {
|
||||
return p.prepare.BuildBatchInfo(ctx, req, result, importOss, resultOss, downloadId)
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package renew
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type renewIdentityResolver struct {
|
||||
byValue map[string]map[int64]*entity.TSimCardInfo
|
||||
}
|
||||
|
||||
var renewIdentityResolverLoader = loadRenewIdentityResolver
|
||||
|
||||
func newRenewIdentityResolverFromCards(cards []*entity.TSimCardInfo) *renewIdentityResolver {
|
||||
resolver := &renewIdentityResolver{
|
||||
byValue: make(map[string]map[int64]*entity.TSimCardInfo),
|
||||
}
|
||||
for _, card := range cards {
|
||||
resolver.addCard(card)
|
||||
}
|
||||
return resolver
|
||||
}
|
||||
|
||||
func loadRenewIdentityResolver(ctx context.Context, rows []*Row) (*renewIdentityResolver, error) {
|
||||
keys := collectRenewIdentityKeys(rows)
|
||||
resolver := newRenewIdentityResolverFromCards(nil)
|
||||
if len(keys) == 0 {
|
||||
return resolver, nil
|
||||
}
|
||||
|
||||
for _, column := range []string{
|
||||
dao.TSimCardInfo.Columns().Iccid,
|
||||
dao.TSimCardInfo.Columns().Msisdn,
|
||||
dao.TSimCardInfo.Columns().Imsi,
|
||||
} {
|
||||
cards, err := queryRenewIdentityCardsByColumn(ctx, column, keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, card := range cards {
|
||||
resolver.addCard(card)
|
||||
}
|
||||
}
|
||||
return resolver, nil
|
||||
}
|
||||
|
||||
func queryRenewIdentityCardsByColumn(ctx context.Context, column string, values []string) ([]*entity.TSimCardInfo, error) {
|
||||
if len(values) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var cards []*entity.TSimCardInfo
|
||||
if err := service.IotTSimCardInfo().Model(ctx).
|
||||
Fields(
|
||||
dao.TSimCardInfo.Columns().Id,
|
||||
dao.TSimCardInfo.Columns().Iccid,
|
||||
dao.TSimCardInfo.Columns().Msisdn,
|
||||
dao.TSimCardInfo.Columns().Imsi,
|
||||
).
|
||||
WhereIn(column, values).
|
||||
Scan(&cards); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询续费卡信息失败")
|
||||
}
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
func collectRenewIdentityKeys(rows []*Row) []string {
|
||||
keys := make([]string, 0, len(rows)*3)
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
keys = append(keys,
|
||||
strings.TrimSpace(row.Iccid),
|
||||
strings.TrimSpace(row.Msisdn),
|
||||
strings.TrimSpace(row.Imsi),
|
||||
)
|
||||
}
|
||||
return uniqueStrings(keys)
|
||||
}
|
||||
|
||||
func (r *renewIdentityResolver) addCard(card *entity.TSimCardInfo) {
|
||||
if r == nil || card == nil || card.Id <= 0 {
|
||||
return
|
||||
}
|
||||
r.addValue(strings.TrimSpace(card.Iccid), card)
|
||||
r.addValue(strings.TrimSpace(card.Msisdn), card)
|
||||
r.addValue(strings.TrimSpace(card.Imsi), card)
|
||||
}
|
||||
|
||||
func (r *renewIdentityResolver) addValue(value string, card *entity.TSimCardInfo) {
|
||||
if r == nil || card == nil || card.Id <= 0 || value == "" {
|
||||
return
|
||||
}
|
||||
if r.byValue == nil {
|
||||
r.byValue = make(map[string]map[int64]*entity.TSimCardInfo)
|
||||
}
|
||||
cards := r.byValue[value]
|
||||
if cards == nil {
|
||||
cards = make(map[int64]*entity.TSimCardInfo)
|
||||
r.byValue[value] = cards
|
||||
}
|
||||
cards[card.Id] = card
|
||||
}
|
||||
|
||||
func (r *renewIdentityResolver) resolveRow(row *Row) {
|
||||
if r == nil || row == nil {
|
||||
return
|
||||
}
|
||||
identities := uniqueStrings([]string{
|
||||
strings.TrimSpace(row.Iccid),
|
||||
strings.TrimSpace(row.Msisdn),
|
||||
strings.TrimSpace(row.Imsi),
|
||||
})
|
||||
if len(identities) == 0 {
|
||||
row.AddReason("ICCID/MSISDN/IMSI不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
card, err := r.resolve(identities)
|
||||
if err != nil {
|
||||
row.AddReason(err.Error())
|
||||
return
|
||||
}
|
||||
if card == nil {
|
||||
row.AddReason("卡号不存在或无权限访问")
|
||||
return
|
||||
}
|
||||
for _, value := range identities {
|
||||
if !cardIdentityMatches(card, value) {
|
||||
row.AddReason("ICCID/MSISDN/IMSI与卡信息不一致")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
row.Iccid = strings.TrimSpace(card.Iccid)
|
||||
row.Msisdn = strings.TrimSpace(card.Msisdn)
|
||||
row.Imsi = strings.TrimSpace(card.Imsi)
|
||||
}
|
||||
|
||||
func (r *renewIdentityResolver) resolve(values []string) (*entity.TSimCardInfo, error) {
|
||||
if r == nil || len(values) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
matches := make(map[int64]*entity.TSimCardInfo)
|
||||
for _, value := range uniqueStrings(values) {
|
||||
for id, card := range r.byValue[strings.TrimSpace(value)] {
|
||||
matches[id] = card
|
||||
}
|
||||
}
|
||||
switch len(matches) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
case 1:
|
||||
for _, card := range matches {
|
||||
return card, nil
|
||||
}
|
||||
}
|
||||
return nil, gerror.New("卡号对应多条卡记录,请输入 ICCID")
|
||||
}
|
||||
|
||||
func cardIdentityMatches(card *entity.TSimCardInfo, value string) bool {
|
||||
if card == nil {
|
||||
return false
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
return value == strings.TrimSpace(card.Iccid) ||
|
||||
value == strings.TrimSpace(card.Msisdn) ||
|
||||
value == strings.TrimSpace(card.Imsi)
|
||||
}
|
||||
|
||||
func normalizeRenewIdentityRows(ctx context.Context, rows []*Row) error {
|
||||
resolver, err := renewIdentityResolverLoader(ctx, rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
resolver.resolveRow(row)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package renew
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/text"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
func ParseInputRows(ctx context.Context, input string) ([]batchimport.Row, error) {
|
||||
lines, err := text.ParseWhitespaceFields(ctx, input)
|
||||
if err != nil {
|
||||
if errors.Is(err, text.ErrEmptyInput) {
|
||||
return nil, gerror.New("批量输入续费数据为空")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
renewRows := make([]*Row, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
renewRows = append(renewRows, parseRenewInputLine(line))
|
||||
}
|
||||
|
||||
rows := make([]batchimport.Row, 0, len(renewRows))
|
||||
for _, row := range renewRows {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func parseRenewInputLine(line text.FieldLine) *Row {
|
||||
row := &Row{RowNo: line.RowNo}
|
||||
switch len(line.Fields) {
|
||||
case 1:
|
||||
row.InputCode = strings.TrimSpace(line.Fields[0])
|
||||
row.Iccid = row.InputCode
|
||||
case 3, 4:
|
||||
row.Iccid = strings.TrimSpace(line.Fields[0])
|
||||
row.Msisdn = strings.TrimSpace(line.Fields[1])
|
||||
row.Imsi = strings.TrimSpace(line.Fields[2])
|
||||
row.InputCode = firstNonEmpty(row.Iccid, row.Msisdn, row.Imsi)
|
||||
default:
|
||||
row.AddReason("格式错误,应为 ICCID/MSISDN/IMSI 任一号码,或 ICCID MSISDN IMSI")
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package renew
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/excel"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
)
|
||||
|
||||
type ExcelSubmitOptions struct {
|
||||
DurationYears int
|
||||
PaymentMode string
|
||||
}
|
||||
|
||||
func ExcelTemplateHeaders() []string {
|
||||
return []string{
|
||||
"ICCID",
|
||||
"MSISDN",
|
||||
"IMSI",
|
||||
"续费年限",
|
||||
"是否走线下",
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ExcelSubmitOptions) ToBatchSubmitParam() *model.TSimCardRenewBatchSubmitParam {
|
||||
if o == nil {
|
||||
return &model.TSimCardRenewBatchSubmitParam{}
|
||||
}
|
||||
return &model.TSimCardRenewBatchSubmitParam{
|
||||
DurationYears: o.DurationYears,
|
||||
DurationMonths: o.DurationYears * 12,
|
||||
PaymentMode: strings.TrimSpace(o.PaymentMode),
|
||||
OfflineContractCompleted: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ExcelSubmitOptions) ToBatchConfirmParam(batchNo string) *model.TSimCardRenewBatchConfirmParam {
|
||||
if o == nil {
|
||||
return &model.TSimCardRenewBatchConfirmParam{BatchNo: strings.TrimSpace(batchNo), OfflineContractCompleted: true}
|
||||
}
|
||||
return &model.TSimCardRenewBatchConfirmParam{
|
||||
BatchNo: strings.TrimSpace(batchNo),
|
||||
DurationYears: o.DurationYears,
|
||||
DurationMonths: o.DurationYears * 12,
|
||||
PaymentMode: strings.TrimSpace(o.PaymentMode),
|
||||
OfflineContractCompleted: true,
|
||||
}
|
||||
}
|
||||
|
||||
func ParseExcelFileRows(ctx context.Context, filePath string) ([]*Row, *ExcelSubmitOptions, error) {
|
||||
parsedRows, err := excel.ParseFile(ctx, filePath, excel.ParseOptions{
|
||||
RequiredHeaders: []string{"ICCID"},
|
||||
SingleSheet: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rows := make([]*Row, 0, len(parsedRows))
|
||||
var options *ExcelSubmitOptions
|
||||
inconsistent := false
|
||||
|
||||
for _, parsed := range parsedRows {
|
||||
row := &Row{
|
||||
RowNo: parsed.RowNo,
|
||||
Iccid: excel.Field(parsed, "ICCID"),
|
||||
Msisdn: excel.Field(parsed, "MSISDN"),
|
||||
Imsi: excel.Field(parsed, "IMSI"),
|
||||
InputCode: firstNonEmpty(
|
||||
excel.Field(parsed, "ICCID"),
|
||||
excel.Field(parsed, "MSISDN"),
|
||||
excel.Field(parsed, "IMSI"),
|
||||
),
|
||||
}
|
||||
rowOptions, reasons := parseExcelSubmitOptions(parsed)
|
||||
if len(reasons) > 0 {
|
||||
row.AddReason(strings.Join(reasons, "; "))
|
||||
rows = append(rows, row)
|
||||
continue
|
||||
}
|
||||
if options == nil {
|
||||
options = rowOptions
|
||||
} else if !sameExcelSubmitOptions(options, rowOptions) {
|
||||
row.AddReason("同一批续费年限和是否走线下必须一致")
|
||||
inconsistent = true
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
if inconsistent {
|
||||
markRowsWithoutReason(rows, "同一批续费年限和是否走线下必须一致")
|
||||
}
|
||||
return rows, options, nil
|
||||
}
|
||||
|
||||
func parseExcelSubmitOptions(row excel.ParsedRow) (*ExcelSubmitOptions, []string) {
|
||||
var reasons []string
|
||||
|
||||
durationYears, err := parseExcelDurationYears(row)
|
||||
if err != nil {
|
||||
reasons = append(reasons, err.Error())
|
||||
}
|
||||
paymentMode, err := parseExcelPaymentMode(row)
|
||||
if err != nil {
|
||||
reasons = append(reasons, err.Error())
|
||||
}
|
||||
if len(reasons) > 0 {
|
||||
return nil, reasons
|
||||
}
|
||||
|
||||
return &ExcelSubmitOptions{
|
||||
DurationYears: durationYears,
|
||||
PaymentMode: paymentMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseExcelDurationYears(row excel.ParsedRow) (int, error) {
|
||||
if value := strings.TrimSpace(excelField(row, "续费年限(年)")); value != "" {
|
||||
years, err := parsePositiveInt(value)
|
||||
if err != nil {
|
||||
return 0, errors.New("续费年限必须为正整数")
|
||||
}
|
||||
return years, nil
|
||||
}
|
||||
if value := strings.TrimSpace(excelField(row, "续费年限")); value != "" {
|
||||
years, err := parsePositiveInt(value)
|
||||
if err != nil {
|
||||
return 0, errors.New("续费年限必须为正整数")
|
||||
}
|
||||
return years, nil
|
||||
}
|
||||
if value := strings.TrimSpace(excelField(row, "续费时长")); value != "" {
|
||||
years, err := parsePositiveInt(value)
|
||||
if err != nil {
|
||||
return 0, errors.New("续费年限必须为正整数")
|
||||
}
|
||||
return years, nil
|
||||
}
|
||||
if value := strings.TrimSpace(excelField(row, "durationMonths")); value != "" {
|
||||
years, err := parsePositiveInt(value)
|
||||
if err != nil {
|
||||
return 0, errors.New("续费年限必须为正整数")
|
||||
}
|
||||
return years, nil
|
||||
}
|
||||
return 0, errors.New("续费年限不能为空")
|
||||
}
|
||||
|
||||
func parseExcelPaymentMode(row excel.ParsedRow) (string, error) {
|
||||
value := strings.TrimSpace(excelField(row, "是否走线下"))
|
||||
if value == "" {
|
||||
value = strings.TrimSpace(excelField(row, "付款方式"))
|
||||
}
|
||||
if value == "" {
|
||||
value = strings.TrimSpace(excelField(row, "paymentMode"))
|
||||
}
|
||||
if value == "" {
|
||||
return "", nil
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "1", "y", "yes", "true", "offline", "线下", "走线下", "是":
|
||||
return "offline", nil
|
||||
case "0", "n", "no", "false", "balance", "余额", "余额支付", "否":
|
||||
return "balance", nil
|
||||
default:
|
||||
return "", errors.New("是否走线下格式不正确")
|
||||
}
|
||||
}
|
||||
|
||||
func excelField(row excel.ParsedRow, name string) string {
|
||||
return excel.Field(row, name)
|
||||
}
|
||||
|
||||
func parsePositiveInt(value string) (int, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, strconv.ErrSyntax
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil || parsed <= 0 {
|
||||
return 0, strconv.ErrSyntax
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func sameExcelSubmitOptions(left, right *ExcelSubmitOptions) bool {
|
||||
if left == nil || right == nil {
|
||||
return false
|
||||
}
|
||||
return left.DurationYears == right.DurationYears && strings.TrimSpace(left.PaymentMode) == strings.TrimSpace(right.PaymentMode)
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package renew
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/text"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
func TestExcelTemplateHeaders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expected := []string{
|
||||
"ICCID",
|
||||
"MSISDN",
|
||||
"IMSI",
|
||||
"续费年限",
|
||||
"是否走线下",
|
||||
}
|
||||
if !reflect.DeepEqual(ExcelTemplateHeaders(), expected) {
|
||||
t.Fatalf("unexpected headers: %#v", ExcelTemplateHeaders())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewDurationValueToMonths(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
unit string
|
||||
value int
|
||||
want int
|
||||
}{
|
||||
{name: "one month", unit: consts.RenewDurationUnitMonth, value: 1, want: 1},
|
||||
{name: "one year", unit: consts.RenewDurationUnitYear, value: 1, want: 12},
|
||||
{name: "unknown unit defaults to year", unit: "unknown", value: 2, want: 24},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := consts.DurationValueToMonths(test.unit, test.value); got != test.want {
|
||||
t.Fatalf("DurationValueToMonths(%q, %d) = %d, want %d", test.unit, test.value, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewDisplayDurationAndAmount(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
yearSnapshot := &model.TSimCardRenewPriceSnapshot{
|
||||
RenewDurationUnit: consts.RenewDurationUnitYear,
|
||||
RenewUnitPrice: 10,
|
||||
}
|
||||
yearParam := &model.TSimCardRenewBatchSubmitParam{DurationYears: 1}
|
||||
if got := renewDisplayDurationValue(yearParam); got != 1 {
|
||||
t.Fatalf("renewDisplayDurationValue(year) = %d, want 1", got)
|
||||
}
|
||||
if got := renewActualDurationMonths(yearSnapshot, renewDisplayDurationValue(yearParam)); got != 12 {
|
||||
t.Fatalf("renewActualDurationMonths(year) = %d, want 12", got)
|
||||
}
|
||||
yearAmount, err := resolveRenewAmount(yearSnapshot, yearParam, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveRenewAmount(year) failed: %v", err)
|
||||
}
|
||||
if yearAmount != 20 {
|
||||
t.Fatalf("resolveRenewAmount(year) = %.2f, want 20.00", yearAmount)
|
||||
}
|
||||
|
||||
monthSnapshot := &model.TSimCardRenewPriceSnapshot{
|
||||
RenewDurationUnit: consts.RenewDurationUnitMonth,
|
||||
RenewUnitPrice: 10,
|
||||
}
|
||||
monthParam := &model.TSimCardRenewBatchSubmitParam{DurationMonths: 1}
|
||||
if got := renewActualDurationMonths(monthSnapshot, renewDisplayDurationValue(monthParam)); got != 12 {
|
||||
t.Fatalf("renewActualDurationMonths(month snapshot) = %d, want 12", got)
|
||||
}
|
||||
actualMonthParam := &model.TSimCardRenewBatchSubmitParam{DurationMonths: 12}
|
||||
if got := renewDisplayDurationValue(actualMonthParam); got != 1 {
|
||||
t.Fatalf("renewDisplayDurationValue(actual months legacy) = %d, want 1", got)
|
||||
}
|
||||
monthAmount, err := resolveRenewAmount(monthSnapshot, monthParam, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveRenewAmount(month) failed: %v", err)
|
||||
}
|
||||
if monthAmount != 20 {
|
||||
t.Fatalf("resolveRenewAmount(month) = %.2f, want 20.00", monthAmount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyExecutionOptionsKeepsDisplayDurationValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
in := &model.TSimCardRenewBatchSubmitParam{}
|
||||
applyExecutionOptions(in, &executionOptions{
|
||||
DurationYears: 2,
|
||||
DurationMonths: 2,
|
||||
PaymentMode: renewPaymentModeBalance,
|
||||
})
|
||||
if in.DurationYears != 2 || in.DurationMonths != 2 {
|
||||
t.Fatalf("duration should keep display value: %#v", in)
|
||||
}
|
||||
if got := renewDisplayDurationValue(in); got != 2 {
|
||||
t.Fatalf("renewDisplayDurationValue = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewDeptAncestorsContain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ancestors string
|
||||
deptID int64
|
||||
want bool
|
||||
}{
|
||||
{name: "contains direct ancestor", ancestors: "0,100,200", deptID: 100, want: true},
|
||||
{name: "contains with spaces", ancestors: "0, 100 ,200", deptID: 100, want: true},
|
||||
{name: "does not match partial id", ancestors: "0,1001,200", deptID: 100, want: false},
|
||||
{name: "empty ancestors", ancestors: "", deptID: 100, want: false},
|
||||
{name: "invalid dept", ancestors: "0,100,200", deptID: 0, want: false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := renewDeptAncestorsContain(test.ancestors, test.deptID); got != test.want {
|
||||
t.Fatalf("renewDeptAncestorsContain(%q, %d) = %v, want %v", test.ancestors, test.deptID, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExcelFileRowsAndOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
filePath := filepath.Join(t.TempDir(), "renew.xlsx")
|
||||
f := excelize.NewFile()
|
||||
headers := ExcelTemplateHeaders()
|
||||
if err := f.SetSheetRow("Sheet1", "A1", &headers); err != nil {
|
||||
t.Fatalf("set headers failed: %v", err)
|
||||
}
|
||||
row := []any{
|
||||
"8986000000000000001",
|
||||
"13800008000",
|
||||
"460001234567890",
|
||||
"1",
|
||||
"是",
|
||||
}
|
||||
if err := f.SetSheetRow("Sheet1", "A2", &row); err != nil {
|
||||
t.Fatalf("set row failed: %v", err)
|
||||
}
|
||||
if err := f.SaveAs(filePath); err != nil {
|
||||
t.Fatalf("save excel failed: %v", err)
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
rows, options, err := ParseExcelFileRows(context.Background(), filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("parse excel failed: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("unexpected row count: %d", len(rows))
|
||||
}
|
||||
if rows[0].Reason != "" {
|
||||
t.Fatalf("unexpected row reason: %s", rows[0].Reason)
|
||||
}
|
||||
if rows[0].Msisdn != "13800008000" {
|
||||
t.Fatalf("unexpected msisdn: %s", rows[0].Msisdn)
|
||||
}
|
||||
if rows[0].Imsi != "460001234567890" {
|
||||
t.Fatalf("unexpected imsi: %s", rows[0].Imsi)
|
||||
}
|
||||
if options == nil {
|
||||
t.Fatal("options should not be nil")
|
||||
}
|
||||
if options.DurationYears != 1 {
|
||||
t.Fatalf("unexpected durationYears: %d", options.DurationYears)
|
||||
}
|
||||
if options.PaymentMode != "offline" {
|
||||
t.Fatalf("unexpected paymentMode: %s", options.PaymentMode)
|
||||
}
|
||||
|
||||
submitParam := options.ToBatchSubmitParam()
|
||||
if submitParam.DurationYears != 1 || submitParam.DurationMonths != 12 {
|
||||
t.Fatalf("unexpected submit duration fields: %#v", submitParam)
|
||||
}
|
||||
if submitParam.PaymentMode != "offline" {
|
||||
t.Fatalf("unexpected submit paymentMode: %#v", submitParam)
|
||||
}
|
||||
if !submitParam.OfflineContractCompleted {
|
||||
t.Fatalf("excel submit should mark offline contract as confirmed: %#v", submitParam)
|
||||
}
|
||||
if submitParam.SourceBatchNo != "" || submitParam.BuyerNote != "" || submitParam.AdminNote != "" || submitParam.Remark != "" {
|
||||
t.Fatalf("excel submit param should not carry removed fields: %#v", submitParam)
|
||||
}
|
||||
|
||||
confirmParam := options.ToBatchConfirmParam("RB20260706001")
|
||||
if confirmParam.BatchNo != "RB20260706001" {
|
||||
t.Fatalf("unexpected batchNo: %s", confirmParam.BatchNo)
|
||||
}
|
||||
if confirmParam.DurationYears != 1 || confirmParam.DurationMonths != 12 {
|
||||
t.Fatalf("unexpected confirm duration fields: %#v", confirmParam)
|
||||
}
|
||||
if confirmParam.PaymentMode != "offline" {
|
||||
t.Fatalf("unexpected confirm paymentMode: %#v", confirmParam)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExcelFileRowsRejectsMultipleSheetsWithData(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
filePath := filepath.Join(t.TempDir(), "renew-multi.xlsx")
|
||||
f := excelize.NewFile()
|
||||
headers := ExcelTemplateHeaders()
|
||||
if err := f.SetSheetRow("Sheet1", "A1", &headers); err != nil {
|
||||
t.Fatalf("set headers failed: %v", err)
|
||||
}
|
||||
row := []any{
|
||||
"8986000000000000001",
|
||||
"13800008000",
|
||||
"460001234567890",
|
||||
"1",
|
||||
"是",
|
||||
}
|
||||
if err := f.SetSheetRow("Sheet1", "A2", &row); err != nil {
|
||||
t.Fatalf("set row failed: %v", err)
|
||||
}
|
||||
_, err := f.NewSheet("Sheet2")
|
||||
if err != nil {
|
||||
t.Fatalf("new sheet failed: %v", err)
|
||||
}
|
||||
if err := f.SetSheetRow("Sheet2", "A1", &headers); err != nil {
|
||||
t.Fatalf("set sheet2 headers failed: %v", err)
|
||||
}
|
||||
if err := f.SetSheetRow("Sheet2", "A2", &row); err != nil {
|
||||
t.Fatalf("set sheet2 row failed: %v", err)
|
||||
}
|
||||
if err := f.SaveAs(filePath); err != nil {
|
||||
t.Fatalf("save excel failed: %v", err)
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
if _, _, err := ParseExcelFileRows(context.Background(), filePath); err == nil {
|
||||
t.Fatal("expected multiple-sheet excel to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareExcelProcessorUsesPrepareBatchType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
processor := NewPrepareExcelProcessor()
|
||||
cfg := processor.Config()
|
||||
if cfg.BatchType != consts.BatchTypePrepare {
|
||||
t.Fatalf("prepare excel batch type = %d, want %d", cfg.BatchType, consts.BatchTypePrepare)
|
||||
}
|
||||
if cfg.BatchNoPrefix != consts.BatchNoPrefixRenew {
|
||||
t.Fatalf("prepare excel batch prefix = %q, want %q", cfg.BatchNoPrefix, consts.BatchNoPrefixRenew)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRenewInputLineSupportsSingleAndTripleFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
line text.FieldLine
|
||||
wantCode string
|
||||
wantIccid string
|
||||
wantMsisdn string
|
||||
wantImsi string
|
||||
wantReason string
|
||||
}{
|
||||
{
|
||||
name: "single value",
|
||||
line: text.FieldLine{RowNo: 2, Fields: []string{"89860401786597927028"}},
|
||||
wantCode: "89860401786597927028",
|
||||
wantIccid: "89860401786597927028",
|
||||
},
|
||||
{
|
||||
name: "triple values",
|
||||
line: text.FieldLine{RowNo: 3, Fields: []string{"89860401786597927028", "14100007028", "460046597927028"}},
|
||||
wantCode: "89860401786597927028",
|
||||
wantIccid: "89860401786597927028",
|
||||
wantMsisdn: "14100007028",
|
||||
wantImsi: "460046597927028",
|
||||
},
|
||||
{
|
||||
name: "invalid format",
|
||||
line: text.FieldLine{RowNo: 4, Fields: []string{"a", "b"}},
|
||||
wantReason: "格式错误",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
row := parseRenewInputLine(test.line)
|
||||
if row.InputCode != test.wantCode {
|
||||
t.Fatalf("input code = %q, want %q", row.InputCode, test.wantCode)
|
||||
}
|
||||
if row.Iccid != test.wantIccid {
|
||||
t.Fatalf("iccid = %q, want %q", row.Iccid, test.wantIccid)
|
||||
}
|
||||
if row.Msisdn != test.wantMsisdn {
|
||||
t.Fatalf("msisdn = %q, want %q", row.Msisdn, test.wantMsisdn)
|
||||
}
|
||||
if row.Imsi != test.wantImsi {
|
||||
t.Fatalf("imsi = %q, want %q", row.Imsi, test.wantImsi)
|
||||
}
|
||||
if test.wantReason != "" && row.Reason == "" {
|
||||
t.Fatal("expected row reason")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewIdentityResolverResolvesAnyCardIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
resolver := newRenewIdentityResolverFromCards([]*entity.TSimCardInfo{
|
||||
{
|
||||
Id: 1,
|
||||
Iccid: "89860401786597927028",
|
||||
Msisdn: "14100007028",
|
||||
Imsi: "460046597927028",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Iccid: "89860401786597927029",
|
||||
Msisdn: "14100007029",
|
||||
Imsi: "460046597927029",
|
||||
},
|
||||
})
|
||||
|
||||
row := &Row{RowNo: 2, InputCode: "14100007028", Msisdn: "14100007028"}
|
||||
resolver.resolveRow(row)
|
||||
if row.Reason != "" {
|
||||
t.Fatalf("unexpected reason: %s", row.Reason)
|
||||
}
|
||||
if row.Iccid != "89860401786597927028" || row.Msisdn != "14100007028" || row.Imsi != "460046597927028" {
|
||||
t.Fatalf("unexpected resolved row: %#v", row)
|
||||
}
|
||||
|
||||
row = &Row{RowNo: 3, InputCode: "460046597927029", Imsi: "460046597927029"}
|
||||
resolver.resolveRow(row)
|
||||
if row.Reason != "" {
|
||||
t.Fatalf("unexpected reason for imsi row: %s", row.Reason)
|
||||
}
|
||||
if row.Iccid != "89860401786597927029" || row.Msisdn != "14100007029" || row.Imsi != "460046597927029" {
|
||||
t.Fatalf("unexpected imsi resolved row: %#v", row)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewIdentityResolverRejectsAmbiguousOrMismatchedIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
resolver := newRenewIdentityResolverFromCards([]*entity.TSimCardInfo{
|
||||
{Id: 1, Iccid: "89860401786597927028", Msisdn: "14100007028", Imsi: "460046597927028"},
|
||||
{Id: 2, Iccid: "89860401786597927029", Msisdn: "89860401786597927028", Imsi: "460046597927029"},
|
||||
})
|
||||
|
||||
ambiguous := &Row{RowNo: 2, InputCode: "89860401786597927028", Iccid: "89860401786597927028"}
|
||||
resolver.resolveRow(ambiguous)
|
||||
if ambiguous.Reason == "" {
|
||||
t.Fatal("expected ambiguous reason")
|
||||
}
|
||||
|
||||
mismatched := &Row{RowNo: 3, InputCode: "89860401786597927028", Iccid: "89860401786597927028", Msisdn: "00000000000"}
|
||||
resolver.resolveRow(mismatched)
|
||||
if mismatched.Reason == "" {
|
||||
t.Fatal("expected mismatch reason")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
||||
package renew
|
||||
|
||||
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/model"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// PrepareProcessor 描述 batch_type=4 的卡续费预处理批次。
|
||||
// 与正式续费 Processor 共享 row/validate 逻辑,但 BatchType/BuildBatchInfo 走 prepare 语义。
|
||||
type PrepareProcessor struct {
|
||||
inner *Processor
|
||||
repo *Repository
|
||||
config batchimport.ProcessorConfig
|
||||
}
|
||||
|
||||
func NewPrepareProcessor() *PrepareProcessor {
|
||||
inner := NewProcessor()
|
||||
cfg := inner.config
|
||||
cfg.Code = "sim_card_renew_prepare"
|
||||
cfg.BatchType = consts.BatchTypePrepare
|
||||
cfg.BatchNoPrefix = consts.BatchNoPrefixRenew
|
||||
return &PrepareProcessor{
|
||||
inner: inner,
|
||||
repo: inner.repo,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) Config() batchimport.ProcessorConfig {
|
||||
return p.config
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
// Keep submit non-blocking; the worker owns prepare validation.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
|
||||
if req == nil {
|
||||
return gerror.New("续费预处理请求不能为空")
|
||||
}
|
||||
renewRows := make([]*Row, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok || row == nil {
|
||||
return gerror.New("续费行类型错误")
|
||||
}
|
||||
renewRows = append(renewRows, row)
|
||||
}
|
||||
submitParam := renewPrepareSubmitParam(req)
|
||||
prepared, err := p.inner.ValidatePrepareContext(ctx, tx, submitParam, req.Operator, renewRows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
priceSnapshot, err := ensurePriceSnapshot(ctx, tx, req.Operator, prepared, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.TargetDeptId = prepared.CustomerAgentId
|
||||
req.ExecutionResult = priceSnapshot
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
|
||||
info := g.Map{}
|
||||
if result != nil && result.ImportFileOssId > 0 {
|
||||
info["importFileOssId"] = result.ImportFileOssId
|
||||
} else if importOss != nil && importOss.OssId > 0 {
|
||||
info["importFileOssId"] = importOss.OssId
|
||||
}
|
||||
if resultOss != nil {
|
||||
info["resultFileOssId"] = resultOss.OssId
|
||||
}
|
||||
if downloadId > 0 {
|
||||
info["resultDownloadId"] = downloadId
|
||||
}
|
||||
info["targetBatchType"] = consts.BatchTypeRenew
|
||||
info["targetBatchName"] = "卡续费"
|
||||
if in, ok := req.BizParams.(*model.TSimCardRenewBatchPrepareParam); ok && in != nil {
|
||||
if sourceBatchNo := strings.TrimSpace(in.SourceBatchNo); sourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = sourceBatchNo
|
||||
}
|
||||
}
|
||||
if in, ok := req.BizParams.(*model.TSimCardRenewBatchPrepareAllParam); ok && in != nil {
|
||||
if sourceBatchNo := strings.TrimSpace(in.SourceBatchNo); sourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = sourceBatchNo
|
||||
}
|
||||
}
|
||||
if in, ok := req.BizParams.(*model.TSimCardRenewBatchSubmitParam); ok && in != nil {
|
||||
durationValue := renewDisplayDurationValue(in)
|
||||
if durationValue > 0 {
|
||||
actualDurationMonths := renewActualDurationMonths(nil, durationValue)
|
||||
info["durationValue"] = durationValue
|
||||
info["durationYears"] = durationValue
|
||||
info["durationMonths"] = actualDurationMonths
|
||||
info["actualDurationMonths"] = actualDurationMonths
|
||||
}
|
||||
if paymentMode := strings.TrimSpace(in.PaymentMode); paymentMode != "" {
|
||||
info["paymentMode"] = paymentMode
|
||||
}
|
||||
if packageCode := strings.TrimSpace(in.PackageCode); packageCode != "" {
|
||||
info["packageCode"] = packageCode
|
||||
}
|
||||
info["offlineContractCompleted"] = in.OfflineContractCompleted
|
||||
if sourceBatchNo := strings.TrimSpace(in.SourceBatchNo); sourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = sourceBatchNo
|
||||
}
|
||||
}
|
||||
if snapshot, ok := req.ExecutionResult.(*model.TSimCardRenewPriceSnapshot); ok && snapshot != nil {
|
||||
info["customerAgentId"] = snapshot.CustomerAgentId
|
||||
info["packageCode"] = snapshot.PackageCode
|
||||
info["packageName"] = snapshot.PackageName
|
||||
info["billingMethod"] = snapshot.BillingMethod
|
||||
info["renewDurationUnit"] = snapshot.RenewDurationUnit
|
||||
info["renewDurationUnitText"] = snapshot.RenewDurationUnitText
|
||||
info["priceSource"] = snapshot.PriceSource
|
||||
info["priceOwnerDeptId"] = snapshot.PriceOwnerDeptId
|
||||
info["agentDefaultPriceId"] = snapshot.AgentDefaultPriceId
|
||||
info["targetPriceId"] = snapshot.TargetPriceId
|
||||
info["renewUnitPrice"] = snapshot.RenewUnitPrice
|
||||
info["renewableDurationOptions"] = snapshot.RenewableDurationOptions
|
||||
info["renewablePackageCodes"] = snapshot.RenewablePackageCodes
|
||||
info["renewablePackages"] = snapshot.RenewablePackages
|
||||
info["renewBeginDate"] = snapshot.RenewBeginDate
|
||||
info["renewBeginDateMin"] = snapshot.RenewBeginDateMin
|
||||
info["renewBeginDateMax"] = snapshot.RenewBeginDateMax
|
||||
info["sameRenewBeginDate"] = snapshot.SameRenewBeginDate
|
||||
if result != nil {
|
||||
info["quantity"] = result.ValidRows
|
||||
}
|
||||
}
|
||||
return gjson.New(info)
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) ValidatePrepareContext(ctx context.Context, tx gdb.TX, in *model.TSimCardRenewBatchSubmitParam, operator batchimport.OperatorContext, rows []*Row) (*PrepareContext, error) {
|
||||
return p.inner.ValidatePrepareContext(ctx, tx, in, operator, rows)
|
||||
}
|
||||
|
||||
func renewPrepareSubmitParam(req *batchimport.SubmitRequest) *model.TSimCardRenewBatchSubmitParam {
|
||||
submitParam := &model.TSimCardRenewBatchSubmitParam{}
|
||||
if req == nil {
|
||||
return submitParam
|
||||
}
|
||||
switch in := req.BizParams.(type) {
|
||||
case *model.TSimCardRenewBatchSubmitParam:
|
||||
if in != nil {
|
||||
submitParam = in
|
||||
}
|
||||
case *model.TSimCardRenewBatchPrepareParam:
|
||||
if in != nil {
|
||||
submitParam.SourceBatchNo = strings.TrimSpace(in.SourceBatchNo)
|
||||
}
|
||||
case *model.TSimCardRenewBatchPrepareAllParam:
|
||||
if in != nil {
|
||||
submitParam.SourceBatchNo = strings.TrimSpace(in.SourceBatchNo)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(submitParam.SourceBatchNo) == "" {
|
||||
submitParam.SourceBatchNo = strings.TrimSpace(req.SourceBatchNo)
|
||||
}
|
||||
return submitParam
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
package renew
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/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"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
repo *Repository
|
||||
config batchimport.ProcessorConfig
|
||||
}
|
||||
|
||||
func NewProcessor() *Processor {
|
||||
return &Processor{
|
||||
repo: NewRepository(),
|
||||
config: batchimport.ProcessorConfig{
|
||||
Code: "sim_card_renew",
|
||||
BatchType: consts.BatchTypeRenew,
|
||||
BatchNoPrefix: consts.BatchNoPrefixRenew,
|
||||
TaskName: consts.ResourceBatchDownloadTaskSimCardRenew,
|
||||
ExportScene: consts.ResourceExportSceneRenew,
|
||||
ImportDir: consts.SimCardRenewSubDirImportExcel,
|
||||
ResultDir: consts.SimCardRenewSubDirResultExcel,
|
||||
MaxFileSize: consts.SimCardRenewMaxFileSize,
|
||||
TemplateHeaders: ExcelTemplateHeaders(),
|
||||
RequiredHeaders: []string{"ICCID"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) Config() batchimport.ProcessorConfig {
|
||||
return p.config
|
||||
}
|
||||
|
||||
func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
return nil, gerror.New("卡续费不支持文件导入")
|
||||
}
|
||||
|
||||
func (p *Processor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
renewRows := make([]*Row, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok {
|
||||
item.AddErrorReason("续费行类型错误")
|
||||
continue
|
||||
}
|
||||
renewRows = append(renewRows, row)
|
||||
}
|
||||
if err := normalizeRenewIdentityRows(ctx, renewRows); err != nil {
|
||||
return err
|
||||
}
|
||||
return validateRows(renewRows)
|
||||
}
|
||||
|
||||
func (p *Processor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
|
||||
if req == nil {
|
||||
return gerror.New("续费请求不能为空")
|
||||
}
|
||||
in, ok := req.BizParams.(*model.TSimCardRenewBatchSubmitParam)
|
||||
if !ok || in == nil {
|
||||
return gerror.New("续费参数类型错误")
|
||||
}
|
||||
|
||||
renewRows := make([]*Row, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok {
|
||||
return gerror.New("续费行类型错误")
|
||||
}
|
||||
renewRows = append(renewRows, row)
|
||||
}
|
||||
options, err := loadExecutionOptions(ctx, tx, req.BatchNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
applyExecutionOptions(in, options)
|
||||
|
||||
prepared, err := p.validateRenewContext(ctx, tx, in, req.Operator, renewRows, req.BatchNo, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
priceSnapshot, err := ensurePriceSnapshot(ctx, tx, req.Operator, prepared, options.PriceSnapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
durationValue := renewDisplayDurationValue(in)
|
||||
if durationValue <= 0 {
|
||||
return gerror.New("续费时长必须大于0")
|
||||
}
|
||||
if !containsRenewDurationOption(priceSnapshot.RenewableDurationOptions, durationValue) {
|
||||
return gerror.New("续费时长不在当前套餐可续费范围内")
|
||||
}
|
||||
|
||||
output, err := p.Complete(ctx, tx, &CompleteInput{
|
||||
SubmitParam: in,
|
||||
BatchNo: req.BatchNo,
|
||||
Rows: renewRows,
|
||||
Operator: req.Operator,
|
||||
PriceSnapshot: priceSnapshot,
|
||||
})
|
||||
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{}
|
||||
if result != nil && result.ImportFileOssId > 0 {
|
||||
info["importFileOssId"] = result.ImportFileOssId
|
||||
} else if importOss != nil && importOss.OssId > 0 {
|
||||
info["importFileOssId"] = importOss.OssId
|
||||
}
|
||||
if resultOss != nil {
|
||||
info["resultFileOssId"] = resultOss.OssId
|
||||
}
|
||||
if downloadId > 0 {
|
||||
info["resultDownloadId"] = downloadId
|
||||
}
|
||||
if in, ok := req.BizParams.(*model.TSimCardRenewBatchSubmitParam); ok && in != nil {
|
||||
durationValue := renewDisplayDurationValue(in)
|
||||
if durationValue > 0 {
|
||||
actualDurationMonths := renewActualDurationMonths(nil, durationValue)
|
||||
info["durationYears"] = durationValue
|
||||
info["durationMonths"] = actualDurationMonths
|
||||
info["durationValue"] = durationValue
|
||||
info["actualDurationMonths"] = actualDurationMonths
|
||||
}
|
||||
info["giftDurationMonths"] = in.GiftDurationMonths
|
||||
info["amount"] = strings.TrimSpace(in.Amount)
|
||||
if in.PaymentSnapshot != nil {
|
||||
appendRenewPaymentSnapshot(info, in.PaymentSnapshot)
|
||||
} else if paymentMode := strings.TrimSpace(in.PaymentMode); paymentMode != "" {
|
||||
info["paymentMode"] = paymentMode
|
||||
}
|
||||
if packageCode := strings.TrimSpace(in.PackageCode); packageCode != "" {
|
||||
info["packageCode"] = packageCode
|
||||
}
|
||||
info["offlineContractCompleted"] = in.OfflineContractCompleted
|
||||
if sourceBatchNo := strings.TrimSpace(in.SourceBatchNo); sourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = sourceBatchNo
|
||||
}
|
||||
if buyerNote := strings.TrimSpace(in.BuyerNote); buyerNote != "" {
|
||||
info["buyerNote"] = buyerNote
|
||||
}
|
||||
if adminNote := strings.TrimSpace(in.AdminNote); adminNote != "" {
|
||||
info["adminNote"] = adminNote
|
||||
}
|
||||
if remark := strings.TrimSpace(in.Remark); remark != "" {
|
||||
info["remark"] = remark
|
||||
}
|
||||
}
|
||||
if snapshot, ok := req.ExecutionResult.(*model.TSimCardRenewPriceSnapshot); ok && snapshot != nil {
|
||||
if result != nil && result.BatchType == consts.BatchTypePrepare {
|
||||
info["targetBatchType"] = consts.BatchTypeRenew
|
||||
info["targetBatchName"] = "卡续费"
|
||||
}
|
||||
info["customerAgentId"] = snapshot.CustomerAgentId
|
||||
info["packageCode"] = snapshot.PackageCode
|
||||
info["packageName"] = snapshot.PackageName
|
||||
info["billingMethod"] = snapshot.BillingMethod
|
||||
info["renewDurationUnit"] = snapshot.RenewDurationUnit
|
||||
info["renewDurationUnitText"] = snapshot.RenewDurationUnitText
|
||||
if durationValue := gconv.Int(info["durationValue"]); durationValue > 0 {
|
||||
actualDurationMonths := renewActualDurationMonths(snapshot, durationValue)
|
||||
info["durationMonths"] = actualDurationMonths
|
||||
info["actualDurationMonths"] = actualDurationMonths
|
||||
}
|
||||
info["quantity"] = result.ValidRows
|
||||
info["priceSource"] = snapshot.PriceSource
|
||||
info["priceOwnerDeptId"] = snapshot.PriceOwnerDeptId
|
||||
info["agentDefaultPriceId"] = snapshot.AgentDefaultPriceId
|
||||
info["targetPriceId"] = snapshot.TargetPriceId
|
||||
info["renewUnitPrice"] = snapshot.RenewUnitPrice
|
||||
info["renewableDurationOptions"] = snapshot.RenewableDurationOptions
|
||||
info["renewablePackageCodes"] = snapshot.RenewablePackageCodes
|
||||
info["renewablePackages"] = snapshot.RenewablePackages
|
||||
info["renewBeginDate"] = snapshot.RenewBeginDate
|
||||
info["renewBeginDateMin"] = snapshot.RenewBeginDateMin
|
||||
info["renewBeginDateMax"] = snapshot.RenewBeginDateMax
|
||||
info["sameRenewBeginDate"] = snapshot.SameRenewBeginDate
|
||||
}
|
||||
if output, ok := req.ExecutionResult.(*CompleteOutput); ok && output != nil {
|
||||
info["quantity"] = output.Quantity
|
||||
info["packageCode"] = output.PackageCode
|
||||
info["orderSn"] = output.OrderSn
|
||||
info["renewTime"] = output.RenewTime
|
||||
if sourceBatchNo := strings.TrimSpace(output.SourceBatchNo); sourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = sourceBatchNo
|
||||
}
|
||||
}
|
||||
return gjson.New(info)
|
||||
}
|
||||
|
||||
func appendRenewPaymentSnapshot(info g.Map, snapshot *model.TSimCardRenewPaymentSnapshot) {
|
||||
if info == nil || snapshot == nil {
|
||||
return
|
||||
}
|
||||
info["paymentMode"] = strings.TrimSpace(snapshot.PaymentMode)
|
||||
info["payType"] = snapshot.PayType
|
||||
info["payStatus"] = snapshot.PayStatus
|
||||
info["payAmount"] = snapshot.PayAmount
|
||||
info["balanceAmount"] = snapshot.BalanceAmount
|
||||
info["offlinePaidAmount"] = snapshot.OfflinePaidAmount
|
||||
info["balanceBefore"] = snapshot.BalanceBefore
|
||||
info["balanceAfter"] = snapshot.BalanceAfter
|
||||
info["balanceDetailSerialNo"] = strings.TrimSpace(snapshot.BalanceDetailSerialNo)
|
||||
info["payerDeptId"] = snapshot.PayerDeptId
|
||||
info["payerUserId"] = snapshot.PayerUserId
|
||||
info["isTopDept"] = snapshot.IsTopDept
|
||||
if snapshot.PaymentTime != nil {
|
||||
info["paymentTime"] = snapshot.PaymentTime.Format("Y-m-d H:i:s")
|
||||
}
|
||||
info["paymentSnapshot"] = snapshot
|
||||
}
|
||||
|
||||
func (p *Processor) ValidatePrepareContext(ctx context.Context, tx gdb.TX, in *model.TSimCardRenewBatchSubmitParam, operator batchimport.OperatorContext, rows []*Row) (*PrepareContext, error) {
|
||||
return p.validateRenewContext(ctx, tx, in, operator, rows, "", false)
|
||||
}
|
||||
|
||||
func (p *Processor) RevalidatePrepareContext(ctx context.Context, tx gdb.TX, in *model.TSimCardRenewBatchSubmitParam, operator batchimport.OperatorContext, rows []*Row, currentBatchNo string) (*PrepareContext, bool, error) {
|
||||
prepared, err := p.validateRenewContext(ctx, tx, in, operator, rows, currentBatchNo, true)
|
||||
return prepared, hasInvalidRows(rows), err
|
||||
}
|
||||
|
||||
func (p *Processor) validateRenewContext(ctx context.Context, tx gdb.TX, in *model.TSimCardRenewBatchSubmitParam, operator batchimport.OperatorContext, rows []*Row, currentBatchNo string, allowPartial bool) (*PrepareContext, error) {
|
||||
iccids := rowsToIccids(rows)
|
||||
cardsByIccid, err := p.repo.LockCardsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "校验续费卡信息失败")
|
||||
}
|
||||
bindsByIccid, err := p.repo.LockLatestBindsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "校验续费订购记录失败")
|
||||
}
|
||||
conflictingBatches, err := p.repo.UnfinishedRenewBatchNosByIccids(
|
||||
ctx,
|
||||
tx,
|
||||
operator.TenantId,
|
||||
iccids,
|
||||
currentBatchNo,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "校验未完成续费批次失败")
|
||||
}
|
||||
|
||||
sourceBatchNo := strings.TrimSpace(in.SourceBatchNo)
|
||||
packageCodes := make(map[string]struct{})
|
||||
customerAgentIds := make(map[int64]struct{})
|
||||
currentPackagePeriods := make(map[string]struct{})
|
||||
renewBeginDates := make([]string, 0, len(rows))
|
||||
packageCode := ""
|
||||
customerAgentId := int64(0)
|
||||
for _, row := range rows {
|
||||
if row == nil || row.Reason != "" {
|
||||
continue
|
||||
}
|
||||
card := cardsByIccid[row.Iccid]
|
||||
bind := bindsByIccid[row.Iccid]
|
||||
if strings.TrimSpace(conflictingBatches[row.Iccid]) != "" {
|
||||
row.AddReason("存在未完成续费批次,不能重复续费")
|
||||
continue
|
||||
}
|
||||
reason := validateRenewRow(sourceBatchNo, operator, card, bind)
|
||||
if reason != "" {
|
||||
row.AddReason(reason)
|
||||
continue
|
||||
}
|
||||
baseExpireAt := renewBaseExpireTime(card, bind)
|
||||
|
||||
periodKey := currentPackagePeriodKeyWithExpire(bind, baseExpireAt)
|
||||
if periodKey == "" {
|
||||
row.AddReason("不是统一批次")
|
||||
continue
|
||||
}
|
||||
currentPackageCode := strings.TrimSpace(card.CurrentPackageCode)
|
||||
packageCodes[currentPackageCode] = struct{}{}
|
||||
customerAgentIds[card.DeptId] = struct{}{}
|
||||
currentPackagePeriods[periodKey] = struct{}{}
|
||||
packageCode = currentPackageCode
|
||||
customerAgentId = card.DeptId
|
||||
renewBeginAt := renewBeginDateFromExpire(baseExpireAt)
|
||||
if renewBeginAt != nil {
|
||||
renewBeginDates = append(renewBeginDates, renewBeginAt.Format("Y-m-d"))
|
||||
}
|
||||
}
|
||||
|
||||
if len(packageCodes) > 1 {
|
||||
markRowsWithoutReason(rows, "当前批次下存在多个套餐,不能统一续费")
|
||||
}
|
||||
if len(customerAgentIds) > 1 {
|
||||
markRowsWithoutReason(rows, "当前批次下存在多个客户,不能统一续费")
|
||||
}
|
||||
if len(currentPackagePeriods) > 1 {
|
||||
markRowsWithoutReason(rows, "不是统一批次")
|
||||
}
|
||||
if len(customerAgentIds) == 1 && customerAgentId > 0 {
|
||||
if err := validateRenewCustomerDeptScope(ctx, tx, operator, customerAgentId); err != nil {
|
||||
markRowsWithoutReason(rows, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if hasInvalidRows(rows) && !allowPartial {
|
||||
markRemainingRowsSkipped(rows)
|
||||
return nil, gerror.New("存在不满足续费条件的卡,本次未执行")
|
||||
}
|
||||
validCount := countValidRows(rows)
|
||||
if validCount == 0 {
|
||||
return &PrepareContext{}, nil
|
||||
}
|
||||
renewBeginDate, renewBeginDateMin, renewBeginDateMax, sameRenewBeginDate := summarizeRenewBeginDates(renewBeginDates)
|
||||
return &PrepareContext{
|
||||
CustomerAgentId: customerAgentId,
|
||||
PackageCode: packageCode,
|
||||
Quantity: validCount,
|
||||
RenewBeginDate: renewBeginDate,
|
||||
RenewBeginDateMin: renewBeginDateMin,
|
||||
RenewBeginDateMax: renewBeginDateMax,
|
||||
SameRenewBeginDate: sameRenewBeginDate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func currentPackagePeriodKey(bind *entity.TSimCardBindPkg) string {
|
||||
if bind == nil {
|
||||
return ""
|
||||
}
|
||||
return currentPackagePeriodKeyWithExpire(bind, bind.ExpectedExpireTime)
|
||||
}
|
||||
|
||||
func currentPackagePeriodKeyWithExpire(bind *entity.TSimCardBindPkg, expireAt *gtime.Time) string {
|
||||
if bind == nil || bind.EffectiveTime == nil {
|
||||
return ""
|
||||
}
|
||||
if expireAt == nil {
|
||||
expireAt = bind.ExpectedExpireTime
|
||||
}
|
||||
if expireAt == nil {
|
||||
return ""
|
||||
}
|
||||
return bind.EffectiveTime.Format("Y-m-d") + "|" + expireAt.Format("Y-m-d")
|
||||
}
|
||||
|
||||
func summarizeRenewBeginDates(dates []string) (sameDate string, minDate string, maxDate string, same bool) {
|
||||
cleaned := make([]string, 0, len(dates))
|
||||
for _, date := range dates {
|
||||
date = strings.TrimSpace(date)
|
||||
if date != "" {
|
||||
cleaned = append(cleaned, date)
|
||||
}
|
||||
}
|
||||
if len(cleaned) == 0 {
|
||||
return "", "", "", false
|
||||
}
|
||||
sort.Strings(cleaned)
|
||||
minDate = cleaned[0]
|
||||
maxDate = cleaned[len(cleaned)-1]
|
||||
same = minDate == maxDate
|
||||
if same {
|
||||
sameDate = minDate
|
||||
}
|
||||
return sameDate, minDate, maxDate, same
|
||||
}
|
||||
|
||||
func renewBeginDateFromExpire(expireAt *gtime.Time) *gtime.Time {
|
||||
if expireAt == nil {
|
||||
return nil
|
||||
}
|
||||
return normalizeRenewDateAtDayEnd(expireAt)
|
||||
}
|
||||
|
||||
func normalizeRenewDateAtDayEnd(expireAt *gtime.Time) *gtime.Time {
|
||||
if expireAt == nil || expireAt.IsZero() {
|
||||
return nil
|
||||
}
|
||||
t := expireAt.Time
|
||||
return gtime.NewFromTime(
|
||||
time.Date(
|
||||
t.Year(),
|
||||
t.Month(),
|
||||
t.Day(),
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
0,
|
||||
t.Location(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func validateRenewRow(sourceBatchNo string, operator batchimport.OperatorContext, card *entity.TSimCardInfo, bind *entity.TSimCardBindPkg) string {
|
||||
if card == nil {
|
||||
return "ICCID不存在正式卡记录,不能续费"
|
||||
}
|
||||
if card.BossCardStatus == consts.V3ApiSimBossCardStatusStopped {
|
||||
return "ICCID当前为停机状态,不能续费"
|
||||
}
|
||||
if bind == nil {
|
||||
return "ICCID没有订购记录,不能续费"
|
||||
}
|
||||
if strings.TrimSpace(card.CurrentPackageCode) == "" || card.PackageStatus == consts.PackageStatusNone {
|
||||
return "ICCID没有当前套餐,不能续费"
|
||||
}
|
||||
if strings.TrimSpace(card.CurrentPackageCode) != strings.TrimSpace(bind.PackageCode) {
|
||||
return "ICCID当前套餐与最新订购记录不一致,不能续费"
|
||||
}
|
||||
baseExpireAt := renewBaseExpireTime(card, bind)
|
||||
if baseExpireAt == nil {
|
||||
return "ICCID最新订购没有预计到期时间,不能续费"
|
||||
}
|
||||
if renewExpireAfterAllowedWindow(baseExpireAt) {
|
||||
return "距离到期超过6个月,不能续费"
|
||||
}
|
||||
if sourceBatchNo != "" && strings.TrimSpace(card.OutboundBatchNo) != sourceBatchNo {
|
||||
return "ICCID不属于来源出库批次,不能续费"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func renewBaseExpireTime(card *entity.TSimCardInfo, bind *entity.TSimCardBindPkg) *gtime.Time {
|
||||
var expireAt *gtime.Time
|
||||
if bind != nil {
|
||||
expireAt = laterRenewExpireTime(expireAt, bind.ExpectedExpireTime)
|
||||
}
|
||||
if card != nil {
|
||||
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgEndAt)
|
||||
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgPresentAt)
|
||||
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgRealPresentAt)
|
||||
}
|
||||
return expireAt
|
||||
}
|
||||
|
||||
func laterRenewExpireTime(current, candidate *gtime.Time) *gtime.Time {
|
||||
if candidate == nil || candidate.IsZero() {
|
||||
return current
|
||||
}
|
||||
if current == nil || current.IsZero() || candidate.After(current) {
|
||||
return candidate
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func renewExpireAfterAllowedWindow(expireAt *gtime.Time) bool {
|
||||
if expireAt == nil {
|
||||
return false
|
||||
}
|
||||
return expireAt.After(gtime.Now().AddDate(0, 6, 0))
|
||||
}
|
||||
|
||||
func validateRenewCustomerDeptScope(ctx context.Context, tx gdb.TX, operator batchimport.OperatorContext, customerDeptID int64) error {
|
||||
if operator.DeptId <= 0 || operator.TenantId == "" {
|
||||
return gerror.New("当前用户未绑定组织或租户")
|
||||
}
|
||||
if customerDeptID <= 0 {
|
||||
return gerror.New("ICCID不属于当前机构,不能续费")
|
||||
}
|
||||
if customerDeptID == operator.DeptId {
|
||||
return nil
|
||||
}
|
||||
dept := &entity.SysDept{}
|
||||
if err := tx.Ctx(ctx).Model(dao.SysDept.Table()).
|
||||
Fields(dao.SysDept.Columns().DeptId, dao.SysDept.Columns().ParentId, dao.SysDept.Columns().Ancestors).
|
||||
Where(dao.SysDept.Columns().TenantId, operator.TenantId).
|
||||
Where(dao.SysDept.Columns().DeptId, customerDeptID).
|
||||
WhereNull(dao.SysDept.Columns().DeletedAt).
|
||||
Scan(dept); err != nil {
|
||||
return gerror.Wrap(err, "校验ICCID归属失败")
|
||||
}
|
||||
if dept.DeptId == 0 {
|
||||
return gerror.New("ICCID不属于当前机构,不能续费")
|
||||
}
|
||||
if dept.ParentId == operator.DeptId || renewDeptAncestorsContain(dept.Ancestors, operator.DeptId) {
|
||||
return nil
|
||||
}
|
||||
return gerror.New("ICCID不属于当前机构,不能续费")
|
||||
}
|
||||
|
||||
func renewDeptAncestorsContain(ancestors string, deptID int64) bool {
|
||||
if deptID <= 0 {
|
||||
return false
|
||||
}
|
||||
target := strconv.FormatInt(deptID, 10)
|
||||
for _, item := range strings.Split(strings.Trim(ancestors, ","), ",") {
|
||||
if strings.TrimSpace(item) == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasInvalidRows(rows []*Row) bool {
|
||||
for _, row := range rows {
|
||||
if row != nil && row.Reason != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func countValidRows(rows []*Row) int {
|
||||
count := 0
|
||||
for _, row := range rows {
|
||||
if row != nil && row.Valid() {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func markRowsWithoutReason(rows []*Row, reason string) {
|
||||
for _, row := range rows {
|
||||
if row != nil && row.Reason == "" {
|
||||
row.AddReason(reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markRemainingRowsSkipped(rows []*Row) {
|
||||
for _, row := range rows {
|
||||
if row != nil && row.Reason == "" {
|
||||
row.AddReason("同批存在不满足续费条件的卡,本行未执行")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateRows(rows []*Row) error {
|
||||
firstByIccid := make(map[string]*Row, len(rows))
|
||||
for _, row := range rows {
|
||||
row.Iccid = strings.TrimSpace(row.Iccid)
|
||||
if row.Iccid == "" {
|
||||
row.AddReason("ICCID不能为空")
|
||||
continue
|
||||
}
|
||||
if first, ok := firstByIccid[row.Iccid]; ok {
|
||||
row.AddReason("ICCID重复")
|
||||
first.AddReason("ICCID重复")
|
||||
continue
|
||||
}
|
||||
firstByIccid[row.Iccid] = row
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package renew
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"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/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func TestBuildBatchInfoOmitsRemovedEmptyFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
info := NewProcessor().BuildBatchInfo(context.Background(), batchimport.SubmitRequest{
|
||||
BizParams: &model.TSimCardRenewBatchSubmitParam{
|
||||
DurationMonths: 12,
|
||||
GiftDurationMonths: 2,
|
||||
Amount: "100.00",
|
||||
OfflineContractCompleted: true,
|
||||
},
|
||||
ExecutionResult: &CompleteOutput{
|
||||
Quantity: 1,
|
||||
PackageCode: "PKG001",
|
||||
OrderSn: "ORDER001",
|
||||
},
|
||||
}, &batchimport.Result{}, nil, nil, 0)
|
||||
|
||||
data := info.Map()
|
||||
for _, key := range []string{"sourceBatchNo", "buyerNote", "adminNote", "remark"} {
|
||||
if _, ok := data[key]; ok {
|
||||
t.Fatalf("batch info should omit %s when empty: %#v", key, data)
|
||||
}
|
||||
}
|
||||
if data["durationMonths"] != 12 {
|
||||
t.Fatalf("unexpected durationMonths: %#v", data["durationMonths"])
|
||||
}
|
||||
if data["giftDurationMonths"] != 2 {
|
||||
t.Fatalf("unexpected giftDurationMonths: %#v", data["giftDurationMonths"])
|
||||
}
|
||||
if data["offlineContractCompleted"] != true {
|
||||
t.Fatalf("unexpected offlineContractCompleted: %#v", data["offlineContractCompleted"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentPackagePeriodKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
bind := &entity.TSimCardBindPkg{
|
||||
EffectiveTime: gtime.NewFromStr("2026-01-01"),
|
||||
ExpectedExpireTime: gtime.NewFromStr("2026-12-31"),
|
||||
}
|
||||
if got, want := currentPackagePeriodKey(bind), "2026-01-01|2026-12-31"; got != want {
|
||||
t.Fatalf("currentPackagePeriodKey() = %q, want %q", got, want)
|
||||
}
|
||||
if got := currentPackagePeriodKey(&entity.TSimCardBindPkg{}); got != "" {
|
||||
t.Fatalf("currentPackagePeriodKey() for empty bind = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewBaseExpireTimePrefersLaterCardExpire(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
card := &entity.TSimCardInfo{
|
||||
FlowPkgEndAt: gtime.NewFromStr("2026-10-31 00:00:00"),
|
||||
}
|
||||
bind := &entity.TSimCardBindPkg{
|
||||
ExpectedExpireTime: gtime.NewFromStr("2026-08-31 00:00:00"),
|
||||
}
|
||||
|
||||
if got, want := renewBaseExpireTime(card, bind).Format("Y-m-d"), "2026-10-31"; got != want {
|
||||
t.Fatalf("renewBaseExpireTime() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewBeginDateFromExpireUsesExpireDate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
beginAt := renewBeginDateFromExpire(gtime.NewFromStr("2026-08-31 23:59:59"))
|
||||
if beginAt == nil {
|
||||
t.Fatal("renew begin date is nil")
|
||||
}
|
||||
if got, want := beginAt.Format("Y-m-d H:i:s"), "2026-08-31 23:59:59"; got != want {
|
||||
t.Fatalf("renewBeginDateFromExpire() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRenewRowRejectsExpireAfterSixMonths(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
card := &entity.TSimCardInfo{
|
||||
CurrentPackageCode: "PKG001",
|
||||
PackageStatus: consts.PackageStatusEffective,
|
||||
}
|
||||
bind := &entity.TSimCardBindPkg{
|
||||
PackageCode: "PKG001",
|
||||
ExpectedExpireTime: gtime.Now().AddDate(0, 6, 1),
|
||||
EffectiveTime: gtime.Now().AddDate(-1, 0, 0),
|
||||
EffectiveStatus: consts.EffectiveStatusEffective,
|
||||
}
|
||||
|
||||
if got, want := validateRenewRow("", batchimport.OperatorContext{}, card, bind), "距离到期超过6个月,不能续费"; got != want {
|
||||
t.Fatalf("validateRenewRow() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRenewRowAllowsExpireWithinSixMonths(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
card := &entity.TSimCardInfo{
|
||||
CurrentPackageCode: "PKG001",
|
||||
PackageStatus: consts.PackageStatusEffective,
|
||||
}
|
||||
bind := &entity.TSimCardBindPkg{
|
||||
PackageCode: "PKG001",
|
||||
ExpectedExpireTime: gtime.Now().AddDate(0, 6, 0),
|
||||
EffectiveTime: gtime.Now().AddDate(-1, 0, 0),
|
||||
EffectiveStatus: consts.EffectiveStatusEffective,
|
||||
}
|
||||
|
||||
if got := validateRenewRow("", batchimport.OperatorContext{}, card, bind); got != "" {
|
||||
t.Fatalf("validateRenewRow() = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRenewRowRejectsStoppedCard(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
card := &entity.TSimCardInfo{
|
||||
BossCardStatus: consts.V3ApiSimBossCardStatusStopped,
|
||||
CurrentPackageCode: "PKG001",
|
||||
PackageStatus: consts.PackageStatusEffective,
|
||||
}
|
||||
bind := &entity.TSimCardBindPkg{
|
||||
PackageCode: "PKG001",
|
||||
}
|
||||
|
||||
if got, want := validateRenewRow("", batchimport.OperatorContext{}, card, bind), "ICCID当前为停机状态,不能续费"; got != want {
|
||||
t.Fatalf("validateRenewRow() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewPrepareSubmitParamUsesPrepareSourceBatchNo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := &batchimport.SubmitRequest{
|
||||
BizParams: &model.TSimCardRenewBatchPrepareParam{SourceBatchNo: " OUT20260807001 "},
|
||||
}
|
||||
got := renewPrepareSubmitParam(req)
|
||||
if got.SourceBatchNo != "OUT20260807001" {
|
||||
t.Fatalf("SourceBatchNo = %q, want OUT20260807001", got.SourceBatchNo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewPrepareSubmitParamFallsBackToRequestSourceBatchNo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := &batchimport.SubmitRequest{
|
||||
BizParams: &model.TSimCardRenewBatchSubmitParam{},
|
||||
SourceBatchNo: " OUT20260807002 ",
|
||||
}
|
||||
got := renewPrepareSubmitParam(req)
|
||||
if got.SourceBatchNo != "OUT20260807002" {
|
||||
t.Fatalf("SourceBatchNo = %q, want OUT20260807002", got.SourceBatchNo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareProcessorValidateRowsDoesNotBlockSubmit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rows := []batchimport.Row{
|
||||
&Row{RowNo: 1, Iccid: "8986001"},
|
||||
&Row{RowNo: 2, Iccid: "8986001"},
|
||||
}
|
||||
if err := NewPrepareProcessor().ValidateRows(context.Background(), rows); err != nil {
|
||||
t.Fatalf("ValidateRows() error = %v", err)
|
||||
}
|
||||
for _, item := range rows {
|
||||
row := item.(*Row)
|
||||
if row.Reason != "" {
|
||||
t.Fatalf("row %d reason = %q, want empty before worker prepare", row.RowNo, row.Reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package renew
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type Repository struct{}
|
||||
|
||||
func NewRepository() *Repository {
|
||||
return &Repository{}
|
||||
}
|
||||
|
||||
func (r *Repository) GetSourceBatch(ctx context.Context, batchNo string) (*entity.BatchMain, error) {
|
||||
batch := &entity.BatchMain{}
|
||||
columns := dao.BatchMain.Columns()
|
||||
err := dao.BatchMain.Ctx(ctx).
|
||||
Fields(
|
||||
columns.Id,
|
||||
columns.BatchNo,
|
||||
columns.BatchType,
|
||||
columns.BatchStatus,
|
||||
columns.RunResult,
|
||||
columns.TenantId,
|
||||
columns.DeptId,
|
||||
columns.CreatedDept,
|
||||
columns.CreatedBy,
|
||||
).
|
||||
Where(columns.BatchNo, strings.TrimSpace(batchNo)).
|
||||
Scan(batch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if batch.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return batch, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetRenewBatchRows(ctx context.Context, batchNo string, batchType int) ([]*Row, error) {
|
||||
return r.getRenewBatchRows(ctx, nil, batchNo, batchType)
|
||||
}
|
||||
|
||||
func (r *Repository) GetRenewBatchRowsTx(ctx context.Context, tx gdb.TX, batchNo string, batchType int) ([]*Row, error) {
|
||||
return r.getRenewBatchRows(ctx, tx, batchNo, batchType)
|
||||
}
|
||||
|
||||
func (r *Repository) getRenewBatchRows(ctx context.Context, tx gdb.TX, batchNo string, batchType int) ([]*Row, error) {
|
||||
var details []*entity.BatchDetail
|
||||
columns := dao.BatchDetail.Columns()
|
||||
if batchType == 0 {
|
||||
batchType = consts.BatchTypeRenew
|
||||
}
|
||||
model := dao.BatchDetail.Ctx(ctx)
|
||||
if tx != nil {
|
||||
model = tx.Ctx(ctx).Model(dao.BatchDetail.Table())
|
||||
}
|
||||
err := model.
|
||||
Fields(columns.RowNo, columns.ItemId, columns.Snapshot).
|
||||
Where(columns.BatchNo, strings.TrimSpace(batchNo)).
|
||||
Where(columns.BatchType, batchType).
|
||||
Where(columns.DetailStatus, consts.BatchDetailStatusSuccess).
|
||||
OrderAsc(columns.RowNo).
|
||||
Scan(&details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return detailsToRows(details), nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetSourceBatchRows(ctx context.Context, batchNo string) ([]*Row, error) {
|
||||
var details []*entity.BatchDetail
|
||||
columns := dao.BatchDetail.Columns()
|
||||
err := dao.BatchDetail.Ctx(ctx).
|
||||
Fields(columns.RowNo, columns.ItemId, columns.Snapshot).
|
||||
Where(columns.BatchNo, strings.TrimSpace(batchNo)).
|
||||
Where(columns.BatchType, consts.BatchTypeOutbound).
|
||||
WhereIn(columns.DetailStatus, []int{consts.BatchDetailStatusSuccess, consts.BatchAsyncStatusSuccess}).
|
||||
OrderAsc(columns.RowNo).
|
||||
Scan(&details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return detailsToRows(details), nil
|
||||
}
|
||||
|
||||
func (r *Repository) BatchDetailItemIDs(ctx context.Context, tx gdb.TX, batchNo string, batchType int, statuses []int) ([]string, error) {
|
||||
batchNo = strings.TrimSpace(batchNo)
|
||||
if batchNo == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var details []*entity.BatchDetail
|
||||
model := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Fields(dao.BatchDetail.Columns().ItemId).
|
||||
Where(dao.BatchDetail.Columns().BatchNo, batchNo)
|
||||
if batchType > 0 {
|
||||
model = model.Where(dao.BatchDetail.Columns().BatchType, batchType)
|
||||
}
|
||||
if len(statuses) > 0 {
|
||||
model = model.WhereIn(dao.BatchDetail.Columns().DetailStatus, statuses)
|
||||
}
|
||||
if err := model.Scan(&details); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]string, 0, len(details))
|
||||
for _, detail := range details {
|
||||
if detail == nil {
|
||||
continue
|
||||
}
|
||||
if itemID := strings.TrimSpace(detail.ItemId); itemID != "" {
|
||||
ids = append(ids, itemID)
|
||||
}
|
||||
}
|
||||
return uniqueStrings(ids), nil
|
||||
}
|
||||
|
||||
func (r *Repository) UnfinishedRenewBatchNosByIccids(ctx context.Context, tx gdb.TX, tenantID string, iccids []string, currentBatchNo string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
iccids = uniqueStrings(iccids)
|
||||
if len(iccids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
currentBatchID, err := r.batchIDByNo(ctx, tx, currentBatchNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type conflictRow struct {
|
||||
ItemId string `orm:"item_id"`
|
||||
BatchNo string `orm:"batch_no"`
|
||||
}
|
||||
|
||||
bd := "bd"
|
||||
bm := "bm"
|
||||
detailColumns := dao.BatchDetail.Columns()
|
||||
mainColumns := dao.BatchMain.Columns()
|
||||
model := tx.Ctx(ctx).Model(dao.BatchDetail.Table()+" "+bd).
|
||||
InnerJoin(dao.BatchMain.Table()+" "+bm, bm+"."+mainColumns.BatchNo+" = "+bd+"."+detailColumns.BatchNo).
|
||||
Fields(bd+"."+detailColumns.ItemId, bm+"."+mainColumns.BatchNo).
|
||||
Where(bd+"."+detailColumns.BatchType, consts.BatchTypeRenew).
|
||||
Where(bm+"."+mainColumns.BatchType, consts.BatchTypeRenew).
|
||||
Where(bm+"."+mainColumns.BatchStatus, consts.BatchStatusNormal).
|
||||
WhereIn(bm+"."+mainColumns.BatchAsyncStatus, []int{
|
||||
consts.BatchAsyncStatusCreated,
|
||||
consts.BatchAsyncStatusPending,
|
||||
consts.BatchAsyncStatusProcessing,
|
||||
}).
|
||||
WhereIn(bm+"."+mainColumns.RunResult, []int{
|
||||
consts.BatchRunResultPending,
|
||||
consts.BatchRunResultRunning,
|
||||
}).
|
||||
WhereIn(bd+"."+detailColumns.DetailStatus, []int{
|
||||
consts.BatchDetailStatusPending,
|
||||
consts.BatchDetailStatusRunning,
|
||||
}).
|
||||
WhereIn(bd+"."+detailColumns.ItemId, iccids).
|
||||
OrderAsc(bm + "." + mainColumns.Id)
|
||||
if strings.TrimSpace(tenantID) != "" {
|
||||
model = model.Where(bm+"."+mainColumns.TenantId, strings.TrimSpace(tenantID))
|
||||
}
|
||||
if currentBatchID > 0 {
|
||||
// When duplicate formal renew batches already exist, the earliest batch
|
||||
// wins and later batches fail. This prevents two batches from blocking
|
||||
// each other forever.
|
||||
model = model.Where(bm+"."+mainColumns.Id+" < ?", currentBatchID)
|
||||
} else if currentBatchNo = strings.TrimSpace(currentBatchNo); currentBatchNo != "" {
|
||||
model = model.Where(bm+"."+mainColumns.BatchNo+" <> ?", currentBatchNo)
|
||||
}
|
||||
|
||||
var rows []conflictRow
|
||||
if err := model.Scan(&rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
itemID := strings.TrimSpace(row.ItemId)
|
||||
if itemID == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := result[itemID]; !exists {
|
||||
result[itemID] = strings.TrimSpace(row.BatchNo)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) batchIDByNo(ctx context.Context, tx gdb.TX, batchNo string) (int64, error) {
|
||||
batchNo = strings.TrimSpace(batchNo)
|
||||
if batchNo == "" {
|
||||
return 0, nil
|
||||
}
|
||||
batch := &entity.BatchMain{}
|
||||
if err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Fields(dao.BatchMain.Columns().Id).
|
||||
Where(dao.BatchMain.Columns().BatchNo, batchNo).
|
||||
Limit(1).
|
||||
Scan(batch); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return batch.Id, nil
|
||||
}
|
||||
|
||||
func detailsToRows(details []*entity.BatchDetail) []*Row {
|
||||
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 {
|
||||
if row.Iccid == "" {
|
||||
row.Iccid = strings.TrimSpace(detail.Snapshot.Get("iccid").String())
|
||||
}
|
||||
if row.Iccid == "" {
|
||||
row.Iccid = strings.TrimSpace(detail.Snapshot.Get("ICCID").String())
|
||||
}
|
||||
row.Msisdn = strings.TrimSpace(detail.Snapshot.Get("msisdn").String())
|
||||
if row.Msisdn == "" {
|
||||
row.Msisdn = strings.TrimSpace(detail.Snapshot.Get("MSISDN").String())
|
||||
}
|
||||
row.Imsi = strings.TrimSpace(detail.Snapshot.Get("imsi").String())
|
||||
if row.Imsi == "" {
|
||||
row.Imsi = strings.TrimSpace(detail.Snapshot.Get("IMSI").String())
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (r *Repository) LockCardsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*entity.TSimCardInfo, error) {
|
||||
result := make(map[string]*entity.TSimCardInfo)
|
||||
if len(iccids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var list []*entity.TSimCardInfo
|
||||
if err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
|
||||
WhereIn(dao.TSimCardInfo.Columns().Iccid, uniqueStrings(iccids)).
|
||||
LockUpdate().
|
||||
Scan(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
if item != nil && strings.TrimSpace(item.Iccid) != "" {
|
||||
result[strings.TrimSpace(item.Iccid)] = item
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) LockLatestBindsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*entity.TSimCardBindPkg, error) {
|
||||
result := make(map[string]*entity.TSimCardBindPkg)
|
||||
if len(iccids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var list []*entity.TSimCardBindPkg
|
||||
if err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().Iccid, uniqueStrings(iccids)).
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().PackageType, []int{101, 102}).
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().EffectiveStatus, []int{
|
||||
consts.EffectiveStatusPending,
|
||||
consts.EffectiveStatusEffective,
|
||||
consts.EffectiveStatusExpired,
|
||||
}).
|
||||
OrderDesc(dao.TSimCardBindPkg.Columns().ExpectedExpireTime).
|
||||
OrderDesc(dao.TSimCardBindPkg.Columns().Id).
|
||||
LockUpdate().
|
||||
Scan(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
effectiveCount := make(map[string]int)
|
||||
for _, item := range list {
|
||||
if item == nil || strings.TrimSpace(item.Iccid) == "" {
|
||||
continue
|
||||
}
|
||||
iccid := strings.TrimSpace(item.Iccid)
|
||||
if item.EffectiveStatus == consts.EffectiveStatusEffective {
|
||||
effectiveCount[iccid]++
|
||||
if effectiveCount[iccid] > 1 {
|
||||
return nil, gerror.Newf("同一张卡存在多个生效中的套餐订购记录:%s", iccid)
|
||||
}
|
||||
}
|
||||
if _, exists := result[iccid]; exists {
|
||||
continue
|
||||
}
|
||||
result[iccid] = item
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package renew
|
||||
|
||||
import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
"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
|
||||
InputCode string
|
||||
Iccid string
|
||||
Msisdn string
|
||||
Imsi 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,
|
||||
})
|
||||
}
|
||||
|
||||
type CompleteInput struct {
|
||||
SubmitParam *model.TSimCardRenewBatchSubmitParam
|
||||
BatchNo string
|
||||
Rows []*Row
|
||||
Operator batchimport.OperatorContext
|
||||
PriceSnapshot *model.TSimCardRenewPriceSnapshot
|
||||
RenewDate *gtime.Time
|
||||
}
|
||||
|
||||
type CompleteOutput struct {
|
||||
BatchNo string
|
||||
BatchType int
|
||||
SourceBatchNo string
|
||||
Quantity int
|
||||
PackageCode string
|
||||
PackageName string
|
||||
OrderSn string
|
||||
Amount float64
|
||||
RenewUnitPrice float64
|
||||
RenewDate *gtime.Time
|
||||
RenewTime *gtime.Time
|
||||
}
|
||||
|
||||
type PrepareContext struct {
|
||||
CustomerAgentId int64
|
||||
PackageCode string
|
||||
Quantity int
|
||||
RenewBeginDate string
|
||||
RenewBeginDateMin string
|
||||
RenewBeginDateMax string
|
||||
SameRenewBeginDate bool
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package renew
|
||||
|
||||
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