59eb34976a
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 已说明。
545 lines
16 KiB
Go
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)
|
|
}
|