package cardpackage import ( "context" "encoding/json" "fmt" "math" "strconv" "strings" "time" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) const ( orderNoPrefix = "SO" subscriptionNoPrefix = "SUB" ) func (s *Service) CreatePackageSubscription(ctx context.Context, tx gdb.TX, in *CreatePackageSubscriptionInput) (*CreatePackageSubscriptionOutput, error) { if err := validateSubscribeInput(in); err != nil { return nil, err } customer, err := s.loadTargetDept(ctx, tx, in.CustomerAgentId, in.Operator, false) if err != nil { return nil, err } pkg, err := s.loadPackage(ctx, tx, strings.TrimSpace(in.PackageCode)) if err != nil { return nil, err } now := gtime.Now() periodBegin := in.PackageBeginTime periodEnd := packageEndAt(periodBegin, in.DurationMonths+in.GiftDurationMonths, in.EndAtPreviousMonthEnd) packageStatus := CalcPackageStatus(now, periodBegin, periodEnd) effectiveStatus := CalcEffectiveStatus(now, periodBegin, periodEnd) amount, err := parseAmount(in.Amount) if err != nil { return nil, err } quantity := len(in.Cards) unitPrice := 0.0 if quantity > 0 { unitPrice = roundAmount(amount / float64(quantity)) } businessType := in.BusinessType if businessType == 0 { businessType = consts.PackageBusinessTypeSubscribe } orderSn, err := batchimport.NextRandomNo(orderNoPrefix) if err != nil { return nil, err } orderId, err := s.insertOrder(ctx, tx, in, orderSn, amount, now) if err != nil { return nil, err } if err = s.insertOrderItem(ctx, tx, in, orderId, orderSn, pkg, amount, unitPrice, quantity, periodBegin, periodEnd, now); err != nil { return nil, err } bindRows := make([]do.TSimCardBindPkg, 0, quantity) bindsByIccid := make(map[string]*entity.TSimCardBindPkg, quantity) for _, card := range in.Cards { iccid := strings.TrimSpace(card.Iccid) if iccid == "" { return nil, gerror.New("订购卡ICCID不能为空") } subscriptionNo, err := batchimport.NextRandomNo(subscriptionNoPrefix) if err != nil { return nil, err } bindRow := do.TSimCardBindPkg{ SubscriptionNo: subscriptionNo, BatchNo: in.BatchNo, Iccid: iccid, Msisdn: strings.TrimSpace(card.Msisdn), DeptId: fmt.Sprintf("%d", in.CustomerAgentId), PackageType: pkg.PackageType, PackageCode: pkg.PackageCode, PreviousPackageCode: strings.TrimSpace(in.PreviousPackageCode), BusinessType: businessType, EffectiveStatus: effectiveStatus, HandleTime: now, EffectiveTime: periodBegin, ExpectedExpireTime: periodEnd, DurationMonths: in.DurationMonths, GiftDurationMonths: in.GiftDurationMonths, OrderCode: orderSn, Remark: strings.TrimSpace(in.BindRemark), TenantId: in.Operator.TenantId, CreatedDept: in.Operator.DeptId, CreatedBy: in.Operator.UserId, CreatedAt: now, UpdatedBy: in.Operator.UserId, UpdatedAt: now, } bindRows = append(bindRows, bindRow) bindsByIccid[iccid] = &entity.TSimCardBindPkg{ SubscriptionNo: subscriptionNo, BatchNo: in.BatchNo, Iccid: iccid, Msisdn: strings.TrimSpace(card.Msisdn), DeptId: fmt.Sprintf("%d", in.CustomerAgentId), PackageType: pkg.PackageType, PackageCode: pkg.PackageCode, PreviousPackageCode: strings.TrimSpace(in.PreviousPackageCode), BusinessType: businessType, EffectiveStatus: effectiveStatus, HandleTime: now, EffectiveTime: periodBegin, ExpectedExpireTime: periodEnd, DurationMonths: in.DurationMonths, GiftDurationMonths: in.GiftDurationMonths, OrderCode: orderSn, Remark: strings.TrimSpace(in.BindRemark), TenantId: in.Operator.TenantId, CreatedDept: in.Operator.DeptId, CreatedBy: in.Operator.UserId, CreatedAt: now, UpdatedBy: in.Operator.UserId, UpdatedAt: now, } } if !in.SkipBindInsert { if err = s.InsertCardPackageBinds(ctx, tx, bindRows); err != nil { return nil, err } } return &CreatePackageSubscriptionOutput{ OrderId: orderId, OrderSn: orderSn, Customer: customer, Package: pkg, PackageBeginAt: periodBegin, PackageEndAt: periodEnd, SubscribeTime: now, PackageStatus: packageStatus, EffectiveStatus: effectiveStatus, Amount: amount, UnitPrice: unitPrice, Quantity: quantity, BindsByIccid: bindsByIccid, BindRows: bindRows, }, nil } func (s *Service) InsertCardPackageBinds(ctx context.Context, tx gdb.TX, rows []do.TSimCardBindPkg) error { if err := s.repo.InsertCardPackageBinds(ctx, tx, rows); err != nil { return gerror.Wrap(err, "写入卡套餐绑定失败") } return nil } func validateSubscribeInput(in *CreatePackageSubscriptionInput) error { if in == nil { return gerror.New("订购参数不能为空") } if strings.TrimSpace(in.BatchNo) == "" { return gerror.New("订购批次号不能为空") } if len(in.Cards) == 0 { return gerror.New("订购卡不能为空") } if in.CustomerAgentId <= 0 { return gerror.New("客户不能为空") } if strings.TrimSpace(in.PackageCode) == "" { return gerror.New("套餐不能为空") } if in.PackageBeginTime == nil { return gerror.New("套餐开始时间不能为空") } if in.DurationMonths <= 0 { return gerror.New("套餐时长必须大于0") } if in.GiftDurationMonths < 0 { return gerror.New("赠送时长不能小于0") } payType := in.PayType if payType == 0 { payType = consts.PayTypeOffline } if payType == consts.PayTypeOffline && !in.OfflineContractCompleted { return gerror.New("请确认线下合同、销售和走账已完成") } return nil } func (s *Service) loadTargetDept(ctx context.Context, tx gdb.TX, deptId int64, operator batchimport.OperatorContext, allowDescendant bool) (*entity.SysDept, error) { if operator.DeptId == 0 { return nil, gerror.New("当前用户未绑定机构") } customer, err := s.repo.LoadTargetDept(ctx, tx, deptId, operator.TenantId) if err != nil { return nil, gerror.Wrap(err, "查询客户/机构失败") } if customer == nil || customer.DeptId == 0 { return nil, gerror.New("客户/机构不存在或不在可操作范围内") } if customer.DeptId == operator.DeptId || customer.ParentId == operator.DeptId { return customer, nil } if allowDescendant && deptAncestorsContain(customer.Ancestors, operator.DeptId) { return customer, nil } if customer.DeptId != operator.DeptId && customer.ParentId != operator.DeptId { return nil, gerror.New("客户/机构不存在或不在可操作范围内") } return customer, nil } func deptAncestorsContain(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 (s *Service) loadPackage(ctx context.Context, tx gdb.TX, packageCode string) (*entity.TSimFlowPackageInfo, error) { pkg, err := s.repo.LoadPackage(ctx, tx, packageCode) if err != nil { return nil, gerror.Wrap(err, "查询套餐失败") } if pkg == nil { return nil, gerror.New("套餐不存在") } return pkg, nil } func (s *Service) insertOrder(ctx context.Context, tx gdb.TX, in *CreatePackageSubscriptionInput, orderSn string, amount float64, now *gtime.Time) (int64, error) { remark := strings.TrimSpace(in.OrderRemark) if remark == "" { remark = "顶级出库划拨业务订单载体" } payment := normalizeSubscriptionPayment(in, amount, now) orderId, err := s.repo.InsertOrder(ctx, tx, do.TOrder{ OrderSn: orderSn, ParentOrderId: 0, ParentOrderSn: "", CustomerType: consts.CustomerTypeAgent, CustomerAgentId: in.CustomerAgentId, EndUserId: 0, AgentId: in.Operator.DeptId, OrderType: orderTypeForBusinessType(in.BusinessType), OrderStatus: consts.OrderStatusCompleted, ShippingStatus: 0, ReturnStatus: 0, Consignee: "", Mobile: "", Email: "", Address: "", BuyerNote: strings.TrimSpace(in.BuyerNote), AdminNote: strings.TrimSpace(in.AdminNote), Remark: remark, ShippingMethod: 3, LogisticsId: 0, LogisticsName: "", ShippingTypeId: 0, ShippingTypeName: "", TrackingNo: "", PayType: payment.PayType, PayStatus: payment.PayStatus, PayOrderSn: "", PayTime: payment.PayTime, TotalAmount: amount, ProductAmount: amount, Balance: payment.BalanceAmount, PaidAmount: amount, OfflinePaidAmount: payment.OfflinePaidAmount, IsNeedCommission: 0, DistributionStatus: 0, TenantId: in.Operator.TenantId, CreatedDept: in.Operator.DeptId, CreatedBy: in.Operator.UserId, CreatedAt: now, UpdatedBy: in.Operator.UserId, UpdatedAt: now, }) if err != nil { return 0, gerror.Wrap(err, "创建订单失败") } return orderId, nil } func orderTypeForBusinessType(businessType int) int { switch businessType { case consts.PackageBusinessTypeRenew: return consts.OrderTypeCardRenew case consts.PackageBusinessTypeChange: return consts.OrderTypeCardChange default: return consts.OrderTypeCardSubscribe } } type subscriptionPayment struct { PayType int PayStatus int BalanceAmount float64 OfflinePaidAmount float64 PayTime *gtime.Time } func normalizeSubscriptionPayment(in *CreatePackageSubscriptionInput, amount float64, now *gtime.Time) subscriptionPayment { payment := subscriptionPayment{ PayType: consts.PayTypeOffline, PayStatus: consts.PayStatusPaid, OfflinePaidAmount: amount, PayTime: now, } if in == nil { return payment } if in.PayType > 0 { payment.PayType = in.PayType } if in.PayStatus > 0 { payment.PayStatus = in.PayStatus } if in.PayTime != nil { payment.PayTime = in.PayTime } switch payment.PayType { case consts.PayTypeBalance: payment.BalanceAmount = roundAmount(in.BalanceAmount) if payment.BalanceAmount == 0 && amount > 0 { payment.BalanceAmount = amount } payment.OfflinePaidAmount = 0 default: payment.OfflinePaidAmount = roundAmount(in.OfflinePaidAmount) if payment.OfflinePaidAmount == 0 && amount > 0 { payment.OfflinePaidAmount = amount } payment.BalanceAmount = roundAmount(in.BalanceAmount) } return payment } func (s *Service) insertOrderItem(ctx context.Context, tx gdb.TX, in *CreatePackageSubscriptionInput, orderId int64, orderSn string, pkg *entity.TSimFlowPackageInfo, amount, unitPrice float64, quantity int, beginAt, endAt *gtime.Time, now *gtime.Time) error { durationLabel := strings.TrimSpace(in.DurationDisplayLabel) if durationLabel == "" { durationLabel = fmt.Sprintf("%d个月", in.DurationMonths) } if strings.TrimSpace(in.DurationDisplayLabel) == "" && in.DurationDisplayValue > 0 && strings.TrimSpace(in.DurationUnit) != "" { durationLabel = fmt.Sprintf("%d%s", in.DurationDisplayValue, consts.RenewDurationUnitText(strings.TrimSpace(in.DurationUnit))) } skuData := marshalJSON([]g.Map{ {"skuName": "套餐", "skuValue": pkg.PackageName, "packageCode": pkg.PackageCode, "packageType": pkg.PackageType}, {"skuName": "数量", "skuValue": fmt.Sprintf("%d", quantity)}, {"skuName": "生效时间", "skuValue": formatTime(beginAt)}, {"skuName": "时长", "skuValue": durationLabel}, }) extraSkuData := marshalJSON(g.Map{ "outboundBatchNo": in.BatchNo, "businessBatchNo": in.BatchNo, "offlineContractCompleted": in.OfflineContractCompleted, "packageBeginTime": formatTime(beginAt), "packageEndTime": formatTime(endAt), "durationMonths": in.DurationMonths, "giftDurationMonths": in.GiftDurationMonths, "durationValue": in.DurationDisplayValue, "giftDurationValue": in.GiftDurationDisplayValue, "durationLabel": durationLabel, "giftDurationLabel": strings.TrimSpace(in.GiftDurationDisplayLabel), "durationUnit": strings.TrimSpace(in.DurationUnit), "amount": amount, }) if err := s.repo.InsertOrderItem(ctx, tx, do.TOrderItem{ OrderId: orderId, OrderSn: orderSn, CustomerType: consts.CustomerTypeAgent, CustomerAgentId: in.CustomerAgentId, EndUserId: 0, Price: unitPrice, Quantity: quantity, ProductId: pkg.Id, ProductName: pkg.PackageName, ProductSn: pkg.PackageCode, PicThumb: "", SkuId: 0, SkuData: skuData, DeliveryQuantity: 0, ProductType: consts.ProductTypeCard, IsGift: 0, AgentId: in.Operator.DeptId, PrepayPrice: 0, Commission: "", OriginPrice: unitPrice, ExtraSkuData: extraSkuData, PayMode: consts.PayModeYearMonth, ServiceBeginTime: beginAt, ServiceEndTime: endAt, TenantId: in.Operator.TenantId, CreatedDept: in.Operator.DeptId, CreatedBy: in.Operator.UserId, CreatedAt: now, UpdatedBy: in.Operator.UserId, UpdatedAt: now, }); err != nil { return gerror.Wrap(err, "创建订单项失败") } return nil } func addMonths(t *gtime.Time, months int) *gtime.Time { if t == nil { return nil } if months <= 0 { return endOfDayAt(t.Time) } beginAt := t.Time targetMonth := time.Date( beginAt.Year(), beginAt.Month()+time.Month(months), 1, beginAt.Hour(), beginAt.Minute(), beginAt.Second(), beginAt.Nanosecond(), beginAt.Location(), ) day := beginAt.Day() lastDay := time.Date( targetMonth.Year(), targetMonth.Month()+1, 0, beginAt.Hour(), beginAt.Minute(), beginAt.Second(), beginAt.Nanosecond(), beginAt.Location(), ).Day() if day > lastDay { day = lastDay } return endOfDayAt(time.Date( targetMonth.Year(), targetMonth.Month(), day, beginAt.Hour(), beginAt.Minute(), beginAt.Second(), beginAt.Nanosecond(), beginAt.Location(), )) } func endOfDayAt(t time.Time) *gtime.Time { return gtime.NewFromTime(time.Date( t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location(), )) } func packageEndAt(beginAt *gtime.Time, months int, previousMonthEnd bool) *gtime.Time { endAt := addMonths(beginAt, months) if !previousMonthEnd || endAt == nil { return endAt } return previousMonthEndAt(endAt) } func previousMonthEndAt(t *gtime.Time) *gtime.Time { if t == nil { return nil } tt := t.Time return gtime.NewFromTime(time.Date(tt.Year(), tt.Month(), 0, 23, 59, 59, 0, tt.Location())) } func CalcPackageStatus(now, beginAt, endAt *gtime.Time) int { if beginAt != nil && beginAt.After(now) { return consts.PackageStatusPending } if endAt != nil && !endAt.After(now) { return consts.PackageStatusExpired } return consts.PackageStatusEffective } func CalcEffectiveStatus(now, beginAt, endAt *gtime.Time) int { if beginAt != nil && beginAt.After(now) { return consts.EffectiveStatusPending } if endAt != nil && !endAt.After(now) { return consts.EffectiveStatusExpired } return consts.EffectiveStatusEffective } func parseAmount(value string) (float64, error) { value = strings.TrimSpace(value) if value == "" { return 0, nil } amount, err := strconv.ParseFloat(value, 64) if err != nil { return 0, gerror.New("订单金额格式不正确") } if amount < 0 { return 0, gerror.New("订单金额不能小于0") } return roundAmount(amount), nil } func roundAmount(value float64) float64 { return math.Round(value*100) / 100 } func marshalJSON(value any) string { data, err := json.Marshal(value) if err != nil { return "{}" } return string(data) }