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,777 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"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/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/cardpackage"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"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/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const (
|
||||
outboundPaymentModeOffline = "offline"
|
||||
outboundPaymentModeBalance = "balance"
|
||||
outboundDetailUpdateChunkSize = 200
|
||||
|
||||
outboundFinanceIncomeExpenseExpense = 1
|
||||
outboundFinanceTransactionTypeConsume = 1
|
||||
outboundFinanceTransactionChannelBalance = 3
|
||||
outboundFinanceTransactionStatusPaid = 1
|
||||
outboundFinanceAccountStatusNormal = 0
|
||||
)
|
||||
|
||||
func (p *Processor) Complete(ctx context.Context, tx gdb.TX, in *CompleteInput) (*CompleteOutput, error) {
|
||||
if in == nil || in.SubmitParam == nil {
|
||||
return nil, gerror.New("出库参数不能为空")
|
||||
}
|
||||
if in.BatchNo == "" {
|
||||
return nil, gerror.New("出库批次号不能为空")
|
||||
}
|
||||
if len(in.Rows) == 0 {
|
||||
return nil, gerror.New("出库卡不能为空")
|
||||
}
|
||||
normalizeSubmitParam(in.SubmitParam)
|
||||
if err := validateSubmitParam(in.SubmitParam); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iccids, err := strictRowsToIccids(in.Rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stockByIccid, err := p.repo.LockStockCardsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定库存卡失败")
|
||||
}
|
||||
if len(stockByIccid) != len(iccids) {
|
||||
return nil, gerror.New("部分库存卡已变化,请刷新后重试")
|
||||
}
|
||||
|
||||
for _, iccid := range iccids {
|
||||
stock := stockByIccid[iccid]
|
||||
if stock == nil {
|
||||
return nil, gerror.Newf("ICCID不存在库存记录:%s", iccid)
|
||||
}
|
||||
if stock.CreatedDept != in.Operator.DeptId {
|
||||
return nil, gerror.Newf("ICCID不属于当前机构库存:%s", iccid)
|
||||
}
|
||||
if in.RequiredSourceBatchNo != "" && strings.TrimSpace(stock.BatchNo) != in.RequiredSourceBatchNo {
|
||||
return nil, gerror.Newf("ICCID 不属于指定批次:%s", iccid)
|
||||
}
|
||||
}
|
||||
if in.StrictStockIdentity {
|
||||
for _, row := range in.Rows {
|
||||
stock := stockByIccid[row.Iccid]
|
||||
if stock == nil {
|
||||
return nil, gerror.Newf("ICCID不存在库存记录:%s", row.Iccid)
|
||||
}
|
||||
if strings.TrimSpace(row.Msisdn) != strings.TrimSpace(stock.Msisdn) {
|
||||
return nil, gerror.Newf("MSISDN与库存记录不一致:%s", row.Iccid)
|
||||
}
|
||||
if strings.TrimSpace(row.Imsi) != strings.TrimSpace(stock.Imsi) {
|
||||
return nil, gerror.Newf("IMSI与库存记录不一致:%s", row.Iccid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formalByIccid, err := p.repo.GetExistingFormalIccids(ctx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询正式卡失败")
|
||||
}
|
||||
for _, iccid := range iccids {
|
||||
if formalByIccid[iccid] {
|
||||
return nil, gerror.Newf("ICCID已存在正式卡记录:%s", iccid)
|
||||
}
|
||||
}
|
||||
|
||||
quantity := len(iccids)
|
||||
subscribeCards := make([]cardpackage.SubscribeCard, 0, quantity)
|
||||
cardRows := make([]do.TSimCardInfo, 0, quantity)
|
||||
detailUpdates := make(map[string]*gjsonJsonHolder, quantity)
|
||||
sourceBatchNos := make([]string, 0, quantity)
|
||||
sourceBatchSet := make(map[string]struct{}, quantity)
|
||||
|
||||
for _, row := range in.Rows {
|
||||
stock := stockByIccid[row.Iccid]
|
||||
if stock == nil {
|
||||
return nil, gerror.Newf("ICCID不存在库存记录:%s", row.Iccid)
|
||||
}
|
||||
if strings.TrimSpace(stock.Iccid) != strings.TrimSpace(row.Iccid) {
|
||||
return nil, gerror.Newf("库存卡映射异常:第%d行%s匹配到%s", row.RowNo, row.Iccid, stock.Iccid)
|
||||
}
|
||||
row.SourceBatchNo = strings.TrimSpace(stock.BatchNo)
|
||||
row.Msisdn = chooseString(row.Msisdn, stock.Msisdn)
|
||||
row.Imsi = chooseString(row.Imsi, stock.Imsi)
|
||||
if row.SourceBatchNo != "" {
|
||||
if _, ok := sourceBatchSet[row.SourceBatchNo]; !ok {
|
||||
sourceBatchSet[row.SourceBatchNo] = struct{}{}
|
||||
sourceBatchNos = append(sourceBatchNos, row.SourceBatchNo)
|
||||
}
|
||||
}
|
||||
subscribeCards = append(subscribeCards, cardpackage.SubscribeCard{
|
||||
Iccid: stock.Iccid,
|
||||
Msisdn: stock.Msisdn,
|
||||
Imsi: stock.Imsi,
|
||||
})
|
||||
}
|
||||
inboundBatchInfos, err := p.repo.GetInboundBatchInfos(ctx, tx, sourceBatchNos)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询入库批次信息失败")
|
||||
}
|
||||
payment, err := prepareOutboundPayment(ctx, tx, in.BatchNo, subscriptionAmount(in.SubmitParam), in.Operator, in.SubmitParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packageSvc := cardpackage.NewService()
|
||||
subscription, err := packageSvc.CreatePackageSubscription(ctx, tx, &cardpackage.CreatePackageSubscriptionInput{
|
||||
BatchNo: in.BatchNo,
|
||||
Cards: subscribeCards,
|
||||
CustomerAgentId: in.SubmitParam.CustomerAgentId,
|
||||
PackageCode: strings.TrimSpace(in.SubmitParam.PackageCode),
|
||||
PackageBeginTime: in.SubmitParam.PackageBeginTime,
|
||||
DurationMonths: in.SubmitParam.DurationMonths,
|
||||
GiftDurationMonths: in.SubmitParam.GiftDurationMonths,
|
||||
DurationDisplayValue: displayDurationValue(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonths),
|
||||
GiftDurationDisplayValue: displayDurationValue(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonths),
|
||||
DurationDisplayLabel: displayDurationLabel(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonthPart, in.SubmitParam.DurationMonths),
|
||||
GiftDurationDisplayLabel: displayDurationLabel(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonthPart, in.SubmitParam.GiftDurationMonths),
|
||||
DurationUnit: consts.RenewDurationUnitYear,
|
||||
EndAtPreviousMonthEnd: true,
|
||||
Amount: in.SubmitParam.Amount,
|
||||
PayType: payment.PayType,
|
||||
PayStatus: payment.PayStatus,
|
||||
BalanceAmount: payment.BalanceAmount,
|
||||
OfflinePaidAmount: payment.OfflinePaidAmount,
|
||||
PayTime: payment.PaymentTime,
|
||||
OfflineContractCompleted: in.SubmitParam.OfflineContractCompleted,
|
||||
BuyerNote: normalizedRemark(in.SubmitParam),
|
||||
AdminNote: normalizedRemark(in.SubmitParam),
|
||||
BindRemark: in.BatchNo,
|
||||
SkipBindInsert: true,
|
||||
Operator: in.Operator,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(sourceBatchNos)
|
||||
if err = updateBatchMainAfterComplete(
|
||||
ctx,
|
||||
tx,
|
||||
in,
|
||||
subscription.OrderSn,
|
||||
subscription.Customer,
|
||||
subscription.Package,
|
||||
subscription.Amount,
|
||||
subscription.PackageBeginAt,
|
||||
subscription.PackageEndAt,
|
||||
sourceBatchNos,
|
||||
subscription.PackageStatus,
|
||||
subscription.EffectiveStatus,
|
||||
payment,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := subscription.SubscribeTime
|
||||
for _, row := range in.Rows {
|
||||
stock := stockByIccid[row.Iccid]
|
||||
bind := subscription.BindsByIccid[row.Iccid]
|
||||
if bind == nil {
|
||||
return nil, gerror.Newf("ICCID订购绑定生成失败:%s", row.Iccid)
|
||||
}
|
||||
cardType := inboundCardTypeValue(inboundBatchInfos[stock.BatchNo])
|
||||
cardRows = append(cardRows, do.TSimCardInfo{
|
||||
Iccid: stock.Iccid,
|
||||
Msisdn: stock.Msisdn,
|
||||
Imsi: stock.Imsi,
|
||||
Imei: stock.Imei,
|
||||
DeptId: in.SubmitParam.CustomerAgentId,
|
||||
CurrentPackageCode: subscription.Package.PackageCode,
|
||||
PackageStatus: subscription.PackageStatus,
|
||||
PackageTotalFlowMb: packageTotalFlowMB(subscription.Package),
|
||||
BossCardStatus: consts.V3ApiSimBossCardStatusPendingActivate,
|
||||
CardType: cardType,
|
||||
InboundBatchNo: stock.BatchNo,
|
||||
OutboundBatchNo: in.BatchNo,
|
||||
OutStockTime: now,
|
||||
FlowPkgBeginAt: subscription.PackageBeginAt,
|
||||
FlowPkgEndAt: subscription.PackageEndAt,
|
||||
ChanKey: stock.ChanKey,
|
||||
TenantId: in.Operator.TenantId,
|
||||
CreatedDept: in.Operator.DeptId,
|
||||
CreatedBy: in.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
|
||||
detailUpdates[row.Iccid] = &gjsonJsonHolder{Data: buildDetailSnapshot(row, stock, cardType, subscription.Customer, subscription.Package, in, subscription.OrderSn, bind.SubscriptionNo, subscription.Amount, subscription.PackageBeginAt, subscription.PackageEndAt)}
|
||||
}
|
||||
|
||||
for start := 0; start < len(cardRows); start += outboundDatabaseInsertChunkSize {
|
||||
end := start + outboundDatabaseInsertChunkSize
|
||||
if end > len(cardRows) {
|
||||
end = len(cardRows)
|
||||
}
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).Data(cardRows[start:end]).Insert(); err != nil {
|
||||
return nil, gerror.Wrap(err, "新增正式卡失败")
|
||||
}
|
||||
}
|
||||
if err = packageSvc.InsertCardPackageBinds(ctx, tx, subscription.BindRows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = updateBatchDetailSnapshots(ctx, tx, in.BatchNo, detailUpdates, in.Operator.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var affected int64
|
||||
for start := 0; start < len(iccids); start += outboundDatabaseChunkSize {
|
||||
end := start + outboundDatabaseChunkSize
|
||||
if end > len(iccids) {
|
||||
end = len(iccids)
|
||||
}
|
||||
result, deleteErr := tx.Ctx(ctx).Model(dao.TSimCardStock.Table()).
|
||||
WhereIn(dao.TSimCardStock.Columns().Iccid, iccids[start:end]).
|
||||
Where(dao.TSimCardStock.Columns().CreatedDept, in.Operator.DeptId).
|
||||
Delete()
|
||||
if deleteErr != nil {
|
||||
return nil, gerror.Wrap(deleteErr, "删除库存卡失败")
|
||||
}
|
||||
chunkAffected, rowsErr := result.RowsAffected()
|
||||
if rowsErr != nil {
|
||||
return nil, gerror.Wrap(rowsErr, "读取库存删除数量失败")
|
||||
}
|
||||
affected += chunkAffected
|
||||
}
|
||||
if int(affected) != quantity {
|
||||
return nil, gerror.New("部分库存卡已变化,请刷新后重试")
|
||||
}
|
||||
|
||||
return &CompleteOutput{
|
||||
OrderId: subscription.OrderId,
|
||||
OrderSn: subscription.OrderSn,
|
||||
BatchNo: in.BatchNo,
|
||||
BatchType: consts.BatchTypeOutbound,
|
||||
Quantity: quantity,
|
||||
SourceBatchNos: sourceBatchNos,
|
||||
Customer: subscription.Customer,
|
||||
Package: subscription.Package,
|
||||
PackageBeginAt: subscription.PackageBeginAt,
|
||||
PackageEndAt: subscription.PackageEndAt,
|
||||
PackageStatus: subscription.PackageStatus,
|
||||
EffectiveStatus: subscription.EffectiveStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type gjsonJsonHolder struct {
|
||||
Data any
|
||||
}
|
||||
|
||||
func validateSubmitParam(in *model.TSimCardOutboundSubmitParam) error {
|
||||
normalizeSubmitParam(in)
|
||||
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.DurationMonthPart < 0 || in.DurationMonthPart > 11 {
|
||||
return gerror.New("套餐购买月份必须在0到11之间")
|
||||
}
|
||||
if in.GiftDurationMonthPart < 0 || in.GiftDurationMonthPart > 11 {
|
||||
return gerror.New("赠送月份必须在0到11之间")
|
||||
}
|
||||
if in.GiftDurationMonths < 0 {
|
||||
return gerror.New("赠送时长不能小于0")
|
||||
}
|
||||
amount, err := parseOutboundAmount(in.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if amount <= 0 {
|
||||
return gerror.New("订单金额必须大于0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rowsToIccids(rows []*Row) []string {
|
||||
iccids := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
iccid := strings.TrimSpace(row.Iccid)
|
||||
if iccid != "" {
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
}
|
||||
return uniqueStrings(iccids)
|
||||
}
|
||||
|
||||
func strictRowsToIccids(rows []*Row) ([]string, error) {
|
||||
seen := make(map[string]int, len(rows))
|
||||
iccids := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
return nil, gerror.New("出库行不能为空")
|
||||
}
|
||||
iccid := strings.TrimSpace(row.Iccid)
|
||||
if iccid == "" {
|
||||
return nil, gerror.Newf("第%d行ICCID为空,不能出库", row.RowNo)
|
||||
}
|
||||
if firstRowNo, ok := seen[iccid]; ok {
|
||||
return nil, gerror.Newf("第%d行ICCID与第%d行重复:%s", row.RowNo, firstRowNo, iccid)
|
||||
}
|
||||
seen[iccid] = row.RowNo
|
||||
row.Iccid = iccid
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
return iccids, nil
|
||||
}
|
||||
|
||||
func updateBatchMainAfterComplete(
|
||||
ctx context.Context,
|
||||
tx gdb.TX,
|
||||
in *CompleteInput,
|
||||
orderSn string,
|
||||
customer *entity.SysDept,
|
||||
pkg *entity.TSimFlowPackageInfo,
|
||||
amount float64,
|
||||
beginAt, endAt *gtime.Time,
|
||||
sourceBatchNos []string,
|
||||
packageStatus, effectiveStatus int,
|
||||
payment *outboundPaymentSnapshot,
|
||||
) error {
|
||||
current := &struct {
|
||||
BatchInfo *gjson.Json
|
||||
}{}
|
||||
if err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Fields(dao.BatchMain.Columns().BatchInfo).
|
||||
Where(dao.BatchMain.Columns().BatchNo, in.BatchNo).
|
||||
Scan(current); err != nil {
|
||||
return gerror.Wrap(err, "读取出库批次信息失败")
|
||||
}
|
||||
info := g.Map{}
|
||||
if current.BatchInfo != nil {
|
||||
for key, value := range current.BatchInfo.Map() {
|
||||
info[key] = value
|
||||
}
|
||||
}
|
||||
completedInfo := g.Map{
|
||||
"orderSn": orderSn,
|
||||
"customerAgentId": in.SubmitParam.CustomerAgentId,
|
||||
"customerName": customer.DeptName,
|
||||
"packageCode": pkg.PackageCode,
|
||||
"packageName": pkg.PackageName,
|
||||
"packageType": pkg.PackageType,
|
||||
"packageBeginTime": formatTime(beginAt),
|
||||
"packageEndTime": formatTime(endAt),
|
||||
"durationYears": in.SubmitParam.DurationYears,
|
||||
"durationMonthPart": in.SubmitParam.DurationMonthPart,
|
||||
"durationMonths": in.SubmitParam.DurationMonths,
|
||||
"giftDurationYears": in.SubmitParam.GiftDurationYears,
|
||||
"giftDurationMonthPart": in.SubmitParam.GiftDurationMonthPart,
|
||||
"giftDurationMonths": in.SubmitParam.GiftDurationMonths,
|
||||
"durationLabel": displayDurationLabel(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonthPart, in.SubmitParam.DurationMonths),
|
||||
"giftDurationLabel": displayDurationLabel(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonthPart, in.SubmitParam.GiftDurationMonths),
|
||||
"amount": amount,
|
||||
"quantity": len(in.Rows),
|
||||
"offlineContractCompleted": in.SubmitParam.OfflineContractCompleted,
|
||||
"remark": normalizedRemark(in.SubmitParam),
|
||||
"buyerNote": normalizedRemark(in.SubmitParam),
|
||||
"adminNote": normalizedRemark(in.SubmitParam),
|
||||
"sourceBatchNos": sourceBatchNos,
|
||||
"packageStatus": packageStatus,
|
||||
"effectiveStatus": effectiveStatus,
|
||||
}
|
||||
for key, value := range completedInfo {
|
||||
info[key] = value
|
||||
}
|
||||
if payment != nil {
|
||||
info["paymentMode"] = payment.PaymentMode
|
||||
info["payType"] = payment.PayType
|
||||
info["payStatus"] = payment.PayStatus
|
||||
info["balanceAmount"] = payment.BalanceAmount
|
||||
info["offlinePaidAmount"] = payment.OfflinePaidAmount
|
||||
info["balanceBefore"] = payment.BalanceBefore
|
||||
info["balanceAfter"] = payment.BalanceAfter
|
||||
info["balanceDetailSerialNo"] = payment.BalanceDetailSerialNo
|
||||
info["payerDeptId"] = payment.PayerDeptId
|
||||
}
|
||||
_, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(dao.BatchMain.Columns().BatchNo, in.BatchNo).
|
||||
Data(do.BatchMain{
|
||||
OrderSn: orderSn,
|
||||
TargetDeptId: in.SubmitParam.CustomerAgentId,
|
||||
BatchInfo: gjson.New(info),
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新出库批次失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateBatchDetailSnapshots(ctx context.Context, tx gdb.TX, batchNo string, snapshots map[string]*gjsonJsonHolder, userId int64) error {
|
||||
if len(snapshots) == 0 {
|
||||
return nil
|
||||
}
|
||||
iccids := make([]string, 0, len(snapshots))
|
||||
for iccid := range snapshots {
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
sort.Strings(iccids)
|
||||
|
||||
columns := dao.BatchDetail.Columns()
|
||||
for start := 0; start < len(iccids); start += outboundDetailUpdateChunkSize {
|
||||
end := start + outboundDetailUpdateChunkSize
|
||||
if end > len(iccids) {
|
||||
end = len(iccids)
|
||||
}
|
||||
chunk := iccids[start:end]
|
||||
var sqlBuilder strings.Builder
|
||||
args := make([]any, 0, len(chunk)*3+2)
|
||||
sqlBuilder.WriteString("UPDATE `")
|
||||
sqlBuilder.WriteString(dao.BatchDetail.Table())
|
||||
sqlBuilder.WriteString("` SET `")
|
||||
sqlBuilder.WriteString(columns.Snapshot)
|
||||
sqlBuilder.WriteString("` = CASE `")
|
||||
sqlBuilder.WriteString(columns.ItemId)
|
||||
sqlBuilder.WriteString("`")
|
||||
for _, iccid := range chunk {
|
||||
sqlBuilder.WriteString(" WHEN ? THEN ?")
|
||||
args = append(args, iccid, gjson.New(snapshots[iccid].Data).String())
|
||||
}
|
||||
sqlBuilder.WriteString(" ELSE `")
|
||||
sqlBuilder.WriteString(columns.Snapshot)
|
||||
sqlBuilder.WriteString("` END, `")
|
||||
sqlBuilder.WriteString(columns.UpdatedBy)
|
||||
sqlBuilder.WriteString("` = ? WHERE `")
|
||||
sqlBuilder.WriteString(columns.BatchNo)
|
||||
sqlBuilder.WriteString("` = ? AND `")
|
||||
sqlBuilder.WriteString(columns.ItemId)
|
||||
sqlBuilder.WriteString("` IN (")
|
||||
args = append(args, userId, batchNo)
|
||||
for index, iccid := range chunk {
|
||||
if index > 0 {
|
||||
sqlBuilder.WriteByte(',')
|
||||
}
|
||||
sqlBuilder.WriteByte('?')
|
||||
args = append(args, iccid)
|
||||
}
|
||||
sqlBuilder.WriteByte(')')
|
||||
if _, err := tx.Exec(sqlBuilder.String(), args...); err != nil {
|
||||
return gerror.Wrapf(err, "更新批次明细快照失败,范围:%d-%d", start+1, end)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildDetailSnapshot(row *Row, stock *entity.TSimCardStock, cardType any, customer *entity.SysDept, pkg *entity.TSimFlowPackageInfo, in *CompleteInput, orderSn, subscriptionNo string, amount float64, beginAt, endAt *gtime.Time) g.Map {
|
||||
return g.Map{
|
||||
"ICCID": stock.Iccid,
|
||||
"MSISDN": stock.Msisdn,
|
||||
"IMSI": stock.Imsi,
|
||||
"IMEI": stock.Imei,
|
||||
"cardType": cardType,
|
||||
"inboundBatchNo": stock.BatchNo,
|
||||
"outboundBatchNo": in.BatchNo,
|
||||
"orderSn": orderSn,
|
||||
"subscriptionNo": subscriptionNo,
|
||||
"customerAgentId": in.SubmitParam.CustomerAgentId,
|
||||
"customerName": customer.DeptName,
|
||||
"packageCode": pkg.PackageCode,
|
||||
"packageName": pkg.PackageName,
|
||||
"packageType": pkg.PackageType,
|
||||
"packageBeginTime": formatTime(beginAt),
|
||||
"packageEndTime": formatTime(endAt),
|
||||
"durationYears": in.SubmitParam.DurationYears,
|
||||
"durationMonthPart": in.SubmitParam.DurationMonthPart,
|
||||
"durationMonths": in.SubmitParam.DurationMonths,
|
||||
"giftDurationYears": in.SubmitParam.GiftDurationYears,
|
||||
"giftDurationMonthPart": in.SubmitParam.GiftDurationMonthPart,
|
||||
"giftDurationMonths": in.SubmitParam.GiftDurationMonths,
|
||||
"durationLabel": displayDurationLabel(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonthPart, in.SubmitParam.DurationMonths),
|
||||
"giftDurationLabel": displayDurationLabel(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonthPart, in.SubmitParam.GiftDurationMonths),
|
||||
"packageTotalFlowMb": packageTotalFlowMB(pkg),
|
||||
"amount": amount,
|
||||
"remark": normalizedRemark(in.SubmitParam),
|
||||
"rowNo": row.RowNo,
|
||||
"stock": g.Map{
|
||||
"chanKey": stock.ChanKey,
|
||||
"sourceInboundBatchNo": stock.BatchNo,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func inboundCardTypeValue(info *gjson.Json) any {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
value := strings.TrimSpace(info.Get("inboundOptions.cardType").String())
|
||||
if value == "" {
|
||||
value = strings.TrimSpace(info.Get("cardType").String())
|
||||
}
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
cardType, err := strconv.Atoi(value)
|
||||
if err != nil || cardType <= 0 {
|
||||
return nil
|
||||
}
|
||||
return cardType
|
||||
}
|
||||
|
||||
func formatTime(t *gtime.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Format("Y-m-d H:i:s")
|
||||
}
|
||||
|
||||
func chooseString(primary, fallback string) string {
|
||||
primary = strings.TrimSpace(primary)
|
||||
if primary != "" {
|
||||
return primary
|
||||
}
|
||||
return strings.TrimSpace(fallback)
|
||||
}
|
||||
|
||||
type outboundPaymentSnapshot struct {
|
||||
PaymentMode string
|
||||
PayType int
|
||||
PayStatus int
|
||||
PayAmount float64
|
||||
BalanceAmount float64
|
||||
OfflinePaidAmount float64
|
||||
BalanceBefore float64
|
||||
BalanceAfter float64
|
||||
BalanceDetailSerialNo string
|
||||
PaymentTime *gtime.Time
|
||||
PayerDeptId int64
|
||||
}
|
||||
|
||||
func prepareOutboundPayment(ctx context.Context, tx gdb.TX, batchNo string, amount float64, operator batchimport.OperatorContext, in *model.TSimCardOutboundSubmitParam) (*outboundPaymentSnapshot, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("出库付款参数不能为空")
|
||||
}
|
||||
if amount < 0 {
|
||||
return nil, gerror.New("订单金额不能小于0")
|
||||
}
|
||||
now := gtime.Now()
|
||||
if in.OfflineContractCompleted {
|
||||
return &outboundPaymentSnapshot{
|
||||
PaymentMode: outboundPaymentModeOffline,
|
||||
PayType: consts.PayTypeOffline,
|
||||
PayStatus: consts.PayStatusPaid,
|
||||
PayAmount: amount,
|
||||
OfflinePaidAmount: amount,
|
||||
PaymentTime: now,
|
||||
PayerDeptId: in.CustomerAgentId,
|
||||
}, nil
|
||||
}
|
||||
return deductOutboundBalanceTx(ctx, tx, batchNo, amount, now, operator, in.CustomerAgentId)
|
||||
}
|
||||
|
||||
func deductOutboundBalanceTx(ctx context.Context, tx gdb.TX, batchNo string, amount float64, paymentTime *gtime.Time, operator batchimport.OperatorContext, payerDeptID int64) (*outboundPaymentSnapshot, error) {
|
||||
if tx == nil {
|
||||
return nil, gerror.New("出库余额支付必须在事务中执行")
|
||||
}
|
||||
if operator.DeptId <= 0 || operator.TenantId == "" || operator.UserId <= 0 {
|
||||
return nil, gerror.New("当前用户未绑定组织、租户或用户信息")
|
||||
}
|
||||
if payerDeptID <= 0 {
|
||||
return nil, gerror.New("出库客户不能为空")
|
||||
}
|
||||
if paymentTime == nil {
|
||||
paymentTime = gtime.Now()
|
||||
}
|
||||
serialNo := outboundBalanceSerialNo(batchNo)
|
||||
if serialNo == "" {
|
||||
return nil, gerror.New("出库余额流水号生成失败")
|
||||
}
|
||||
|
||||
account := &entity.TAgentBalance{}
|
||||
if err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
|
||||
Where(dao.TAgentBalance.Columns().DeptId, payerDeptID).
|
||||
Where(dao.TAgentBalance.Columns().TenantId, operator.TenantId).
|
||||
LockUpdate().
|
||||
Scan(account); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, gerror.Wrap(err, "查询账户余额失败")
|
||||
}
|
||||
if account.Id == 0 {
|
||||
return nil, gerror.New("客户暂无余额账户,请先充值")
|
||||
}
|
||||
if account.Status != outboundFinanceAccountStatusNormal {
|
||||
return nil, gerror.New("账户余额已冻结,不能扣余额")
|
||||
}
|
||||
record, err := tx.Ctx(ctx).Model(dao.TBalanceDetail.Table()).
|
||||
Where(dao.TBalanceDetail.Columns().TenantId, operator.TenantId).
|
||||
Where(dao.TBalanceDetail.Columns().SerialNo, serialNo).
|
||||
LockUpdate().
|
||||
One()
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, gerror.Wrap(err, "查询出库余额流水失败")
|
||||
}
|
||||
if record != nil && !record.IsEmpty() {
|
||||
return nil, gerror.New("当前出库批次已扣款,请勿重复提交")
|
||||
}
|
||||
|
||||
before := roundOutboundAmount(account.Balance)
|
||||
if before+0.0001 < amount {
|
||||
return nil, gerror.Newf("账户余额不足,需支付%.2f元,当前余额%.2f元", amount, before)
|
||||
}
|
||||
after := roundOutboundAmount(before - amount)
|
||||
now := gtime.Now()
|
||||
result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
|
||||
WherePri(account.Id).
|
||||
Data(g.Map{
|
||||
dao.TAgentBalance.Columns().Balance: after,
|
||||
dao.TAgentBalance.Columns().UpdatedBy: operator.UserId,
|
||||
dao.TAgentBalance.Columns().UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "扣减账户余额失败")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取账户余额更新结果失败")
|
||||
}
|
||||
if affected != 1 {
|
||||
return nil, gerror.New("账户余额已变化,请重试")
|
||||
}
|
||||
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TBalanceDetail.Table()).Data(g.Map{
|
||||
dao.TBalanceDetail.Columns().DeptId: payerDeptID,
|
||||
dao.TBalanceDetail.Columns().SerialNo: serialNo,
|
||||
dao.TBalanceDetail.Columns().TransactionTime: paymentTime,
|
||||
dao.TBalanceDetail.Columns().IncomeExpenseType: outboundFinanceIncomeExpenseExpense,
|
||||
dao.TBalanceDetail.Columns().TransactionType: outboundFinanceTransactionTypeConsume,
|
||||
dao.TBalanceDetail.Columns().TransactionChannel: outboundFinanceTransactionChannelBalance,
|
||||
dao.TBalanceDetail.Columns().TransactionStatus: outboundFinanceTransactionStatusPaid,
|
||||
dao.TBalanceDetail.Columns().Amount: amount,
|
||||
dao.TBalanceDetail.Columns().BeforeBalance: before,
|
||||
dao.TBalanceDetail.Columns().BalanceAfter: after,
|
||||
dao.TBalanceDetail.Columns().OrderSn: batchNo,
|
||||
dao.TBalanceDetail.Columns().Remark: fmt.Sprintf("SIM卡出库余额支付,批次号:%s", batchNo),
|
||||
dao.TBalanceDetail.Columns().TenantId: operator.TenantId,
|
||||
dao.TBalanceDetail.Columns().CreatedDept: operator.DeptId,
|
||||
dao.TBalanceDetail.Columns().CreatedBy: operator.UserId,
|
||||
dao.TBalanceDetail.Columns().CreatedAt: now,
|
||||
}).Insert(); err != nil {
|
||||
return nil, gerror.Wrap(err, "写入出库余额流水失败")
|
||||
}
|
||||
|
||||
return &outboundPaymentSnapshot{
|
||||
PaymentMode: outboundPaymentModeBalance,
|
||||
PayType: consts.PayTypeBalance,
|
||||
PayStatus: consts.PayStatusPaid,
|
||||
PayAmount: amount,
|
||||
BalanceAmount: amount,
|
||||
BalanceBefore: before,
|
||||
BalanceAfter: after,
|
||||
BalanceDetailSerialNo: serialNo,
|
||||
PaymentTime: paymentTime,
|
||||
PayerDeptId: payerDeptID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func subscriptionAmount(in *model.TSimCardOutboundSubmitParam) float64 {
|
||||
amount, _ := parseOutboundAmount(in.Amount)
|
||||
return amount
|
||||
}
|
||||
|
||||
func parseOutboundAmount(value string) (float64, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, gerror.New("订单金额不能为空")
|
||||
}
|
||||
amount, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return 0, gerror.New("订单金额格式不正确")
|
||||
}
|
||||
if amount < 0 {
|
||||
return 0, gerror.New("订单金额不能小于0")
|
||||
}
|
||||
return roundOutboundAmount(amount), nil
|
||||
}
|
||||
|
||||
func roundOutboundAmount(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
func outboundBalanceSerialNo(batchNo string) string {
|
||||
batchNo = strings.TrimSpace(batchNo)
|
||||
if batchNo == "" {
|
||||
return ""
|
||||
}
|
||||
return "OUTPAY" + batchNo
|
||||
}
|
||||
|
||||
func displayDurationValue(years, months int) int {
|
||||
if years > 0 {
|
||||
return years
|
||||
}
|
||||
if months > 0 && months%12 == 0 {
|
||||
return months / 12
|
||||
}
|
||||
return months
|
||||
}
|
||||
|
||||
func displayDurationLabel(years, monthPart, totalMonths int) string {
|
||||
if years <= 0 && monthPart <= 0 && totalMonths > 0 {
|
||||
years = totalMonths / 12
|
||||
monthPart = totalMonths % 12
|
||||
}
|
||||
parts := make([]string, 0, 2)
|
||||
if years > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d年", years))
|
||||
}
|
||||
if monthPart > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d个月", monthPart))
|
||||
}
|
||||
if len(parts) == 0 && totalMonths > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d个月", totalMonths))
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
func packageTotalFlowMB(pkg *entity.TSimFlowPackageInfo) float64 {
|
||||
if pkg == nil {
|
||||
return 0
|
||||
}
|
||||
if pkg.FlowMb > 0 {
|
||||
return pkg.FlowMb
|
||||
}
|
||||
if pkg.FlowGb > 0 {
|
||||
return pkg.FlowGb * 1024
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user