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 已说明。
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
package cardpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/logic/iot/packageduration"
|
||||
"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/os/gtime"
|
||||
)
|
||||
|
||||
func (s *Service) RenewCurrentPackage(ctx context.Context, tx gdb.TX, in *RenewCurrentPackageInput) (*RenewCurrentPackageOutput, error) {
|
||||
if err := validateRenewInput(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iccids := uniqueStrings(in.Iccids)
|
||||
cardsByIccid, err := s.repo.LockCardsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定卡信息失败")
|
||||
}
|
||||
if len(cardsByIccid) != len(iccids) {
|
||||
return nil, gerror.New("部分卡信息已变化,请刷新后重试")
|
||||
}
|
||||
|
||||
latestBindsByIccid, err := s.repo.LockLatestBindsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定最新订购失败")
|
||||
}
|
||||
if len(latestBindsByIccid) != len(iccids) {
|
||||
return nil, gerror.New("部分卡没有订购记录,不能续费")
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
sourceBatchNo := strings.TrimSpace(in.SourceBatchNo)
|
||||
currentPackageCode := ""
|
||||
customerAgentId := int64(0)
|
||||
var renewBeginAt *gtime.Time
|
||||
for _, iccid := range iccids {
|
||||
card := cardsByIccid[iccid]
|
||||
bind := latestBindsByIccid[iccid]
|
||||
if err := validateRenewCardStatus(iccid, card); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bind == nil {
|
||||
return nil, gerror.Newf("ICCID没有订购记录:%s", iccid)
|
||||
}
|
||||
if strings.TrimSpace(card.CurrentPackageCode) == "" || card.PackageStatus == consts.PackageStatusNone {
|
||||
return nil, gerror.Newf("ICCID没有可续费套餐:%s", iccid)
|
||||
}
|
||||
if strings.TrimSpace(card.CurrentPackageCode) != strings.TrimSpace(bind.PackageCode) {
|
||||
return nil, gerror.Newf("ICCID当前套餐与最新订购记录不一致:%s", iccid)
|
||||
}
|
||||
baseExpireAt := renewBaseExpireTime(card, bind)
|
||||
if baseExpireAt == nil {
|
||||
return nil, gerror.Newf("ICCID最新订购没有预计到期时间:%s", iccid)
|
||||
}
|
||||
periodBegin := renewPeriodBeginAt(baseExpireAt, in.PackageBeginTime)
|
||||
if renewExpireAfterAllowedWindow(baseExpireAt, now) {
|
||||
return nil, gerror.Newf("距离到期超过6个月,不能续费:%s", iccid)
|
||||
}
|
||||
if renewBeginAt == nil {
|
||||
renewBeginAt = periodBegin
|
||||
} else if !sameRenewTime(renewBeginAt, periodBegin) {
|
||||
return nil, gerror.New("当前批次下卡不是统一续费批次")
|
||||
}
|
||||
if sourceBatchNo != "" && strings.TrimSpace(card.OutboundBatchNo) != sourceBatchNo {
|
||||
return nil, gerror.Newf("ICCID不属于来源出库批次:%s", iccid)
|
||||
}
|
||||
if currentPackageCode == "" {
|
||||
currentPackageCode = strings.TrimSpace(card.CurrentPackageCode)
|
||||
}
|
||||
if currentPackageCode != strings.TrimSpace(card.CurrentPackageCode) {
|
||||
return nil, gerror.New("当前批次下存在多个套餐,不能统一续费")
|
||||
}
|
||||
if customerAgentId == 0 {
|
||||
customerAgentId = card.DeptId
|
||||
}
|
||||
if customerAgentId != card.DeptId {
|
||||
return nil, gerror.New("当前批次下存在多个客户,不能统一续费")
|
||||
}
|
||||
}
|
||||
|
||||
customer, err := s.loadTargetDept(ctx, tx, customerAgentId, in.Operator, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targetPackageCode := strings.TrimSpace(in.PackageCode)
|
||||
if targetPackageCode == "" {
|
||||
targetPackageCode = currentPackageCode
|
||||
}
|
||||
currentPkg, err := s.loadPackage(ctx, tx, currentPackageCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !renewPackageAllowed(currentPkg, targetPackageCode) {
|
||||
return nil, gerror.New("续费套餐不在当前套餐可续费范围内")
|
||||
}
|
||||
pkg := currentPkg
|
||||
if targetPackageCode != currentPackageCode {
|
||||
pkg, err = s.loadPackage(ctx, tx, targetPackageCode)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
durationUnit, durationValue, durationMonths, giftDurationValue, giftDurationMonths := normalizeRenewPackageDuration(in)
|
||||
if durationMonths <= 0 {
|
||||
return nil, gerror.New("续费时长必须大于0")
|
||||
}
|
||||
amount, err := parseAmount(in.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quantity := len(iccids)
|
||||
unitPrice := 0.0
|
||||
if quantity > 0 {
|
||||
unitPrice = roundAmount(amount / float64(quantity))
|
||||
}
|
||||
|
||||
firstBaseExpireAt := renewBaseExpireTime(cardsByIccid[iccids[0]], latestBindsByIccid[iccids[0]])
|
||||
minBeginAt := renewPeriodBeginAt(firstBaseExpireAt, in.PackageBeginTime)
|
||||
maxEndAt := firstBaseExpireAt
|
||||
for _, iccid := range iccids {
|
||||
baseExpireAt := renewBaseExpireTime(cardsByIccid[iccid], latestBindsByIccid[iccid])
|
||||
periodBegin := renewPeriodBeginAt(baseExpireAt, in.PackageBeginTime)
|
||||
if minBeginAt == nil || (periodBegin != nil && periodBegin.Before(minBeginAt)) {
|
||||
minBeginAt = periodBegin
|
||||
}
|
||||
endAt := addMonths(periodBegin, durationMonths+giftDurationMonths)
|
||||
if maxEndAt == nil || (endAt != nil && endAt.After(maxEndAt)) {
|
||||
maxEndAt = endAt
|
||||
}
|
||||
}
|
||||
if in.PackageEndTime != nil && !sameRenewTime(maxEndAt, in.PackageEndTime) {
|
||||
return nil, gerror.New("续费结束日期与续费时长不一致")
|
||||
}
|
||||
|
||||
orderSn, err := batchimport.NextRandomNo(orderNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orderInput := &CreatePackageSubscriptionInput{
|
||||
BatchNo: in.BatchNo,
|
||||
CustomerAgentId: customerAgentId,
|
||||
PackageCode: targetPackageCode,
|
||||
PackageBeginTime: minBeginAt,
|
||||
DurationMonths: durationMonths,
|
||||
GiftDurationMonths: giftDurationMonths,
|
||||
DurationDisplayValue: durationValue,
|
||||
GiftDurationDisplayValue: giftDurationValue,
|
||||
DurationUnit: durationUnit,
|
||||
Amount: in.Amount,
|
||||
PayType: in.PayType,
|
||||
PayStatus: in.PayStatus,
|
||||
BalanceAmount: in.BalanceAmount,
|
||||
OfflinePaidAmount: in.OfflinePaidAmount,
|
||||
PayTime: in.PayTime,
|
||||
OfflineContractCompleted: in.OfflineContractCompleted,
|
||||
BuyerNote: in.BuyerNote,
|
||||
AdminNote: in.AdminNote,
|
||||
BusinessType: consts.PackageBusinessTypeRenew,
|
||||
OrderRemark: chooseString(in.OrderRemark, "卡续费业务订单载体"),
|
||||
BindRemark: in.BindRemark,
|
||||
Operator: in.Operator,
|
||||
}
|
||||
orderId, err := s.insertOrder(ctx, tx, orderInput, orderSn, amount, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = s.insertOrderItem(ctx, tx, orderInput, orderId, orderSn, pkg, amount, unitPrice, quantity, minBeginAt, maxEndAt, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bindRows := make([]do.TSimCardBindPkg, 0, quantity)
|
||||
newBindsByIccid := make(map[string]*entity.TSimCardBindPkg, quantity)
|
||||
for _, iccid := range iccids {
|
||||
card := cardsByIccid[iccid]
|
||||
latestBind := latestBindsByIccid[iccid]
|
||||
periodBegin := renewPeriodBeginAt(renewBaseExpireTime(card, latestBind), in.PackageBeginTime)
|
||||
periodEnd := addMonths(periodBegin, durationMonths+giftDurationMonths)
|
||||
subscriptionNo, err := batchimport.NextRandomNo(subscriptionNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row := do.TSimCardBindPkg{
|
||||
SubscriptionNo: subscriptionNo,
|
||||
BatchNo: in.BatchNo,
|
||||
Iccid: iccid,
|
||||
Msisdn: strings.TrimSpace(card.Msisdn),
|
||||
DeptId: fmt.Sprintf("%d", customerAgentId),
|
||||
PackageType: pkg.PackageType,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PreviousPackageCode: currentPackageCode,
|
||||
BusinessType: consts.PackageBusinessTypeRenew,
|
||||
EffectiveStatus: consts.EffectiveStatusPending,
|
||||
HandleTime: now,
|
||||
EffectiveTime: periodBegin,
|
||||
ExpectedExpireTime: periodEnd,
|
||||
DurationMonths: durationMonths,
|
||||
GiftDurationMonths: 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, row)
|
||||
newBindsByIccid[iccid] = &entity.TSimCardBindPkg{
|
||||
SubscriptionNo: subscriptionNo,
|
||||
BatchNo: in.BatchNo,
|
||||
Iccid: iccid,
|
||||
Msisdn: strings.TrimSpace(card.Msisdn),
|
||||
DeptId: fmt.Sprintf("%d", customerAgentId),
|
||||
PackageType: pkg.PackageType,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PreviousPackageCode: currentPackageCode,
|
||||
BusinessType: consts.PackageBusinessTypeRenew,
|
||||
EffectiveStatus: consts.EffectiveStatusPending,
|
||||
HandleTime: now,
|
||||
EffectiveTime: periodBegin,
|
||||
ExpectedExpireTime: periodEnd,
|
||||
DurationMonths: durationMonths,
|
||||
GiftDurationMonths: 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 err = s.InsertCardPackageBinds(ctx, tx, bindRows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := &RenewCurrentPackageOutput{
|
||||
Iccids: iccids,
|
||||
OrderId: orderId,
|
||||
OrderSn: orderSn,
|
||||
Customer: customer,
|
||||
Package: pkg,
|
||||
CardsByIccid: cardsByIccid,
|
||||
LatestBindsByIccid: latestBindsByIccid,
|
||||
NewBindsByIccid: newBindsByIccid,
|
||||
BindRows: bindRows,
|
||||
RenewTime: now,
|
||||
PackageCode: targetPackageCode,
|
||||
Amount: amount,
|
||||
UnitPrice: unitPrice,
|
||||
Quantity: quantity,
|
||||
}
|
||||
if err = s.UpdateCardsAfterRenew(ctx, tx, out, in.Operator.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func renewExpireAfterAllowedWindow(expireAt, now *gtime.Time) bool {
|
||||
if expireAt == nil {
|
||||
return false
|
||||
}
|
||||
if now == nil {
|
||||
now = gtime.Now()
|
||||
}
|
||||
return expireAt.After(now.AddDate(0, 6, 0))
|
||||
}
|
||||
|
||||
func sameRenewTime(left, right *gtime.Time) bool {
|
||||
switch {
|
||||
case left == nil && right == nil:
|
||||
return true
|
||||
case left == nil || right == nil:
|
||||
return false
|
||||
default:
|
||||
return !left.Before(right) && !left.After(right)
|
||||
}
|
||||
}
|
||||
|
||||
func renewPeriodBeginAt(latest, requested *gtime.Time) *gtime.Time {
|
||||
if requested != nil {
|
||||
return renewDayEndAt(requested)
|
||||
}
|
||||
return renewBeginDateFromExpire(latest)
|
||||
}
|
||||
|
||||
func renewBeginDateFromExpire(expireAt *gtime.Time) *gtime.Time {
|
||||
if expireAt == nil {
|
||||
return nil
|
||||
}
|
||||
return renewDayEndAt(expireAt)
|
||||
}
|
||||
|
||||
func renewBaseExpireTime(card *entity.TSimCardInfo, bind *entity.TSimCardBindPkg) *gtime.Time {
|
||||
var expireAt *gtime.Time
|
||||
if bind != nil {
|
||||
expireAt = laterRenewExpireTime(expireAt, bind.ExpectedExpireTime)
|
||||
}
|
||||
if card != nil {
|
||||
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgEndAt)
|
||||
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgPresentAt)
|
||||
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgRealPresentAt)
|
||||
}
|
||||
return renewDayEndAt(expireAt)
|
||||
}
|
||||
|
||||
func laterRenewExpireTime(current, candidate *gtime.Time) *gtime.Time {
|
||||
if candidate == nil || candidate.IsZero() {
|
||||
return current
|
||||
}
|
||||
if current == nil || current.IsZero() || candidate.After(current) {
|
||||
return candidate
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func renewDayEndAt(t *gtime.Time) *gtime.Time {
|
||||
if t == nil || t.IsZero() {
|
||||
return nil
|
||||
}
|
||||
tt := t.Time
|
||||
return gtime.NewFromTime(time.Date(tt.Year(), tt.Month(), tt.Day(), 23, 59, 59, 0, tt.Location()))
|
||||
}
|
||||
|
||||
func renewPackageAllowed(currentPkg *entity.TSimFlowPackageInfo, targetPackageCode string) bool {
|
||||
targetPackageCode = strings.TrimSpace(targetPackageCode)
|
||||
if currentPkg == nil || targetPackageCode == "" {
|
||||
return false
|
||||
}
|
||||
var codes []string
|
||||
if currentPkg.RenewablePackageCodes != nil {
|
||||
_ = currentPkg.RenewablePackageCodes.Scan(&codes)
|
||||
}
|
||||
for _, code := range uniqueStrings(codes) {
|
||||
if strings.TrimSpace(code) == targetPackageCode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeRenewPackageDuration(in *RenewCurrentPackageInput) (durationUnit string, durationValue int, durationMonths int, giftDurationValue int, giftDurationMonths int) {
|
||||
durationUnit = consts.RenewDurationUnitYear
|
||||
if in == nil {
|
||||
return
|
||||
}
|
||||
durationValue = in.DurationDisplayValue
|
||||
durationMonths = in.DurationMonths
|
||||
if durationValue > 0 {
|
||||
// Renewal duration is always expressed in years. The database keeps
|
||||
// the resulting subscription duration in months for lifecycle logic.
|
||||
durationMonths = packageduration.RenewMonths(durationValue)
|
||||
}
|
||||
|
||||
giftDurationMonths = in.GiftDurationMonths
|
||||
giftDurationValue = in.GiftDurationDisplayValue
|
||||
if giftDurationValue <= 0 {
|
||||
giftDurationValue = giftDurationMonths
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func validateRenewCardStatus(iccid string, card *entity.TSimCardInfo) error {
|
||||
if card == nil {
|
||||
return gerror.Newf("ICCID不存在正式卡记录:%s", iccid)
|
||||
}
|
||||
if card.BossCardStatus == consts.V3ApiSimBossCardStatusStopped {
|
||||
return gerror.Newf("ICCID当前为停机状态,不能续费:%s", iccid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRenewInput(in *RenewCurrentPackageInput) error {
|
||||
if in == nil {
|
||||
return gerror.New("续费参数不能为空")
|
||||
}
|
||||
if strings.TrimSpace(in.BatchNo) == "" {
|
||||
return gerror.New("续费批次号不能为空")
|
||||
}
|
||||
if len(in.Iccids) == 0 {
|
||||
return gerror.New("续费卡不能为空")
|
||||
}
|
||||
if in.DurationMonths <= 0 && in.DurationDisplayValue <= 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 chooseString(primary, fallback string) string {
|
||||
primary = strings.TrimSpace(primary)
|
||||
if primary != "" {
|
||||
return primary
|
||||
}
|
||||
return strings.TrimSpace(fallback)
|
||||
}
|
||||
Reference in New Issue
Block a user