Files
iotlcm/server/internal/logic/app/app_card_renew.go
T

1206 lines
38 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}