chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)

This commit is contained in:
MiFaZhan
2026-11-02 13:27:59 +08:00
1250 changed files with 154111 additions and 0 deletions
@@ -0,0 +1,187 @@
package change
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
NewPackageCode string
DurationMonths int
GiftDurationMonths int
EffectiveMode string
OfflineContract bool
BuyerNote string
AdminNote string
Remark string
PriceSnapshot *model.TSimCardChangePriceSnapshot
SourceBatchNo string
PackageBeginTimeStr 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=102 的待处理变更批次:重置主表正式执行相关字段并把明细的 BatchType 切换到 102、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.BatchTypeChange
}
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
}
if newPackageCode := strings.TrimSpace(in.NewPackageCode); newPackageCode != "" {
data["newPackageCode"] = newPackageCode
}
if in.DurationMonths > 0 {
data["durationMonths"] = in.DurationMonths
}
if in.GiftDurationMonths >= 0 {
data["giftDurationMonths"] = in.GiftDurationMonths
}
if effectiveMode := strings.TrimSpace(in.EffectiveMode); effectiveMode != "" {
data["effectiveMode"] = effectiveMode
}
if strings.TrimSpace(in.PackageBeginTimeStr) != "" {
data["packageBeginTime"] = strings.TrimSpace(in.PackageBeginTimeStr)
}
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["changeUnitPrice"] = in.PriceSnapshot.ChangeUnitPrice
}
if strings.TrimSpace(in.SourceBatchNo) != "" {
data["sourceBatchNo"] = strings.TrimSpace(in.SourceBatchNo)
}
return data
}
@@ -0,0 +1,515 @@
package change
import (
"context"
"encoding/json"
"fmt"
"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/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("change params cannot be empty")
}
sourceBatchNo := strings.TrimSpace(in.SourceBatchNo)
if strings.TrimSpace(in.BatchNo) == "" {
return nil, gerror.New("change batchNo cannot be empty")
}
if len(in.Rows) == 0 {
return nil, gerror.New("change cards cannot be empty")
}
if in.PriceSnapshot == nil {
return nil, gerror.New("change price snapshot cannot be empty")
}
if in.PackageBeginTime == nil {
return nil, gerror.New("change package begin time cannot be empty")
}
iccids := rowsToIccids(in.Rows)
packageSvc := cardpackage.NewService()
termination, err := packageSvc.TerminateCurrentPackage(ctx, tx, &cardpackage.TerminateCurrentPackageInput{
BatchNo: in.BatchNo,
SourceBatchNo: sourceBatchNo,
Iccids: iccids,
Operator: in.Operator,
})
if err != nil {
return nil, err
}
voidedByIccid, err := p.voidFuturePendingBinds(ctx, tx, iccids, in.BatchNo, termination.UnsubTime, in.Operator.UserId)
if err != nil {
return nil, err
}
quantity := len(iccids)
totalAmount := roundAmount(in.PriceSnapshot.ChangeUnitPrice * float64(in.SubmitParam.DurationMonths) * float64(quantity))
cards := make([]cardpackage.SubscribeCard, 0, quantity)
for _, iccid := range iccids {
card := termination.CardsByIccid[iccid]
if card == nil {
return nil, gerror.Newf("ICCID card info not found: %s", iccid)
}
cards = append(cards, cardpackage.SubscribeCard{
Iccid: iccid,
Msisdn: strings.TrimSpace(card.Msisdn),
Imsi: strings.TrimSpace(card.Imsi),
})
}
subscribe, err := packageSvc.CreatePackageSubscription(ctx, tx, &cardpackage.CreatePackageSubscriptionInput{
BatchNo: in.BatchNo,
Cards: cards,
CustomerAgentId: in.PriceSnapshot.CustomerAgentId,
PackageCode: in.PriceSnapshot.NewPackageCode,
PackageBeginTime: in.PackageBeginTime,
DurationMonths: in.SubmitParam.DurationMonths,
GiftDurationMonths: in.SubmitParam.GiftDurationMonths,
Amount: fmt.Sprintf("%.2f", totalAmount),
OfflineContractCompleted: in.SubmitParam.OfflineContractCompleted,
BuyerNote: in.SubmitParam.BuyerNote,
AdminNote: in.SubmitParam.AdminNote,
PreviousPackageCode: termination.PackageCode,
BusinessType: consts.PackageBusinessTypeChange,
OrderRemark: "sim card change order",
BindRemark: in.BatchNo,
Operator: in.Operator,
})
if err != nil {
return nil, err
}
if err = p.updateCardsAfterChange(ctx, tx, iccids, termination.CardsByIccid, subscribe, in.Operator.UserId); err != nil {
return nil, err
}
detailUpdates := make(map[string]any, len(iccids))
for _, row := range in.Rows {
card := termination.CardsByIccid[row.Iccid]
oldBind := termination.BindsByIccid[row.Iccid]
newBind := subscribe.BindsByIccid[row.Iccid]
detailUpdates[row.Iccid] = buildDetailSnapshot(row, card, oldBind, newBind, voidedByIccid[row.Iccid], in, termination, subscribe)
}
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
}
output := &CompleteOutput{
BatchNo: in.BatchNo,
BatchType: consts.BatchTypeChange,
SourceBatchNo: sourceBatchNo,
SourceMode: sourceMode(sourceBatchNo),
Quantity: quantity,
CustomerAgentId: in.PriceSnapshot.CustomerAgentId,
CustomerName: "",
OldPackageCode: termination.PackageCode,
NewPackageCode: subscribe.Package.PackageCode,
NewPackageName: subscribe.Package.PackageName,
OrderSn: subscribe.OrderSn,
Amount: subscribe.Amount,
ChangeUnitPrice: in.PriceSnapshot.ChangeUnitPrice,
DurationMonths: in.SubmitParam.DurationMonths,
GiftDurationMonths: in.SubmitParam.GiftDurationMonths,
EffectiveMode: strings.TrimSpace(in.SubmitParam.EffectiveMode),
OfflineContractCompleted: in.SubmitParam.OfflineContractCompleted,
PackageBeginTime: subscribe.PackageBeginAt,
PackageEndTime: subscribe.PackageEndAt,
ChangeTime: subscribe.SubscribeTime,
}
if in.PrepareSnapshot != nil {
output.CustomerName = in.PrepareSnapshot.CustomerName
output.OldPackageName = in.PrepareSnapshot.OldPackageName
}
if err = updateBatchMainAfterComplete(ctx, tx, in, output); err != nil {
return nil, err
}
return output, nil
}
func (p *Processor) voidFuturePendingBinds(ctx context.Context, tx gdb.TX, iccids []string, batchNo string, voidTime *gtime.Time, userId int64) (map[string][]string, error) {
result := make(map[string][]string)
pendingBinds, err := p.repo.LockPendingBindsByIccids(ctx, tx, iccids)
if err != nil {
return nil, gerror.Wrap(err, "lock future pending binds failed")
}
if len(pendingBinds) == 0 {
return result, nil
}
orderCodeSet := make(map[string]struct{})
orderVoidCounts := make(map[string]int)
subscriptionNos := make([]string, 0, len(pendingBinds))
logRows := make([]do.TSimBindPackageLogs, 0, len(pendingBinds))
for _, bind := range pendingBinds {
if bind == nil {
continue
}
subscriptionNo := strings.TrimSpace(bind.SubscriptionNo)
if subscriptionNo == "" {
continue
}
subscriptionNos = append(subscriptionNos, subscriptionNo)
result[strings.TrimSpace(bind.Iccid)] = append(result[strings.TrimSpace(bind.Iccid)], subscriptionNo)
logRows = append(logRows, bindToVoidedHistory(bind, voidTime, batchNo, userId))
orderCode := strings.TrimSpace(bind.OrderCode)
if orderCode != "" {
orderCodeSet[orderCode] = struct{}{}
orderVoidCounts[orderCode]++
}
}
if len(logRows) > 0 {
if _, err = tx.Ctx(ctx).Model(dao.TSimBindPackageLogs.Table()).
Data(logRows).
OnConflict(dao.TSimBindPackageLogs.Columns().SubscriptionNo).
Save(); err != nil {
return nil, gerror.Wrap(err, "save voided bind history failed")
}
}
affected, err := p.repo.DeleteBindsBySubscriptionNos(ctx, tx, subscriptionNos)
if err != nil {
return nil, gerror.Wrap(err, "delete future pending binds failed")
}
if int(affected) != len(subscriptionNos) {
return nil, gerror.New("some future pending binds changed, please retry")
}
if err = p.updateOrdersAfterChangeVoid(ctx, tx, mapKeys(orderCodeSet), orderVoidCounts, batchNo, voidTime, userId); err != nil {
return nil, err
}
return result, nil
}
func bindToVoidedHistory(bind *entity.TSimCardBindPkg, actualExpireAt *gtime.Time, changeBatchNo string, userId int64) do.TSimBindPackageLogs {
return do.TSimBindPackageLogs{
SubscriptionNo: bind.SubscriptionNo,
BatchNo: bind.BatchNo,
Iccid: bind.Iccid,
Msisdn: bind.Msisdn,
DeptId: bind.DeptId,
PackageType: bind.PackageType,
PackageCode: bind.PackageCode,
PreviousPackageCode: bind.PreviousPackageCode,
BusinessType: bind.BusinessType,
EffectiveStatus: consts.EffectiveStatusVoided,
HandleTime: bind.HandleTime,
EffectiveTime: bind.EffectiveTime,
ExpectedExpireTime: bind.ExpectedExpireTime,
ActualExpireTime: actualExpireAt,
DurationMonths: bind.DurationMonths,
GiftDurationMonths: bind.GiftDurationMonths,
OrderCode: bind.OrderCode,
Remark: appendRemark(bind.Remark, fmt.Sprintf("changeBatchNo=%s", changeBatchNo)),
TenantId: bind.TenantId,
CreatedDept: bind.CreatedDept,
CreatedBy: bind.CreatedBy,
CreatedAt: bind.CreatedAt,
UpdatedBy: userId,
UpdatedAt: actualExpireAt,
}
}
func (p *Processor) updateOrdersAfterChangeVoid(ctx context.Context, tx gdb.TX, orderCodes []string, voidCounts map[string]int, batchNo string, voidTime *gtime.Time, userId int64) error {
if len(orderCodes) == 0 {
return nil
}
activeCounts, err := p.repo.ActiveBindCountsByOrderCodes(ctx, tx, orderCodes)
if err != nil {
return gerror.Wrap(err, "count active binds by order failed")
}
for _, orderCode := range orderCodes {
returnStatus := consts.OrderReturnStatusSuccess
if activeCounts[orderCode] > 0 {
returnStatus = consts.OrderReturnStatusPartial
}
if _, err = tx.Ctx(ctx).Model(dao.TOrder.Table()).
Where(dao.TOrder.Columns().OrderSn, orderCode).
Data(do.TOrder{
ReturnStatus: returnStatus,
UpdatedBy: userId,
UpdatedAt: voidTime,
}).
Update(); err != nil {
return gerror.Wrap(err, "update order return status failed")
}
if err = updateOrderItemChangeExtra(ctx, tx, orderCode, batchNo, voidCounts[orderCode], voidTime, userId); err != nil {
return err
}
}
return nil
}
func updateOrderItemChangeExtra(ctx context.Context, tx gdb.TX, orderCode, batchNo string, voidQuantity int, voidTime *gtime.Time, userId int64) error {
var item *entity.TOrderItem
if err := tx.Ctx(ctx).Model(dao.TOrderItem.Table()).
Where(dao.TOrderItem.Columns().OrderSn, orderCode).
Limit(1).
Scan(&item); err != nil {
return gerror.Wrap(err, "load order item failed")
}
if item == nil {
return nil
}
extra := g.Map{}
if strings.TrimSpace(item.ExtraSkuData) != "" {
_ = json.Unmarshal([]byte(item.ExtraSkuData), &extra)
}
extra["lastChangeBatchNo"] = batchNo
extra["lastChangeTime"] = formatTime(voidTime)
extra["voidedByChangeQuantity"] = voidQuantity
data, err := json.Marshal(extra)
if err != nil {
return gerror.Wrap(err, "marshal order item extra failed")
}
if _, err = tx.Ctx(ctx).Model(dao.TOrderItem.Table()).
Where(dao.TOrderItem.Columns().ItemId, item.ItemId).
Data(do.TOrderItem{
ExtraSkuData: string(data),
UpdatedBy: userId,
UpdatedAt: voidTime,
}).
Update(); err != nil {
return gerror.Wrap(err, "update order item extra failed")
}
return nil
}
func (p *Processor) updateCardsAfterChange(ctx context.Context, tx gdb.TX, iccids []string, cardsByIccid map[string]*entity.TSimCardInfo, subscribe *cardpackage.CreatePackageSubscriptionOutput, userId int64) error {
if subscribe == nil || subscribe.Package == nil {
return gerror.New("change subscription output is empty")
}
now := gtime.Now()
columns := dao.TSimCardInfo.Columns()
for _, iccid := range iccids {
card := cardsByIccid[iccid]
if card == nil {
return gerror.Newf("ICCID card info not found: %s", iccid)
}
result, err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
Where(columns.Iccid, iccid).
Data(g.Map{
columns.CurrentPackageCode: subscribe.Package.PackageCode,
columns.PackageStatus: subscribe.PackageStatus,
columns.FlowPkgBeginAt: subscribe.PackageBeginAt,
columns.FlowPkgPresentAt: nil,
columns.FlowPkgRealPresentAt: nil,
columns.FlowPkgEndAt: subscribe.PackageEndAt,
columns.DataServiceStatus: 1,
columns.PackageTotalFlowMb: subscribe.Package.FlowMb,
columns.UsedFlowMb: 0,
columns.TrafficCutoffStatus: 0,
columns.TrafficCutoffThreshold: 0,
columns.SmsServiceStatus: 0,
columns.PackageTotalSms: 0,
columns.UsedSms: 0,
columns.VoiceServiceStatus: 0,
columns.PackageTotalVoice: 0,
columns.UsedVoice: 0,
columns.RoamServiceStatus: 0,
columns.PackageTotalRoamFlowMb: 0,
columns.UsedRoamFlowMb: 0,
columns.UpdatedBy: userId,
columns.UpdatedAt: now,
}).
Update()
if err != nil {
return gerror.Wrap(err, "update card after change failed")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "get updated card count failed")
}
if affected != 1 {
return gerror.Newf("ICCID card changed concurrently, retry: %s", iccid)
}
}
return nil
}
func updateBatchDetailSnapshots(ctx context.Context, tx gdb.TX, batchNo string, snapshots map[string]any, 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),
UpdatedBy: userId,
UpdatedAt: gtime.Now(),
}).
Update()
if err != nil {
return gerror.Wrap(err, "update change batch detail snapshot failed")
}
}
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.BatchTypeChange,
UpdatedBy: userId,
UpdatedAt: gtime.Now(),
}).
Update()
if err != nil {
return gerror.Wrap(err, "update change batch detail type failed")
}
return nil
}
func updateBatchMainAfterComplete(ctx context.Context, tx gdb.TX, in *CompleteInput, output *CompleteOutput) error {
info := g.Map{
"sourceMode": output.SourceMode,
"orderSn": output.OrderSn,
"customerAgentId": output.CustomerAgentId,
"customerName": output.CustomerName,
"oldPackageCode": output.OldPackageCode,
"oldPackageName": output.OldPackageName,
"newPackageCode": output.NewPackageCode,
"newPackageName": output.NewPackageName,
"durationMonths": output.DurationMonths,
"giftDurationMonths": output.GiftDurationMonths,
"effectiveMode": output.EffectiveMode,
"changeUnitPrice": output.ChangeUnitPrice,
"amount": output.Amount,
"quantity": output.Quantity,
"packageBeginTime": output.PackageBeginTime,
"packageEndTime": output.PackageEndTime,
"offlineContractCompleted": output.OfflineContractCompleted,
"changeTime": output.ChangeTime,
}
if output.SourceBatchNo != "" {
info["sourceBatchNo"] = output.SourceBatchNo
}
if in.PriceSnapshot != nil {
info["priceSource"] = in.PriceSnapshot.PriceSource
info["agentDefaultPriceId"] = in.PriceSnapshot.AgentDefaultPriceId
info["targetPriceId"] = in.PriceSnapshot.TargetPriceId
info["renewableDurationOptions"] = in.PriceSnapshot.RenewableDurationOptions
}
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
}
_, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().BatchNo, in.BatchNo).
Data(do.BatchMain{
BatchType: consts.BatchTypeChange,
OrderSn: output.OrderSn,
TargetDeptId: output.CustomerAgentId,
BatchInfo: gjson.New(info),
UpdatedBy: in.Operator.UserId,
UpdatedAt: output.ChangeTime,
}).
Update()
if err != nil {
return gerror.Wrap(err, "update change batch failed")
}
return nil
}
func buildDetailSnapshot(row *Row, card *entity.TSimCardInfo, oldBind, newBind *entity.TSimCardBindPkg, voidedSubscriptionNos []string, in *CompleteInput, termination *cardpackage.TerminateCurrentPackageOutput, subscribe *cardpackage.CreatePackageSubscriptionOutput) g.Map {
data := g.Map{
"ICCID": row.Iccid,
"MSISDN": row.Msisdn,
"IMSI": row.Imsi,
"sourceMode": sourceMode(in.SourceBatchNo),
"changeBatchNo": in.BatchNo,
"oldPackageCode": termination.PackageCode,
"newPackageCode": subscribe.Package.PackageCode,
"newPackageName": subscribe.Package.PackageName,
"voidedSubscriptionNos": voidedSubscriptionNos,
"durationMonths": in.SubmitParam.DurationMonths,
"giftDurationMonths": in.SubmitParam.GiftDurationMonths,
"effectiveMode": strings.TrimSpace(in.SubmitParam.EffectiveMode),
"offlineContractCompleted": in.SubmitParam.OfflineContractCompleted,
"changeUnitPrice": in.PriceSnapshot.ChangeUnitPrice,
"amount": roundAmount(in.PriceSnapshot.ChangeUnitPrice * float64(in.SubmitParam.DurationMonths)),
"newEffectiveTime": formatTime(subscribe.PackageBeginAt),
"newExpectedExpireTime": formatTime(subscribe.PackageEndAt),
"oldActualExpireTime": formatTime(termination.UnsubTime),
"rowNo": row.RowNo,
"rawFields": row.RawFields,
}
if strings.TrimSpace(in.SourceBatchNo) != "" {
data["sourceBatchNo"] = strings.TrimSpace(in.SourceBatchNo)
}
if card != nil {
data["ICCID"] = card.Iccid
data["MSISDN"] = card.Msisdn
data["IMSI"] = card.Imsi
}
if in.PrepareSnapshot != nil {
data["oldPackageName"] = in.PrepareSnapshot.OldPackageName
}
if oldBind != nil {
data["oldSubscriptionNo"] = oldBind.SubscriptionNo
data["oldOrderSn"] = oldBind.OrderCode
data["oldEffectiveTime"] = formatTime(oldBind.EffectiveTime)
data["oldExpectedExpireTime"] = formatTime(oldBind.ExpectedExpireTime)
}
if newBind != nil {
data["newSubscriptionNo"] = newBind.SubscriptionNo
data["newOrderSn"] = newBind.OrderCode
}
return data
}
func appendRemark(old, addition string) string {
old = strings.TrimSpace(old)
addition = strings.TrimSpace(addition)
if old == "" {
return addition
}
if addition == "" {
return old
}
return old + "; " + addition
}
func mapKeys(set map[string]struct{}) []string {
result := make([]string, 0, len(set))
for key := range set {
if strings.TrimSpace(key) != "" {
result = append(result, key)
}
}
return result
}
func roundAmount(value float64) float64 {
if value < 0 {
return 0
}
return float64(int(value*100+0.5)) / 100
}
func sourceMode(sourceBatchNo string) int {
if strings.TrimSpace(sourceBatchNo) != "" {
return consts.BatchSourceTypeBatchSelect
}
return 0
}
@@ -0,0 +1,41 @@
package change
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
)
type ExcelProcessor struct {
*Processor
options *ExcelSubmitOptions
}
func NewExcelProcessor() *ExcelProcessor {
return &ExcelProcessor{Processor: NewProcessor()}
}
func (p *ExcelProcessor) Config() batchimport.ProcessorConfig {
cfg := p.Processor.Config()
cfg.TemplateHeaders = ExcelTemplateHeaders()
cfg.RequiredHeaders = ExcelTemplateHeaders()
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) ExcelSubmitOptions() *ExcelSubmitOptions {
return p.options
}
@@ -0,0 +1,17 @@
package change
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common/cardsource"
)
func ParseInputRows(ctx context.Context, input string) ([]batchimport.Row, error) {
return cardsource.BuildIccidRows(ctx, input, "batch input change data is empty", func(line cardsource.IccidLine) *Row {
return &Row{
RowNo: line.RowNo,
Iccid: line.Iccid,
}
})
}
@@ -0,0 +1,164 @@
package change
import (
"context"
"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 {
NewPackageCode string
DurationMonths int
GiftDurationMonths int
EffectiveMode string
OfflineContractCompleted bool
}
func ExcelTemplateHeaders() []string {
return []string{
"ICCID",
"MSISDN",
"IMSI",
"newPackageCode",
"durationMonths",
"giftDurationMonths",
"effectiveMode",
"offlineContractCompleted",
}
}
func (o *ExcelSubmitOptions) ToBatchSubmitParam() *model.TSimCardChangeBatchSubmitParam {
if o == nil {
return &model.TSimCardChangeBatchSubmitParam{}
}
return &model.TSimCardChangeBatchSubmitParam{
NewPackageCode: strings.TrimSpace(o.NewPackageCode),
DurationMonths: o.DurationMonths,
GiftDurationMonths: o.GiftDurationMonths,
EffectiveMode: strings.TrimSpace(o.EffectiveMode),
OfflineContractCompleted: o.OfflineContractCompleted,
}
}
func (o *ExcelSubmitOptions) ToBatchSubmitParamWithBatchNo(batchNo string) *model.TSimCardChangeBatchSubmitParam {
param := o.ToBatchSubmitParam()
param.BatchNo = strings.TrimSpace(batchNo)
return param
}
func ParseExcelFileRows(ctx context.Context, filePath string) ([]*Row, *ExcelSubmitOptions, error) {
parsedRows, err := excel.ParseFile(ctx, filePath, excel.ParseOptions{
RequiredHeaders: ExcelTemplateHeaders(),
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"),
RawFields: map[string]string{
"ICCID": excel.Field(parsed, "ICCID"),
"MSISDN": excel.Field(parsed, "MSISDN"),
"IMSI": excel.Field(parsed, "IMSI"),
"newPackageCode": excel.Field(parsed, "newPackageCode"),
"durationMonths": excel.Field(parsed, "durationMonths"),
"giftDurationMonths": excel.Field(parsed, "giftDurationMonths"),
"effectiveMode": excel.Field(parsed, "effectiveMode"),
"offlineContractCompleted": excel.Field(parsed, "offlineContractCompleted"),
},
}
rowOptions, reasons := parseExcelSubmitOptions(parsed)
if len(reasons) > 0 {
row.AddReason(strings.Join(reasons, "; "))
rows = append(rows, row)
continue
}
row.NewPackageCode = rowOptions.NewPackageCode
row.DurationMonths = rowOptions.DurationMonths
row.GiftDurationMonths = rowOptions.GiftDurationMonths
row.EffectiveMode = rowOptions.EffectiveMode
row.OfflineContractCompleted = rowOptions.OfflineContractCompleted
if options == nil {
options = rowOptions
} else if !sameExcelSubmitOptions(options, rowOptions) {
row.AddReason("same excel batch change params must be consistent")
inconsistent = true
}
rows = append(rows, row)
}
if inconsistent {
markRowsWithoutReason(rows, "same excel batch change params must be consistent")
}
return rows, options, nil
}
func parseExcelSubmitOptions(row excel.ParsedRow) (*ExcelSubmitOptions, []string) {
var reasons []string
newPackageCode := strings.TrimSpace(excel.Field(row, "newPackageCode"))
if newPackageCode == "" {
reasons = append(reasons, "newPackageCode is required")
}
durationMonths, err := excel.ParsePositiveInt(excel.Field(row, "durationMonths"))
if err != nil {
reasons = append(reasons, "durationMonths must be a positive integer")
}
giftDurationValue := strings.TrimSpace(excel.Field(row, "giftDurationMonths"))
giftDurationMonths := 0
if giftDurationValue != "" {
giftDurationMonths, err = excel.ParseNonNegativeInt(giftDurationValue)
if err != nil {
reasons = append(reasons, "giftDurationMonths must be a non-negative integer")
}
}
effectiveMode := strings.TrimSpace(excel.Field(row, "effectiveMode"))
if effectiveMode == "" {
reasons = append(reasons, "effectiveMode is required")
}
offlineContractCompleted, err := excel.ParseBool(excel.Field(row, "offlineContractCompleted"))
if err != nil {
reasons = append(reasons, "offlineContractCompleted format is invalid")
} else if !offlineContractCompleted {
reasons = append(reasons, "offlineContractCompleted must be true")
}
if len(reasons) > 0 {
return nil, reasons
}
return &ExcelSubmitOptions{
NewPackageCode: newPackageCode,
DurationMonths: durationMonths,
GiftDurationMonths: giftDurationMonths,
EffectiveMode: effectiveMode,
OfflineContractCompleted: offlineContractCompleted,
}, nil
}
func sameExcelSubmitOptions(left, right *ExcelSubmitOptions) bool {
if left == nil || right == nil {
return false
}
return strings.TrimSpace(left.NewPackageCode) == strings.TrimSpace(right.NewPackageCode) &&
left.DurationMonths == right.DurationMonths &&
left.GiftDurationMonths == right.GiftDurationMonths &&
strings.TrimSpace(left.EffectiveMode) == strings.TrimSpace(right.EffectiveMode) &&
left.OfflineContractCompleted == right.OfflineContractCompleted
}
@@ -0,0 +1,107 @@
package change
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/frame/g"
)
// PrepareProcessor 描述 batch_type=4 的卡变更预处理批次。
type PrepareProcessor struct {
inner *Processor
repo *Repository
config batchimport.ProcessorConfig
}
func NewPrepareProcessor() *PrepareProcessor {
inner := NewProcessor()
cfg := inner.config
cfg.Code = "sim_card_change_prepare"
cfg.BatchType = consts.BatchTypePrepare
cfg.BatchNoPrefix = consts.BatchNoPrefixChange
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 {
return p.inner.ValidateRows(ctx, rows)
}
func (p *PrepareProcessor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
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.BatchTypeChange
info["targetBatchName"] = "卡变更"
if in, ok := req.BizParams.(*model.TSimCardChangeBatchPrepareParam); ok && in != nil {
if sourceBatchNo := strings.TrimSpace(in.SourceBatchNo); sourceBatchNo != "" {
info["sourceBatchNo"] = sourceBatchNo
}
}
if in, ok := req.BizParams.(*model.TSimCardChangeBatchSubmitParam); ok && in != nil {
if newPackageCode := strings.TrimSpace(in.NewPackageCode); newPackageCode != "" {
info["newPackageCode"] = newPackageCode
}
if in.DurationMonths > 0 {
info["durationMonths"] = in.DurationMonths
}
if in.GiftDurationMonths >= 0 {
info["giftDurationMonths"] = in.GiftDurationMonths
}
if effectiveMode := strings.TrimSpace(in.EffectiveMode); effectiveMode != "" {
info["effectiveMode"] = effectiveMode
}
info["offlineContractCompleted"] = in.OfflineContractCompleted
}
if snapshot, ok := req.ExecutionResult.(*model.TSimCardChangePrepareSnapshot); ok && snapshot != nil {
if snapshot.SourceBatchNo != "" {
info["sourceBatchNo"] = snapshot.SourceBatchNo
}
info["customerAgentId"] = snapshot.CustomerAgentId
info["customerName"] = snapshot.CustomerName
info["oldPackageCode"] = snapshot.OldPackageCode
info["oldPackageName"] = snapshot.OldPackageName
info["oldPackageType"] = snapshot.OldPackageType
info["oldPackageFlowMb"] = snapshot.OldPackageFlowMb
if result != nil {
info["quantity"] = result.ValidRows
}
}
return gjson.New(info)
}
func (p *PrepareProcessor) ValidatePrepareContext(ctx context.Context, tx gdb.TX, in *model.TSimCardChangeBatchPrepareParam, operator batchimport.OperatorContext, rows []*Row) (*PrepareContext, error) {
return p.inner.ValidatePrepareContext(ctx, tx, in, operator, rows)
}
@@ -0,0 +1,329 @@
package change
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/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
type Processor struct {
repo *Repository
config batchimport.ProcessorConfig
}
func NewProcessor() *Processor {
return &Processor{
repo: NewRepository(),
config: batchimport.ProcessorConfig{
Code: "sim_card_change",
BatchType: consts.BatchTypeChange,
BatchNoPrefix: consts.BatchNoPrefixChange,
TaskName: consts.ResourceBatchDownloadTaskSimCardChange,
ExportScene: consts.ResourceExportSceneChange,
ImportDir: consts.SimCardChangeSubDirImportExcel,
ResultDir: consts.SimCardChangeSubDirResultExcel,
MaxFileSize: consts.SimCardChangeMaxFileSize,
TemplateHeaders: []string{"ICCID", "MSISDN", "IMSI"},
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("sim card change does not support file import on base processor")
}
func (p *Processor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
changeRows := make([]*Row, 0, len(rows))
for _, item := range rows {
row, ok := item.(*Row)
if !ok || row == nil {
item.AddErrorReason("change row type is invalid")
continue
}
changeRows = append(changeRows, row)
}
return validateRows(changeRows)
}
func (p *Processor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
return gerror.New("sim card change must be submitted from prepare batch")
}
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 resultOss != nil {
info["resultFileOssId"] = resultOss.OssId
}
if downloadId > 0 {
info["resultDownloadId"] = downloadId
}
if in, ok := req.BizParams.(*model.TSimCardChangeBatchPrepareParam); ok && in != nil {
if sourceBatchNo := strings.TrimSpace(in.SourceBatchNo); sourceBatchNo != "" {
info["sourceBatchNo"] = sourceBatchNo
}
}
if in, ok := req.BizParams.(*model.TSimCardChangeBatchSubmitParam); ok && in != nil {
if newPackageCode := strings.TrimSpace(in.NewPackageCode); newPackageCode != "" {
info["newPackageCode"] = newPackageCode
}
if in.DurationMonths > 0 {
info["durationMonths"] = in.DurationMonths
}
if in.GiftDurationMonths >= 0 {
info["giftDurationMonths"] = in.GiftDurationMonths
}
if effectiveMode := strings.TrimSpace(in.EffectiveMode); effectiveMode != "" {
info["effectiveMode"] = effectiveMode
}
info["offlineContractCompleted"] = in.OfflineContractCompleted
}
if snapshot, ok := req.ExecutionResult.(*model.TSimCardChangePrepareSnapshot); ok && snapshot != nil {
if result != nil && result.BatchType == consts.BatchTypePrepare {
info["targetBatchType"] = consts.BatchTypeChange
info["targetBatchName"] = "sim_card_change"
}
if snapshot.SourceBatchNo != "" {
info["sourceBatchNo"] = snapshot.SourceBatchNo
}
info["customerAgentId"] = snapshot.CustomerAgentId
info["customerName"] = snapshot.CustomerName
info["oldPackageCode"] = snapshot.OldPackageCode
info["oldPackageName"] = snapshot.OldPackageName
info["oldPackageType"] = snapshot.OldPackageType
info["oldPackageFlowMb"] = snapshot.OldPackageFlowMb
info["quantity"] = snapshot.Quantity
}
if output, ok := req.ExecutionResult.(*CompleteOutput); ok && output != nil {
if output.SourceBatchNo != "" {
info["sourceBatchNo"] = output.SourceBatchNo
}
info["sourceMode"] = output.SourceMode
info["orderSn"] = output.OrderSn
info["customerAgentId"] = output.CustomerAgentId
info["customerName"] = output.CustomerName
info["oldPackageCode"] = output.OldPackageCode
info["oldPackageName"] = output.OldPackageName
info["newPackageCode"] = output.NewPackageCode
info["newPackageName"] = output.NewPackageName
info["durationMonths"] = output.DurationMonths
info["giftDurationMonths"] = output.GiftDurationMonths
info["effectiveMode"] = output.EffectiveMode
info["offlineContractCompleted"] = output.OfflineContractCompleted
info["changeUnitPrice"] = output.ChangeUnitPrice
info["amount"] = output.Amount
info["quantity"] = output.Quantity
info["packageBeginTime"] = output.PackageBeginTime
info["packageEndTime"] = output.PackageEndTime
info["changeTime"] = output.ChangeTime
}
return gjson.New(info)
}
func (p *Processor) ValidatePrepareContext(ctx context.Context, tx gdb.TX, in *model.TSimCardChangeBatchPrepareParam, operator batchimport.OperatorContext, rows []*Row) (*PrepareContext, error) {
if in == nil {
in = &model.TSimCardChangeBatchPrepareParam{}
}
sourceBatchNo := strings.TrimSpace(in.SourceBatchNo)
iccids := rowsToIccids(rows)
cardsByIccid, err := p.repo.LockCardsByIccids(ctx, tx, iccids)
if err != nil {
return nil, gerror.Wrap(err, "validate change cards failed")
}
bindsByIccid, err := p.repo.LockCurrentActiveBindsByIccids(ctx, tx, iccids)
if err != nil {
return nil, gerror.Wrap(err, "validate change subscriptions failed")
}
packageCodes := make(map[string]struct{})
customerAgentIds := make(map[int64]struct{})
oldPackageCode := ""
customerAgentId := int64(0)
for _, row := range rows {
if row == nil || row.Reason != "" {
continue
}
card := cardsByIccid[row.Iccid]
bind := bindsByIccid[row.Iccid]
reason := validateChangeRow(sourceBatchNo, operator, row, card, bind)
if reason != "" {
row.AddReason(reason)
continue
}
packageCode := strings.TrimSpace(bind.PackageCode)
packageCodes[packageCode] = struct{}{}
customerAgentIds[card.DeptId] = struct{}{}
oldPackageCode = packageCode
customerAgentId = card.DeptId
row.Msisdn = strings.TrimSpace(card.Msisdn)
row.Imsi = strings.TrimSpace(card.Imsi)
row.OldPackageCode = packageCode
row.OldSubscriptionNo = strings.TrimSpace(bind.SubscriptionNo)
row.OldOrderSn = strings.TrimSpace(bind.OrderCode)
row.OldEffectiveTime = bind.EffectiveTime
row.OldExpireTime = bind.ExpectedExpireTime
}
if len(packageCodes) > 1 {
markRowsWithoutReason(rows, "current batch contains multiple old packages and cannot be changed together")
}
if len(customerAgentIds) > 1 {
markRowsWithoutReason(rows, "current batch contains multiple customers and cannot be changed together")
}
if hasInvalidRows(rows) {
markRemainingRowsSkipped(rows)
return nil, gerror.New("some cards do not meet change conditions, batch was not executed")
}
if oldPackageCode == "" || customerAgentId <= 0 {
return nil, gerror.New("change prepare result is incomplete")
}
pkg, err := loadPackageByCode(ctx, tx, operator.TenantId, oldPackageCode)
if err != nil {
return nil, err
}
if pkg.ChangeablePackageCodes == nil || pkg.ChangeablePackageCodes.IsNil() || len(pkg.ChangeablePackageCodes.Interfaces()) == 0 {
return nil, gerror.New("current package has no changeable packages configured")
}
customerName, err := loadDeptName(ctx, tx, operator.TenantId, customerAgentId)
if err != nil {
return nil, err
}
for _, row := range rows {
if row != nil {
row.OldPackageName = pkg.PackageName
}
}
return &PrepareContext{
CustomerAgentId: customerAgentId,
CustomerName: customerName,
OldPackage: pkg,
Quantity: len(iccids),
}, nil
}
func validateChangeRow(sourceBatchNo string, operator batchimport.OperatorContext, row *Row, card *entity.TSimCardInfo, bind *entity.TSimCardBindPkg) string {
if card == nil {
return "ICCID does not exist as formal card"
}
if bind == nil {
return "ICCID current subscription data is missing or invalid"
}
if card.DeptId != operator.DeptId && card.CreatedDept != operator.DeptId {
return "ICCID does not belong to current department"
}
if strings.TrimSpace(card.CurrentPackageCode) == "" || card.PackageStatus == consts.PackageStatusNone {
return "ICCID has no current package"
}
if strings.TrimSpace(card.CurrentPackageCode) != strings.TrimSpace(bind.PackageCode) {
return "ICCID current package and subscription package do not match"
}
if sourceBatchNo != "" && strings.TrimSpace(bind.BatchNo) != sourceBatchNo {
return "ICCID current subscription does not belong to sourceBatchNo"
}
if row != nil {
if msisdn := strings.TrimSpace(row.Msisdn); msisdn != "" && msisdn != strings.TrimSpace(card.Msisdn) {
return "MSISDN does not match card fact"
}
if imsi := strings.TrimSpace(row.Imsi); imsi != "" && imsi != strings.TrimSpace(card.Imsi) {
return "IMSI does not match card fact"
}
}
return ""
}
func loadPackageByCode(ctx context.Context, tx gdb.TX, tenantId, packageCode string) (*entity.TSimFlowPackageInfo, error) {
pkg := &entity.TSimFlowPackageInfo{}
columns := dao.TSimFlowPackageInfo.Columns()
err := tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()).
Where(columns.TenantId, tenantId).
Where(columns.PackageCode, strings.TrimSpace(packageCode)).
Scan(pkg)
if err != nil {
return nil, gerror.Wrap(err, "load package failed")
}
if pkg.Id == 0 {
return nil, gerror.New("package not found")
}
return pkg, nil
}
func loadDeptName(ctx context.Context, tx gdb.TX, tenantId string, deptId int64) (string, error) {
dept := &entity.SysDept{}
columns := dao.SysDept.Columns()
err := tx.Ctx(ctx).Model(dao.SysDept.Table()).
Where(columns.TenantId, tenantId).
Where(columns.DeptId, deptId).
Scan(dept)
if err != nil {
return "", gerror.Wrap(err, "load customer failed")
}
if dept.DeptId == 0 {
return "", gerror.New("customer not found")
}
return strings.TrimSpace(dept.DeptName), nil
}
func hasInvalidRows(rows []*Row) bool {
for _, row := range rows {
if row != nil && row.Reason != "" {
return true
}
}
return false
}
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("same batch has rows failing change conditions, this row was skipped")
}
}
}
func validateRows(rows []*Row) error {
firstByIccid := make(map[string]*Row, len(rows))
for _, row := range rows {
if row == nil {
continue
}
row.Iccid = strings.TrimSpace(row.Iccid)
row.Msisdn = strings.TrimSpace(row.Msisdn)
row.Imsi = strings.TrimSpace(row.Imsi)
if row.Iccid == "" {
row.AddReason("ICCID cannot be empty")
continue
}
if first, ok := firstByIccid[row.Iccid]; ok {
row.AddReason("ICCID duplicated")
first.AddReason("ICCID duplicated")
continue
}
firstByIccid[row.Iccid] = row
}
return nil
}
@@ -0,0 +1,183 @@
package change
import (
"context"
"os"
"path/filepath"
"testing"
"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/xuri/excelize/v2"
)
func TestExcelTemplateHeaders(t *testing.T) {
got := ExcelTemplateHeaders()
want := []string{"ICCID", "MSISDN", "IMSI", "newPackageCode", "durationMonths", "giftDurationMonths", "effectiveMode", "offlineContractCompleted"}
if len(got) != len(want) {
t.Fatalf("headers len = %d, want %d", len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("header[%d] = %q, want %q", i, got[i], want[i])
}
}
}
func TestParseExcelFileRows(t *testing.T) {
path := buildTestExcelFile(t, [][]string{
ExcelTemplateHeaders(),
{"8986001", "", "", "PKG_NEW", "12", "", "immediate", "true"},
})
rows, opts, err := ParseExcelFileRows(context.Background(), path)
if err != nil {
t.Fatalf("ParseExcelFileRows error = %v", err)
}
if len(rows) != 1 {
t.Fatalf("rows len = %d, want 1", len(rows))
}
if rows[0].GiftDurationMonths != 0 {
t.Fatalf("giftDurationMonths = %d, want 0", rows[0].GiftDurationMonths)
}
if opts == nil || opts.GiftDurationMonths != 0 || !opts.OfflineContractCompleted {
t.Fatalf("opts = %#v", opts)
}
}
func TestParseExcelFileRowsRequireTrueOfflineContractCompleted(t *testing.T) {
path := buildTestExcelFile(t, [][]string{
ExcelTemplateHeaders(),
{"8986001", "", "", "PKG_NEW", "12", "", "immediate", "false"},
})
rows, _, err := ParseExcelFileRows(context.Background(), path)
if err != nil {
t.Fatalf("ParseExcelFileRows error = %v", err)
}
if len(rows) != 1 {
t.Fatalf("rows len = %d, want 1", len(rows))
}
if rows[0].Reason == "" {
t.Fatal("expected row error when offlineContractCompleted is false")
}
}
func TestValidateChangeRowSourceBatchOptional(t *testing.T) {
operator := batchimport.OperatorContext{DeptId: 100}
row := &Row{Iccid: "8986001"}
card := &entity.TSimCardInfo{
Iccid: "8986001",
Msisdn: "13800008000",
Imsi: "460001234567890",
DeptId: 100,
CurrentPackageCode: "PKG_OLD",
PackageStatus: 1,
}
bind := &entity.TSimCardBindPkg{
Iccid: "8986001",
Msisdn: "13800008000",
PackageCode: "PKG_OLD",
BatchNo: "BATCH-OTHER",
}
if reason := validateChangeRow("", operator, row, card, bind); reason != "" {
t.Fatalf("validateChangeRow without sourceBatchNo = %q, want empty", reason)
}
if reason := validateChangeRow("BATCH-REQ", operator, row, card, bind); reason == "" {
t.Fatal("expected sourceBatchNo mismatch error")
}
}
func TestValidateChangeRowIdentityMatch(t *testing.T) {
operator := batchimport.OperatorContext{DeptId: 100}
card := &entity.TSimCardInfo{
Iccid: "8986001",
Msisdn: "13800008000",
Imsi: "460001234567890",
DeptId: 100,
CurrentPackageCode: "PKG_OLD",
PackageStatus: 1,
}
bind := &entity.TSimCardBindPkg{
Iccid: "8986001",
Msisdn: "13800008000",
PackageCode: "PKG_OLD",
}
if reason := validateChangeRow("", operator, &Row{Iccid: "8986001", Msisdn: "bad"}, card, bind); reason == "" {
t.Fatal("expected MSISDN mismatch error")
}
if reason := validateChangeRow("", operator, &Row{Iccid: "8986001", Imsi: "bad"}, card, bind); reason == "" {
t.Fatal("expected IMSI mismatch error")
}
}
func TestBuildBatchInfoOmitsEmptySourceBatchNo(t *testing.T) {
p := NewProcessor()
info := p.BuildBatchInfo(context.Background(), batchimport.SubmitRequest{
BizParams: &model.TSimCardChangeBatchSubmitParam{
NewPackageCode: "PKG_NEW",
DurationMonths: 12,
GiftDurationMonths: 0,
EffectiveMode: "immediate",
OfflineContractCompleted: true,
},
ExecutionResult: &CompleteOutput{
SourceBatchNo: "",
SourceMode: 0,
NewPackageCode: "PKG_NEW",
NewPackageName: "New Package",
OldPackageCode: "PKG_OLD",
OldPackageName: "Old Package",
DurationMonths: 12,
GiftDurationMonths: 0,
EffectiveMode: "immediate",
OfflineContractCompleted: true,
},
}, nil, nil, nil, 0)
if info == nil {
t.Fatal("info is nil")
}
data := info.Map()
if _, ok := data["sourceBatchNo"]; ok {
t.Fatalf("sourceBatchNo should be omitted when empty: %#v", data)
}
if _, ok := data["remark"]; ok {
t.Fatalf("remark should not exist: %#v", data)
}
if _, ok := data["buyerNote"]; ok {
t.Fatalf("buyerNote should not exist: %#v", data)
}
if _, ok := data["adminNote"]; ok {
t.Fatalf("adminNote should not exist: %#v", data)
}
}
func buildTestExcelFile(t *testing.T, rows [][]string) string {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "change.xlsx")
f := excelize.NewFile()
for i, row := range rows {
cell, err := excelize.CoordinatesToCellName(1, i+1)
if err != nil {
t.Fatalf("CoordinatesToCellName error = %v", err)
}
if err = f.SetSheetRow("Sheet1", cell, &row); err != nil {
t.Fatalf("SetSheetRow error = %v", err)
}
}
if err := f.SaveAs(path); err != nil {
t.Fatalf("SaveAs error = %v", err)
}
_ = f.Close()
if _, err := os.Stat(path); err != nil {
t.Fatalf("Stat error = %v", err)
}
return path
}
@@ -0,0 +1,32 @@
package change
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common/cardsource"
)
const maxRangeRows = 100000
func BuildRangeRows(ctx context.Context, startIccid, endIccid, startMsisdn, endMsisdn, startImsi, endImsi string) ([]batchimport.Row, error) {
return cardsource.BuildRangeRows(ctx, cardsource.RangeConfig{
StartIccid: startIccid,
EndIccid: endIccid,
StartMsisdn: startMsisdn,
EndMsisdn: endMsisdn,
StartImsi: startImsi,
EndImsi: endImsi,
MaxRows: maxRangeRows,
EmptyMessage: "range change data is empty",
OverLimitMessage: "range change count cannot exceed %d",
CountMismatchMessage: "ICCID range count(%d), MSISDN range count(%d), IMSI range count(%d) must match",
}, func(rowNo int, iccid, msisdn, imsi string) *Row {
return &Row{
RowNo: rowNo,
Iccid: iccid,
Msisdn: msisdn,
Imsi: imsi,
}
})
}
@@ -0,0 +1,251 @@
package change
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/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"github.com/gogf/gf/v2/database/gdb"
)
type Repository struct{}
func NewRepository() *Repository {
return &Repository{}
}
// GetSourceBindRows 获取源绑定记录
func (r *Repository) GetSourceBindRows(ctx context.Context, batchNo string) ([]*Row, error) {
var binds []*entity.TSimCardBindPkg
columns := dao.TSimCardBindPkg.Columns()
err := dao.TSimCardBindPkg.Ctx(ctx).
Fields(
columns.Id,
columns.Iccid,
columns.Msisdn,
columns.PackageCode,
columns.SubscriptionNo,
columns.OrderCode,
columns.EffectiveTime,
columns.ExpectedExpireTime,
).
Where(columns.BatchNo, strings.TrimSpace(batchNo)).
Where(columns.TenantId, contexts.GetTenantId(ctx)).
WhereIn(columns.EffectiveStatus, []int{consts.EffectiveStatusPending, consts.EffectiveStatusEffective}).
OrderAsc(columns.Id).
Scan(&binds)
if err != nil {
return nil, err
}
rows := make([]*Row, 0, len(binds))
for i, bind := range binds {
if bind == nil {
continue
}
rows = append(rows, &Row{
RowNo: i + 1,
Iccid: strings.TrimSpace(bind.Iccid),
Msisdn: strings.TrimSpace(bind.Msisdn),
OldPackageCode: strings.TrimSpace(bind.PackageCode),
OldSubscriptionNo: strings.TrimSpace(bind.SubscriptionNo),
OldOrderSn: strings.TrimSpace(bind.OrderCode),
OldEffectiveTime: bind.EffectiveTime,
OldExpireTime: bind.ExpectedExpireTime,
})
}
return rows, nil
}
// GetChangeBatchRows 获取变更批次记录
func (r *Repository) GetChangeBatchRows(ctx context.Context, batchNo string, batchType int) ([]*Row, error) {
var details []*entity.BatchDetail
columns := dao.BatchDetail.Columns()
if batchType == 0 {
batchType = consts.BatchTypeChange
}
err := dao.BatchDetail.Ctx(ctx).
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
}
// detailsToRows 转换变更批次记录为行数据
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())
}
row.OldPackageCode = strings.TrimSpace(detail.Snapshot.Get("oldPackageCode").String())
row.OldPackageName = strings.TrimSpace(detail.Snapshot.Get("oldPackageName").String())
row.OldSubscriptionNo = strings.TrimSpace(detail.Snapshot.Get("oldSubscriptionNo").String())
row.OldOrderSn = strings.TrimSpace(detail.Snapshot.Get("oldOrderSn").String())
}
rows = append(rows, row)
}
return rows
}
// LockCardsByIccids 锁定ICCID对应的SIM卡信息
func (r *Repository) LockCardsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*entity.TSimCardInfo, error) {
result := make(map[string]*entity.TSimCardInfo)
uniqueIccids := uniqueStrings(iccids)
if len(uniqueIccids) == 0 {
return result, nil
}
var list []*entity.TSimCardInfo
if err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
WhereIn(dao.TSimCardInfo.Columns().Iccid, uniqueIccids).
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
}
// LockCurrentActiveBindsByIccids 锁定ICCID对应的当前有效绑定记录
func (r *Repository) LockCurrentActiveBindsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*entity.TSimCardBindPkg, error) {
result := make(map[string]*entity.TSimCardBindPkg)
priorities := make(map[string]int)
uniqueIccids := uniqueStrings(iccids)
if len(uniqueIccids) == 0 {
return result, nil
}
var list []*entity.TSimCardBindPkg
if err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
WhereIn(dao.TSimCardBindPkg.Columns().Iccid, uniqueIccids).
WhereIn(dao.TSimCardBindPkg.Columns().EffectiveStatus, []int{consts.EffectiveStatusPending, consts.EffectiveStatusEffective}).
OrderDesc(dao.TSimCardBindPkg.Columns().EffectiveStatus).
OrderAsc(dao.TSimCardBindPkg.Columns().EffectiveTime).
OrderAsc(dao.TSimCardBindPkg.Columns().Id).
LockUpdate().
Scan(&list); err != nil {
return nil, err
}
for _, item := range list {
if item == nil || strings.TrimSpace(item.Iccid) == "" {
continue
}
iccid := strings.TrimSpace(item.Iccid)
priority := bindPriority(item.EffectiveStatus)
currentPriority, exists := priorities[iccid]
if exists && priority < currentPriority {
continue
}
if exists && priority == currentPriority {
result[iccid] = nil
continue
}
priorities[iccid] = priority
result[iccid] = item
}
return result, nil
}
// LockPendingBindsByIccids 锁定ICCID对应的待生效绑定记录
func (r *Repository) LockPendingBindsByIccids(ctx context.Context, tx gdb.TX, iccids []string) ([]*entity.TSimCardBindPkg, error) {
uniqueIccids := uniqueStrings(iccids)
if len(uniqueIccids) == 0 {
return nil, nil
}
var list []*entity.TSimCardBindPkg
if err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
WhereIn(dao.TSimCardBindPkg.Columns().Iccid, uniqueIccids).
Where(dao.TSimCardBindPkg.Columns().EffectiveStatus, consts.EffectiveStatusPending).
OrderAsc(dao.TSimCardBindPkg.Columns().EffectiveTime).
OrderAsc(dao.TSimCardBindPkg.Columns().Id).
LockUpdate().
Scan(&list); err != nil {
return nil, err
}
return list, nil
}
// DeleteBindsBySubscriptionNos 删除指定订阅号的绑定记录
func (r *Repository) DeleteBindsBySubscriptionNos(ctx context.Context, tx gdb.TX, subscriptionNos []string) (int64, error) {
uniqueNos := uniqueStrings(subscriptionNos)
if len(uniqueNos) == 0 {
return 0, nil
}
result, err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
WhereIn(dao.TSimCardBindPkg.Columns().SubscriptionNo, uniqueNos).
Delete()
if err != nil {
return 0, err
}
return result.RowsAffected()
}
// ActiveBindCountsByOrderCodes 获取指定订单号的当前有效绑定记录数量
func (r *Repository) ActiveBindCountsByOrderCodes(ctx context.Context, tx gdb.TX, orderCodes []string) (map[string]int, error) {
result := make(map[string]int)
uniqueCodes := uniqueStrings(orderCodes)
if len(uniqueCodes) == 0 {
return result, nil
}
type row struct {
OrderCode string `orm:"order_code"`
Count int `orm:"count"`
}
var rows []row
if err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
Fields(dao.TSimCardBindPkg.Columns().OrderCode, "COUNT(1) AS count").
WhereIn(dao.TSimCardBindPkg.Columns().OrderCode, uniqueCodes).
Group(dao.TSimCardBindPkg.Columns().OrderCode).
Scan(&rows); err != nil {
return nil, err
}
for _, item := range rows {
result[strings.TrimSpace(item.OrderCode)] = item.Count
}
return result, nil
}
// bindPriority 获取绑定记录的优先级
func bindPriority(status int) int {
switch status {
case consts.EffectiveStatusEffective:
return 2
case consts.EffectiveStatusPending:
return 1
default:
return 0
}
}
@@ -0,0 +1,133 @@
package change
import (
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type Row struct {
RowNo int
Iccid string
Msisdn string
Imsi string
NewPackageCode string
DurationMonths int
GiftDurationMonths int
EffectiveMode string
OfflineContractCompleted bool
OldPackageCode string
OldPackageName string
OldSubscriptionNo string
OldOrderSn string
OldEffectiveTime *gtime.Time
OldExpireTime *gtime.Time
RawFields map[string]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,
"newPackageCode": r.NewPackageCode,
"durationMonths": r.DurationMonths,
"giftDurationMonths": r.GiftDurationMonths,
"effectiveMode": r.EffectiveMode,
"offlineContractCompleted": r.OfflineContractCompleted,
"oldPackageCode": r.OldPackageCode,
"oldPackageName": r.OldPackageName,
"oldSubscriptionNo": r.OldSubscriptionNo,
"oldOrderSn": r.OldOrderSn,
"oldEffectiveTime": formatTime(r.OldEffectiveTime),
"oldExpectedExpireTime": formatTime(r.OldExpireTime),
"rawFields": r.RawFields,
"rowNo": r.RowNo,
})
}
type PrepareContext struct {
CustomerAgentId int64
CustomerName string
OldPackage *entity.TSimFlowPackageInfo
Quantity int
}
type CompleteInput struct {
SubmitParam *model.TSimCardChangeBatchSubmitParam
BatchNo string
SourceBatchNo string
Rows []*Row
Operator batchimport.OperatorContext
PrepareSnapshot *model.TSimCardChangePrepareSnapshot
PriceSnapshot *model.TSimCardChangePriceSnapshot
PackageBeginTime *gtime.Time
}
type CompleteOutput struct {
BatchNo string
BatchType int
SourceBatchNo string
SourceMode int
Quantity int
CustomerAgentId int64
CustomerName string
OldPackageCode string
OldPackageName string
NewPackageCode string
NewPackageName string
OrderSn string
Amount float64
ChangeUnitPrice float64
DurationMonths int
GiftDurationMonths int
EffectiveMode string
OfflineContractCompleted bool
PackageBeginTime *gtime.Time
PackageEndTime *gtime.Time
ChangeTime *gtime.Time
}
@@ -0,0 +1,50 @@
package change
import (
"sort"
"strings"
"github.com/gogf/gf/v2/os/gtime"
)
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
}
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 formatTime(t *gtime.Time) string {
if t == nil {
return ""
}
return t.Format("Y-m-d H:i:s")
}