Files
iotlcm/worker-svr/internal/repository/cancel/repository.go
T

1053 lines
34 KiB
Go

package cancel
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"
cancelservice "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/cancel"
)
const (
cardInfoTable = "t_sim_card_info"
cardBindPkgTable = "t_sim_card_bind_pkg"
bindPackageLogsTable = "t_sim_bind_package_logs"
orderTable = "t_order"
orderItemTable = "t_order_item"
bindStatusPending = 0
bindStatusEffective = 1
bindStatusTerminated = 3
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 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 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 orderUpdate struct {
g.Meta `orm:"table:t_order, do:true"`
ReturnStatus any
UpdatedBy any
}
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"`
ExtraSkuData string `orm:"extra_sku_data"`
}
type orderItemExtra struct {
LastCancelBatchNo string `json:"lastCancelBatchNo"`
LastCancelTime string `json:"lastCancelTime"`
CancelledQuantity int `json:"cancelledQuantity"`
}
func (r *Repository) Load(ctx context.Context, batchNo string) (*cancelservice.Task, error) {
if strings.TrimSpace(batchNo) == "" {
return nil, gerror.New("cancel 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.BatchTypeSimCardCancel).
Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing).
Scan(&main); err != nil {
return nil, gerror.Wrap(err, "query cancel batch main")
}
if main.Id == 0 {
return nil, gerror.New("processing cancel 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.BatchTypeSimCardCancel).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning).
OrderAsc(dao.BatchDetail.Columns().RowNo).
Scan(&details); err != nil {
return nil, gerror.Wrap(err, "query cancel running details")
}
if len(details) == 0 {
return nil, gerror.New("cancel batch has no running details")
}
task := &cancelservice.Task{
Batch: cancelservice.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,
},
Details: make([]*cancelservice.Detail, 0, len(details)),
}
for _, raw := range details {
if raw != nil {
task.Details = append(task.Details, buildDetailFromBatch(raw))
}
}
if len(task.Details) == 0 {
return nil, gerror.New("cancel batch has no readable running details")
}
return task, nil
}
func buildDetailFromBatch(raw *entity.BatchDetail) *cancelservice.Detail {
detail := &cancelservice.Detail{
ID: raw.Id,
RowNo: raw.RowNo,
ICCID: strings.TrimSpace(raw.ItemId),
Reason: strings.TrimSpace(raw.ErrorReason),
TargetStatus: consts.SimBossCardStatusCanceled,
}
if raw.Snapshot != nil {
if value := firstSnapshotString(raw.Snapshot, "ICCID", "iccid"); value != "" {
detail.ICCID = value
}
detail.MSISDN = firstSnapshotString(raw.Snapshot, "MSISDN", "msisdn")
detail.IMSI = firstSnapshotString(raw.Snapshot, "IMSI", "imsi")
detail.InputMSISDN = detail.MSISDN
detail.InputIMSI = detail.IMSI
detail.Authorized = raw.Snapshot.Get("authorized").Bool()
}
return detail
}
func firstSnapshotString(snapshot *gjson.Json, keys ...string) string {
if snapshot == nil {
return ""
}
for _, key := range keys {
if value := strings.TrimSpace(snapshot.Get(key).String()); value != "" {
return value
}
}
return ""
}
func (r *Repository) SaveValidationFailure(ctx context.Context, task *cancelservice.Task) error {
if task == nil {
return gerror.New("cancel task is nil")
}
reason := task.FailureReason
if reason == "" {
reason = "批次校验未通过,已跳过取消操作"
}
now := gtime.Now()
return r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if err := updateDetailsFailed(ctx, tx, task, reason); err != nil {
return err
}
return updateMainFailed(ctx, tx, task, len(task.Details), reason, now)
})
}
func (r *Repository) CommitBusiness(ctx context.Context, task *cancelservice.Task) error {
if task == nil {
return gerror.New("cancel task is nil")
}
iccids := uniqueSortedICCID(task.Details)
now := gtime.Now()
return r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
cardsByICCID, err := r.lockCardsByIccids(ctx, tx, task.Batch.TenantID, iccids)
if err != nil {
return gerror.Wrap(err, "lock cancel SIM cards")
}
cancelled, err := r.lockCancellationsByIccids(ctx, tx, iccids)
if err != nil {
return gerror.Wrap(err, "lock existing cancellation records")
}
bindsByICCID, err := r.lockCurrentActiveBindsByIccids(ctx, tx, iccids)
if err != nil {
return gerror.Wrap(err, "lock current active binds")
}
hasFailure := false
for _, detail := range task.Details {
if detail == nil {
hasFailure = true
continue
}
card := cardsByICCID[detail.ICCID]
if reason := validateBusiness(task, detail, card, bindsByICCID[detail.ICCID], cancelled[detail.ICCID], now); reason != "" {
detail.Reason = reason
hasFailure = true
}
}
if hasFailure {
reason := "same batch contains SIM cards that cannot be cancelled; cancellation was skipped"
return finalizeFailure(ctx, tx, task, reason, now)
}
if err = writeBindPackageLogs(ctx, tx, task, bindsByICCID, now); err != nil {
return err
}
if err = deleteCurrentBinds(ctx, tx, bindsByICCID); err != nil {
return err
}
if err = updateOrderCancelStatus(ctx, tx, task, bindsByICCID, now); err != nil {
return err
}
if err = insertCancellationCards(ctx, tx, task, cardsByICCID, now); err != nil {
return err
}
if err = deleteFormalCards(ctx, tx, task, cardsByICCID); err != nil {
return err
}
if err = updateDetailsSuccess(ctx, tx, task); err != nil {
return err
}
return updateMainSuccess(ctx, tx, task, len(iccids), now)
})
}
func validateBusiness(task *cancelservice.Task, detail *cancelservice.Detail, card *entity.TSimCardInfo, binds []*bindPkgQuery, existsInCancel bool, now *gtime.Time) string {
if detail.Reason != "" {
return detail.Reason
}
if !detail.Authorized {
return "ICCID is outside the current operator authorization scope"
}
if card == nil {
return "ICCID does not exist in formal SIM card records"
}
populateDetailFromCard(detail, card, binds, now)
if detail.InputMSISDN != "" && detail.InputMSISDN != strings.TrimSpace(card.Msisdn) {
return "MSISDN does not match the SIM card record"
}
if detail.InputIMSI != "" && detail.InputIMSI != strings.TrimSpace(card.Imsi) {
return "IMSI does not match the SIM card record"
}
if task.Batch.DeptID > 0 && card.DeptId != task.Batch.DeptID && card.CreatedDept != task.Batch.DeptID {
return "ICCID is outside the current department scope"
}
if card.BossCardStatus != consts.SimBossCardStatusStopped {
return "SIM card must be stopped before cancellation"
}
if existsInCancel {
return "ICCID already exists in cancellation records"
}
return ""
}
func populateDetailFromCard(detail *cancelservice.Detail, card *entity.TSimCardInfo, binds []*bindPkgQuery, now *gtime.Time) {
detail.MSISDN = strings.TrimSpace(card.Msisdn)
detail.IMSI = strings.TrimSpace(card.Imsi)
detail.StatusBefore = card.BossCardStatus
detail.TargetStatus = consts.SimBossCardStatusCanceled
detail.CancelTime = formatTime(now)
detail.CardDeptID = card.DeptId
bind := firstCurrentBind(binds)
if bind == nil {
return
}
detail.SubscriptionNo = strings.TrimSpace(bind.SubscriptionNo)
detail.OrderCode = strings.TrimSpace(bind.OrderCode)
detail.PackageCode = strings.TrimSpace(bind.PackageCode)
detail.PackageType = bind.PackageType
}
func (r *Repository) lockCardsByIccids(ctx context.Context, tx gdb.TX, tenantID string, iccids []string) (map[string]*entity.TSimCardInfo, error) {
result := make(map[string]*entity.TSimCardInfo)
if len(iccids) == 0 {
return result, nil
}
var rows []*entity.TSimCardInfo
model := r.applyLock(tx.Ctx(ctx).Model(dao.TSimCardInfo.Table())).
WhereIn(dao.TSimCardInfo.Columns().Iccid, iccids)
if strings.TrimSpace(tenantID) != "" {
model = model.Where(dao.TSimCardInfo.Columns().TenantId, tenantID)
}
if err := model.Scan(&rows); err != nil {
return nil, err
}
for _, row := range rows {
if row != nil && strings.TrimSpace(row.Iccid) != "" {
result[strings.TrimSpace(row.Iccid)] = row
}
}
return result, nil
}
func (r *Repository) lockCancellationsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]bool, error) {
result := make(map[string]bool)
if len(iccids) == 0 {
return result, nil
}
var rows []*entity.TSimCardCancellation
if err := r.applyLock(tx.Ctx(ctx).Model(dao.TSimCardCancellation.Table())).
WhereIn(dao.TSimCardCancellation.Columns().Iccid, iccids).
Scan(&rows); err != nil {
return nil, err
}
for _, row := range rows {
if row != nil && strings.TrimSpace(row.Iccid) != "" {
result[strings.TrimSpace(row.Iccid)] = true
}
}
return result, nil
}
func (r *Repository) lockCurrentActiveBindsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string][]*bindPkgQuery, error) {
result := make(map[string][]*bindPkgQuery)
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}).
OrderAsc("id").
Scan(&rows); err != nil {
return nil, err
}
for _, row := range rows {
if row == nil || strings.TrimSpace(row.Iccid) == "" {
continue
}
iccid := strings.TrimSpace(row.Iccid)
result[iccid] = append(result[iccid], row)
}
return result, nil
}
func writeBindPackageLogs(ctx context.Context, tx gdb.TX, task *cancelservice.Task, binds map[string][]*bindPkgQuery, now *gtime.Time) error {
currentBinds := flattenCurrentBinds(binds)
rows := make([]bindPackageLogInsert, 0, len(currentBinds))
for _, bind := range currentBinds {
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: appendRemark(bind.Remark, "cancelBatchNo="+task.Batch.BatchNo),
TenantId: bind.TenantId,
CreatedDept: bind.CreatedDept,
CreatedBy: bind.CreatedBy,
UpdatedBy: task.Batch.CreatedBy,
})
}
if len(rows) == 0 {
return nil
}
if _, err := tx.Ctx(ctx).Model(bindPackageLogsTable).Data(rows).Insert(); err != nil {
return gerror.Wrap(err, "insert cancel bind package logs")
}
return nil
}
func deleteCurrentBinds(ctx context.Context, tx gdb.TX, binds map[string][]*bindPkgQuery) error {
currentBinds := flattenCurrentBinds(binds)
ids := make([]int64, 0, len(currentBinds))
for _, bind := range currentBinds {
if bind.Id <= 0 {
return gerror.New("current subscription record has no valid ID")
}
ids = append(ids, bind.Id)
}
if len(ids) == 0 {
return nil
}
result, err := tx.Ctx(ctx).Model(cardBindPkgTable).
WhereIn("id", ids).
Delete()
if err != nil {
return gerror.Wrap(err, "delete current cancel binds")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read current cancel bind delete result")
}
if int(affected) != len(ids) {
return gerror.New("some current subscription records changed")
}
return nil
}
func updateOrderCancelStatus(ctx context.Context, tx gdb.TX, task *cancelservice.Task, binds map[string][]*bindPkgQuery, now *gtime.Time) error {
orderICCIDs := make(map[string]map[string]struct{})
for _, bind := range flattenCurrentBinds(binds) {
if orderCode := strings.TrimSpace(bind.OrderCode); orderCode != "" {
if orderICCIDs[orderCode] == nil {
orderICCIDs[orderCode] = make(map[string]struct{})
}
orderICCIDs[orderCode][strings.TrimSpace(bind.Iccid)] = struct{}{}
}
}
if len(orderICCIDs) == 0 {
return nil
}
orderCodes := make([]string, 0, len(orderICCIDs))
for orderCode := range orderICCIDs {
orderCodes = append(orderCodes, orderCode)
}
sort.Strings(orderCodes)
activeCounts, err := activeBindCountsByOrderCodes(ctx, tx, orderCodes)
if err != nil {
return gerror.Wrap(err, "count remaining active binds by order")
}
for _, orderCode := range orderCodes {
returnStatus := orderReturnStatusSuccess
if activeCounts[orderCode] > 0 {
returnStatus = orderReturnStatusPartial
}
if _, err = tx.Ctx(ctx).Model(orderTable).
Where("order_sn", orderCode).
Data(orderUpdate{ReturnStatus: returnStatus, UpdatedBy: task.Batch.CreatedBy}).
Update(); err != nil {
return gerror.Wrap(err, "update cancel order return status")
}
if err = updateOrderItemExtra(ctx, tx, orderCode, task.Batch.BatchNo, len(orderICCIDs[orderCode]), now, task.Batch.CreatedBy); err != nil {
return err
}
}
return nil
}
func firstCurrentBind(binds []*bindPkgQuery) *bindPkgQuery {
for _, bind := range binds {
if bind != nil {
return bind
}
}
return nil
}
func flattenCurrentBinds(bindsByICCID map[string][]*bindPkgQuery) []*bindPkgQuery {
iccids := make([]string, 0, len(bindsByICCID))
for iccid := range bindsByICCID {
iccids = append(iccids, iccid)
}
sort.Strings(iccids)
result := make([]*bindPkgQuery, 0)
for _, iccid := range iccids {
for _, bind := range bindsByICCID[iccid] {
if bind != nil {
result = append(result, bind)
}
}
}
return result
}
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).
WhereIn("effective_status", []int{bindStatusPending, bindStatusEffective}).
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, now *gtime.Time, userID int64) error {
var item orderItemQuery
if err := tx.Ctx(ctx).Model(orderItemTable).
Where("order_sn", orderCode).
Limit(1).
Scan(&item); err != nil {
return gerror.Wrap(err, "query cancel order item")
}
if item.ItemId == 0 {
return nil
}
extra := decodeOrderItemExtra(item.ExtraSkuData)
extra.LastCancelBatchNo = batchNo
extra.LastCancelTime = formatTime(now)
extra.CancelledQuantity = quantity
payload, err := json.Marshal(extra)
if err != nil {
return gerror.Wrap(err, "encode cancel order item extra")
}
if _, err = tx.Ctx(ctx).Model(orderItemTable).
Where("item_id", item.ItemId).
Data(orderItemUpdate{ExtraSkuData: string(payload), UpdatedBy: userID}).
Update(); err != nil {
return gerror.Wrap(err, "update cancel 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 insertCancellationCards(ctx context.Context, tx gdb.TX, task *cancelservice.Task, cards map[string]*entity.TSimCardInfo, now *gtime.Time) error {
rows := make([]do.TSimCardCancellation, 0, len(cards))
for _, detail := range task.Details {
if detail == nil {
continue
}
card := cards[detail.ICCID]
if card == nil {
continue
}
rows = append(rows, cancellationRow(task, card, now))
}
if len(rows) == 0 {
return gerror.New("no cancellation rows to insert")
}
if _, err := tx.Ctx(ctx).Model(dao.TSimCardCancellation.Table()).Data(rows).Insert(); err != nil {
return gerror.Wrap(err, "insert cancellation cards")
}
return nil
}
func cancellationRow(task *cancelservice.Task, card *entity.TSimCardInfo, now *gtime.Time) do.TSimCardCancellation {
return do.TSimCardCancellation{
Iccid: card.Iccid,
Msisdn: card.Msisdn,
Imsi: card.Imsi,
Imei: card.Imei,
RegionCode: card.RegionCode,
DeptId: card.DeptId,
CurrentPackageCode: card.CurrentPackageCode,
PackageStatus: card.PackageStatus,
AccessType: card.AccessType,
BossCardStatus: consts.SimBossCardStatusCanceled,
CancelBatchNo: task.Batch.BatchNo,
CancelledAt: now,
CardType: card.CardType,
BossOpenAt: card.BossOpenAt,
BossActivateAt: card.BossActivateAt,
IsBossActivateEnabled: card.IsBossActivateEnabled,
InboundBatchNo: card.InboundBatchNo,
OutboundBatchNo: card.OutboundBatchNo,
AllocationBatchNo: card.AllocationBatchNo,
IsFlowPool: card.IsFlowPool,
FlowPoolCode: card.FlowPoolCode,
FlowPkgBeginAt: card.FlowPkgBeginAt,
FlowPkgPresentAt: card.FlowPkgPresentAt,
FlowPkgRealPresentAt: card.FlowPkgRealPresentAt,
FlowPkgEndAt: card.FlowPkgEndAt,
DataServiceStatus: card.DataServiceStatus,
PackageTotalFlowMb: card.PackageTotalFlowMb,
UsedFlowMb: card.UsedFlowMb,
TrafficCutoffStatus: card.TrafficCutoffStatus,
TrafficCutoffThreshold: card.TrafficCutoffThreshold,
SmsServiceStatus: card.SmsServiceStatus,
PackageTotalSms: card.PackageTotalSms,
UsedSms: card.UsedSms,
VoiceServiceStatus: card.VoiceServiceStatus,
PackageTotalVoice: card.PackageTotalVoice,
UsedVoice: card.UsedVoice,
RoamServiceStatus: card.RoamServiceStatus,
PackageTotalRoamFlowMb: card.PackageTotalRoamFlowMb,
UsedRoamFlowMb: card.UsedRoamFlowMb,
StatusSyncTime: card.StatusSyncTime,
FlowSyncTime: card.FlowSyncTime,
SmsSyncTime: card.SmsSyncTime,
VoiceSyncTime: card.VoiceSyncTime,
RoamSyncTime: card.RoamSyncTime,
ExtraFeeEnabled: card.ExtraFeeEnabled,
ExtraFeeMultiple: card.ExtraFeeMultiple,
ExtraFee: card.ExtraFee,
ChanKey: card.ChanKey,
NetModel: card.NetModel,
Remark: card.Remark,
TenantId: card.TenantId,
CreatedDept: card.CreatedDept,
CreatedBy: task.Batch.CreatedBy,
UpdatedBy: task.Batch.CreatedBy,
}
}
func deleteFormalCards(ctx context.Context, tx gdb.TX, task *cancelservice.Task, cards map[string]*entity.TSimCardInfo) error {
ids := make([]int64, 0, len(cards))
for _, detail := range task.Details {
if detail == nil {
continue
}
if card := cards[detail.ICCID]; card != nil {
ids = append(ids, card.Id)
}
}
if len(ids) == 0 {
return gerror.New("no formal SIM cards to delete")
}
result, err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
WhereIn(dao.TSimCardInfo.Columns().Id, ids).
Where(dao.TSimCardInfo.Columns().TenantId, task.Batch.TenantID).
Where(dao.TSimCardInfo.Columns().BossCardStatus, consts.SimBossCardStatusStopped).
Delete()
if err != nil {
return gerror.Wrap(err, "delete formal cancelled cards")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read formal card delete result")
}
if int(affected) != len(ids) {
return gerror.New("some formal SIM cards changed before cancellation")
}
return nil
}
func updateDetailsSuccess(ctx context.Context, tx gdb.TX, task *cancelservice.Task) error {
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.BatchDetailStatusSuccess,
ErrorReason: "",
Snapshot: detail.Snapshot(),
UpdatedBy: task.Batch.CreatedBy,
}).Update()
if err != nil {
return gerror.Wrap(err, "update cancel success detail")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read cancel success detail result")
}
if affected != 1 {
return gerror.Newf("cancel detail %d is no longer running", detail.ID)
}
}
return nil
}
func updateMainSuccess(ctx context.Context, tx gdb.TX, task *cancelservice.Task, quantity int, now *gtime.Time) error {
info := buildSuccessBatchInfo(task, quantity, now)
result, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeSimCardCancel).
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: now,
UpdatedBy: task.Batch.CreatedBy,
}).Update()
if err != nil {
return gerror.Wrap(err, "update cancel batch main success")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read cancel batch main success result")
}
if affected != 1 {
return gerror.New("cancel batch is no longer processing")
}
task.Batch.BatchInfo = info
return nil
}
func finalizeFailure(ctx context.Context, tx gdb.TX, task *cancelservice.Task, reason string, now *gtime.Time) error {
if err := updateDetailsFailed(ctx, tx, task, reason); err != nil {
return err
}
return updateMainFailed(ctx, tx, task, len(task.Details), reason, now)
}
func updateDetailsFailed(ctx context.Context, tx gdb.TX, task *cancelservice.Task, fallback string) error {
for _, detail := range task.Details {
if detail == nil {
continue
}
if strings.TrimSpace(detail.Reason) == "" {
detail.Reason = fallback
}
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 cancel failed detail")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read cancel failed detail result")
}
if affected != 1 {
return gerror.Newf("cancel detail %d is no longer running", detail.ID)
}
}
return nil
}
func updateMainFailed(ctx context.Context, tx gdb.TX, task *cancelservice.Task, failCount int, reason string, now *gtime.Time) error {
info := buildFailureBatchInfo(task, reason)
result, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeSimCardCancel).
Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing).
Data(do.BatchMain{
BatchAsyncStatus: consts.BatchAsyncStatusFailed,
TotalCount: len(task.Details),
ValidCount: len(task.Details),
SuccessCount: 0,
FailCount: failCount,
RunResult: consts.BatchRunResultFailed,
FailReason: reason,
BatchInfo: info,
EndTime: now,
UpdatedBy: task.Batch.CreatedBy,
}).Update()
if err != nil {
return gerror.Wrap(err, "update cancel batch main failed")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read cancel batch main failed result")
}
if affected != 1 {
return gerror.New("cancel batch is no longer processing")
}
task.Batch.BatchInfo = info
task.FailureReason = reason
return nil
}
func (r *Repository) SaveResultFile(ctx context.Context, task *cancelservice.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 && strings.TrimSpace(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 cancel 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 cancel 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 *cancelservice.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 *cancelservice.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.BatchTypeSimCardCancel).
Data(do.BatchMain{
BatchInfo: info,
UpdatedBy: task.Batch.CreatedBy,
}).Update()
if err != nil {
return gerror.Wrap(err, "update cancel result file status")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "read cancel result file update result")
}
if affected != 1 {
return gerror.New("cancel batch disappeared while saving result file")
}
task.Batch.BatchInfo = info
return nil
}
func buildSuccessBatchInfo(task *cancelservice.Task, quantity int, now *gtime.Time) *gjson.Json {
info := cloneBatchInfo(task)
info["operation"] = "cancel"
info["mode"] = "internal"
info["requiresStopped"] = true
info["targetBossCardStatus"] = consts.SimBossCardStatusCanceled
info["quantity"] = quantity
info["cancelTime"] = formatTime(now)
return gjson.New(info)
}
func buildFailureBatchInfo(task *cancelservice.Task, reason string) *gjson.Json {
info := cloneBatchInfo(task)
info["operation"] = "cancel"
info["mode"] = "internal"
info["requiresStopped"] = true
info["targetBossCardStatus"] = consts.SimBossCardStatusCanceled
if reason != "" {
info["failureReason"] = reason
}
return gjson.New(info)
}
func buildResultBatchInfo(task *cancelservice.Task, status string, ossID, downloadID int64, reason string) *gjson.Json {
info := cloneBatchInfo(task)
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)
}
func cloneBatchInfo(task *cancelservice.Task) map[string]any {
info := make(map[string]any)
if task != nil && task.Batch.BatchInfo != nil {
for key, value := range task.Batch.BatchInfo.Map() {
info[key] = value
}
}
return info
}
func uniqueSortedICCID(details []*cancelservice.Detail) []string {
seen := make(map[string]struct{}, len(details))
result := make([]string, 0, len(details))
for _, detail := range details {
if detail == nil {
continue
}
iccid := strings.TrimSpace(detail.ICCID)
if iccid == "" {
continue
}
if _, ok := seen[iccid]; ok {
continue
}
seen[iccid] = struct{}{}
result = append(result, iccid)
}
sort.Strings(result)
return result
}
func appendRemark(base, addition string) string {
base = strings.TrimSpace(base)
addition = strings.TrimSpace(addition)
if base == "" {
return addition
}
if addition == "" {
return base
}
return base + "; " + addition
}
func formatTime(t *gtime.Time) string {
if t == nil {
return ""
}
return t.Format("Y-m-d H:i:s")
}
var _ cancelservice.Repository = (*Repository)(nil)