Files
iotlcm/admin-svr/server/internal/logic/iot/simcardbatch/renew/processor.go
T

559 lines
18 KiB
Go

package renew
import (
"context"
"sort"
"strconv"
"strings"
"time"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type Processor struct {
repo *Repository
config batchimport.ProcessorConfig
}
func NewProcessor() *Processor {
return &Processor{
repo: NewRepository(),
config: batchimport.ProcessorConfig{
Code: "sim_card_renew",
BatchType: consts.BatchTypeRenew,
BatchNoPrefix: consts.BatchNoPrefixRenew,
TaskName: consts.ResourceBatchDownloadTaskSimCardRenew,
ExportScene: consts.ResourceExportSceneRenew,
ImportDir: consts.SimCardRenewSubDirImportExcel,
ResultDir: consts.SimCardRenewSubDirResultExcel,
MaxFileSize: consts.SimCardRenewMaxFileSize,
TemplateHeaders: ExcelTemplateHeaders(),
RequiredHeaders: []string{"ICCID"},
},
}
}
func (p *Processor) Config() batchimport.ProcessorConfig {
return p.config
}
func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
return nil, gerror.New("卡续费不支持文件导入")
}
func (p *Processor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
renewRows := make([]*Row, 0, len(rows))
for _, item := range rows {
row, ok := item.(*Row)
if !ok {
item.AddErrorReason("续费行类型错误")
continue
}
renewRows = append(renewRows, row)
}
if err := normalizeRenewIdentityRows(ctx, renewRows); err != nil {
return err
}
return validateRows(renewRows)
}
func (p *Processor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
if req == nil {
return gerror.New("续费请求不能为空")
}
in, ok := req.BizParams.(*model.TSimCardRenewBatchSubmitParam)
if !ok || in == nil {
return gerror.New("续费参数类型错误")
}
renewRows := make([]*Row, 0, len(rows))
for _, item := range rows {
row, ok := item.(*Row)
if !ok {
return gerror.New("续费行类型错误")
}
renewRows = append(renewRows, row)
}
options, err := loadExecutionOptions(ctx, tx, req.BatchNo)
if err != nil {
return err
}
applyExecutionOptions(in, options)
prepared, err := p.validateRenewContext(ctx, tx, in, req.Operator, renewRows, req.BatchNo, false)
if err != nil {
return err
}
priceSnapshot, err := ensurePriceSnapshot(ctx, tx, req.Operator, prepared, options.PriceSnapshot)
if err != nil {
return err
}
durationValue := renewDisplayDurationValue(in)
if durationValue <= 0 {
return gerror.New("续费时长必须大于0")
}
if !containsRenewDurationOption(priceSnapshot.RenewableDurationOptions, durationValue) {
return gerror.New("续费时长不在当前套餐可续费范围内")
}
output, err := p.Complete(ctx, tx, &CompleteInput{
SubmitParam: in,
BatchNo: req.BatchNo,
Rows: renewRows,
Operator: req.Operator,
PriceSnapshot: priceSnapshot,
})
if err != nil {
return err
}
req.ExecutionResult = output
return nil
}
func (p *Processor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
info := g.Map{}
if result != nil && result.ImportFileOssId > 0 {
info["importFileOssId"] = result.ImportFileOssId
} else if importOss != nil && importOss.OssId > 0 {
info["importFileOssId"] = importOss.OssId
}
if resultOss != nil {
info["resultFileOssId"] = resultOss.OssId
}
if downloadId > 0 {
info["resultDownloadId"] = downloadId
}
if in, ok := req.BizParams.(*model.TSimCardRenewBatchSubmitParam); ok && in != nil {
durationValue := renewDisplayDurationValue(in)
if durationValue > 0 {
actualDurationMonths := renewActualDurationMonths(nil, durationValue)
info["durationYears"] = durationValue
info["durationMonths"] = actualDurationMonths
info["durationValue"] = durationValue
info["actualDurationMonths"] = actualDurationMonths
}
info["giftDurationMonths"] = in.GiftDurationMonths
info["amount"] = strings.TrimSpace(in.Amount)
if in.PaymentSnapshot != nil {
appendRenewPaymentSnapshot(info, in.PaymentSnapshot)
} else if paymentMode := strings.TrimSpace(in.PaymentMode); paymentMode != "" {
info["paymentMode"] = paymentMode
}
if packageCode := strings.TrimSpace(in.PackageCode); packageCode != "" {
info["packageCode"] = packageCode
}
info["offlineContractCompleted"] = in.OfflineContractCompleted
if sourceBatchNo := strings.TrimSpace(in.SourceBatchNo); sourceBatchNo != "" {
info["sourceBatchNo"] = sourceBatchNo
}
if buyerNote := strings.TrimSpace(in.BuyerNote); buyerNote != "" {
info["buyerNote"] = buyerNote
}
if adminNote := strings.TrimSpace(in.AdminNote); adminNote != "" {
info["adminNote"] = adminNote
}
if remark := strings.TrimSpace(in.Remark); remark != "" {
info["remark"] = remark
}
}
if snapshot, ok := req.ExecutionResult.(*model.TSimCardRenewPriceSnapshot); ok && snapshot != nil {
if result != nil && result.BatchType == consts.BatchTypePrepare {
info["targetBatchType"] = consts.BatchTypeRenew
info["targetBatchName"] = "卡续费"
}
info["customerAgentId"] = snapshot.CustomerAgentId
info["packageCode"] = snapshot.PackageCode
info["packageName"] = snapshot.PackageName
info["billingMethod"] = snapshot.BillingMethod
info["renewDurationUnit"] = snapshot.RenewDurationUnit
info["renewDurationUnitText"] = snapshot.RenewDurationUnitText
if durationValue := gconv.Int(info["durationValue"]); durationValue > 0 {
actualDurationMonths := renewActualDurationMonths(snapshot, durationValue)
info["durationMonths"] = actualDurationMonths
info["actualDurationMonths"] = actualDurationMonths
}
info["quantity"] = result.ValidRows
info["priceSource"] = snapshot.PriceSource
info["priceOwnerDeptId"] = snapshot.PriceOwnerDeptId
info["agentDefaultPriceId"] = snapshot.AgentDefaultPriceId
info["targetPriceId"] = snapshot.TargetPriceId
info["renewUnitPrice"] = snapshot.RenewUnitPrice
info["renewableDurationOptions"] = snapshot.RenewableDurationOptions
info["renewablePackageCodes"] = snapshot.RenewablePackageCodes
info["renewablePackages"] = snapshot.RenewablePackages
info["renewBeginDate"] = snapshot.RenewBeginDate
info["renewBeginDateMin"] = snapshot.RenewBeginDateMin
info["renewBeginDateMax"] = snapshot.RenewBeginDateMax
info["sameRenewBeginDate"] = snapshot.SameRenewBeginDate
}
if output, ok := req.ExecutionResult.(*CompleteOutput); ok && output != nil {
info["quantity"] = output.Quantity
info["packageCode"] = output.PackageCode
info["orderSn"] = output.OrderSn
info["renewTime"] = output.RenewTime
if sourceBatchNo := strings.TrimSpace(output.SourceBatchNo); sourceBatchNo != "" {
info["sourceBatchNo"] = sourceBatchNo
}
}
return gjson.New(info)
}
func appendRenewPaymentSnapshot(info g.Map, snapshot *model.TSimCardRenewPaymentSnapshot) {
if info == nil || snapshot == nil {
return
}
info["paymentMode"] = strings.TrimSpace(snapshot.PaymentMode)
info["payType"] = snapshot.PayType
info["payStatus"] = snapshot.PayStatus
info["payAmount"] = snapshot.PayAmount
info["balanceAmount"] = snapshot.BalanceAmount
info["offlinePaidAmount"] = snapshot.OfflinePaidAmount
info["balanceBefore"] = snapshot.BalanceBefore
info["balanceAfter"] = snapshot.BalanceAfter
info["balanceDetailSerialNo"] = strings.TrimSpace(snapshot.BalanceDetailSerialNo)
info["payerDeptId"] = snapshot.PayerDeptId
info["payerUserId"] = snapshot.PayerUserId
info["isTopDept"] = snapshot.IsTopDept
if snapshot.PaymentTime != nil {
info["paymentTime"] = snapshot.PaymentTime.Format("Y-m-d H:i:s")
}
info["paymentSnapshot"] = snapshot
}
func (p *Processor) ValidatePrepareContext(ctx context.Context, tx gdb.TX, in *model.TSimCardRenewBatchSubmitParam, operator batchimport.OperatorContext, rows []*Row) (*PrepareContext, error) {
return p.validateRenewContext(ctx, tx, in, operator, rows, "", false)
}
func (p *Processor) RevalidatePrepareContext(ctx context.Context, tx gdb.TX, in *model.TSimCardRenewBatchSubmitParam, operator batchimport.OperatorContext, rows []*Row, currentBatchNo string) (*PrepareContext, bool, error) {
prepared, err := p.validateRenewContext(ctx, tx, in, operator, rows, currentBatchNo, true)
return prepared, hasInvalidRows(rows), err
}
func (p *Processor) validateRenewContext(ctx context.Context, tx gdb.TX, in *model.TSimCardRenewBatchSubmitParam, operator batchimport.OperatorContext, rows []*Row, currentBatchNo string, allowPartial bool) (*PrepareContext, error) {
iccids := rowsToIccids(rows)
cardsByIccid, err := p.repo.LockCardsByIccids(ctx, tx, iccids)
if err != nil {
return nil, gerror.Wrap(err, "校验续费卡信息失败")
}
bindsByIccid, err := p.repo.LockLatestBindsByIccids(ctx, tx, iccids)
if err != nil {
return nil, gerror.Wrap(err, "校验续费订购记录失败")
}
conflictingBatches, err := p.repo.UnfinishedRenewBatchNosByIccids(
ctx,
tx,
operator.TenantId,
iccids,
currentBatchNo,
)
if err != nil {
return nil, gerror.Wrap(err, "校验未完成续费批次失败")
}
sourceBatchNo := strings.TrimSpace(in.SourceBatchNo)
packageCodes := make(map[string]struct{})
customerAgentIds := make(map[int64]struct{})
currentPackagePeriods := make(map[string]struct{})
renewBeginDates := make([]string, 0, len(rows))
packageCode := ""
customerAgentId := int64(0)
for _, row := range rows {
if row == nil || row.Reason != "" {
continue
}
card := cardsByIccid[row.Iccid]
bind := bindsByIccid[row.Iccid]
if strings.TrimSpace(conflictingBatches[row.Iccid]) != "" {
row.AddReason("存在未完成续费批次,不能重复续费")
continue
}
reason := validateRenewRow(sourceBatchNo, operator, card, bind)
if reason != "" {
row.AddReason(reason)
continue
}
baseExpireAt := renewBaseExpireTime(card, bind)
periodKey := currentPackagePeriodKeyWithExpire(bind, baseExpireAt)
if periodKey == "" {
row.AddReason("不是统一批次")
continue
}
currentPackageCode := strings.TrimSpace(card.CurrentPackageCode)
packageCodes[currentPackageCode] = struct{}{}
customerAgentIds[card.DeptId] = struct{}{}
currentPackagePeriods[periodKey] = struct{}{}
packageCode = currentPackageCode
customerAgentId = card.DeptId
renewBeginAt := renewBeginDateFromExpire(baseExpireAt)
if renewBeginAt != nil {
renewBeginDates = append(renewBeginDates, renewBeginAt.Format("Y-m-d"))
}
}
if len(packageCodes) > 1 {
markRowsWithoutReason(rows, "当前批次下存在多个套餐,不能统一续费")
}
if len(customerAgentIds) > 1 {
markRowsWithoutReason(rows, "当前批次下存在多个客户,不能统一续费")
}
if len(currentPackagePeriods) > 1 {
markRowsWithoutReason(rows, "不是统一批次")
}
if len(customerAgentIds) == 1 && customerAgentId > 0 {
if err := validateRenewCustomerDeptScope(ctx, tx, operator, customerAgentId); err != nil {
markRowsWithoutReason(rows, err.Error())
}
}
if hasInvalidRows(rows) && !allowPartial {
markRemainingRowsSkipped(rows)
return nil, gerror.New("存在不满足续费条件的卡,本次未执行")
}
validCount := countValidRows(rows)
if validCount == 0 {
return &PrepareContext{}, nil
}
renewBeginDate, renewBeginDateMin, renewBeginDateMax, sameRenewBeginDate := summarizeRenewBeginDates(renewBeginDates)
return &PrepareContext{
CustomerAgentId: customerAgentId,
PackageCode: packageCode,
Quantity: validCount,
RenewBeginDate: renewBeginDate,
RenewBeginDateMin: renewBeginDateMin,
RenewBeginDateMax: renewBeginDateMax,
SameRenewBeginDate: sameRenewBeginDate,
}, nil
}
func currentPackagePeriodKey(bind *entity.TSimCardBindPkg) string {
if bind == nil {
return ""
}
return currentPackagePeriodKeyWithExpire(bind, bind.ExpectedExpireTime)
}
func currentPackagePeriodKeyWithExpire(bind *entity.TSimCardBindPkg, expireAt *gtime.Time) string {
if bind == nil || bind.EffectiveTime == nil {
return ""
}
if expireAt == nil {
expireAt = bind.ExpectedExpireTime
}
if expireAt == nil {
return ""
}
return bind.EffectiveTime.Format("Y-m-d") + "|" + expireAt.Format("Y-m-d")
}
func summarizeRenewBeginDates(dates []string) (sameDate string, minDate string, maxDate string, same bool) {
cleaned := make([]string, 0, len(dates))
for _, date := range dates {
date = strings.TrimSpace(date)
if date != "" {
cleaned = append(cleaned, date)
}
}
if len(cleaned) == 0 {
return "", "", "", false
}
sort.Strings(cleaned)
minDate = cleaned[0]
maxDate = cleaned[len(cleaned)-1]
same = minDate == maxDate
if same {
sameDate = minDate
}
return sameDate, minDate, maxDate, same
}
func renewBeginDateFromExpire(expireAt *gtime.Time) *gtime.Time {
if expireAt == nil {
return nil
}
return normalizeRenewDateAtDayEnd(expireAt)
}
func normalizeRenewDateAtDayEnd(expireAt *gtime.Time) *gtime.Time {
if expireAt == nil || expireAt.IsZero() {
return nil
}
t := expireAt.Time
return gtime.NewFromTime(
time.Date(
t.Year(),
t.Month(),
t.Day(),
23,
59,
59,
0,
t.Location(),
),
)
}
func validateRenewRow(sourceBatchNo string, operator batchimport.OperatorContext, card *entity.TSimCardInfo, bind *entity.TSimCardBindPkg) string {
if card == nil {
return "ICCID不存在正式卡记录,不能续费"
}
if card.BossCardStatus == consts.V3ApiSimBossCardStatusStopped {
return "ICCID当前为停机状态,不能续费"
}
if bind == nil {
return "ICCID没有订购记录,不能续费"
}
if strings.TrimSpace(card.CurrentPackageCode) == "" || card.PackageStatus == consts.PackageStatusNone {
return "ICCID没有当前套餐,不能续费"
}
if strings.TrimSpace(card.CurrentPackageCode) != strings.TrimSpace(bind.PackageCode) {
return "ICCID当前套餐与最新订购记录不一致,不能续费"
}
baseExpireAt := renewBaseExpireTime(card, bind)
if baseExpireAt == nil {
return "ICCID最新订购没有预计到期时间,不能续费"
}
if renewExpireAfterAllowedWindow(baseExpireAt) {
return "距离到期超过6个月,不能续费"
}
if sourceBatchNo != "" && strings.TrimSpace(card.OutboundBatchNo) != sourceBatchNo {
return "ICCID不属于来源出库批次,不能续费"
}
return ""
}
func renewBaseExpireTime(card *entity.TSimCardInfo, bind *entity.TSimCardBindPkg) *gtime.Time {
var expireAt *gtime.Time
if bind != nil {
expireAt = laterRenewExpireTime(expireAt, bind.ExpectedExpireTime)
}
if card != nil {
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgEndAt)
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgPresentAt)
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgRealPresentAt)
}
return expireAt
}
func laterRenewExpireTime(current, candidate *gtime.Time) *gtime.Time {
if candidate == nil || candidate.IsZero() {
return current
}
if current == nil || current.IsZero() || candidate.After(current) {
return candidate
}
return current
}
func renewExpireAfterAllowedWindow(expireAt *gtime.Time) bool {
if expireAt == nil {
return false
}
return expireAt.After(gtime.Now().AddDate(0, 6, 0))
}
func validateRenewCustomerDeptScope(ctx context.Context, tx gdb.TX, operator batchimport.OperatorContext, customerDeptID int64) error {
if operator.DeptId <= 0 || operator.TenantId == "" {
return gerror.New("当前用户未绑定组织或租户")
}
if customerDeptID <= 0 {
return gerror.New("ICCID不属于当前机构,不能续费")
}
if customerDeptID == operator.DeptId {
return nil
}
dept := &entity.SysDept{}
if err := tx.Ctx(ctx).Model(dao.SysDept.Table()).
Fields(dao.SysDept.Columns().DeptId, dao.SysDept.Columns().ParentId, dao.SysDept.Columns().Ancestors).
Where(dao.SysDept.Columns().TenantId, operator.TenantId).
Where(dao.SysDept.Columns().DeptId, customerDeptID).
WhereNull(dao.SysDept.Columns().DeletedAt).
Scan(dept); err != nil {
return gerror.Wrap(err, "校验ICCID归属失败")
}
if dept.DeptId == 0 {
return gerror.New("ICCID不属于当前机构,不能续费")
}
if dept.ParentId == operator.DeptId || renewDeptAncestorsContain(dept.Ancestors, operator.DeptId) {
return nil
}
return gerror.New("ICCID不属于当前机构,不能续费")
}
func renewDeptAncestorsContain(ancestors string, deptID int64) bool {
if deptID <= 0 {
return false
}
target := strconv.FormatInt(deptID, 10)
for _, item := range strings.Split(strings.Trim(ancestors, ","), ",") {
if strings.TrimSpace(item) == target {
return true
}
}
return false
}
func hasInvalidRows(rows []*Row) bool {
for _, row := range rows {
if row != nil && row.Reason != "" {
return true
}
}
return false
}
func countValidRows(rows []*Row) int {
count := 0
for _, row := range rows {
if row != nil && row.Valid() {
count++
}
}
return count
}
func markRowsWithoutReason(rows []*Row, reason string) {
for _, row := range rows {
if row != nil && row.Reason == "" {
row.AddReason(reason)
}
}
}
func markRemainingRowsSkipped(rows []*Row) {
for _, row := range rows {
if row != nil && row.Reason == "" {
row.AddReason("同批存在不满足续费条件的卡,本行未执行")
}
}
}
func validateRows(rows []*Row) error {
firstByIccid := make(map[string]*Row, len(rows))
for _, row := range rows {
row.Iccid = strings.TrimSpace(row.Iccid)
if row.Iccid == "" {
row.AddReason("ICCID不能为空")
continue
}
if first, ok := firstByIccid[row.Iccid]; ok {
row.AddReason("ICCID重复")
first.AddReason("ICCID重复")
continue
}
firstByIccid[row.Iccid] = row
}
return nil
}