package app import ( "context" "encoding/json" "fmt" "math" "reflect" "sort" "strings" "time" "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchmetadata" "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" ) type appRenewPriceSnapshot struct { UnitPrice float64 PriceSource string AgentDefaultPriceId int64 TargetPriceId int64 } type appRenewEffectiveBaseCosts struct { AgentCost float64 TerminalCost float64 } type appRenewPriceRule struct { PriceSource string AgentDefaultPriceId int64 TargetPriceId int64 AgentCostRatio float64 AgentCostProfit float64 TerminalCostRatio float64 TerminalCostProfit float64 } type appRenewEffectiveSalePrice struct { Rule appRenewPriceRule AgentShipmentPrice float64 TerminalSalePrice float64 } const ( appRenewDurationUnitYear = "year" appRenewDurationUnitYearText = "年" ) type appRenewDurationSelection struct { Years int Months int } type appRenewOrderParty struct { CustomerType int CustomerAgentId int64 EndUserId int64 AgentId int64 CreatedDept int64 } type appSimTerminalRenewLockRow struct { Id int64 `orm:"id"` Iccid string `orm:"iccid"` TerminalId int64 `orm:"terminal_id"` DeptId int64 `orm:"dept_id"` CurrentPackageCode string `orm:"current_package_code"` TenantId string `orm:"tenant_id"` } // RenewPrepare 按当前套餐的 renewable_package_codes 读取可续费目标套餐, // 再按用户身份为目标套餐生成单年价和续费年限档位。 func (s *sAppCard) RenewPrepare(ctx context.Context, param *model.AppCardRenewPrepareParam) (res *model.AppCardRenewPrepareModel, err error) { if param == nil { param = &model.AppCardRenewPrepareParam{} } user, err := appCurrentUser(ctx) if err != nil { return nil, err } card, err := s.findVisibleCard(ctx, user, param.CardValue) if err != nil { return nil, err } if card == nil { return nil, gerror.New("卡信息不存在或无权续费") } if err = s.ensureOperateCard(ctx, user, card, "续费"); err != nil { return nil, err } renewCtx, err := s.resolveRenewPackageContext(ctx, card, param.SubscriptionNo) if err != nil { return nil, err } renewableCodes, renewableTargets, selectedTarget, err := s.resolveRenewTargetPackages(ctx, user, renewCtx, param.PackageCode) if err != nil { return nil, err } pkg := selectedTarget.Package price := selectedTarget.Price durations := selectedTarget.Durations ownerType := s.ownerType(ctx, user) cardModel := s.cardRowToModel(ctx, card, user, ownerType, true) currentPackageModel := s.renewPackageModel(card, renewCtx.Package, ownerType) packageModel := s.renewPackageModel(card, pkg, ownerType) effectiveRule := appRenewEffectiveRule(renewCtx.expireAt()) recommendIndex := appRenewRecommendIndex(durations) options := appRenewOptionModels(durations, price.UnitPrice, 1, func(years int) *gtime.Time { return appRenewExpireAfter(renewCtx.expireAt(), appRenewActualDurationMonths(years)) }) return &model.AppCardRenewPrepareModel{ Card: cardModel, Cards: []*model.AppCardModel{cardModel}, Quantity: 1, CurrentPackage: currentPackageModel, Package: packageModel, RenewablePackageCodes: renewableCodes, RenewablePackages: s.renewTargetPackageOptionModels(card, renewableTargets, ownerType, 1, func(years int) *gtime.Time { return appRenewExpireAfter(renewCtx.expireAt(), appRenewActualDurationMonths(years)) }), Options: options, PriceSource: price.PriceSource, UnitPrice: price.UnitPrice, EffectiveRule: effectiveRule, RecommendIndex: recommendIndex, }, nil } // RenewSubmit 使用账户余额支付,并把单卡续费也写入正式续费批次。 func (s *sAppCard) RenewSubmit(ctx context.Context, param *model.AppCardRenewSubmitParam) (res *model.AppCardRenewSubmitModel, err error) { if param == nil { param = &model.AppCardRenewSubmitParam{} } user, err := appCurrentUser(ctx) if err != nil { return nil, err } card, err := s.findVisibleCard(ctx, user, param.CardValue) if err != nil { return nil, err } if card == nil { return nil, gerror.New("卡信息不存在或无权续费") } if err = s.ensureOperateCard(ctx, user, card, "续费"); err != nil { return nil, err } renewCtx, err := s.resolveRenewPackageContext(ctx, card, param.SubscriptionNo) if err != nil { return nil, err } renewableCodes, renewableTargets, selectedTarget, err := s.resolveRenewTargetPackages(ctx, user, renewCtx, param.PackageCode) if err != nil { return nil, err } pkg := selectedTarget.Package price := selectedTarget.Price durations := selectedTarget.Durations duration := appRenewDurationSelectionFromParam(param.DurationYears, param.DurationMonths, durations) if duration.Years <= 0 || duration.Months <= 0 { return nil, gerror.New("请选择续费年限") } if !appContainsInt(durations, duration.Years) { return nil, gerror.New("续费时长不在目标套餐可续费范围内") } totalAmount := appRenewTotalAmount(price.UnitPrice, duration.Years, 1) now := gtime.Now() party, err := s.renewOrderParty(ctx, user, card) if err != nil { return nil, err } var orderId int64 var orderSn string var batchNo string var serviceBeginAt *gtime.Time var serviceEndAt *gtime.Time err = dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { batch := &appRenewBatchContext{ Items: []*appRenewPackageContext{renewCtx}, CurrentPackage: renewCtx.Package, Package: pkg, Price: price, Party: party, Durations: durations, OwnerType: s.ownerType(ctx, user), RenewablePackageCodes: renewableCodes, RenewableTargets: renewableTargets, } exec, err := s.prepareAppRenewExecution(ctx, tx, batch, duration.Months) if err != nil { return err } serviceBeginAt, serviceEndAt = exec.MinBeginAt, exec.MaxEndAt orderSn = appOrderNo("SO", user.UserId, now) batchNo, err = appNextBatchNo(ctx, consts.BatchNoPrefixRenew) if err != nil { return err } if _, err = s.deductAppBalanceForDept(ctx, tx, user, orderSn, totalAmount, "套餐续费", party.CustomerAgentId, now); err != nil { return err } orderId, err = s.insertAppRenewOrder(ctx, tx, user, party, strings.TrimSpace(param.BuyerNote), orderSn, totalAmount, now) if err != nil { return err } if err = s.insertAppRenewSubscriptionBinds(ctx, tx, user, batch, exec, batchNo, orderSn, duration.Months, now); err != nil { return err } buyerNote := strings.TrimSpace(param.BuyerNote) if err = s.insertAppRenewBatchOrderItem(ctx, tx, user, batch, orderId, orderSn, batchNo, "", duration.Months, totalAmount, totalAmount, serviceBeginAt, serviceEndAt, exec.CardPeriods, now); err != nil { return err } return s.insertAppRenewSubmitBatch(ctx, tx, user, batch, batchNo, "", orderSn, duration.Months, totalAmount, totalAmount, serviceBeginAt, serviceEndAt, exec.CardPeriods, buyerNote, batchmetadata.Single, now) }) if err != nil { return nil, err } ownerType := s.ownerType(ctx, user) cardModel := s.cardRowToModel(ctx, card, user, ownerType, true) currentPackageModel := s.renewPackageModel(card, renewCtx.Package, ownerType) packageModel := s.renewPackageModel(card, pkg, ownerType) option := appRenewOptionModel(duration.Years, price.UnitPrice, totalAmount, serviceEndAt, appRenewRecommendIndex(durations) == appIndexOfInt(durations, duration.Years)) cardModels := []*model.AppCardModel{cardModel} res = &model.AppCardRenewSubmitModel{ BatchNo: batchNo, BatchType: consts.BatchTypeRenew, OrderId: orderId, OrderSn: orderSn, TotalAmount: totalAmount, PriceText: option.PriceText, Quantity: 1, OrderStatus: consts.OrderStatusCompleted, PayStatus: consts.PayStatusPaid, Status: "success", EffectiveRule: appRenewEffectiveRule(renewCtx.expireAt()), Card: cardModel, Cards: cardModels, CurrentPackage: currentPackageModel, Package: packageModel, Option: option, } _ = s.publishAppBusinessAsyncEvent(ctx, appBusinessAsyncEvent{ EventType: appAsyncEventRenewCompleted, OrderSn: orderSn, BatchNo: batchNo, BatchType: consts.BatchTypeRenew, TenantId: user.TenantId, UserId: user.UserId, DeptId: user.DeptId, Trigger: "renew_submit", }) return res, nil } func (s *sAppCard) renewPackageModel(card *appCardQueryRow, pkg *entity.TSimFlowPackageInfo, ownerType string) *model.AppPackageModel { effectiveStatus := appEffectiveStatusFromCardPackageStatus(card.PackageStatus) status, statusType := appPackageStatus(effectiveStatus, card.FlowPkgEndAt) flowMb := pkg.FlowMb if flowMb <= 0 { flowMb = card.PackageTotalFlowMb } return &model.AppPackageModel{ Id: pkg.Id, PackageCode: pkg.PackageCode, PackageName: pkg.PackageName, PackageType: pkg.PackageType, PackageTypeName: appPackageTypeName(pkg.PackageType), CategoryCode: appFirstNotEmpty(pkg.CategoryCode, appCardCategoryCode(card)), CategoryName: appCardCategoryName(card), Iccid: card.Iccid, Msisdn: card.Msisdn, CardId: card.Id, CardValue: appFirstNotEmpty(card.Msisdn, card.Iccid), CardRemark: card.Remark, TerminalName: card.TerminalName, EffectiveStatus: effectiveStatus, Status: status, StatusType: statusType, FilterValue: pkg.PackageCode, EffectiveTime: card.FlowPkgBeginAt, ExpireTime: card.FlowPkgEndAt, ValidText: appPackageValidText(card.FlowPkgBeginAt, card.FlowPkgEndAt), EffectText: appPackageEffectText(effectiveStatus), FlowMb: flowMb, FlowText: appFlowText(flowMb), OwnerType: ownerType, CanBusiness: true, } } func (s *sAppCard) renewTargetPackageOptionModels(card *appCardQueryRow, targets []*appRenewTargetContext, ownerType string, quantity int, expireAfter func(years int) *gtime.Time) []*model.AppRenewPackageOptionModel { result := make([]*model.AppRenewPackageOptionModel, 0, len(targets)) for _, target := range targets { if target == nil || target.Package == nil || target.Price == nil { continue } options := appRenewOptionModels(target.Durations, target.Price.UnitPrice, quantity, expireAfter) result = append(result, &model.AppRenewPackageOptionModel{ Package: s.renewPackageModel(card, target.Package, ownerType), PackageCode: strings.TrimSpace(target.Package.PackageCode), PackageName: strings.TrimSpace(target.Package.PackageName), RenewUnitPrice: target.Price.UnitPrice, UnitPrice: target.Price.UnitPrice, PriceSource: target.Price.PriceSource, PriceText: fmt.Sprintf("¥%.2f/年", target.Price.UnitPrice), RenewableDurationOptions: append([]int(nil), target.Durations...), Options: options, }) } return result } func appRenewOptionModels(durations []int, unitPrice float64, quantity int, expireAfter func(years int) *gtime.Time) []*model.AppRenewOptionModel { recommendIndex := appRenewRecommendIndex(durations) options := make([]*model.AppRenewOptionModel, 0, len(durations)) for index, years := range durations { var expire *gtime.Time if expireAfter != nil { expire = expireAfter(years) } totalAmount := appRenewTotalAmount(unitPrice, years, quantity) options = append(options, appRenewOptionModel(years, unitPrice, totalAmount, expire, index == recommendIndex)) } return options } func appRenewableDurationOptions(pkg *entity.TSimFlowPackageInfo) ([]int, error) { if pkg == nil || pkg.RenewableDurationConfig == nil || pkg.RenewableDurationConfig.IsNil() { return nil, gerror.New("当前套餐未配置可续费时长") } options := appNormalizeRenewDurationOptions(appRenewDurationOptionsFromValue(pkg.RenewableDurationConfig.Interface())) sort.Ints(options) if len(options) == 0 { return nil, gerror.New("当前套餐未配置可续费时长") } return options, nil } func appRenewableDurationYearOptions(pkg *entity.TSimFlowPackageInfo) ([]int, error) { if pkg == nil || pkg.RenewableDurationConfig == nil || pkg.RenewableDurationConfig.IsNil() { return nil, gerror.New("当前套餐未配置可续费年限") } options := appNormalizeRenewYearOptions(appRenewDurationOptionsFromValue(pkg.RenewableDurationConfig.Interface())) sort.Ints(options) if len(options) == 0 { return nil, gerror.New("当前套餐未配置可续费年限") } return options, nil } func appRenewDurationOptionsFromValue(value any) []int { switch typed := value.(type) { case nil: return nil case []any: options := make([]int, 0, len(typed)) for _, item := range typed { options = append(options, appRenewDurationOptionsFromValue(item)...) } return options case map[string]any: return appRenewDurationOptionsFromMap(typed) case string: return appRenewDurationOptionsFromString(typed) default: if options := appRenewDurationOptionsFromReflectValue(value); len(options) > 0 { return options } if month := gconv.Int(value); month > 0 { return []int{month} } return nil } } func appRenewDurationOptionsFromReflectValue(value any) []int { rv := reflect.ValueOf(value) if !rv.IsValid() { return nil } switch rv.Kind() { case reflect.Slice, reflect.Array: options := make([]int, 0, rv.Len()) for i := 0; i < rv.Len(); i++ { options = append(options, appRenewDurationOptionsFromValue(rv.Index(i).Interface())...) } return options case reflect.Map: values := make(map[string]any, rv.Len()) iter := rv.MapRange() for iter.Next() { values[gconv.String(iter.Key().Interface())] = iter.Value().Interface() } return appRenewDurationOptionsFromMap(values) default: return nil } } func appRenewDurationOptionsFromMap(value map[string]any) []int { if len(value) == 0 { return nil } keys := []string{ "durationMonths", "durationMonth", "duration", "durations", "month", "months", "value", "values", "time", "times", "option", "options", "item", "items", "data", "list", "renewableDurationOptions", "renewableDurationConfig", } options := make([]int, 0, len(value)) for _, key := range keys { if item, ok := value[key]; ok { options = append(options, appRenewDurationOptionsFromValue(item)...) } } if len(options) > 0 { return options } for key := range value { if month := appRenewDurationMonthFromToken(key); month > 0 { options = append(options, month) } } return options } func appRenewDurationOptionsFromString(value string) []int { value = strings.TrimSpace(value) if value == "" { return nil } if strings.HasPrefix(value, "[") || strings.HasPrefix(value, "{") { var decoded any if err := json.Unmarshal([]byte(value), &decoded); err == nil { return appRenewDurationOptionsFromValue(decoded) } } parts := strings.FieldsFunc(value, func(r rune) bool { switch r { case ',', ',', ';', ';', '|', '/', '、', '\n', '\r', '\t', ' ': return true default: return false } }) if len(parts) == 0 { parts = []string{value} } options := make([]int, 0, len(parts)) for _, part := range parts { if month := appRenewDurationMonthFromToken(part); month > 0 { options = append(options, month) } } return options } func appRenewDurationMonthFromToken(value string) int { value = strings.TrimSpace(value) if value == "" { return 0 } if month := gconv.Int(value); month > 0 { return month } var digits strings.Builder for _, r := range value { if r >= '0' && r <= '9' { digits.WriteRune(r) continue } if digits.Len() > 0 { break } } month := gconv.Int(digits.String()) if month <= 0 { return 0 } if strings.Contains(value, "年") && !strings.Contains(value, "月") { return month * 12 } return month } func appNormalizeRenewDurationOptions(values []int) []int { seen := map[int]struct{}{} options := make([]int, 0, len(values)) for _, month := range values { if month <= 0 { continue } if _, exists := seen[month]; exists { continue } seen[month] = struct{}{} options = append(options, month) } return options } func appNormalizeRenewYearOptions(values []int) []int { values = appNormalizeRenewDurationOptions(values) if len(values) == 0 { return nil } legacyMonthConfig := true for _, value := range values { if value < 12 || value%12 != 0 { legacyMonthConfig = false break } } if !legacyMonthConfig { return values } years := make([]int, 0, len(values)) for _, month := range values { if month > 0 && month%12 == 0 { years = append(years, month/12) } } return appNormalizeRenewDurationOptions(years) } func (s *sAppCard) resolveRenewUnitPrice(ctx context.Context, user *entity.SysUser, card *appCardQueryRow, pkg *entity.TSimFlowPackageInfo) (*appRenewPriceSnapshot, error) { if s.isTerminalUser(ctx, user) { return s.resolveTerminalRenewUnitPrice(ctx, card, pkg) } return s.resolveAgentRenewUnitPrice(ctx, user, card, pkg) } func (s *sAppCard) resolveTerminalRenewUnitPrice(ctx context.Context, card *appCardQueryRow, pkg *entity.TSimFlowPackageInfo) (*appRenewPriceSnapshot, error) { if pkg == nil { return nil, gerror.New("续费套餐不存在") } rule := appRenewSystemPriceRule(pkg) snapshot := &appRenewPriceSnapshot{ UnitPrice: appRenewApplyPriceRule(appRenewSystemBaseCosts(pkg).TerminalCost, rule.TerminalCostRatio, rule.TerminalCostProfit), PriceSource: "system", } targetAgentId := appTerminalBindDeptId(card) if targetAgentId <= 0 { return snapshot, nil } // 定价表遵循管理平台格式:dept_id 是价格归属方,target_agent_id 是当前终端卡所属渠道客户。 sellerDeptId, err := s.renewPriceOwnerDeptIdByDept(ctx, card.TenantId, targetAgentId) if err != nil { return nil, err } if sellerDeptId <= 0 { return snapshot, nil } salePrice, err := s.resolveAppRenewEffectiveSalePrice(ctx, card.TenantId, sellerDeptId, targetAgentId, pkg) if err != nil { return nil, err } snapshot.UnitPrice = salePrice.TerminalSalePrice snapshot.PriceSource = appRenewTerminalPriceSource(salePrice.Rule.PriceSource) snapshot.AgentDefaultPriceId = salePrice.Rule.AgentDefaultPriceId snapshot.TargetPriceId = salePrice.Rule.TargetPriceId return snapshot, nil } func (s *sAppCard) resolveAgentRenewUnitPrice(ctx context.Context, user *entity.SysUser, card *appCardQueryRow, pkg *entity.TSimFlowPackageInfo) (*appRenewPriceSnapshot, error) { if pkg == nil { return nil, gerror.New("续费套餐不存在") } customerAgentId := appCardOwnerDeptId(card) if customerAgentId <= 0 { return nil, gerror.New("卡片归属机构为空,暂不能续费") } snapshot := &appRenewPriceSnapshot{ UnitPrice: appRenewApplyPriceRule(pkg.CostPrice, pkg.SysDefaultRatio, pkg.SysDefaultProfit), PriceSource: "system", } if user == nil || user.DeptId <= 0 { return snapshot, nil } priceOwnerDeptId, err := s.renewPriceOwnerDeptIdByDept(ctx, card.TenantId, customerAgentId) if err != nil { return nil, err } salePrice, err := s.resolveAppRenewEffectiveSalePrice(ctx, card.TenantId, priceOwnerDeptId, customerAgentId, pkg) if err != nil { return nil, err } snapshot.UnitPrice = salePrice.AgentShipmentPrice snapshot.PriceSource = salePrice.Rule.PriceSource snapshot.AgentDefaultPriceId = salePrice.Rule.AgentDefaultPriceId snapshot.TargetPriceId = salePrice.Rule.TargetPriceId return snapshot, nil } func (s *sAppCard) resolveAppRenewEffectiveSalePrice(ctx context.Context, tenantId string, ownerDeptId int64, targetDeptId int64, pkg *entity.TSimFlowPackageInfo) (appRenewEffectiveSalePrice, error) { baseCosts, err := s.resolveRenewEffectiveBaseCosts(ctx, tenantId, ownerDeptId, pkg, make(map[int64]struct{})) if err != nil { return appRenewEffectiveSalePrice{}, err } rule, err := s.resolveAppRenewPriceRule(ctx, tenantId, ownerDeptId, targetDeptId, pkg) if err != nil { return appRenewEffectiveSalePrice{}, err } return appRenewEffectiveSalePriceFromBaseCosts(baseCosts, rule), nil } func appRenewEffectiveSalePriceFromBaseCosts(baseCosts appRenewEffectiveBaseCosts, rule appRenewPriceRule) appRenewEffectiveSalePrice { return appRenewEffectiveSalePrice{ Rule: rule, AgentShipmentPrice: appRenewApplyPriceRule(baseCosts.AgentCost, rule.AgentCostRatio, rule.AgentCostProfit), TerminalSalePrice: appRenewApplyPriceRule(baseCosts.TerminalCost, rule.TerminalCostRatio, rule.TerminalCostProfit), } } func (s *sAppCard) resolveRenewEffectiveBaseCosts(ctx context.Context, tenantId string, deptId int64, pkg *entity.TSimFlowPackageInfo, visited map[int64]struct{}) (appRenewEffectiveBaseCosts, error) { if pkg == nil || strings.TrimSpace(pkg.PackageCode) == "" { return appRenewEffectiveBaseCosts{}, gerror.New("套餐编码不能为空") } if deptId <= 0 { return appRenewSystemBaseCosts(pkg), nil } if _, exists := visited[deptId]; exists { return appRenewEffectiveBaseCosts{}, gerror.New("组织定价层级存在循环,无法计算生效成本") } visited[deptId] = struct{}{} parentDeptId, err := s.renewPriceOwnerDeptIdByDept(ctx, tenantId, deptId) if err != nil { return appRenewEffectiveBaseCosts{}, err } if parentDeptId <= 0 { return appRenewSystemBaseCosts(pkg), nil } parentCosts, err := s.resolveRenewEffectiveBaseCosts(ctx, tenantId, parentDeptId, pkg, visited) if err != nil { return appRenewEffectiveBaseCosts{}, err } rule, err := s.resolveAppRenewPriceRule(ctx, tenantId, parentDeptId, deptId, pkg) if err != nil { return appRenewEffectiveBaseCosts{}, err } return appRenewEffectiveBaseCosts{ AgentCost: appRenewApplyPriceRule(parentCosts.AgentCost, rule.AgentCostRatio, rule.AgentCostProfit), TerminalCost: appRenewApplyPriceRule(parentCosts.TerminalCost, rule.TerminalCostRatio, rule.TerminalCostProfit), }, nil } func appRenewSystemBaseCosts(pkg *entity.TSimFlowPackageInfo) appRenewEffectiveBaseCosts { terminalCost := pkg.TerminalCost if terminalCost <= 0 { terminalCost = pkg.CostPrice } return appRenewEffectiveBaseCosts{ AgentCost: appRoundAmount(pkg.CostPrice), TerminalCost: appRoundAmount(terminalCost), } } func (s *sAppCard) resolveAppRenewPriceRule(ctx context.Context, tenantId string, ownerDeptId int64, targetDeptId int64, pkg *entity.TSimFlowPackageInfo) (appRenewPriceRule, error) { rule := appRenewSystemPriceRule(pkg) if ownerDeptId <= 0 { return rule, nil } if targetDeptId > 0 { targetPrice, err := s.loadAppRenewPriceOverride(ctx, tenantId, ownerDeptId, 1, targetDeptId, pkg.PackageCode) if err != nil { return rule, err } if targetPrice != nil { return appRenewPriceRuleFromOverride("target", targetPrice), nil } } defaultPrice, err := s.loadAppRenewPriceOverride(ctx, tenantId, ownerDeptId, 0, 0, pkg.PackageCode) if err != nil { return rule, err } if defaultPrice != nil { return appRenewPriceRuleFromOverride("agentDefault", defaultPrice), nil } return rule, nil } func appRenewSystemPriceRule(pkg *entity.TSimFlowPackageInfo) appRenewPriceRule { return appRenewPriceRule{ PriceSource: "system", AgentCostRatio: appNormalizePriceRatio(pkg.SysDefaultRatio), AgentCostProfit: pkg.SysDefaultProfit, TerminalCostRatio: appNormalizePriceRatio(pkg.SysDefaultTerminalRatio), TerminalCostProfit: pkg.SysDefaultTerminalProfit, } } func appRenewPriceRuleFromOverride(priceSource string, price *entity.TSimFlowPackagePrice) appRenewPriceRule { if price == nil { return appRenewPriceRule{PriceSource: priceSource} } return appRenewPriceRule{ PriceSource: priceSource, AgentDefaultPriceId: appRenewDefaultPriceId(priceSource, price.Id), TargetPriceId: appRenewTargetPriceId(priceSource, price.Id), AgentCostRatio: appNormalizePriceRatio(price.AgentCostRatio), AgentCostProfit: price.AgentCostProfit, TerminalCostRatio: appNormalizePriceRatio(price.UserCostRatio), TerminalCostProfit: price.UserCostProfit, } } func appRenewDefaultPriceId(priceSource string, priceId int64) int64 { if priceSource == "agentDefault" { return priceId } return 0 } func appRenewTargetPriceId(priceSource string, priceId int64) int64 { if priceSource == "target" { return priceId } return 0 } func appRenewTerminalPriceSource(priceSource string) string { if priceSource == "agentDefault" { return "agentDefaultTerminal" } if priceSource == "target" { return "targetTerminal" } return priceSource } func appRenewApplyPriceRule(baseCost float64, ratio float64, profit float64) float64 { return appRoundAmount(baseCost*appNormalizePriceRatio(ratio) + profit) } func (s *sAppCard) renewSellerDeptId(ctx context.Context, user *entity.SysUser) (int64, error) { if user == nil || user.DeptId <= 0 { return 0, nil } return s.renewSellerDeptIdByDept(ctx, user.TenantId, user.DeptId) } func (s *sAppCard) renewSellerDeptIdByDept(ctx context.Context, tenantId string, deptId int64) (int64, error) { dept, err := s.loadAppRenewDept(ctx, tenantId, deptId) if err != nil { return 0, err } if dept != nil && dept.ParentId > 0 { return dept.ParentId, nil } return deptId, nil } // renewPriceOwnerDeptIdByDept 与控制台续费定价一致:定价归属方始终为客户部门的直接上级。 func (s *sAppCard) renewPriceOwnerDeptIdByDept(ctx context.Context, tenantId string, deptId int64) (int64, error) { dept, err := s.loadAppRenewDept(ctx, tenantId, deptId) if err != nil { return 0, err } if dept == nil || dept.DeptId == 0 { return 0, gerror.New("代理组织不存在") } return dept.ParentId, nil } func (s *sAppCard) loadAppRenewDept(ctx context.Context, tenantId string, deptId int64) (*entity.SysDept, error) { if deptId <= 0 { return nil, nil } var dept *entity.SysDept err := dao.SysDept.Ctx(ctx). Where(dao.SysDept.Columns().TenantId, tenantId). Where(dao.SysDept.Columns().DeptId, deptId). Limit(1). Scan(&dept) if err != nil { return nil, gerror.Wrap(err, "查询代理归属失败") } return dept, nil } func (s *sAppCard) loadAppRenewPriceOverride(ctx context.Context, tenantId string, deptId int64, scopeType int, targetAgentId int64, packageCode string) (*entity.TSimFlowPackagePrice, error) { var price *entity.TSimFlowPackagePrice err := dao.TSimFlowPackagePrice.Ctx(ctx). Where(dao.TSimFlowPackagePrice.Columns().TenantId, tenantId). Where(dao.TSimFlowPackagePrice.Columns().DeptId, deptId). Where(dao.TSimFlowPackagePrice.Columns().AgentScopeType, scopeType). Where(dao.TSimFlowPackagePrice.Columns().TargetAgentId, targetAgentId). Where(dao.TSimFlowPackagePrice.Columns().PackageCode, packageCode). Limit(1). Scan(&price) if err != nil { return nil, gerror.Wrap(err, "查询续费定价失败") } if price == nil || price.Id == 0 { return nil, nil } return price, nil } func appTerminalBindDeptId(card *appCardQueryRow) int64 { return appCardOwnerDeptId(card) } func (s *sAppCard) renewOrderParty(ctx context.Context, user *entity.SysUser, card *appCardQueryRow) (*appRenewOrderParty, error) { if user == nil || card == nil { return nil, gerror.New("续费订单归属信息不完整") } if s.isTerminalUser(ctx, user) { priceOwnerDeptId := appTerminalBindDeptId(card) if priceOwnerDeptId <= 0 { return nil, gerror.New("卡片归属代理商为空,暂不能续费") } return &appRenewOrderParty{ CustomerType: consts.CustomerTypeTerminal, CustomerAgentId: priceOwnerDeptId, EndUserId: user.UserId, AgentId: priceOwnerDeptId, CreatedDept: user.DeptId, }, nil } if user.DeptId <= 0 { return nil, gerror.New("当前用户未绑定机构,暂不能续费") } customerAgentId := appCardOwnerDeptId(card) if customerAgentId <= 0 { return nil, gerror.New("卡片归属机构为空,暂不能续费") } return &appRenewOrderParty{ CustomerType: consts.CustomerTypeAgent, CustomerAgentId: customerAgentId, EndUserId: 0, AgentId: user.DeptId, CreatedDept: user.DeptId, }, nil } func (s *sAppCard) lockRenewCardForSubmit(ctx context.Context, tx gdb.TX, card *appCardQueryRow, expectedCurrentPackageCode string) (*entity.TSimCardInfo, error) { if card == nil || card.Id <= 0 { return nil, gerror.New("卡信息不存在或无权续费") } var locked *entity.TSimCardInfo err := dao.TSimCardInfo.Ctx(ctx).TX(tx). Where(dao.TSimCardInfo.Columns().Id, card.Id). LockUpdate(). Scan(&locked) if err != nil { return nil, gerror.Wrap(err, "锁定卡信息失败") } if locked == nil || locked.Id == 0 { return nil, gerror.New("卡信息不存在或已变化,请刷新后重试") } if strings.TrimSpace(locked.TenantId) != strings.TrimSpace(card.TenantId) || strings.TrimSpace(locked.Iccid) != strings.TrimSpace(card.Iccid) { return nil, gerror.New("卡片归属已变化,请刷新后重试") } if card.TerminalBindId > 0 || card.TerminalUserId > 0 { bind, err := s.lockRenewTerminalBindForSubmit(ctx, tx, card) if err != nil { return nil, err } bindDeptId := bind.DeptId if bindDeptId <= 0 { bindDeptId = locked.DeptId } if bindDeptId != appTerminalBindDeptId(card) { return nil, gerror.New("卡片归属已变化,请刷新后重试") } bindPackageCode := strings.TrimSpace(bind.CurrentPackageCode) if bindPackageCode == "" { bindPackageCode = strings.TrimSpace(locked.CurrentPackageCode) } if bindPackageCode != strings.TrimSpace(expectedCurrentPackageCode) { return nil, gerror.New("当前卡片套餐已变化,请刷新后重试") } card.TerminalDeptId = bind.DeptId card.TerminalCurrentPackageCode = strings.TrimSpace(bind.CurrentPackageCode) } else if locked.DeptId != card.DeptId { return nil, gerror.New("卡片归属已变化,请刷新后重试") } else if strings.TrimSpace(locked.CurrentPackageCode) != strings.TrimSpace(expectedCurrentPackageCode) { return nil, gerror.New("当前卡片套餐已变化,请刷新后重试") } card.TSimCardInfo = *locked return locked, nil } func (s *sAppCard) lockRenewTerminalBindForSubmit(ctx context.Context, tx gdb.TX, card *appCardQueryRow) (*appSimTerminalRenewLockRow, error) { var bind *appSimTerminalRenewLockRow mod := dao.TSimTerminal.Ctx(ctx).TX(tx). Where(dao.TSimTerminal.Columns().TenantId, card.TenantId). Where(dao.TSimTerminal.Columns().Iccid, card.Iccid) if card.TerminalBindId > 0 { mod = mod.Where(dao.TSimTerminal.Columns().Id, card.TerminalBindId) } else { mod = mod.Where(dao.TSimTerminal.Columns().TerminalId, card.TerminalUserId) } err := mod.LockUpdate().Limit(1).Scan(&bind) if err != nil { return nil, gerror.Wrap(err, "锁定终端卡关联失败") } if bind == nil || bind.Id == 0 { return nil, gerror.New("终端卡关联已变化,请刷新后重试") } if strings.TrimSpace(bind.TenantId) != strings.TrimSpace(card.TenantId) || strings.TrimSpace(bind.Iccid) != strings.TrimSpace(card.Iccid) || bind.TerminalId != card.TerminalUserId { return nil, gerror.New("终端卡关联已变化,请刷新后重试") } return bind, nil } func (s *sAppCard) insertAppRenewOrder(ctx context.Context, tx gdb.TX, user *entity.SysUser, party *appRenewOrderParty, buyerNote string, orderSn string, totalAmount float64, now *gtime.Time) (int64, error) { orderId, err := dao.TOrder.Ctx(ctx).TX(tx).Data(do.TOrder{ OrderSn: orderSn, ParentOrderId: 0, ParentOrderSn: "", CustomerType: party.CustomerType, CustomerAgentId: party.CustomerAgentId, EndUserId: party.EndUserId, AgentId: party.AgentId, OrderType: consts.OrderTypeCardSubscribe, OrderStatus: consts.OrderStatusCompleted, ShippingStatus: 0, ReturnStatus: 0, Consignee: "", Mobile: "", Email: "", Address: "", AreaCode: "", BuyerNote: strings.TrimSpace(buyerNote), AdminNote: "", Remark: "小程序套餐续费余额扣款订单", ShippingMethod: consts.ShippingMethodNone, LogisticsId: 0, LogisticsName: "", ShippingTypeId: 0, ShippingTypeName: "", TrackingNo: "", PayType: consts.PayTypeBalance, PayStatus: consts.PayStatusPaid, PayOrderSn: "", PayTime: now, TotalAmount: totalAmount, ProductAmount: totalAmount, ShippingFee: 0, ServiceFee: 0, InvoiceFee: 0, CouponAmount: 0, DiscountAmount: 0, Balance: totalAmount, PaidAmount: totalAmount, OnlinePaidAmount: 0, OfflinePaidAmount: 0, UnpaidAmount: 0, UnrefundAmount: 0, IsNeedCommission: 0, DistributionStatus: 0, TenantId: user.TenantId, CreatedDept: party.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }).InsertAndGetId() if err != nil { return 0, gerror.Wrap(err, "创建续费订单失败") } return orderId, nil } func appRenewServicePeriod(expireAt *gtime.Time, months int) (*gtime.Time, *gtime.Time) { beginAt := appRenewBeginDateFromExpire(expireAt) if beginAt == nil || beginAt.IsZero() { beginAt = appRenewBeginDateFromExpire(gtime.Now()) } return beginAt, beginAt.AddDate(0, months, 0) } func appRenewSubmitExpireAt(lockedCard *entity.TSimCardInfo, renewCtx *appRenewPackageContext) *gtime.Time { var bind *entity.TSimCardBindPkg if renewCtx != nil { bind = renewCtx.LatestBind } return appRenewBaseExpireTimeFromEntity(lockedCard, bind) } func appRenewMarshalJSON(value any) string { data, err := json.Marshal(value) if err != nil { return "{}" } return string(data) } func appRenewFormatTime(value *gtime.Time) string { if value == nil || value.IsZero() { return "" } return value.Format("Y-m-d H:i:s") } func appContainsInt(values []int, target int) bool { for _, value := range values { if value == target { return true } } return false } func appRenewRecommendIndex(durations []int) int { if len(durations) == 0 { return -1 } return len(durations) - 1 } func appRenewDurationSelectionFromParam(durationYears int, durationMonths int, options []int) appRenewDurationSelection { years := durationYears if years <= 0 { years = appRenewDurationYearsFromLegacy(durationMonths, options) } if years <= 0 { return appRenewDurationSelection{} } return appRenewDurationSelection{ Years: years, Months: appRenewActualDurationMonths(years), } } func appRenewDurationYearsFromLegacy(durationMonths int, options []int) int { if durationMonths <= 0 { return 0 } options = appNormalizeRenewYearOptions(options) if appContainsInt(options, durationMonths) { return durationMonths } if durationMonths%12 == 0 && appContainsInt(options, durationMonths/12) { return durationMonths / 12 } if durationMonths%12 == 0 { return durationMonths / 12 } return durationMonths } func appRenewActualDurationMonths(years int) int { if years <= 0 { return 0 } return years * 12 } func appRenewTotalAmount(unitPrice float64, durationYears int, quantity int) float64 { if quantity <= 0 { quantity = 1 } return appRoundAmount(unitPrice * float64(durationYears) * float64(quantity)) } func appRenewOptionModel(durationYears int, unitPrice float64, totalAmount float64, expireAfter *gtime.Time, recommended bool) *model.AppRenewOptionModel { durationMonths := appRenewActualDurationMonths(durationYears) return &model.AppRenewOptionModel{ DurationMonths: durationMonths, ActualDurationMonths: durationMonths, DurationYears: durationYears, DurationValue: durationYears, DurationUnit: appRenewDurationUnitYear, DurationUnitText: appRenewDurationUnitYearText, Label: appRenewDurationYearLabel(durationYears), UnitPrice: unitPrice, TotalAmount: totalAmount, PriceText: fmt.Sprintf("¥%.2f", totalAmount), ExpireAfter: expireAfter, Recommended: recommended, } } func appRenewDurationYearLabel(years int) string { if years <= 0 { return "" } return fmt.Sprintf("%d 年", years) } func appRenewDurationLabel(months int) string { if months%12 == 0 { years := months / 12 if years == 1 { return "1 年" } return fmt.Sprintf("%d 年", years) } return fmt.Sprintf("%d 个月", months) } func appRenewExpireAfter(expireAt *gtime.Time, months int) *gtime.Time { base := appRenewBeginDateFromExpire(expireAt) if base == nil || base.IsZero() { base = appRenewBeginDateFromExpire(gtime.Now()) } return base.AddDate(0, months, 0) } func appRenewEffectiveRule(expireAt *gtime.Time) string { if expireAt != nil && !expireAt.IsZero() && expireAt.After(gtime.Now()) { return "当前套餐到期后顺延" } return "按当前套餐到期日顺延" } func appRenewBaseExpireTimeFromRow(card *appCardQueryRow, bind *entity.TSimCardBindPkg) *gtime.Time { var expireAt *gtime.Time if bind != nil && bind.ExpectedExpireTime != nil && !bind.ExpectedExpireTime.IsZero() { expireAt = bind.ExpectedExpireTime } if card == nil || card.FlowPkgEndAt == nil || card.FlowPkgEndAt.IsZero() { return expireAt } if expireAt == nil || card.FlowPkgEndAt.After(expireAt) { return card.FlowPkgEndAt } return expireAt } func appRenewBaseExpireTimeFromEntity(card *entity.TSimCardInfo, bind *entity.TSimCardBindPkg) *gtime.Time { var expireAt *gtime.Time if bind != nil && bind.ExpectedExpireTime != nil && !bind.ExpectedExpireTime.IsZero() { expireAt = bind.ExpectedExpireTime } if card == nil || card.FlowPkgEndAt == nil || card.FlowPkgEndAt.IsZero() { return expireAt } if expireAt == nil || card.FlowPkgEndAt.After(expireAt) { return card.FlowPkgEndAt } return expireAt } func appRenewBeginDateFromExpire(expireAt *gtime.Time) *gtime.Time { if expireAt == nil || expireAt.IsZero() { return nil } t := expireAt.Time return gtime.NewFromTime(time.Date(t.Year(), t.Month(), 0, 23, 59, 59, 0, t.Location())) } func appRenewExpireAfterAllowedWindow(expireAt *gtime.Time) bool { if expireAt == nil || expireAt.IsZero() { return false } return expireAt.After(gtime.Now().AddDate(0, 6, 0)) } func appNormalizePriceRatio(ratio float64) float64 { if ratio > 10 { return ratio / 100 } return ratio } func appRoundAmount(value float64) float64 { return math.Round(value*100) / 100 }