Files
iotlcm/server/internal/logic/iot/cardpackage/subscribe.go
T
MiFaZhan 59eb34976a chore: 初始开源发布
iotlcm-admin-svr:管理后台服务。

- Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件)
- 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、
  MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位;
  私网 IP → 文档地址段;库名 xiujie_admin → iotlcm
- 真实手机号/邮箱 → 合法 11 位编造号 / example.com
- 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、
  私有依赖 go get 行、明文 registry 登录口令
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- Dockerfile 改用公共基础镜像
- 拼错的 RADEME.md → README.md,README 重写
- 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用
- 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes

数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
2026-10-23 14:48:30 +08:00

545 lines
16 KiB
Go

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)
}