Files

1094 lines
40 KiB
Go

package iot
import (
"context"
"database/sql"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
"strings"
"time"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/packageduration"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/change"
"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"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/iot_excelize"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"github.com/xuri/excelize/v2"
)
const (
cardChangeEffectiveImmediate = "immediate"
cardChangeEffectiveNextMonth = "next_month"
)
type sIotTSimCardChange struct{}
func NewIotTSimCardChange() *sIotTSimCardChange {
return &sIotTSimCardChange{}
}
func init() {
service.RegisterIotTSimCardChange(NewIotTSimCardChange())
}
func (s *sIotTSimCardChange) DownloadExcelTemplate(ctx context.Context) error {
f := excelize.NewFile()
defer func() { _ = f.Close() }()
headers := change.ExcelTemplateHeaders()
if err := f.SetSheetRow("Sheet1", "A1", &headers); err != nil {
return err
}
if err := iot_excelize.ApplyHeaderColumnsTextStyle(f); err != nil {
return err
}
return iot_excelize.WriteHTTP(ctx, f, "sim_card_change_template")
}
// SubmitExcel Excel 仅作为 prepare source:构造 batch_type=4 的 Pending 变更预处理批次。
func (s *sIotTSimCardChange) SubmitExcel(ctx context.Context, in *model.TSimCardChangeExcelSubmitParam) (*model.TSimCardChangeExcelSubmitRes, error) {
if in == nil {
in = &model.TSimCardChangeExcelSubmitParam{}
}
file, err := s.getChangeUploadFile(ctx)
if err != nil {
return nil, err
}
excelProcessor := change.NewExcelProcessor()
processor := change.NewPrepareProcessor()
req := batchimport.SubmitRequest{
SourceChannel: batchmetadata.Console,
ExecutionMode: batchmetadata.Bulk,
File: file,
SourceType: consts.BatchSourceTypeFile,
Operator: currentOperator(ctx),
BeforeSave: func(ctx context.Context, tx gdb.TX, request *batchimport.SubmitRequest, rows []batchimport.Row) error {
opts := excelProcessor.ExcelSubmitOptions()
if opts == nil {
return gerror.New("excel change params are empty")
}
submitParam := opts.ToBatchSubmitParam()
request.BizParams = submitParam
changeRows, err := batchRowsToChangeRows(rows)
if err != nil {
return err
}
return s.prepareChangeRows(ctx, tx, request, processor, submitParam, changeRows)
},
}
result, err := batchimport.CreatePendingUpload(ctx, req, processor, batchimport.PendingOptions{
PrepareRows: func(ctx context.Context, rows []batchimport.Row) error {
if len(rows) == 0 {
return nil
}
return excelProcessor.ValidateRows(ctx, rows)
},
})
if err != nil {
return nil, err
}
res := changeExcelSubmitResult(result)
if result.ImportStatus != consts.ImportStatusSuccess {
return res, nil
}
if err = s.mergeChangeExcelBatchFileInfo(ctx, result.BatchNo, result.ImportFileOssId, result.ResultFileOssId, result.ResultDownloadId); err != nil {
return nil, err
}
publishPendingExecute(ctx, result.BatchNo, consts.BatchTypePrepare, contexts.GetTenantId(ctx))
return res, nil
}
func (s *sIotTSimCardChange) PrepareBatch(ctx context.Context, in *model.TSimCardChangeBatchPrepareParam) (*model.TSimCardChangeBatchPrepareRes, error) {
if in == nil {
in = &model.TSimCardChangeBatchPrepareParam{}
}
in.SourceBatchNo = strings.TrimSpace(in.SourceBatchNo)
if in.SourceBatchNo == "" {
return nil, gerror.New("sourceBatchNo cannot be empty")
}
rows, err := s.buildChangeRows(ctx, in.SourceBatchNo)
if err != nil {
return nil, err
}
return s.prepareRows(ctx, &model.TSimCardChangeBatchSubmitParam{}, rows, consts.BatchSourceTypeBatchSelect, in.SourceBatchNo, in.SourceBatchNo, &model.TSimCardChangeBatchPrepareParam{
SourceBatchNo: in.SourceBatchNo,
})
}
func (s *sIotTSimCardChange) PrepareInput(ctx context.Context, in *model.TSimCardChangeInputPrepareParam) (*model.TSimCardChangeBatchPrepareRes, error) {
if in == nil {
in = &model.TSimCardChangeInputPrepareParam{}
}
rows, err := change.ParseInputRows(ctx, in.Input)
if err != nil {
return nil, err
}
changeRows, err := batchRowsToChangeRows(rows)
if err != nil {
return nil, err
}
return s.prepareRows(ctx, changeSubmitParamFromInput(in), changeRows, consts.BatchSourceTypeBatchInput, "", "input-prepare", nil)
}
func (s *sIotTSimCardChange) PrepareRange(ctx context.Context, in *model.TSimCardChangeRangePrepareParam) (*model.TSimCardChangeBatchPrepareRes, error) {
if in == nil {
in = &model.TSimCardChangeRangePrepareParam{}
}
rows, err := change.BuildRangeRows(ctx, in.StartIccid, in.EndIccid, in.StartMsisdn, in.EndMsisdn, in.StartImsi, in.EndImsi)
if err != nil {
return nil, err
}
changeRows, err := batchRowsToChangeRows(rows)
if err != nil {
return nil, err
}
return s.prepareRows(ctx, changeSubmitParamFromRange(in), changeRows, consts.BatchSourceTypeRange, "", "range-prepare", nil)
}
func (s *sIotTSimCardChange) InputOptions(ctx context.Context, in *model.TSimCardChangeInputOptionsParam) (*model.TSimCardChangeOptionsRes, error) {
if in == nil {
in = &model.TSimCardChangeInputOptionsParam{}
}
rows, err := change.ParseInputRows(ctx, in.Input)
if err != nil {
return nil, err
}
changeRows, err := batchRowsToChangeRows(rows)
if err != nil {
return nil, err
}
return s.changeOptions(ctx, changeRows, "")
}
func (s *sIotTSimCardChange) RangeOptions(ctx context.Context, in *model.TSimCardChangeRangeOptionsParam) (*model.TSimCardChangeOptionsRes, error) {
if in == nil {
in = &model.TSimCardChangeRangeOptionsParam{}
}
rows, err := change.BuildRangeRows(ctx, in.StartIccid, in.EndIccid, in.StartMsisdn, in.EndMsisdn, in.StartImsi, in.EndImsi)
if err != nil {
return nil, err
}
changeRows, err := batchRowsToChangeRows(rows)
if err != nil {
return nil, err
}
return s.changeOptions(ctx, changeRows, "")
}
func (s *sIotTSimCardChange) changeOptions(ctx context.Context, rows []*change.Row, sourceBatchNo string) (*model.TSimCardChangeOptionsRes, error) {
if len(rows) == 0 {
return nil, gerror.New("change cards cannot be empty")
}
processor := change.NewPrepareProcessor()
operator := currentOperator(ctx)
var prepared *change.PrepareContext
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var err error
prepared, err = processor.ValidatePrepareContext(ctx, tx, &model.TSimCardChangeBatchPrepareParam{
SourceBatchNo: strings.TrimSpace(sourceBatchNo),
}, operator, rows)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
if prepared == nil || prepared.OldPackage == nil {
return nil, gerror.New("change prepare result is incomplete")
}
packages, err := s.loadChangeablePackageOptions(ctx, prepared.CustomerAgentId, prepared.OldPackage)
if err != nil {
return nil, err
}
if len(packages) == 0 {
return nil, gerror.New("current package has no available changeable packages")
}
return &model.TSimCardChangeOptionsRes{
CustomerAgentId: prepared.CustomerAgentId,
CustomerName: prepared.CustomerName,
OldPackageCode: prepared.OldPackage.PackageCode,
OldPackageName: prepared.OldPackage.PackageName,
OldPackageType: prepared.OldPackage.PackageType,
Quantity: prepared.Quantity,
Packages: packages,
}, nil
}
func (s *sIotTSimCardChange) prepareRows(ctx context.Context, submitParam *model.TSimCardChangeBatchSubmitParam, rows []*change.Row, sourceType int, sourceBatchNo string, sourceName string, prepareParam *model.TSimCardChangeBatchPrepareParam) (*model.TSimCardChangeBatchPrepareRes, error) {
if submitParam == nil {
submitParam = &model.TSimCardChangeBatchSubmitParam{}
}
processor := change.NewPrepareProcessor()
importRows := changeRowsToBatchRows(rows)
req := batchimport.SubmitRequest{
SourceChannel: batchmetadata.Console,
ExecutionMode: batchmetadata.Bulk,
BizParams: prepareParamOrSubmitParam(prepareParam, submitParam),
SourceType: sourceType,
SourceBatchNo: strings.TrimSpace(sourceBatchNo),
Operator: currentOperator(ctx),
BeforeSave: func(ctx context.Context, tx gdb.TX, request *batchimport.SubmitRequest, batchRows []batchimport.Row) error {
changeRows, err := batchRowsToChangeRows(batchRows)
if err != nil {
return err
}
return s.prepareChangeRows(ctx, tx, request, processor, submitParam, changeRows)
},
}
result, err := batchimport.CreatePendingRows(ctx, req, processor, importRows, batchimport.PendingOptions{
PrepareRows: func(ctx context.Context, rows []batchimport.Row) error {
if len(rows) == 0 {
return nil
}
return processor.ValidateRows(ctx, rows)
},
})
if err != nil {
return nil, err
}
res := &model.TSimCardChangeBatchPrepareRes{
BatchNo: result.BatchNo,
BatchType: result.BatchType,
TargetBatchType: consts.BatchTypeChange,
Status: pendingStatus(result.ImportStatus),
BatchAsyncStatus: consts.BatchAsyncStatusPending,
RunResult: consts.BatchRunResultPending,
Total: result.TotalRows,
ValidCount: result.ValidRows,
FailCount: result.ErrorRows,
ResultDownloadId: result.ResultDownloadId,
ResultFileOssId: result.ResultFileOssId,
}
if snapshot, ok := result.ExecutionResult.(*model.TSimCardChangePrepareSnapshot); ok && snapshot != nil {
fillChangePrepareRes(res, snapshot)
}
publishPendingExecute(ctx, res.BatchNo, res.BatchType, contexts.GetTenantId(ctx))
return res, nil
}
func (s *sIotTSimCardChange) prepareChangeRows(ctx context.Context, tx gdb.TX, request *batchimport.SubmitRequest, processor *change.PrepareProcessor, submitParam *model.TSimCardChangeBatchSubmitParam, rows []*change.Row) error {
prepared, err := processor.ValidatePrepareContext(ctx, tx, &model.TSimCardChangeBatchPrepareParam{
SourceBatchNo: request.SourceBatchNo,
}, request.Operator, rows)
if err != nil {
return err
}
if prepared == nil || prepared.CustomerAgentId <= 0 || prepared.OldPackage == nil || strings.TrimSpace(prepared.OldPackage.PackageCode) == "" {
return gerror.New("change prepare result is incomplete")
}
if submitParam != nil && strings.TrimSpace(submitParam.NewPackageCode) != "" {
if strings.TrimSpace(submitParam.NewPackageCode) == strings.TrimSpace(prepared.OldPackage.PackageCode) {
return gerror.New("newPackageCode cannot equal current package")
}
if !containsStringJSON(prepared.OldPackage.ChangeablePackageCodes, submitParam.NewPackageCode) {
return gerror.New("newPackageCode is not in current package changeable range")
}
}
request.TargetDeptId = prepared.CustomerAgentId
request.ExecutionResult = &model.TSimCardChangePrepareSnapshot{
SourceBatchNo: strings.TrimSpace(request.SourceBatchNo),
CustomerAgentId: prepared.CustomerAgentId,
CustomerName: prepared.CustomerName,
OldPackageCode: prepared.OldPackage.PackageCode,
OldPackageName: prepared.OldPackage.PackageName,
OldPackageType: prepared.OldPackage.PackageType,
OldPackageFlowMb: prepared.OldPackage.FlowMb,
Quantity: prepared.Quantity,
}
return nil
}
// SubmitBatch 作为 confirm 的语义:CAS 转换 batch_type=4+Success -> batch_type=102 Pending,发布异步任务。
func (s *sIotTSimCardChange) SubmitBatch(ctx context.Context, in *model.TSimCardChangeBatchSubmitParam) (*model.TSimCardChangeBatchSubmitRes, error) {
if in == nil {
in = &model.TSimCardChangeBatchSubmitParam{}
}
in.BatchNo = strings.TrimSpace(in.BatchNo)
in.NewPackageCode = strings.TrimSpace(in.NewPackageCode)
in.EffectiveMode = strings.TrimSpace(in.EffectiveMode)
if in.BatchNo == "" {
return nil, gerror.New("batchNo cannot be empty")
}
if in.NewPackageCode == "" {
return nil, gerror.New("newPackageCode cannot be empty")
}
if in.DurationMonths <= 0 {
return nil, gerror.New("durationMonths must be greater than 0")
}
if in.GiftDurationMonths < 0 {
return nil, gerror.New("giftDurationMonths cannot be negative")
}
if !in.OfflineContractCompleted {
return nil, gerror.New("please confirm offline contract is completed")
}
packageBeginTime, err := calcChangeBeginTime(in.EffectiveMode)
if err != nil {
return nil, err
}
operator := currentOperator(ctx)
var prepareSnapshot *model.TSimCardChangePrepareSnapshot
var priceSnapshot *model.TSimCardChangePriceSnapshot
var converted *change.AsyncConfirmResult
var sourceBatchNo string
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
batch, err := s.lockPrepareBatchForConfirm(ctx, tx, in.BatchNo)
if err != nil {
return err
}
prepareSnapshot, err = changePrepareSnapshotFromBatch(batch)
if err != nil {
return err
}
priceSnapshot, err = changePriceSnapshotFromBatch(batch)
if err != nil {
return err
}
priceSnapshot, err = s.refreshChangeConfirmPriceSnapshot(ctx, tx, prepareSnapshot, priceSnapshot)
if err != nil {
return err
}
sourceBatchNo = prepareSnapshot.SourceBatchNo
if err = validateChangeConfirmContract(
prepareSnapshot,
priceSnapshot,
jsonString(batch.BatchInfo, "effectiveMode", "changePrepareSnapshot.effectiveMode"),
in,
); err != nil {
return err
}
converted, err = change.ConvertPrepareToAsync(ctx, tx, &change.AsyncConfirmInput{
BatchNo: in.BatchNo,
TargetBatchType: consts.BatchTypeChange,
NewPackageCode: priceSnapshot.NewPackageCode,
DurationMonths: in.DurationMonths,
GiftDurationMonths: in.GiftDurationMonths,
EffectiveMode: in.EffectiveMode,
OfflineContract: in.OfflineContractCompleted,
BuyerNote: in.BuyerNote,
AdminNote: in.AdminNote,
Remark: in.Remark,
PriceSnapshot: priceSnapshot,
SourceBatchNo: prepareSnapshot.SourceBatchNo,
PackageBeginTimeStr: formatChangeTime(packageBeginTime),
Operator: operator,
})
return err
})
if err != nil {
return nil, err
}
if converted == nil || prepareSnapshot == nil || priceSnapshot == nil {
return nil, gerror.New("change submit result is empty")
}
publishPendingExecute(ctx, converted.BatchNo, converted.BatchType, contexts.GetTenantId(ctx))
return &model.TSimCardChangeBatchSubmitRes{
BatchNo: converted.BatchNo,
BatchType: converted.BatchType,
BatchAsyncStatus: converted.BatchAsyncStatus,
SourceBatchNo: sourceBatchNo,
Status: "pending",
NewPackageCode: priceSnapshot.NewPackageCode,
NewPackageName: priceSnapshot.NewPackageName,
OldPackageCode: prepareSnapshot.OldPackageCode,
OldPackageName: prepareSnapshot.OldPackageName,
DurationMonths: in.DurationMonths,
GiftDurationMonths: in.GiftDurationMonths,
EffectiveMode: in.EffectiveMode,
PackageBeginTime: packageBeginTime,
PackageEndTime: nil,
}, nil
}
func validateChangeConfirmContract(
prepareSnapshot *model.TSimCardChangePrepareSnapshot,
priceSnapshot *model.TSimCardChangePriceSnapshot,
preparedEffectiveMode string,
in *model.TSimCardChangeBatchSubmitParam,
) error {
if prepareSnapshot == nil || priceSnapshot == nil || in == nil {
return gerror.New("change prepare contract is incomplete")
}
if strings.TrimSpace(in.NewPackageCode) == strings.TrimSpace(prepareSnapshot.OldPackageCode) {
return gerror.New("newPackageCode cannot equal current package")
}
if strings.TrimSpace(in.NewPackageCode) != strings.TrimSpace(priceSnapshot.NewPackageCode) {
return gerror.New("newPackageCode differs from prepare contract, please prepare again")
}
if !containsInt(priceSnapshot.RenewableDurationOptions, in.DurationMonths) {
return gerror.New("durationMonths is not allowed for target package")
}
preparedEffectiveMode = strings.TrimSpace(preparedEffectiveMode)
if preparedEffectiveMode != "" && preparedEffectiveMode != strings.TrimSpace(in.EffectiveMode) {
return gerror.New("effectiveMode differs from prepare contract, please prepare again")
}
return nil
}
// PrepareResult 查询预处理批次当前异步状态。
func (s *sIotTSimCardChange) PrepareResult(ctx context.Context, batchNo string) (*model.TSimCardChangePrepareResultRes, error) {
return loadPrepareResultChange(ctx, batchNo)
}
func (s *sIotTSimCardChange) refreshChangeConfirmPriceSnapshot(ctx context.Context, tx gdb.TX, prepareSnapshot *model.TSimCardChangePrepareSnapshot, priceSnapshot *model.TSimCardChangePriceSnapshot) (*model.TSimCardChangePriceSnapshot, error) {
if prepareSnapshot == nil || priceSnapshot == nil {
return nil, gerror.New("change prepare contract is incomplete")
}
oldPackageCode := strings.TrimSpace(prepareSnapshot.OldPackageCode)
newPackageCode := strings.TrimSpace(priceSnapshot.NewPackageCode)
if oldPackageCode == "" || newPackageCode == "" {
return nil, gerror.New("change package contract is incomplete")
}
if oldPackageCode == newPackageCode {
return nil, gerror.New("newPackageCode cannot equal current package")
}
oldPackage := &entity.TSimFlowPackageInfo{}
columns := dao.TSimFlowPackageInfo.Columns()
if err := tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()).
Where(columns.TenantId, contexts.GetTenantId(ctx)).
Where(columns.PackageCode, oldPackageCode).
Where(columns.Enabled, 1).
LockUpdate().
Scan(oldPackage); err != nil {
return nil, gerror.Wrap(err, "load current package failed")
}
if oldPackage.Id == 0 {
return nil, gerror.New("current package not found or disabled")
}
if !containsStringJSON(oldPackage.ChangeablePackageCodes, newPackageCode) {
return nil, gerror.Newf("target package %s is not in current package %s changeable range", newPackageCode, oldPackageCode)
}
refreshed, err := s.buildChangePriceSnapshot(ctx, tx, priceSnapshot.CustomerAgentId, newPackageCode, prepareSnapshot.OldPackageType)
if err != nil {
return nil, err
}
return refreshed, nil
}
func loadPrepareResultChange(ctx context.Context, batchNo string) (*model.TSimCardChangePrepareResultRes, error) {
batchNo = strings.TrimSpace(batchNo)
if batchNo == "" {
return nil, gerror.New("批次号不能为空")
}
columns := dao.BatchMain.Columns()
batch := &entity.BatchMain{}
if err := dao.BatchMain.Ctx(ctx).
Where(columns.BatchNo, batchNo).
Where(columns.TenantId, contexts.GetTenantId(ctx)).
Where(columns.CreatedDept, contexts.GetDeptId(ctx)).
Scan(batch); err != nil {
return nil, gerror.Wrap(err, "查询预处理批次失败")
}
if batch.Id == 0 {
return nil, gerror.New("预处理批次不存在或不可见")
}
if batch.BatchType != consts.BatchTypePrepare && !(batch.BatchInfo != nil && batch.BatchInfo.Get("targetBatchType").Int() == consts.BatchTypeChange) {
return nil, gerror.New("批次类型不匹配")
}
asyncStatus := consts.BatchAsyncStatusPending
runResult := consts.BatchRunResultPending
statusKey := "pending"
switch {
case batch.BatchAsyncStatus == consts.BatchAsyncStatusProcessing || batch.RunResult == consts.BatchRunResultRunning:
asyncStatus = consts.BatchAsyncStatusProcessing
runResult = consts.BatchRunResultRunning
statusKey = "processing"
case batch.BatchAsyncStatus == consts.BatchAsyncStatusSuccess && batch.RunResult == consts.BatchRunResultSuccess:
asyncStatus = consts.BatchAsyncStatusSuccess
runResult = consts.BatchRunResultSuccess
statusKey = "success"
case batch.BatchAsyncStatus == consts.BatchAsyncStatusFailed || batch.RunResult == consts.BatchRunResultFailed:
asyncStatus = consts.BatchAsyncStatusFailed
runResult = consts.BatchRunResultFailed
statusKey = "failed"
case batch.BatchAsyncStatus == consts.BatchAsyncStatusPartialSuccess || batch.RunResult == consts.BatchRunResultPartialSuccess:
asyncStatus = consts.BatchAsyncStatusPartialSuccess
runResult = consts.BatchRunResultPartialSuccess
statusKey = "partial_success"
}
fields := buildChangePrepareFields(batch)
res := &model.TSimCardChangePrepareResultRes{
BatchNo: batch.BatchNo,
BatchType: batch.BatchType,
TargetBatchType: consts.BatchTypeChange,
Status: statusKey,
BatchAsyncStatus: asyncStatus,
RunResult: runResult,
Total: batch.TotalCount,
SuccessCount: batch.SuccessCount,
FailCount: batch.FailCount,
FailReason: strings.TrimSpace(batch.FailReason),
StartTime: batch.StartTime,
EndTime: batch.EndTime,
Snapshot: gjson.New(fields),
}
if fields != nil {
res.CustomerAgentId = gconv.Int64(fields["customerAgentId"])
res.CustomerName = strings.TrimSpace(gconv.String(fields["customerName"]))
res.PackageCode = strings.TrimSpace(gconv.String(fields["newPackageCode"]))
res.PackageName = strings.TrimSpace(gconv.String(fields["newPackageName"]))
res.OldPackageCode = strings.TrimSpace(gconv.String(fields["oldPackageCode"]))
res.OldPackageName = strings.TrimSpace(gconv.String(fields["oldPackageName"]))
res.OldPackageType = gconv.Int(fields["oldPackageType"])
res.OldPackageFlowMb = gconv.Float64(fields["oldPackageFlowMb"])
res.Quantity = gconv.Int(fields["quantity"])
res.PriceSource = strings.TrimSpace(gconv.String(fields["priceSource"]))
res.AgentDefaultPriceId = gconv.Int64(fields["agentDefaultPriceId"])
res.TargetPriceId = gconv.Int64(fields["targetPriceId"])
res.ChangeUnitPrice = gconv.Float64(fields["changeUnitPrice"])
res.RenewableDurationOptions = gconv.Ints(fields["renewableDurationOptions"])
}
return res, nil
}
func (s *sIotTSimCardChange) buildChangeRows(ctx context.Context, sourceBatchNo string) ([]*change.Row, error) {
rows, err := change.NewRepository().GetSourceBindRows(ctx, sourceBatchNo)
if err != nil {
return nil, gerror.Wrap(err, "load source order batch card details failed")
}
if len(rows) == 0 {
return nil, gerror.New("source order batch has no changeable cards")
}
return rows, nil
}
func (s *sIotTSimCardChange) lockPrepareBatchForConfirm(ctx context.Context, tx gdb.TX, batchNo string) (*entity.BatchMain, error) {
columns := dao.BatchMain.Columns()
batch := &entity.BatchMain{}
err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(columns.BatchNo, batchNo).
Where(columns.BatchType, consts.BatchTypePrepare).
Where(columns.BatchAsyncStatus, consts.BatchAsyncStatusSuccess).
Where(columns.RunResult, consts.BatchRunResultSuccess).
Where(columns.TenantId, contexts.GetTenantId(ctx)).
Where(columns.CreatedDept, contexts.GetDeptId(ctx)).
LockUpdate().
Scan(batch)
if err != nil {
return nil, gerror.Wrap(err, "load change prepare batch failed")
}
if batch.Id == 0 {
return nil, gerror.New("change prepare batch not found or not yet completed")
}
if batch.BatchInfo == nil {
return nil, gerror.New("change prepare batch info is missing")
}
if batch.BatchInfo.Get("targetBatchType").Int() != consts.BatchTypeChange {
return nil, gerror.New("batch is not a sim card change prepare batch")
}
return batch, nil
}
func (s *sIotTSimCardChange) buildChangePriceSnapshot(ctx context.Context, tx gdb.TX, customerAgentId int64, packageCode string, oldPackageType int) (*model.TSimCardChangePriceSnapshot, error) {
packageCode = strings.TrimSpace(packageCode)
if customerAgentId <= 0 {
return nil, gerror.New("customerAgentId cannot be empty")
}
if packageCode == "" {
return nil, gerror.New("newPackageCode cannot be empty")
}
pkg := &entity.TSimFlowPackageInfo{}
pkgColumns := dao.TSimFlowPackageInfo.Columns()
if err := tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()).
Where(pkgColumns.TenantId, contexts.GetTenantId(ctx)).
Where(pkgColumns.PackageCode, packageCode).
Where(pkgColumns.Enabled, 1).
Scan(pkg); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, gerror.New("target package not found or disabled")
}
return nil, gerror.Wrap(err, "load target package failed")
}
if pkg.Id == 0 {
return nil, gerror.New("target package not found or disabled")
}
if oldPackageType > 0 && pkg.PackageType != oldPackageType {
return nil, gerror.New("target package type does not match current package type")
}
options, err := packageduration.ChangeMonths(pkg)
if err != nil {
return nil, err
}
snapshot := &model.TSimCardChangePriceSnapshot{
CustomerAgentId: customerAgentId,
NewPackageCode: pkg.PackageCode,
NewPackageName: pkg.PackageName,
NewPackageType: pkg.PackageType,
NewPackageFlowMb: pkg.FlowMb,
PriceSource: "system",
ChangeUnitPrice: roundAmount(pkg.CostPrice*normalizePriceRatio(pkg.SysDefaultRatio) + pkg.SysDefaultProfit),
RenewableDurationOptions: options,
}
priceOwnerDeptID, err := loadPackagePriceParentDeptID(ctx, tx, contexts.GetTenantId(ctx), customerAgentId)
if err != nil {
return nil, err
}
salePrice, err := resolvePackageEffectiveSalePrice(ctx, tx, contexts.GetTenantId(ctx), priceOwnerDeptID, customerAgentId, packagePriceSourceFromEntity(pkg))
if err != nil {
return nil, err
}
snapshot.PriceSource = salePrice.Rule.PriceSource
snapshot.AgentDefaultPriceId = salePrice.Rule.AgentDefaultPriceId
snapshot.TargetPriceId = salePrice.Rule.TargetPriceId
snapshot.ChangeUnitPrice = salePrice.AgentShipmentPrice
return snapshot, nil
}
func (s *sIotTSimCardChange) loadChangeablePackageOptions(ctx context.Context, customerAgentId int64, oldPackage *entity.TSimFlowPackageInfo) ([]model.TSimCardChangePackageOption, error) {
if oldPackage == nil {
return nil, gerror.New("current package is empty")
}
codes := stringListFromJSON(oldPackage.ChangeablePackageCodes)
if len(codes) == 0 {
return nil, gerror.New("current package has no changeable packages configured")
}
options := make([]model.TSimCardChangePackageOption, 0, len(codes))
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
for _, code := range codes {
if code == strings.TrimSpace(oldPackage.PackageCode) {
continue
}
snapshot, err := s.buildChangePriceSnapshot(ctx, tx, customerAgentId, code, oldPackage.PackageType)
if err != nil {
continue
}
options = append(options, model.TSimCardChangePackageOption{
PackageCode: snapshot.NewPackageCode,
PackageName: snapshot.NewPackageName,
PackageType: snapshot.NewPackageType,
FlowMb: snapshot.NewPackageFlowMb,
ChangeUnitPrice: snapshot.ChangeUnitPrice,
RenewableDurationOptions: snapshot.RenewableDurationOptions,
})
}
return nil
})
if err != nil {
return nil, err
}
return options, nil
}
func (s *sIotTSimCardChange) loadChangePriceOverride(ctx context.Context, tx gdb.TX, scopeType int, targetAgentId int64, packageCode string) (*entity.TSimFlowPackagePrice, error) {
columns := dao.TSimFlowPackagePrice.Columns()
price := &entity.TSimFlowPackagePrice{}
err := tx.Ctx(ctx).Model(dao.TSimFlowPackagePrice.Table()).
Where(columns.TenantId, contexts.GetTenantId(ctx)).
Where(columns.DeptId, contexts.GetDeptId(ctx)).
Where(columns.AgentScopeType, scopeType).
Where(columns.TargetAgentId, targetAgentId).
Where(columns.PackageCode, packageCode).
Scan(price)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, gerror.Wrap(err, "load change package price failed")
}
if price.Id == 0 {
return nil, nil
}
return price, nil
}
func batchRowsToChangeRows(rows []batchimport.Row) ([]*change.Row, error) {
result := make([]*change.Row, 0, len(rows))
for _, item := range rows {
row, ok := item.(*change.Row)
if !ok || row == nil {
return nil, gerror.New("change row type is invalid")
}
result = append(result, row)
}
return result, nil
}
func changeRowsToBatchRows(rows []*change.Row) []batchimport.Row {
result := make([]batchimport.Row, 0, len(rows))
for _, row := range rows {
result = append(result, row)
}
return result
}
func fillChangePrepareRes(res *model.TSimCardChangeBatchPrepareRes, snapshot *model.TSimCardChangePrepareSnapshot) {
res.SourceBatchNo = snapshot.SourceBatchNo
res.CustomerAgentId = snapshot.CustomerAgentId
res.CustomerName = snapshot.CustomerName
res.OldPackageCode = snapshot.OldPackageCode
res.OldPackageName = snapshot.OldPackageName
res.OldPackageType = snapshot.OldPackageType
res.OldPackageFlowMb = snapshot.OldPackageFlowMb
}
func changePrepareSnapshotFromBatch(batch *entity.BatchMain) (*model.TSimCardChangePrepareSnapshot, error) {
if batch == nil || batch.BatchInfo == nil {
return nil, gerror.New("change prepare batch info is missing")
}
fields := buildChangePrepareFields(batch)
snapshot := &model.TSimCardChangePrepareSnapshot{
SourceBatchNo: normalizedChangePrepareSourceBatchNo(batch, gconv.String(fields["sourceBatchNo"])),
CustomerAgentId: gconv.Int64(fields["customerAgentId"]),
CustomerName: strings.TrimSpace(gconv.String(fields["customerName"])),
OldPackageCode: strings.TrimSpace(gconv.String(fields["oldPackageCode"])),
OldPackageName: strings.TrimSpace(gconv.String(fields["oldPackageName"])),
OldPackageType: gconv.Int(fields["oldPackageType"]),
OldPackageFlowMb: gconv.Float64(fields["oldPackageFlowMb"]),
Quantity: gconv.Int(fields["quantity"]),
}
if snapshot.OldPackageCode == "" {
return nil, gerror.New("change prepare batch info is incomplete")
}
return snapshot, nil
}
func normalizedChangePrepareSourceBatchNo(batch *entity.BatchMain, sourceBatchNo string) string {
if batch == nil || batch.SourceType != consts.BatchSourceTypeBatchSelect {
return ""
}
batchNo := strings.TrimSpace(batch.BatchNo)
sourceBatchNo = strings.TrimSpace(sourceBatchNo)
if sourceBatchNo == "" || sourceBatchNo == batchNo {
sourceBatchNo = strings.TrimSpace(batch.SourceBatchNo)
}
if sourceBatchNo == batchNo {
return ""
}
return sourceBatchNo
}
func changePriceSnapshotFromBatch(batch *entity.BatchMain) (*model.TSimCardChangePriceSnapshot, error) {
if batch == nil || batch.BatchInfo == nil {
return nil, gerror.New("change prepare batch price info is missing")
}
fields := buildChangePrepareFields(batch)
options := gconv.Ints(fields["renewableDurationOptions"])
snapshot := &model.TSimCardChangePriceSnapshot{
CustomerAgentId: gconv.Int64(fields["customerAgentId"]),
NewPackageCode: strings.TrimSpace(gconv.String(fields["newPackageCode"])),
NewPackageName: strings.TrimSpace(gconv.String(fields["newPackageName"])),
NewPackageType: gconv.Int(fields["newPackageType"]),
NewPackageFlowMb: gconv.Float64(fields["newPackageFlowMb"]),
PriceSource: strings.TrimSpace(gconv.String(fields["priceSource"])),
AgentDefaultPriceId: gconv.Int64(fields["agentDefaultPriceId"]),
TargetPriceId: gconv.Int64(fields["targetPriceId"]),
ChangeUnitPrice: roundAmount(gconv.Float64(fields["changeUnitPrice"])),
RenewableDurationOptions: options,
}
if snapshot.CustomerAgentId <= 0 || snapshot.NewPackageCode == "" || snapshot.ChangeUnitPrice < 0 || len(snapshot.RenewableDurationOptions) == 0 {
return nil, gerror.New("change prepare batch price info is incomplete")
}
return snapshot, nil
}
func buildChangePrepareFields(batch *entity.BatchMain) map[string]any {
if batch == nil {
return nil
}
fields := prepareInfoMap(batch.BatchInfo)
if fields == nil {
fields = map[string]any{}
}
if value := jsonField(batch.BatchInfo, "customerAgentId", "customer.deptId", "changePrepareSnapshot.customerAgentId", "changePriceSnapshot.customerAgentId"); value != nil {
fields["customerAgentId"] = gconv.Int64(value)
}
if value := jsonField(batch.BatchInfo, "customerName", "customer.name", "changePrepareSnapshot.customerName"); value != nil {
fields["customerName"] = strings.TrimSpace(value.String())
}
if value := jsonField(batch.BatchInfo, "sourceBatchNo", "changePrepareSnapshot.sourceBatchNo"); value != nil {
fields["sourceBatchNo"] = strings.TrimSpace(value.String())
}
if value := jsonField(batch.BatchInfo, "oldPackageCode", "changePrepareSnapshot.oldPackageCode"); value != nil {
fields["oldPackageCode"] = strings.TrimSpace(value.String())
}
if value := jsonField(batch.BatchInfo, "oldPackageName", "changePrepareSnapshot.oldPackageName"); value != nil {
fields["oldPackageName"] = strings.TrimSpace(value.String())
}
if value := jsonField(batch.BatchInfo, "oldPackageType", "changePrepareSnapshot.oldPackageType"); value != nil {
fields["oldPackageType"] = gconv.Int(value)
}
if value := jsonField(batch.BatchInfo, "oldPackageFlowMb", "changePrepareSnapshot.oldPackageFlowMb"); value != nil {
fields["oldPackageFlowMb"] = gconv.Float64(value)
}
if value := jsonField(batch.BatchInfo, "newPackageCode", "changePriceSnapshot.newPackageCode"); value != nil {
fields["newPackageCode"] = strings.TrimSpace(value.String())
fields["packageCode"] = strings.TrimSpace(value.String())
}
if value := jsonField(batch.BatchInfo, "newPackageName", "changePriceSnapshot.newPackageName"); value != nil {
fields["newPackageName"] = strings.TrimSpace(value.String())
fields["packageName"] = strings.TrimSpace(value.String())
}
if value := jsonField(batch.BatchInfo, "newPackageType", "changePriceSnapshot.newPackageType"); value != nil {
fields["newPackageType"] = gconv.Int(value)
}
if value := jsonField(batch.BatchInfo, "newPackageFlowMb", "changePriceSnapshot.newPackageFlowMb"); value != nil {
fields["newPackageFlowMb"] = gconv.Float64(value)
}
if value := jsonField(batch.BatchInfo, "priceSource", "changePriceSnapshot.priceSource"); value != nil {
fields["priceSource"] = strings.TrimSpace(value.String())
}
if value := jsonField(batch.BatchInfo, "agentDefaultPriceId", "changePriceSnapshot.agentDefaultPriceId"); value != nil {
fields["agentDefaultPriceId"] = gconv.Int64(value)
}
if value := jsonField(batch.BatchInfo, "targetPriceId", "changePriceSnapshot.targetPriceId"); value != nil {
fields["targetPriceId"] = gconv.Int64(value)
}
if value := jsonField(batch.BatchInfo, "changeUnitPrice", "changePriceSnapshot.changeUnitPrice"); value != nil {
fields["changeUnitPrice"] = roundAmount(gconv.Float64(value))
}
if values := jsonInts(batch.BatchInfo, "renewableDurationOptions", "changePriceSnapshot.renewableDurationOptions"); len(values) > 0 {
fields["renewableDurationOptions"] = values
}
if value := jsonField(batch.BatchInfo, "quantity", "changePrepareSnapshot.quantity"); value != nil {
fields["quantity"] = gconv.Int(value)
} else if batch.TotalCount > 0 {
fields["quantity"] = batch.TotalCount
}
return fields
}
func containsStringJSON(jsonValue any, target string) bool {
target = strings.TrimSpace(target)
if target == "" || jsonValue == nil {
return false
}
var values []any
switch value := jsonValue.(type) {
case interface{ Interfaces() []any }:
values = value.Interfaces()
default:
values = gconv.SliceAny(value)
}
for _, item := range values {
if strings.TrimSpace(gconv.String(item)) == target {
return true
}
}
return false
}
func stringListFromJSON(jsonValue any) []string {
if jsonValue == nil {
return nil
}
var values []any
switch value := jsonValue.(type) {
case interface{ Interfaces() []any }:
values = value.Interfaces()
default:
values = gconv.SliceAny(value)
}
if len(values) == 0 {
return nil
}
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, item := range values {
value := strings.TrimSpace(gconv.String(item))
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}
func calcChangeBeginTime(effectiveMode string) (*gtime.Time, error) {
switch strings.TrimSpace(effectiveMode) {
case cardChangeEffectiveImmediate:
return gtime.Now(), nil
case cardChangeEffectiveNextMonth:
now := time.Now()
nextMonth := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location())
return gtime.NewFromTime(nextMonth), nil
default:
return nil, gerror.New("effectiveMode is unsupported")
}
}
func changeSubmitParamFromInput(in *model.TSimCardChangeInputPrepareParam) *model.TSimCardChangeBatchSubmitParam {
if in == nil {
return &model.TSimCardChangeBatchSubmitParam{}
}
return &model.TSimCardChangeBatchSubmitParam{
NewPackageCode: strings.TrimSpace(in.NewPackageCode),
DurationMonths: in.DurationMonths,
GiftDurationMonths: in.GiftDurationMonths,
EffectiveMode: strings.TrimSpace(in.EffectiveMode),
OfflineContractCompleted: in.OfflineContractCompleted,
}
}
func changeSubmitParamFromRange(in *model.TSimCardChangeRangePrepareParam) *model.TSimCardChangeBatchSubmitParam {
if in == nil {
return &model.TSimCardChangeBatchSubmitParam{}
}
return &model.TSimCardChangeBatchSubmitParam{
NewPackageCode: strings.TrimSpace(in.NewPackageCode),
DurationMonths: in.DurationMonths,
GiftDurationMonths: in.GiftDurationMonths,
EffectiveMode: strings.TrimSpace(in.EffectiveMode),
OfflineContractCompleted: in.OfflineContractCompleted,
}
}
func prepareParamOrSubmitParam(prepareParam *model.TSimCardChangeBatchPrepareParam, submitParam *model.TSimCardChangeBatchSubmitParam) any {
if prepareParam != nil {
return prepareParam
}
return submitParam
}
func (s *sIotTSimCardChange) getChangeUploadFile(ctx context.Context) (*ghttp.UploadFile, error) {
r := g.RequestFromCtx(ctx)
if r == nil {
return nil, gerror.New("current context is not an HTTP request")
}
file := r.GetUploadFile("file")
if file == nil {
return nil, gerror.New("please select upload file")
}
return file, nil
}
func changeExcelSubmitResult(result *batchimport.Result) *model.TSimCardChangeExcelSubmitRes {
res := &model.TSimCardChangeExcelSubmitRes{
BatchType: consts.BatchTypePrepare,
TargetBatchType: consts.BatchTypeChange,
Status: pendingStatus(result.ImportStatus),
BatchAsyncStatus: consts.BatchAsyncStatusPending,
RunResult: consts.BatchRunResultPending,
Total: result.TotalRows,
}
if result == nil {
return res
}
res.BatchNo = result.BatchNo
res.ResultDownloadId = result.ResultDownloadId
res.ResultFileOssId = result.ResultFileOssId
return res
}
func (s *sIotTSimCardChange) mergeChangeExcelBatchFileInfo(ctx context.Context, batchNo string, importFileOssId, resultFileOssId, resultDownloadId int64) error {
operator := currentOperator(ctx)
columns := dao.BatchMain.Columns()
batch := &entity.BatchMain{}
if err := dao.BatchMain.Ctx(ctx).
Where(columns.BatchNo, strings.TrimSpace(batchNo)).
Where(columns.TenantId, operator.TenantId).
Where(columns.CreatedDept, operator.DeptId).
Scan(batch); err != nil {
return gerror.Wrap(err, "load change batch failed")
}
if batch.Id == 0 {
return gerror.New("change batch not found")
}
info := changeExcelMergeBatchInfo(batch.BatchInfo, importFileOssId, resultFileOssId, resultDownloadId)
if _, err := dao.BatchMain.Ctx(ctx).
Where(columns.BatchNo, strings.TrimSpace(batchNo)).
Data(do.BatchMain{
SourceOssId: batchFileOssIDValue(importFileOssId),
ResultOssId: batchFileOssIDValue(resultFileOssId),
BatchInfo: info,
UpdatedBy: operator.UserId,
UpdatedAt: gtime.Now(),
}).
Update(); err != nil {
return gerror.Wrap(err, "update change batch file info failed")
}
return nil
}
func changeExcelMergeBatchInfo(info *gjson.Json, importFileOssId, resultFileOssId, resultDownloadId int64) *gjson.Json {
data := g.Map{}
if info != nil {
for key, value := range info.Map() {
data[key] = value
}
}
if importFileOssId > 0 {
data["importFileOssId"] = importFileOssId
}
if resultFileOssId > 0 {
data["resultFileOssId"] = resultFileOssId
}
if resultDownloadId > 0 {
data["resultDownloadId"] = resultDownloadId
}
return gjson.New(data)
}
func batchFileOssIDValue(ossID int64) any {
if ossID <= 0 {
return nil
}
return ossID
}
func trimChangeExcelFailReason(cause error) string {
if cause == nil {
return "change confirm failed"
}
reason := strings.TrimSpace(cause.Error())
if reason == "" {
return "change confirm failed"
}
const maxLen = 500
runes := []rune(reason)
if len(runes) <= maxLen {
return reason
}
return string(runes[:maxLen])
}
func formatChangeTime(t *gtime.Time) string {
if t == nil {
return ""
}
return t.Format("Y-m-d H:i:s")
}