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,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,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")
|
||||
}
|
||||
Reference in New Issue
Block a user