Files
iotlcm/internal/repository/unsub/repository.go
T
2026-10-29 14:52:43 +08:00

1207 lines
38 KiB
Go

package unsub
import (
"context"
"encoding/json"
"sort"
"strings"
"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"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/batchresult"
unsub "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/unsub"
)
const (
cardInfoTable = "t_sim_card_info"
cardBindPkgTable = "t_sim_card_bind_pkg"
bindPackageLogsTable = "t_sim_bind_package_logs"
stockTable = "t_sim_card_stock"
orderTable = "t_order"
orderItemTable = "t_order_item"
bindStatusPending = 0
bindStatusEffective = 1
bindStatusTerminated = 3
pkgStatusNone = 0
pkgStatusEffective = 2
orderReturnStatusPartial = 3
orderReturnStatusSuccess = 4
)
type Repository struct {
db gdb.DB
resultFiles batchresult.Generator
lockEnabled bool
}
func New() *Repository {
return &Repository{resultFiles: batchresult.New(), lockEnabled: true}
}
func (r *Repository) database() gdb.DB {
if r.db != nil {
return r.db
}
return g.DB()
}
func (r *Repository) applyLock(model *gdb.Model) *gdb.Model {
if r.lockEnabled {
return model.LockUpdate()
}
return model
}
type cardQuery struct {
g.Meta `orm:"table:t_sim_card_info"`
Id int64 `orm:"id"`
Iccid string `orm:"iccid"`
Msisdn string `orm:"msisdn"`
Imsi string `orm:"imsi"`
Imei string `orm:"imei"`
ChanKey string `orm:"chan_key"`
DeptId int64 `orm:"dept_id"`
CreatedDept int64 `orm:"created_dept"`
CurrentPackageCode string `orm:"current_package_code"`
PackageStatus int `orm:"package_status"`
InboundBatchNo string `orm:"inbound_batch_no"`
OutboundBatchNo string `orm:"outbound_batch_no"`
TenantId string `orm:"tenant_id"`
}
type bindPkgQuery struct {
g.Meta `orm:"table:t_sim_card_bind_pkg"`
Id int64 `orm:"id"`
SubscriptionNo string `orm:"subscription_no"`
BatchNo string `orm:"batch_no"`
Iccid string `orm:"iccid"`
Msisdn string `orm:"msisdn"`
DeptId string `orm:"dept_id"`
PackageType int `orm:"package_type"`
PackageCode string `orm:"package_code"`
PreviousPackageCode string `orm:"previous_package_code"`
BusinessType int `orm:"business_type"`
EffectiveStatus int `orm:"effective_status"`
HandleTime *gtime.Time `orm:"handle_time"`
EffectiveTime *gtime.Time `orm:"effective_time"`
ExpectedExpireTime *gtime.Time `orm:"expected_expire_time"`
ActualExpireTime *gtime.Time `orm:"actual_expire_time"`
DurationMonths int `orm:"duration_months"`
GiftDurationMonths int `orm:"gift_duration_months"`
OrderCode string `orm:"order_code"`
Remark string `orm:"remark"`
TenantId string `orm:"tenant_id"`
CreatedDept int64 `orm:"created_dept"`
CreatedBy int64 `orm:"created_by"`
CreatedAt *gtime.Time `orm:"created_at"`
}
type stockQuery struct {
g.Meta `orm:"table:t_sim_card_stock"`
Id int64 `orm:"id"`
Iccid string `orm:"iccid"`
}
type bindPackageLogInsert struct {
g.Meta `orm:"table:t_sim_bind_package_logs, do:true"`
SubscriptionNo any
BatchNo any
Iccid any
Msisdn any
DeptId any
PackageType any
PackageCode any
PreviousPackageCode any
BusinessType any
EffectiveStatus any
HandleTime any
EffectiveTime any
ExpectedExpireTime any
ActualExpireTime any
DurationMonths any
GiftDurationMonths any
OrderCode any
Remark any
TenantId any
CreatedDept any
CreatedBy any
UpdatedBy any
}
type stockInsert struct {
g.Meta `orm:"table:t_sim_card_stock, do:true"`
BatchNo any
Iccid any
Msisdn any
Imsi any
Imei any
ChanKey any
TenantId any
CreatedDept any
CreatedBy any
UpdatedBy any
}
type orderUpdate struct {
g.Meta `orm:"table:t_order, do:true"`
ReturnStatus any
UnrefundAmount any
UpdatedBy any
UpdatedAt any
}
type orderItemExtra struct {
LastUnsubscribeBatchNo string `json:"lastUnsubscribeBatchNo"`
LastUnsubscribeTime string `json:"lastUnsubscribeTime"`
UnsubscribedQuantity int `json:"unsubscribedQuantity"`
LastUnsubscribeRefundAmount float64 `json:"lastUnsubscribeRefundAmount"`
LastUnsubscribeRefundSerialNo string `json:"lastUnsubscribeRefundSerialNo"`
LastUnsubscribeRefundStatus string `json:"lastUnsubscribeRefundStatus"`
LastUnsubscribeRefundReason string `json:"lastUnsubscribeRefundReason"`
}
type orderItemUpdate struct {
g.Meta `orm:"table:t_order_item, do:true"`
ExtraSkuData any
UpdatedBy any
}
type orderItemQuery struct {
g.Meta `orm:"table:t_order_item"`
ItemId int64 `orm:"item_id"`
OrderSn string `orm:"order_sn"`
Price float64 `orm:"price"`
Quantity int `orm:"quantity"`
ServiceBeginTime *gtime.Time `orm:"service_begin_time"`
ServiceEndTime *gtime.Time `orm:"service_end_time"`
ExtraSkuData string `orm:"extra_sku_data"`
}
type orderQuery struct {
g.Meta `orm:"table:t_order"`
Id int64 `orm:"id"`
OrderSn string `orm:"order_sn"`
CustomerAgentId int64 `orm:"customer_agent_id"`
PayType int `orm:"pay_type"`
PayStatus int `orm:"pay_status"`
PayOrderSn string `orm:"pay_order_sn"`
TotalAmount float64 `orm:"total_amount"`
PaidAmount float64 `orm:"paid_amount"`
Balance float64 `orm:"balance"`
UnrefundAmount float64 `orm:"unrefund_amount"`
TenantId string `orm:"tenant_id"`
CreatedDept int64 `orm:"created_dept"`
}
func (r *Repository) Load(ctx context.Context, batchNo string) (*unsub.Task, error) {
if strings.TrimSpace(batchNo) == "" {
return nil, gerror.New("unsub batch number is empty")
}
var main entity.BatchMain
if err := r.database().Model(dao.BatchMain.Table()).Safe().Ctx(ctx).
Where(dao.BatchMain.Columns().BatchNo, batchNo).
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeSimCardUnsub).
Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing).
Scan(&main); err != nil {
return nil, gerror.Wrap(err, "query unsub batch main")
}
if main.Id == 0 {
return nil, gerror.New("processing unsub batch does not exist")
}
var details []*entity.BatchDetail
if err := r.database().Model(dao.BatchDetail.Table()).Safe().Ctx(ctx).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypeSimCardUnsub).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning).
OrderAsc(dao.BatchDetail.Columns().RowNo).
Scan(&details); err != nil {
return nil, gerror.Wrap(err, "query unsub running details")
}
if len(details) == 0 {
return nil, gerror.New("unsub batch has no running details")
}
batchSourceBatchNo := resolveSourceBatchNo(main, main.BatchNo)
batchRemark := ""
if main.BatchInfo != nil {
batchRemark = strings.TrimSpace(main.BatchInfo.Get("remark").String())
}
task := &unsub.Task{
Batch: unsub.Batch{
ID: main.Id,
BatchNo: main.BatchNo,
BatchType: main.BatchType,
TenantID: main.TenantId,
DeptID: main.DeptId,
CreatedBy: main.CreatedBy,
CreatedDept: main.CreatedDept,
StartTime: main.StartTime,
BatchInfo: main.BatchInfo,
SourceBatchNo: batchSourceBatchNo,
Remark: batchRemark,
},
Details: make([]*unsub.Detail, 0, len(details)),
}
for _, raw := range details {
if raw == nil {
continue
}
detail := buildDetailFromBatch(raw, task.Batch.SourceBatchNo)
task.Details = append(task.Details, detail)
}
if len(task.Details) == 0 {
return nil, gerror.New("unsub batch has no readable running details")
}
return task, nil
}
func buildDetailFromBatch(raw *entity.BatchDetail, fallbackSourceBatchNo string) *unsub.Detail {
detail := &unsub.Detail{
ID: raw.Id,
RowNo: raw.RowNo,
ICCID: strings.TrimSpace(raw.ItemId),
Reason: strings.TrimSpace(raw.ErrorReason),
}
if raw.Snapshot != nil {
if value := strings.TrimSpace(raw.Snapshot.Get("ICCID").String()); value != "" {
detail.ICCID = value
}
if value := strings.TrimSpace(raw.Snapshot.Get("iccid").String()); value != "" && detail.ICCID == "" {
detail.ICCID = value
}
detail.MSISDN = firstNonEmpty(strings.TrimSpace(raw.Snapshot.Get("MSISDN").String()), strings.TrimSpace(raw.Snapshot.Get("msisdn").String()))
detail.IMSI = firstNonEmpty(strings.TrimSpace(raw.Snapshot.Get("IMSI").String()), strings.TrimSpace(raw.Snapshot.Get("imsi").String()))
detail.Authorized = raw.Snapshot.Get("authorized").Bool()
detail.Remark = strings.TrimSpace(raw.Snapshot.Get("remark").String())
detail.SourceBatchNo = resolveSourceBatchNoFromSnapshot(raw.Snapshot, fallbackSourceBatchNo)
}
return detail
}
func resolveSourceBatchNoFromSnapshot(snapshot *gjson.Json, fallback string) string {
if snapshot == nil {
return fallback
}
for _, key := range []string{"sourceBatchNo", "source_batch_no"} {
if value := strings.TrimSpace(snapshot.Get(key).String()); value != "" {
return value
}
}
return fallback
}
func resolveSourceBatchNo(main entity.BatchMain, currentBatchNo string) string {
if main.BatchInfo != nil {
for _, key := range []string{"sourceBatchNo", "source_batch_no"} {
if value := strings.TrimSpace(main.BatchInfo.Get(key).String()); value != "" && value != currentBatchNo {
return value
}
}
}
if value := strings.TrimSpace(main.SourceBatchNo); value != "" && value != currentBatchNo {
return value
}
return ""
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func (r *Repository) SaveValidationFailure(ctx context.Context, task *unsub.Task) error {
if task == nil {
return gerror.New("unsub task is nil")
}
reason := task.FailureReason
if reason == "" {
reason = "批次校验未通过,已跳过退订请求"
}
now := gtime.Now()
successCount := 0
for _, detail := range task.Details {
if detail != nil && detail.Reason == "" {
successCount++
}
}
failCount := len(task.Details) - successCount
return r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
for _, detail := range task.Details {
if detail == nil {
continue
}
detailReason := detail.Reason
if detailReason == "" {
detailReason = reason
}
result, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().Id, detail.ID).
Where(dao.BatchDetail.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning).
Data(do.BatchDetail{
DetailStatus: consts.BatchDetailStatusFailed,
ErrorReason: detailReason,
Snapshot: detail.Snapshot(),
UpdatedBy: task.Batch.CreatedBy,
}).Update()
if err != nil {
return gerror.Wrap(err, "update unsub validation failure detail")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read unsub detail update result")
}
if affected != 1 {
return gerror.Newf("unsub detail %d is no longer running", detail.ID)
}
}
return updateMainForValidation(ctx, tx, task, len(task.Details), failCount, reason, now)
})
}
func updateMainForValidation(ctx context.Context, tx gdb.TX, task *unsub.Task, total, fail int, reason string, now *gtime.Time) error {
result, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeSimCardUnsub).
Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing).
Data(do.BatchMain{
BatchAsyncStatus: consts.BatchAsyncStatusFailed,
TotalCount: total,
ValidCount: total,
SuccessCount: 0,
FailCount: fail,
RunResult: consts.BatchRunResultFailed,
FailReason: reason,
EndTime: now,
UpdatedBy: task.Batch.CreatedBy,
}).Update()
if err != nil {
return gerror.Wrap(err, "update unsub batch main for validation failure")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read unsub batch main validation update result")
}
if affected != 1 {
return gerror.New("unsub batch is no longer processing")
}
return nil
}
func (r *Repository) CommitBusiness(ctx context.Context, task *unsub.Task) error {
if task == nil {
return gerror.New("unsub task is nil")
}
now := gtime.Now()
unsubTime := unsubscribeTime(task, now)
iccids := make([]string, 0, len(task.Details))
for _, detail := range task.Details {
if detail == nil {
continue
}
iccids = append(iccids, detail.ICCID)
}
iccids = uniqueSortedStrings(iccids)
return r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
cardsByIccid, err := r.lockCardsByIccids(ctx, tx, iccids)
if err != nil {
return gerror.Wrap(err, "lock formal cards")
}
bindsByIccid, err := r.lockCurrentActiveBindsByIccids(ctx, tx, iccids)
if err != nil {
return gerror.Wrap(err, "lock current active binds")
}
stocksByIccid, err := r.lockStockByIccids(ctx, tx, iccids)
if err != nil {
return gerror.Wrap(err, "lock stock")
}
packageCode := ""
orderCodes := make(map[string]int)
businessFailures := make(map[string]string)
for _, detail := range task.Details {
if detail == nil {
continue
}
reason := validateBusiness(ctx, tx, task, detail, cardsByIccid[detail.ICCID], bindsByIccid[detail.ICCID], stocksByIccid[detail.ICCID], packageCode)
if reason != "" {
businessFailures[detail.ICCID] = reason
continue
}
bind := bindsByIccid[detail.ICCID]
packageCode = strings.TrimSpace(bind.PackageCode)
if orderCode := strings.TrimSpace(bind.OrderCode); orderCode != "" {
orderCodes[orderCode]++
}
}
if len(businessFailures) > 0 {
reason := "同批存在不满足退订条件的卡,本次未执行"
for _, detail := range task.Details {
if detail == nil {
continue
}
if failureReason, ok := businessFailures[detail.ICCID]; ok {
detail.Reason = failureReason
} else if detail.Reason == "" {
detail.Reason = reason
}
}
return finalizeFailure(ctx, tx, task, reason)
}
orders, err := r.lockOrdersByOrderCodes(ctx, tx, task.Batch.TenantID, orderCodes)
if err != nil {
return gerror.Wrap(err, "lock unsub orders")
}
orderItems, err := r.lockOrderItemsByOrderCodes(ctx, tx, task.Batch.TenantID, orderCodes)
if err != nil {
return gerror.Wrap(err, "lock unsub order items")
}
activeCountsBefore, err := activeBindCountsByOrderCodes(ctx, tx, sortedOrderCodes(orderCodes))
if err != nil {
return gerror.Wrap(err, "count active binds before unsub")
}
refunds, err := r.refundOrders(ctx, tx, task, orderCodes, bindsByIccid, orders, orderItems, activeCountsBefore, unsubTime, now)
if err != nil {
return err
}
if err := writeBindPackageLogs(ctx, tx, task, cardsByIccid, bindsByIccid, unsubTime); err != nil {
return err
}
if err := deleteCurrentBinds(ctx, tx, task, bindsByIccid); err != nil {
return err
}
if err := updateOrderReturnStatus(ctx, tx, task, orderCodes, orders, refunds, now, unsubTime); err != nil {
return err
}
if err := returnCardsToStock(ctx, tx, task, cardsByIccid, stocksByIccid, now); err != nil {
return err
}
if err := deleteFormalCards(ctx, tx, iccids); err != nil {
return err
}
if err := updateBatchDetailsSuccess(ctx, tx, task, cardsByIccid, bindsByIccid, unsubTime); err != nil {
return err
}
return updateBatchMainSuccess(ctx, tx, task, packageCode, orderCodes, len(iccids), now, unsubTime, refunds)
})
}
func (r *Repository) lockCardsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*cardQuery, error) {
result := make(map[string]*cardQuery)
if len(iccids) == 0 {
return result, nil
}
var rows []*cardQuery
if err := r.applyLock(tx.Ctx(ctx).Model(cardInfoTable)).
WhereIn("iccid", iccids).
Scan(&rows); err != nil {
return nil, err
}
for _, row := range rows {
if row != nil && row.Iccid != "" {
result[row.Iccid] = row
}
}
return result, nil
}
func (r *Repository) lockCurrentActiveBindsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*bindPkgQuery, error) {
result := make(map[string]*bindPkgQuery)
priorities := make(map[string]int)
if len(iccids) == 0 {
return result, nil
}
var rows []*bindPkgQuery
if err := r.applyLock(tx.Ctx(ctx).Model(cardBindPkgTable)).
WhereIn("iccid", iccids).
WhereIn("effective_status", []int{bindStatusPending, bindStatusEffective}).
OrderDesc("effective_status").
OrderAsc("effective_time").
OrderAsc("id").
Scan(&rows); err != nil {
return nil, err
}
for _, row := range rows {
if row == nil || row.Iccid == "" {
continue
}
priority := bindPriority(row.EffectiveStatus)
if existing, ok := priorities[row.Iccid]; ok {
if priority < existing {
continue
}
if priority == existing {
result[row.Iccid] = nil
continue
}
}
priorities[row.Iccid] = priority
result[row.Iccid] = row
}
return result, nil
}
func (r *Repository) lockStockByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*stockQuery, error) {
result := make(map[string]*stockQuery)
if len(iccids) == 0 {
return result, nil
}
var rows []*stockQuery
if err := r.applyLock(tx.Ctx(ctx).Model(stockTable)).
WhereIn("iccid", iccids).
Scan(&rows); err != nil {
return nil, err
}
for _, row := range rows {
if row != nil && row.Iccid != "" {
result[row.Iccid] = row
}
}
return result, nil
}
func bindPriority(status int) int {
switch status {
case bindStatusEffective:
return 2
case bindStatusPending:
return 1
default:
return 0
}
}
func validateBusiness(ctx context.Context, tx gdb.TX, task *unsub.Task, detail *unsub.Detail, card *cardQuery, bind *bindPkgQuery, stock *stockQuery, currentPackage string) string {
if card == nil {
return "ICCID不存在正式卡记录,不能退订"
}
if bind == nil {
return "ICCID当前订购数据异常或不存在,不能退订"
}
if card.DeptId != task.Batch.DeptID && card.CreatedDept != task.Batch.DeptID {
return "ICCID不属于当前机构,不能退订"
}
if strings.TrimSpace(card.CurrentPackageCode) == "" || card.PackageStatus == pkgStatusNone {
return "ICCID没有可退订套餐,不能退订"
}
if currentPackage != "" && strings.TrimSpace(bind.PackageCode) != currentPackage {
return "当前批次下存在多个套餐,不能统一退订"
}
if strings.TrimSpace(card.CurrentPackageCode) != strings.TrimSpace(bind.PackageCode) {
return "ICCID当前套餐与订购记录不一致,不能退订"
}
if detail.MSISDN != "" && detail.MSISDN != strings.TrimSpace(card.Msisdn) {
return "MSISDN与正式卡记录不一致,不能退订"
}
if detail.IMSI != "" && detail.IMSI != strings.TrimSpace(card.Imsi) {
return "IMSI与正式卡记录不一致,不能退订"
}
if task.Batch.SourceBatchNo != "" && strings.TrimSpace(bind.BatchNo) != task.Batch.SourceBatchNo {
return "ICCID当前订购不属于来源出库批次,不能退订"
}
if stock != nil {
return "ICCID已存在库存记录,不能重复回库"
}
return ""
}
func writeBindPackageLogs(ctx context.Context, tx gdb.TX, task *unsub.Task, cards map[string]*cardQuery, binds map[string]*bindPkgQuery, now *gtime.Time) error {
rows := make([]bindPackageLogInsert, 0, len(binds))
for iccid, bind := range binds {
if bind == nil {
continue
}
remark := bind.Remark
if addition := "unsubscribeBatchNo=" + task.Batch.BatchNo; strings.TrimSpace(remark) == "" {
remark = addition
} else {
remark = strings.TrimSpace(remark) + "; " + addition
}
rows = append(rows, bindPackageLogInsert{
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: bindStatusTerminated,
HandleTime: bind.HandleTime,
EffectiveTime: bind.EffectiveTime,
ExpectedExpireTime: bind.ExpectedExpireTime,
ActualExpireTime: now,
DurationMonths: bind.DurationMonths,
GiftDurationMonths: bind.GiftDurationMonths,
OrderCode: bind.OrderCode,
Remark: remark,
TenantId: bind.TenantId,
CreatedDept: bind.CreatedDept,
CreatedBy: bind.CreatedBy,
UpdatedBy: task.Batch.CreatedBy,
})
_ = iccid
}
if len(rows) == 0 {
return nil
}
if _, err := tx.Ctx(ctx).Model(bindPackageLogsTable).Data(rows).Insert(); err != nil {
return gerror.Wrap(err, "insert unsub bind package logs")
}
return nil
}
func deleteCurrentBinds(ctx context.Context, tx gdb.TX, task *unsub.Task, binds map[string]*bindPkgQuery) error {
if len(binds) == 0 {
return nil
}
subscriptionNos := make([]string, 0, len(binds))
for _, bind := range binds {
if bind != nil && bind.SubscriptionNo != "" {
subscriptionNos = append(subscriptionNos, bind.SubscriptionNo)
}
}
if len(subscriptionNos) == 0 {
return nil
}
result, err := tx.Ctx(ctx).Model(cardBindPkgTable).
WhereIn("subscription_no", subscriptionNos).
Delete()
if err != nil {
return gerror.Wrap(err, "delete current binds")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read current binds delete result")
}
if int(affected) != len(subscriptionNos) {
return gerror.New("部分当前订购已变化,请刷新后重试")
}
_ = task
return nil
}
func updateOrderReturnStatus(ctx context.Context, tx gdb.TX, task *unsub.Task, orderCounts map[string]int, orders map[string]*orderQuery, refunds map[string]*unsub.OrderRefund, now, unsubTime *gtime.Time) error {
if len(orderCounts) == 0 {
return nil
}
orderCodes := make([]string, 0, len(orderCounts))
for orderCode := range orderCounts {
orderCodes = append(orderCodes, orderCode)
}
sort.Strings(orderCodes)
activeCounts, err := activeBindCountsByOrderCodes(ctx, tx, orderCodes)
if err != nil {
return gerror.Wrap(err, "count active binds by order")
}
for _, orderCode := range orderCodes {
returnStatus := orderReturnStatusSuccess
if activeCounts[orderCode] > 0 {
returnStatus = orderReturnStatusPartial
}
update := orderUpdate{
ReturnStatus: returnStatus,
UpdatedBy: task.Batch.CreatedBy,
UpdatedAt: now,
}
if order := orders[orderCode]; order != nil {
if refund := refunds[orderCode]; refund != nil && refund.Amount > 0 && order.UnrefundAmount > 0 {
remaining := order.UnrefundAmount - refund.Amount
if remaining < 0 {
remaining = 0
}
update.UnrefundAmount = roundUnsubAmount(remaining)
}
}
_, err = tx.Ctx(ctx).Model(orderTable).
Where("order_sn", orderCode).
Data(update).Update()
if err != nil {
return gerror.Wrap(err, "update order return status")
}
if err = updateOrderItemExtra(ctx, tx, orderCode, task.Batch.BatchNo, orderCounts[orderCode], unsubTime, refunds[orderCode], task.Batch.CreatedBy); err != nil {
return err
}
}
return nil
}
func activeBindCountsByOrderCodes(ctx context.Context, tx gdb.TX, orderCodes []string) (map[string]int, error) {
result := make(map[string]int)
if len(orderCodes) == 0 {
return result, nil
}
var rows []struct {
OrderCode string `orm:"order_code"`
Count int `orm:"count"`
}
err := tx.Ctx(ctx).Model(cardBindPkgTable).
Fields("order_code", "COUNT(1) AS count").
WhereIn("order_code", orderCodes).
Group("order_code").
Scan(&rows)
if err != nil {
return nil, err
}
for _, row := range rows {
if row.OrderCode != "" {
result[row.OrderCode] = row.Count
}
}
return result, nil
}
func updateOrderItemExtra(ctx context.Context, tx gdb.TX, orderCode, batchNo string, quantity int, unsubTime *gtime.Time, refund *unsub.OrderRefund, userId int64) error {
var item orderItemQuery
if err := tx.Ctx(ctx).Model(orderItemTable).
Where("order_sn", orderCode).
Fields("item_id", "order_sn", "price", "quantity", "service_begin_time", "service_end_time", "extra_sku_data").
Limit(1).
Scan(&item); err != nil {
return gerror.Wrap(err, "query order item")
}
if item.ItemId == 0 {
return nil
}
extra := decodeOrderItemExtra(item.ExtraSkuData)
extra.LastUnsubscribeBatchNo = batchNo
extra.LastUnsubscribeTime = formatUnsubTime(unsubTime)
extra.UnsubscribedQuantity = quantity
if refund != nil {
extra.LastUnsubscribeRefundAmount = roundUnsubAmount(refund.Amount)
extra.LastUnsubscribeRefundSerialNo = refund.SerialNo
extra.LastUnsubscribeRefundStatus = refund.Status
extra.LastUnsubscribeRefundReason = refund.Reason
}
payload, err := json.Marshal(extra)
if err != nil {
return gerror.Wrap(err, "encode order item extra")
}
_, err = tx.Ctx(ctx).Model(orderItemTable).
Where("item_id", item.ItemId).
Data(orderItemUpdate{
ExtraSkuData: string(payload),
UpdatedBy: userId,
}).Update()
if err != nil {
return gerror.Wrap(err, "update order item extra")
}
return nil
}
func decodeOrderItemExtra(raw string) orderItemExtra {
extra := orderItemExtra{}
raw = strings.TrimSpace(raw)
if raw == "" {
return extra
}
_ = json.Unmarshal([]byte(raw), &extra)
return extra
}
func formatUnsubTime(t *gtime.Time) string {
if t == nil {
return ""
}
return t.Format("Y-m-d H:i:s")
}
func returnCardsToStock(ctx context.Context, tx gdb.TX, task *unsub.Task, cards map[string]*cardQuery, stocks map[string]*stockQuery, now *gtime.Time) error {
rows := make([]stockInsert, 0, len(cards))
for iccid, card := range cards {
if card == nil {
continue
}
if _, exists := stocks[iccid]; exists {
return gerror.Newf("ICCID=%s 已存在库存记录,不能重复回库", iccid)
}
rows = append(rows, stockInsert{
BatchNo: card.InboundBatchNo,
Iccid: card.Iccid,
Msisdn: card.Msisdn,
Imsi: card.Imsi,
Imei: card.Imei,
ChanKey: card.ChanKey,
TenantId: card.TenantId,
CreatedDept: card.CreatedDept,
CreatedBy: task.Batch.CreatedBy,
UpdatedBy: task.Batch.CreatedBy,
})
}
if len(rows) == 0 {
return nil
}
if _, err := tx.Ctx(ctx).Model(stockTable).Data(rows).Insert(); err != nil {
return gerror.Wrap(err, "insert unsub stock")
}
_ = now
return nil
}
func deleteFormalCards(ctx context.Context, tx gdb.TX, iccids []string) error {
if len(iccids) == 0 {
return nil
}
result, err := tx.Ctx(ctx).Model(cardInfoTable).
WhereIn("iccid", iccids).
Delete()
if err != nil {
return gerror.Wrap(err, "delete formal cards")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read formal card delete result")
}
if int(affected) != len(iccids) {
return gerror.New("部分正式卡已变化,请刷新后重试")
}
return nil
}
func updateBatchDetailsSuccess(ctx context.Context, tx gdb.TX, task *unsub.Task, cards map[string]*cardQuery, binds map[string]*bindPkgQuery, now *gtime.Time) error {
for _, detail := range task.Details {
if detail == nil {
continue
}
card := cards[detail.ICCID]
bind := binds[detail.ICCID]
if card == nil || bind == nil {
continue
}
detail.SubscriptionNo = bind.SubscriptionNo
detail.OrderCode = bind.OrderCode
detail.PackageCode = bind.PackageCode
detail.PackageType = bind.PackageType
detail.EffectiveTime = formatUnsubTime(bind.EffectiveTime)
detail.ExpectedExpireTime = formatUnsubTime(bind.ExpectedExpireTime)
detail.ActualExpireTime = formatUnsubTime(now)
detail.ReturnedToStock = true
detail.StockDeptID = card.CreatedDept
detail.CardDeptID = card.DeptId
detail.StockBatchNo = card.InboundBatchNo
result, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().Id, detail.ID).
Where(dao.BatchDetail.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning).
Data(do.BatchDetail{
DetailStatus: consts.BatchDetailStatusSuccess,
ErrorReason: "",
Snapshot: detail.Snapshot(),
UpdatedBy: task.Batch.CreatedBy,
}).Update()
if err != nil {
return gerror.Wrap(err, "update unsub success detail")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read unsub success detail result")
}
if affected != 1 {
return gerror.Newf("unsub detail %d is no longer running", detail.ID)
}
}
return nil
}
func updateBatchMainSuccess(ctx context.Context, tx gdb.TX, task *unsub.Task, packageCode string, orderCounts map[string]int, quantity int, endTime, unsubTime *gtime.Time, refunds map[string]*unsub.OrderRefund) error {
orderCodes := make([]string, 0, len(orderCounts))
for orderCode := range orderCounts {
orderCodes = append(orderCodes, orderCode)
}
sort.Strings(orderCodes)
info := buildSuccessBatchInfo(task, packageCode, orderCodes, quantity, unsubTime, refunds)
result, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing).
Data(do.BatchMain{
BatchAsyncStatus: consts.BatchAsyncStatusSuccess,
TotalCount: quantity,
ValidCount: quantity,
SuccessCount: quantity,
FailCount: 0,
RunResult: consts.BatchRunResultSuccess,
FailReason: "",
BatchInfo: info,
EndTime: endTime,
UpdatedBy: task.Batch.CreatedBy,
}).Update()
if err != nil {
return gerror.Wrap(err, "update unsub batch main for success")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read unsub batch main success update result")
}
if affected != 1 {
return gerror.New("unsub batch is no longer processing")
}
task.Batch.BatchInfo = info
return nil
}
func finalizeFailure(ctx context.Context, tx gdb.TX, task *unsub.Task, reason string) error {
now := gtime.Now()
failCount := 0
for _, detail := range task.Details {
if detail != nil {
detail.Reason = reasonFor(detail, reason)
if detail.Reason != "" {
failCount++
}
}
}
for _, detail := range task.Details {
if detail == nil {
continue
}
result, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().Id, detail.ID).
Where(dao.BatchDetail.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning).
Data(do.BatchDetail{
DetailStatus: consts.BatchDetailStatusFailed,
ErrorReason: detail.Reason,
Snapshot: detail.Snapshot(),
UpdatedBy: task.Batch.CreatedBy,
}).Update()
if err != nil {
return gerror.Wrap(err, "update unsub business failure detail")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read unsub business failure detail result")
}
if affected != 1 {
return gerror.Newf("unsub detail %d is no longer running", detail.ID)
}
}
return updateMainForValidation(ctx, tx, task, len(task.Details), failCount, reason, now)
}
func reasonFor(detail *unsub.Detail, fallback string) string {
if detail == nil {
return fallback
}
if strings.TrimSpace(detail.Reason) == "" {
return fallback
}
return detail.Reason
}
func buildSuccessBatchInfo(task *unsub.Task, packageCode string, orderCodes []string, quantity int, unsubTime *gtime.Time, refunds map[string]*unsub.OrderRefund) *gjson.Json {
info := make(map[string]any)
if task.Batch.BatchInfo != nil {
for key, value := range task.Batch.BatchInfo.Map() {
info[key] = value
}
}
if packageCode != "" {
info["packageCode"] = packageCode
}
info["quantity"] = quantity
info["orderCodes"] = orderCodes
info["unsubTime"] = formatUnsubTime(unsubTime)
if len(refunds) > 0 {
refundDetails := make(map[string]any, len(refunds))
totalRefundAmount := 0.0
for orderCode, refund := range refunds {
if refund == nil {
continue
}
amount := roundUnsubAmount(refund.Amount)
totalRefundAmount += amount
refundDetails[orderCode] = map[string]any{
"amount": amount,
"serialNo": refund.SerialNo,
"status": refund.Status,
"reason": refund.Reason,
}
}
info["refundMode"] = "balance"
info["refundAmount"] = roundUnsubAmount(totalRefundAmount)
info["refundDetails"] = refundDetails
}
if task.Batch.SourceBatchNo != "" {
info["sourceBatchNo"] = task.Batch.SourceBatchNo
}
if task.Batch.Remark != "" {
info["remark"] = task.Batch.Remark
}
return gjson.New(info)
}
func uniqueSortedStrings(values []string) []string {
if len(values) == 0 {
return nil
}
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
sort.Strings(result)
return result
}
func (r *Repository) SaveResultFile(ctx context.Context, task *unsub.Task, request batchresult.Request) error {
artifact, err := r.artifactGenerator().Create(ctx, request)
if err != nil {
return r.updateResultFileStatus(ctx, task, "failed", 0, 0, err.Error())
}
now := gtime.Now()
successCount := 0
for _, detail := range task.Details {
if detail != nil && detail.Reason == "" {
successCount++
}
}
failCount := len(task.Details) - successCount
var (
ossID int64
downloadID int64
)
txErr := r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var insertErr error
ossID, insertErr = tx.Ctx(ctx).Model(dao.SysOss.Table()).Data(do.SysOss{
TenantId: task.Batch.TenantID,
FileName: artifact.FileName,
OriginalName: artifact.OriginalName,
FileSuffix: artifact.FileSuffix,
Path: artifact.Path,
Url: artifact.URL,
CreatedDept: task.Batch.CreatedDept,
CreatedBy: task.Batch.CreatedBy,
UpdatedBy: task.Batch.CreatedBy,
Service: "local",
Md5: artifact.MD5,
FileSize: artifact.Size,
}).InsertAndGetId()
if insertErr != nil {
return gerror.Wrap(insertErr, "insert unsub result OSS")
}
downloadID, insertErr = tx.Ctx(ctx).Model(dao.TResourceBatchDownload.Table()).Data(do.TResourceBatchDownload{
BatchNo: task.Batch.BatchNo,
TaskName: batchresult.TaskName(task.Batch.BatchType),
ResourceType: consts.ResourceTypeExcel,
ExportScene: task.Batch.BatchType,
TotalCount: len(task.Details),
SuccessCount: successCount,
FailCount: failCount,
DownloadStatus: consts.DownloadStatusGenerated,
OssId: ossID,
FileName: artifact.FileName,
FileExt: artifact.FileSuffix,
FileUrl: artifact.URL,
FilePath: artifact.Path,
FileSize: artifact.Size,
FileMd5: artifact.MD5,
DownloadCount: 0,
MaxDownloadCount: 0,
DeptId: task.Batch.CreatedDept,
StartTime: task.Batch.StartTime,
FinishTime: now,
TenantId: task.Batch.TenantID,
CreatedDept: task.Batch.CreatedDept,
CreatedBy: task.Batch.CreatedBy,
UpdatedBy: task.Batch.CreatedBy,
}).InsertAndGetId()
if insertErr != nil {
return gerror.Wrap(insertErr, "insert unsub result download")
}
return updateBatchInfoOnly(ctx, tx, task, buildResultBatchInfo(task, "success", ossID, downloadID, ""))
})
if txErr != nil {
batchresult.RemoveLocalFile(artifact)
return r.updateResultFileStatus(ctx, task, "failed", 0, 0, txErr.Error())
}
return nil
}
func (r *Repository) artifactGenerator() batchresult.Generator {
if r.resultFiles != nil {
return r.resultFiles
}
return batchresult.New()
}
func (r *Repository) updateResultFileStatus(ctx context.Context, task *unsub.Task, status string, ossID, downloadID int64, reason string) error {
return r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
return updateBatchInfoOnly(ctx, tx, task, buildResultBatchInfo(task, status, ossID, downloadID, reason))
})
}
func updateBatchInfoOnly(ctx context.Context, tx gdb.TX, task *unsub.Task, info *gjson.Json) error {
result, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeSimCardUnsub).
Data(do.BatchMain{
BatchInfo: info,
UpdatedBy: task.Batch.CreatedBy,
}).Update()
if err != nil {
return gerror.Wrap(err, "update unsub result file status")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read unsub result file update result")
}
if affected != 1 {
return gerror.New("unsub batch disappeared while saving result file")
}
task.Batch.BatchInfo = info
return nil
}
func buildResultBatchInfo(task *unsub.Task, status string, ossID, downloadID int64, reason string) *gjson.Json {
info := make(map[string]any)
if task.Batch.BatchInfo != nil {
for key, value := range task.Batch.BatchInfo.Map() {
info[key] = value
}
}
resultFile := map[string]any{"status": status}
if ossID > 0 {
resultFile["ossId"] = ossID
}
if downloadID > 0 {
resultFile["downloadId"] = downloadID
}
if reason != "" {
resultFile["error"] = reason
}
info["resultFile"] = resultFile
return gjson.New(info)
}
var _ unsub.Repository = (*Repository)(nil)