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:
MiFaZhan
2026-10-23 14:46:05 +08:00
commit 59eb34976a
1132 changed files with 138197 additions and 0 deletions
@@ -0,0 +1,50 @@
package cardpackage
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
func (s *Service) UpdateCardsAfterRenew(ctx context.Context, tx gdb.TX, renew *RenewCurrentPackageOutput, userId int64) error {
if renew == nil || len(renew.NewBindsByIccid) == 0 {
return nil
}
now := gtime.Now()
for _, iccid := range renew.Iccids {
card := renew.CardsByIccid[iccid]
if card == nil {
return gerror.Newf("ICCID卡信息不存在:%s", iccid)
}
bind := renew.NewBindsByIccid[iccid]
if bind == nil {
return gerror.Newf("ICCID续费订购记录不存在:%s", iccid)
}
packageStatus := CalcPackageStatus(now, card.FlowPkgBeginAt, bind.ExpectedExpireTime)
result, err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
Where(dao.TSimCardInfo.Columns().Iccid, iccid).
Data(do.TSimCardInfo{
FlowPkgEndAt: bind.ExpectedExpireTime,
PackageStatus: packageStatus,
UpdatedBy: userId,
UpdatedAt: now,
}).
Update()
if err != nil {
return gerror.Wrap(err, "更新续费后卡套餐到期时间失败")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "读取续费后卡信息更新数量失败")
}
if affected != 1 {
return gerror.Newf("ICCID卡信息已变化,请刷新后重试:%s", iccid)
}
}
return nil
}
@@ -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)
}
@@ -0,0 +1,245 @@
package cardpackage
import (
"context"
"sort"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"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"
)
const cardPackageBindInsertBatchSize = 500
type Repository struct{}
func NewRepository() *Repository {
return &Repository{}
}
func (r *Repository) LockCardsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*entity.TSimCardInfo, error) {
result := make(map[string]*entity.TSimCardInfo)
uniqueIccids := uniqueStrings(iccids)
if len(uniqueIccids) == 0 {
return result, nil
}
var list []*entity.TSimCardInfo
if err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
WhereIn(dao.TSimCardInfo.Columns().Iccid, uniqueIccids).
LockUpdate().
Scan(&list); err != nil {
return nil, err
}
for _, item := range list {
if item != nil && item.Iccid != "" {
result[item.Iccid] = item
}
}
return result, nil
}
func (r *Repository) LockCurrentActiveBindsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*entity.TSimCardBindPkg, error) {
result := make(map[string]*entity.TSimCardBindPkg)
priorities := make(map[string]int)
uniqueIccids := uniqueStrings(iccids)
if len(uniqueIccids) == 0 {
return result, nil
}
var list []*entity.TSimCardBindPkg
if err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
WhereIn(dao.TSimCardBindPkg.Columns().Iccid, uniqueIccids).
WhereIn(dao.TSimCardBindPkg.Columns().EffectiveStatus, []int{consts.EffectiveStatusPending, consts.EffectiveStatusEffective}).
OrderDesc(dao.TSimCardBindPkg.Columns().EffectiveStatus).
OrderAsc(dao.TSimCardBindPkg.Columns().EffectiveTime).
OrderAsc(dao.TSimCardBindPkg.Columns().Id).
LockUpdate().
Scan(&list); err != nil {
return nil, err
}
for _, item := range list {
if item == nil || item.Iccid == "" {
continue
}
priority := bindPriority(item.EffectiveStatus)
currentPriority, exists := priorities[item.Iccid]
if exists && priority < currentPriority {
continue
}
if exists && priority == currentPriority {
result[item.Iccid] = nil
continue
}
priorities[item.Iccid] = priority
result[item.Iccid] = item
}
return result, nil
}
func (r *Repository) LockLatestBindsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*entity.TSimCardBindPkg, error) {
result := make(map[string]*entity.TSimCardBindPkg)
uniqueIccids := uniqueStrings(iccids)
if len(uniqueIccids) == 0 {
return result, nil
}
var list []*entity.TSimCardBindPkg
if err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
WhereIn(dao.TSimCardBindPkg.Columns().Iccid, uniqueIccids).
WhereIn(dao.TSimCardBindPkg.Columns().PackageType, []int{101, 102}).
WhereIn(dao.TSimCardBindPkg.Columns().EffectiveStatus, []int{
consts.EffectiveStatusPending,
consts.EffectiveStatusEffective,
consts.EffectiveStatusExpired,
}).
OrderDesc(dao.TSimCardBindPkg.Columns().ExpectedExpireTime).
OrderDesc(dao.TSimCardBindPkg.Columns().Id).
LockUpdate().
Scan(&list); err != nil {
return nil, err
}
effectiveCount := make(map[string]int)
for _, item := range list {
if item == nil || item.Iccid == "" {
continue
}
iccid := strings.TrimSpace(item.Iccid)
if item.EffectiveStatus == consts.EffectiveStatusEffective {
effectiveCount[iccid]++
if effectiveCount[iccid] > 1 {
return nil, gerror.Newf("同一张卡存在多个生效中的套餐订购记录:%s", iccid)
}
}
if _, exists := result[iccid]; exists {
continue
}
result[iccid] = item
}
return result, nil
}
func (r *Repository) ActiveBindCountsByOrderCodes(ctx context.Context, tx gdb.TX, orderCodes []string) (map[string]int, error) {
result := make(map[string]int)
uniqueOrderCodes := uniqueStrings(orderCodes)
if len(uniqueOrderCodes) == 0 {
return result, nil
}
var list []struct {
OrderCode string `orm:"order_code"`
Count int `orm:"count"`
}
err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
Fields(dao.TSimCardBindPkg.Columns().OrderCode, "COUNT(1) AS count").
WhereIn(dao.TSimCardBindPkg.Columns().OrderCode, uniqueOrderCodes).
Group(dao.TSimCardBindPkg.Columns().OrderCode).
Scan(&list)
if err != nil {
return nil, err
}
for _, item := range list {
result[item.OrderCode] = item.Count
}
return result, nil
}
func (r *Repository) LoadTargetDept(ctx context.Context, tx gdb.TX, deptId int64, tenantId string) (*entity.SysDept, error) {
var customer *entity.SysDept
mod := tx.Ctx(ctx).Model(dao.SysDept.Table()).
Where(dao.SysDept.Columns().DeptId, deptId)
if tenantId != "" {
mod = mod.Where(dao.SysDept.Columns().TenantId, tenantId)
}
if err := mod.Scan(&customer); err != nil {
return nil, err
}
return customer, nil
}
func (r *Repository) LoadPackage(ctx context.Context, tx gdb.TX, packageCode string) (*entity.TSimFlowPackageInfo, error) {
var pkg *entity.TSimFlowPackageInfo
err := tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()).
Where(dao.TSimFlowPackageInfo.Columns().PackageCode, strings.TrimSpace(packageCode)).
Scan(&pkg)
if err != nil {
return nil, err
}
return pkg, nil
}
func (r *Repository) InsertOrder(ctx context.Context, tx gdb.TX, data do.TOrder) (int64, error) {
return tx.Ctx(ctx).Model(dao.TOrder.Table()).Data(data).InsertAndGetId()
}
func (r *Repository) InsertOrderItem(ctx context.Context, tx gdb.TX, data do.TOrderItem) error {
_, err := tx.Ctx(ctx).Model(dao.TOrderItem.Table()).Data(data).Insert()
return err
}
func (r *Repository) InsertCardPackageBinds(ctx context.Context, tx gdb.TX, rows []do.TSimCardBindPkg) error {
if len(rows) == 0 {
return nil
}
for start := 0; start < len(rows); start += cardPackageBindInsertBatchSize {
end := start + cardPackageBindInsertBatchSize
if end > len(rows) {
end = len(rows)
}
if _, err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).Data(rows[start:end]).Insert(); err != nil {
return err
}
}
return nil
}
func (r *Repository) DeleteBindsBySubscriptionNos(ctx context.Context, tx gdb.TX, subscriptionNos []string) (int64, error) {
uniqueNos := uniqueStrings(subscriptionNos)
if len(uniqueNos) == 0 {
return 0, nil
}
result, err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
WhereIn(dao.TSimCardBindPkg.Columns().SubscriptionNo, uniqueNos).
Delete()
if err != nil {
return 0, err
}
return result.RowsAffected()
}
func uniqueStrings(list []string) []string {
if len(list) == 0 {
return nil
}
seen := make(map[string]struct{}, len(list))
result := make([]string, 0, len(list))
for _, item := range list {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
result = append(result, item)
}
sort.Strings(result)
return result
}
func bindPriority(status int) int {
switch status {
case consts.EffectiveStatusEffective:
return 2
case consts.EffectiveStatusPending:
return 1
default:
return 0
}
}
@@ -0,0 +1,544 @@
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)
}
@@ -0,0 +1,244 @@
package cardpackage
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"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"
)
func (s *Service) TerminateCurrentPackage(ctx context.Context, tx gdb.TX, in *TerminateCurrentPackageInput) (*TerminateCurrentPackageOutput, error) {
if in == nil || len(in.Iccids) == 0 {
return nil, gerror.New("退订卡不能为空")
}
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("部分卡信息已变化,请刷新后重试")
}
bindsByIccid, err := s.repo.LockCurrentActiveBindsByIccids(ctx, tx, iccids)
if err != nil {
return nil, gerror.Wrap(err, "锁定当前订购失败")
}
if len(bindsByIccid) != len(iccids) {
return nil, gerror.New("部分卡没有当前订购,不能退订")
}
now := gtime.Now()
sourceBatchNo := strings.TrimSpace(in.SourceBatchNo)
packageCode := ""
orderCodeSet := make(map[string]struct{}, len(iccids))
orderUnsubCounts := make(map[string]int, len(iccids))
logRows := make([]do.TSimBindPackageLogs, 0, len(iccids))
subscriptionNos := make([]string, 0, len(iccids))
for _, iccid := range iccids {
card := cardsByIccid[iccid]
bind := bindsByIccid[iccid]
if card == nil {
return nil, gerror.Newf("ICCID不存在正式卡记录:%s", iccid)
}
if bind == nil {
return nil, gerror.Newf("ICCID当前订购数据异常或不存在:%s", iccid)
}
if card.DeptId != in.Operator.DeptId && card.CreatedDept != in.Operator.DeptId {
return nil, gerror.Newf("ICCID不属于当前机构:%s", iccid)
}
if strings.TrimSpace(card.CurrentPackageCode) == "" || card.PackageStatus == consts.PackageStatusNone {
return nil, gerror.Newf("ICCID没有可退订套餐:%s", iccid)
}
if packageCode == "" {
packageCode = strings.TrimSpace(bind.PackageCode)
}
if packageCode != strings.TrimSpace(bind.PackageCode) {
return nil, gerror.New("当前批次下存在多个套餐,不能统一退订")
}
if strings.TrimSpace(card.CurrentPackageCode) != strings.TrimSpace(bind.PackageCode) {
return nil, gerror.Newf("ICCID当前套餐与订购记录不一致:%s", iccid)
}
if sourceBatchNo != "" && strings.TrimSpace(bind.BatchNo) != sourceBatchNo {
return nil, gerror.Newf("ICCID当前订购不属于来源出库批次:%s", iccid)
}
orderCode := strings.TrimSpace(bind.OrderCode)
if orderCode != "" {
orderCodeSet[orderCode] = struct{}{}
orderUnsubCounts[orderCode]++
}
subscriptionNos = append(subscriptionNos, bind.SubscriptionNo)
logRows = append(logRows, bindToHistory(bind, now, in.BatchNo, in.Operator.UserId))
}
if len(logRows) > 0 {
if _, err = tx.Ctx(ctx).Model(dao.TSimBindPackageLogs.Table()).
Data(logRows).
OnConflict(dao.TSimBindPackageLogs.Columns().SubscriptionNo).
Save(); err != nil {
return nil, gerror.Wrap(err, "写入订购历史失败")
}
}
affected, err := s.repo.DeleteBindsBySubscriptionNos(ctx, tx, subscriptionNos)
if err != nil {
return nil, gerror.Wrap(err, "删除当前订购失败")
}
if int(affected) != len(iccids) {
return nil, gerror.New("部分当前订购已变化,请刷新后重试")
}
orderCodes := mapKeys(orderCodeSet)
if err = s.updateOrdersAfterUnsub(ctx, tx, orderCodes, orderUnsubCounts, in.BatchNo, now, in.Operator.UserId); err != nil {
return nil, err
}
return &TerminateCurrentPackageOutput{
Iccids: iccids,
CardsByIccid: cardsByIccid,
BindsByIccid: bindsByIccid,
PackageCode: packageCode,
OrderCodes: orderCodes,
UnsubTime: now,
}, nil
}
func bindToHistory(bind *entity.TSimCardBindPkg, actualExpireAt *gtime.Time, unsubBatchNo string, userId int64) do.TSimBindPackageLogs {
return do.TSimBindPackageLogs{
SubscriptionNo: bind.SubscriptionNo,
BatchNo: bind.BatchNo,
Iccid: bind.Iccid,
Msisdn: bind.Msisdn,
DeptId: bind.DeptId,
PackageType: bind.PackageType,
PackageCode: bind.PackageCode,
PreviousPackageCode: bind.PreviousPackageCode,
BusinessType: bind.BusinessType,
EffectiveStatus: consts.EffectiveStatusTerminated,
HandleTime: bind.HandleTime,
EffectiveTime: bind.EffectiveTime,
ExpectedExpireTime: bind.ExpectedExpireTime,
ActualExpireTime: actualExpireAt,
DurationMonths: bind.DurationMonths,
GiftDurationMonths: bind.GiftDurationMonths,
OrderCode: bind.OrderCode,
Remark: appendRemark(bind.Remark, fmt.Sprintf("unsubscribeBatchNo=%s", unsubBatchNo)),
TenantId: bind.TenantId,
CreatedDept: bind.CreatedDept,
CreatedBy: bind.CreatedBy,
CreatedAt: bind.CreatedAt,
UpdatedBy: userId,
UpdatedAt: actualExpireAt,
}
}
func (s *Service) updateOrdersAfterUnsub(ctx context.Context, tx gdb.TX, orderCodes []string, unsubCounts map[string]int, batchNo string, unsubTime *gtime.Time, userId int64) error {
if len(orderCodes) == 0 {
return nil
}
activeCounts, err := s.repo.ActiveBindCountsByOrderCodes(ctx, tx, orderCodes)
if err != nil {
return gerror.Wrap(err, "统计订单剩余订购失败")
}
for _, orderCode := range orderCodes {
returnStatus := consts.OrderReturnStatusSuccess
if activeCounts[orderCode] > 0 {
returnStatus = consts.OrderReturnStatusPartial
}
if _, err = tx.Ctx(ctx).Model(dao.TOrder.Table()).
Where(dao.TOrder.Columns().OrderSn, orderCode).
Data(do.TOrder{
ReturnStatus: returnStatus,
UpdatedBy: userId,
UpdatedAt: unsubTime,
}).
Update(); err != nil {
return gerror.Wrap(err, "更新订单退订状态失败")
}
if err = updateOrderItemExtra(ctx, tx, orderCode, batchNo, unsubCounts[orderCode], unsubTime, userId); err != nil {
return err
}
}
return nil
}
func updateOrderItemExtra(ctx context.Context, tx gdb.TX, orderCode, batchNo string, unsubQuantity int, unsubTime *gtime.Time, userId int64) error {
var item *entity.TOrderItem
if err := tx.Ctx(ctx).Model(dao.TOrderItem.Table()).
Where(dao.TOrderItem.Columns().OrderSn, orderCode).
Limit(1).
Scan(&item); err != nil {
return gerror.Wrap(err, "查询订单项失败")
}
if item == nil {
return nil
}
extra := g.Map{}
if strings.TrimSpace(item.ExtraSkuData) != "" {
_ = json.Unmarshal([]byte(item.ExtraSkuData), &extra)
}
extra["lastUnsubscribeBatchNo"] = batchNo
extra["lastUnsubscribeTime"] = formatTime(unsubTime)
extra["unsubscribedQuantity"] = unsubQuantity
data, err := json.Marshal(extra)
if err != nil {
return gerror.Wrap(err, "生成订单项退订扩展失败")
}
if _, err = tx.Ctx(ctx).Model(dao.TOrderItem.Table()).
Where(dao.TOrderItem.Columns().ItemId, item.ItemId).
Data(do.TOrderItem{
ExtraSkuData: string(data),
UpdatedBy: userId,
UpdatedAt: unsubTime,
}).
Update(); err != nil {
return gerror.Wrap(err, "更新订单项退订扩展失败")
}
return nil
}
func appendRemark(old, addition string) string {
old = strings.TrimSpace(old)
addition = strings.TrimSpace(addition)
if old == "" {
return addition
}
if addition == "" {
return old
}
return old + "; " + addition
}
func mapKeys(set map[string]struct{}) []string {
result := make([]string, 0, len(set))
for key := range set {
if strings.TrimSpace(key) != "" {
result = append(result, key)
}
}
sort.Strings(result)
return result
}
func formatTime(t *gtime.Time) string {
if t == nil {
return ""
}
return t.Format("Y-m-d H:i:s")
}
@@ -0,0 +1,130 @@
package cardpackage
import (
"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/os/gtime"
)
type Service struct {
repo *Repository
}
func NewService() *Service {
return &Service{repo: NewRepository()}
}
type TerminateCurrentPackageInput struct {
BatchNo string
SourceBatchNo string
Iccids []string
Operator batchimport.OperatorContext
}
type TerminateCurrentPackageOutput struct {
Iccids []string
CardsByIccid map[string]*entity.TSimCardInfo
BindsByIccid map[string]*entity.TSimCardBindPkg
PackageCode string
OrderCodes []string
UnsubTime *gtime.Time
}
type SubscribeCard struct {
Iccid string
Msisdn string
Imsi string
}
type CreatePackageSubscriptionInput struct {
BatchNo string
Cards []SubscribeCard
CustomerAgentId int64
PackageCode string
PackageBeginTime *gtime.Time
DurationMonths int
GiftDurationMonths int
DurationDisplayValue int
GiftDurationDisplayValue int
DurationDisplayLabel string
GiftDurationDisplayLabel string
DurationUnit string
EndAtPreviousMonthEnd bool
Amount string
PayType int
PayStatus int
BalanceAmount float64
OfflinePaidAmount float64
PayTime *gtime.Time
OfflineContractCompleted bool
BuyerNote string
AdminNote string
PreviousPackageCode string
BusinessType int
OrderRemark string
BindRemark string
SkipBindInsert bool
Operator batchimport.OperatorContext
}
type CreatePackageSubscriptionOutput struct {
OrderId int64
OrderSn string
Customer *entity.SysDept
Package *entity.TSimFlowPackageInfo
PackageBeginAt *gtime.Time
PackageEndAt *gtime.Time
SubscribeTime *gtime.Time
PackageStatus int
EffectiveStatus int
Amount float64
UnitPrice float64
Quantity int
BindsByIccid map[string]*entity.TSimCardBindPkg
BindRows []do.TSimCardBindPkg
}
type RenewCurrentPackageInput struct {
BatchNo string
SourceBatchNo string
PackageCode string
Iccids []string
DurationMonths int
GiftDurationMonths int
DurationDisplayValue int
GiftDurationDisplayValue int
DurationUnit string
PackageBeginTime *gtime.Time
PackageEndTime *gtime.Time
Amount string
PayType int
PayStatus int
BalanceAmount float64
OfflinePaidAmount float64
PayTime *gtime.Time
OfflineContractCompleted bool
BuyerNote string
AdminNote string
OrderRemark string
BindRemark string
Operator batchimport.OperatorContext
}
type RenewCurrentPackageOutput struct {
Iccids []string
OrderId int64
OrderSn string
Customer *entity.SysDept
Package *entity.TSimFlowPackageInfo
CardsByIccid map[string]*entity.TSimCardInfo
LatestBindsByIccid map[string]*entity.TSimCardBindPkg
NewBindsByIccid map[string]*entity.TSimCardBindPkg
BindRows []do.TSimCardBindPkg
RenewTime *gtime.Time
PackageCode string
Amount float64
UnitPrice float64
Quantity int
}