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