2bec257289
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。 - Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件) - 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、 库名 xiujie_admin → iotlcm、哨兵名占位 - 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异, 不破坏「校验 MSISDN 格式」等测试断言) - 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像 digest 记录 - Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23), 移除写死的 git 凭据 - README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 已知:3 个测试包在源仓同一提交上同样失败(client/upstream、 service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
412 lines
15 KiB
Go
412 lines
15 KiB
Go
package renewchange
|
|
|
|
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/model/do"
|
|
)
|
|
|
|
// ChangeBindHistoryDO is the repository-local shape of the history table.
|
|
// It deliberately lives outside the generated model because Worker does not
|
|
// generate the card-package domain tables.
|
|
type ChangeBindHistoryDO struct {
|
|
g.Meta `orm:"table:t_sim_bind_package_logs, do:true"`
|
|
SubscriptionNo any `orm:"subscription_no"`
|
|
BatchNo any `orm:"batch_no"`
|
|
Iccid any `orm:"iccid"`
|
|
Msisdn any `orm:"msisdn"`
|
|
DeptId any `orm:"dept_id"`
|
|
PackageType any `orm:"package_type"`
|
|
PackageCode any `orm:"package_code"`
|
|
PreviousPackageCode any `orm:"previous_package_code"`
|
|
BusinessType any `orm:"business_type"`
|
|
EffectiveStatus any `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 any `orm:"duration_months"`
|
|
GiftDurationMonths any `orm:"gift_duration_months"`
|
|
OrderCode any `orm:"order_code"`
|
|
Remark any `orm:"remark"`
|
|
TenantId any `orm:"tenant_id"`
|
|
CreatedDept any `orm:"created_dept"`
|
|
CreatedBy any `orm:"created_by"`
|
|
CreatedAt *gtime.Time `orm:"created_at"`
|
|
UpdatedBy any `orm:"updated_by"`
|
|
UpdatedAt *gtime.Time `orm:"updated_at"`
|
|
}
|
|
|
|
type changeOrderUpdateDO struct {
|
|
g.Meta `orm:"table:t_order, do:true"`
|
|
ReturnStatus any `orm:"return_status"`
|
|
UpdatedBy any `orm:"updated_by"`
|
|
UpdatedAt *gtime.Time `orm:"updated_at"`
|
|
}
|
|
|
|
type changeOrderItemRow struct {
|
|
ItemId int64 `orm:"item_id"`
|
|
ExtraSkuData string `orm:"extra_sku_data"`
|
|
}
|
|
|
|
type changeOrderItemUpdateDO struct {
|
|
g.Meta `orm:"table:t_order_item, do:true"`
|
|
ExtraSkuData any `orm:"extra_sku_data"`
|
|
UpdatedBy any `orm:"updated_by"`
|
|
UpdatedAt *gtime.Time `orm:"updated_at"`
|
|
}
|
|
|
|
type changeCardUpdateDO struct {
|
|
g.Meta `orm:"table:t_sim_card_info, do:true"`
|
|
CurrentPackageCode any `orm:"current_package_code"`
|
|
PackageStatus any `orm:"package_status"`
|
|
FlowPkgBeginAt any `orm:"flow_pkg_begin_at"`
|
|
FlowPkgPresentAt any `orm:"flow_pkg_present_at"`
|
|
FlowPkgRealPresentAt any `orm:"flow_pkg_real_present_at"`
|
|
FlowPkgEndAt any `orm:"flow_pkg_end_at"`
|
|
DataServiceStatus any `orm:"data_service_status"`
|
|
PackageTotalFlowMb any `orm:"package_total_flow_mb"`
|
|
UsedFlowMb any `orm:"used_flow_mb"`
|
|
TrafficCutoffStatus any `orm:"traffic_cutoff_status"`
|
|
TrafficCutoffThreshold any `orm:"traffic_cutoff_threshold"`
|
|
SmsServiceStatus any `orm:"sms_service_status"`
|
|
PackageTotalSms any `orm:"package_total_sms"`
|
|
UsedSms any `orm:"used_sms"`
|
|
VoiceServiceStatus any `orm:"voice_service_status"`
|
|
PackageTotalVoice any `orm:"package_total_voice"`
|
|
UsedVoice any `orm:"used_voice"`
|
|
RoamServiceStatus any `orm:"roam_service_status"`
|
|
PackageTotalRoamFlowMb any `orm:"package_total_roam_flow_mb"`
|
|
UsedRoamFlowMb any `orm:"used_roam_flow_mb"`
|
|
UpdatedBy any `orm:"updated_by"`
|
|
UpdatedAt *gtime.Time `orm:"updated_at"`
|
|
}
|
|
|
|
type ChangeDeptRow struct {
|
|
DeptId int64 `orm:"dept_id"`
|
|
ParentId int64 `orm:"parent_id"`
|
|
TenantId string `orm:"tenant_id"`
|
|
}
|
|
|
|
// LockChangeBatch locks the formal change batch so a duplicate delivery
|
|
// cannot create a second order after the first transaction commits.
|
|
func (r *Repository) LockChangeBatch(ctx context.Context, tx gdb.TX, batchNo string) (*BatchMainRow, error) {
|
|
var main BatchMainRow
|
|
if err := tx.Ctx(ctx).Model(TableBatchMain).
|
|
Where("batch_no", strings.TrimSpace(batchNo)).
|
|
Where("batch_type", consts.BatchTypeChange).
|
|
LockUpdate().
|
|
Scan(&main); err != nil {
|
|
return nil, gerror.Wrap(err, "lock change batch failed")
|
|
}
|
|
if main.Id == 0 {
|
|
return nil, gerror.New("change batch does not exist")
|
|
}
|
|
return &main, nil
|
|
}
|
|
|
|
// LockChangeDetails locks all details, allowing the executor to verify that
|
|
// the claimed batch still has exactly the expected running rows.
|
|
func (r *Repository) LockChangeDetails(ctx context.Context, tx gdb.TX, batchNo string) ([]*BatchDetailRow, error) {
|
|
var rows []*BatchDetailRow
|
|
if err := tx.Ctx(ctx).Model(TableBatchDetail).
|
|
Where("batch_no", strings.TrimSpace(batchNo)).
|
|
Where("batch_type", consts.BatchTypeChange).
|
|
OrderAsc("row_no").
|
|
LockUpdate().
|
|
Scan(&rows); err != nil {
|
|
return nil, gerror.Wrap(err, "lock change details failed")
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
// LockChangeBinds returns every pending/effective bind for the requested
|
|
// cards. The service chooses the single current bind and treats equal-priority
|
|
// duplicates as an integrity error.
|
|
func (r *Repository) LockChangeBinds(ctx context.Context, tx gdb.TX, iccids []string) ([]*SimCardBindPkg, error) {
|
|
return r.LockActiveBindRowsByIccids(ctx, tx, iccids)
|
|
}
|
|
|
|
func (r *Repository) LoadChangePackage(ctx context.Context, tx gdb.TX, tenantID, packageCode string) (*FlowPackageInfo, error) {
|
|
var row FlowPackageInfo
|
|
if err := tx.Ctx(ctx).Model(TableSimFlowPackageInfo).
|
|
Where("tenant_id", strings.TrimSpace(tenantID)).
|
|
Where("package_code", strings.TrimSpace(packageCode)).
|
|
LockUpdate().
|
|
Scan(&row); err != nil {
|
|
return nil, gerror.Wrap(err, "load change package failed")
|
|
}
|
|
if row.Id == 0 {
|
|
return nil, gerror.Newf("package not found: %s", packageCode)
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
func (r *Repository) LoadChangeDept(ctx context.Context, tx gdb.TX, tenantID string, deptID int64) (*ChangeDeptRow, error) {
|
|
var row ChangeDeptRow
|
|
if err := tx.Ctx(ctx).Model(TableSysDept).
|
|
Where("tenant_id", strings.TrimSpace(tenantID)).
|
|
Where("dept_id", deptID).
|
|
Scan(&row); err != nil {
|
|
return nil, gerror.Wrap(err, "load change customer failed")
|
|
}
|
|
if row.DeptId == 0 {
|
|
return nil, gerror.New("change customer does not exist")
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
func (r *Repository) ArchiveChangeBinds(ctx context.Context, tx gdb.TX, binds []*SimCardBindPkg, status int, actualExpireAt *gtime.Time, changeBatchNo string, userID int64) error {
|
|
if len(binds) == 0 {
|
|
return nil
|
|
}
|
|
rows := make([]ChangeBindHistoryDO, 0, len(binds))
|
|
for _, bind := range binds {
|
|
if bind == nil || strings.TrimSpace(bind.SubscriptionNo) == "" {
|
|
return gerror.New("change subscription history is incomplete")
|
|
}
|
|
rows = append(rows, ChangeBindHistoryDO{
|
|
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: status,
|
|
HandleTime: bind.HandleTime,
|
|
EffectiveTime: bind.EffectiveTime,
|
|
ExpectedExpireTime: bind.ExpectedExpireTime,
|
|
ActualExpireTime: actualExpireAt,
|
|
DurationMonths: bind.DurationMonths,
|
|
GiftDurationMonths: bind.GiftDurationMonths,
|
|
OrderCode: bind.OrderCode,
|
|
Remark: appendChangeRemark(bind.Remark, changeBatchNo),
|
|
TenantId: bind.TenantId,
|
|
CreatedDept: bind.CreatedDept,
|
|
CreatedBy: bind.CreatedBy,
|
|
CreatedAt: bind.CreatedAt,
|
|
UpdatedBy: userID,
|
|
UpdatedAt: actualExpireAt,
|
|
})
|
|
}
|
|
if _, err := tx.Ctx(ctx).Model(TableSimBindPackageLogs).
|
|
Data(rows).
|
|
OnConflict("subscription_no").
|
|
Save(); err != nil {
|
|
return gerror.Wrap(err, "archive change subscriptions failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) DeleteChangeBinds(ctx context.Context, tx gdb.TX, subscriptionNos []string) (int64, error) {
|
|
unique := uniqueStrings(subscriptionNos)
|
|
if len(unique) == 0 {
|
|
return 0, nil
|
|
}
|
|
result, err := tx.Ctx(ctx).Model(TableSimCardBindPkg).
|
|
WhereIn("subscription_no", unique).
|
|
Delete()
|
|
if err != nil {
|
|
return 0, gerror.Wrap(err, "delete change subscriptions failed")
|
|
}
|
|
affected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return 0, gerror.Wrap(err, "read deleted change subscriptions count failed")
|
|
}
|
|
return affected, nil
|
|
}
|
|
|
|
func (r *Repository) UpdateChangeOrdersAfterRemoval(ctx context.Context, tx gdb.TX, orderCodes []string, batchNo, batchKey string, quantityByOrder map[string]int, eventTime *gtime.Time, userID int64) error {
|
|
orderCodes = uniqueStrings(orderCodes)
|
|
sort.Strings(orderCodes)
|
|
if len(orderCodes) == 0 {
|
|
return nil
|
|
}
|
|
if eventTime == nil {
|
|
eventTime = gtime.Now()
|
|
}
|
|
activeCounts, err := countChangeBindsByOrder(ctx, tx, orderCodes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, orderCode := range orderCodes {
|
|
returnStatus := consts.OrderReturnStatusSuccess
|
|
if activeCounts[orderCode] > 0 {
|
|
returnStatus = consts.OrderReturnStatusPartial
|
|
}
|
|
result, updateErr := tx.Ctx(ctx).Model(TableOrder).
|
|
Where("order_sn", orderCode).
|
|
Data(buildChangeOrderUpdateDO(returnStatus, userID, eventTime)).
|
|
Update()
|
|
if updateErr != nil {
|
|
return gerror.Wrap(updateErr, "update changed order return status failed")
|
|
}
|
|
affected, affectedErr := result.RowsAffected()
|
|
if affectedErr != nil {
|
|
return gerror.Wrap(affectedErr, "read changed order update count failed")
|
|
}
|
|
if affected != 1 {
|
|
return gerror.Newf("changed source order does not exist: %s", orderCode)
|
|
}
|
|
if err = r.updateChangeOrderItemExtra(ctx, tx, orderCode, batchNo, batchKey, quantityByOrder[orderCode], eventTime, userID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) updateChangeOrderItemExtra(ctx context.Context, tx gdb.TX, orderCode, batchNo, batchKey string, quantity int, eventTime *gtime.Time, userID int64) error {
|
|
var item changeOrderItemRow
|
|
if err := tx.Ctx(ctx).Model(TableOrderItem).
|
|
Where("order_sn", orderCode).
|
|
Limit(1).
|
|
Scan(&item); err != nil {
|
|
return gerror.Wrap(err, "load changed order item failed")
|
|
}
|
|
if item.ItemId == 0 {
|
|
return gerror.Newf("changed order item does not exist: %s", orderCode)
|
|
}
|
|
extra := map[string]any{}
|
|
if strings.TrimSpace(item.ExtraSkuData) != "" {
|
|
if err := json.Unmarshal([]byte(item.ExtraSkuData), &extra); err != nil {
|
|
return gerror.Wrap(err, "parse changed order item extra failed")
|
|
}
|
|
}
|
|
extra[batchKey+"BatchNo"] = batchNo
|
|
extra[batchKey+"Time"] = eventTime.Format("Y-m-d H:i:s")
|
|
if batchKey == "lastUnsubscribe" {
|
|
extra["unsubscribedQuantity"] = quantity
|
|
} else {
|
|
extra["voidedByChangeQuantity"] = quantity
|
|
}
|
|
data, err := json.Marshal(extra)
|
|
if err != nil {
|
|
return gerror.Wrap(err, "marshal changed order item extra failed")
|
|
}
|
|
_, err = tx.Ctx(ctx).Model(TableOrderItem).
|
|
Where("item_id", item.ItemId).
|
|
Data(buildChangeOrderItemUpdateDO(string(data), userID, eventTime)).
|
|
Update()
|
|
return gerror.Wrap(err, "update changed order item extra failed")
|
|
}
|
|
|
|
func buildChangeOrderUpdateDO(returnStatus int, userID int64, updatedAt *gtime.Time) changeOrderUpdateDO {
|
|
return changeOrderUpdateDO{ReturnStatus: returnStatus, UpdatedBy: userID, UpdatedAt: updatedAt}
|
|
}
|
|
|
|
func buildChangeOrderItemUpdateDO(extraSKUData string, userID int64, updatedAt *gtime.Time) changeOrderItemUpdateDO {
|
|
return changeOrderItemUpdateDO{ExtraSkuData: extraSKUData, UpdatedBy: userID, UpdatedAt: updatedAt}
|
|
}
|
|
|
|
func countChangeBindsByOrder(ctx context.Context, tx gdb.TX, orderCodes []string) (map[string]int, error) {
|
|
type orderCountRow struct {
|
|
OrderCode string `orm:"order_code"`
|
|
Count int `orm:"count"`
|
|
}
|
|
var rows []orderCountRow
|
|
if err := tx.Ctx(ctx).Model(TableSimCardBindPkg).
|
|
Fields("order_code", "COUNT(1) AS count").
|
|
WhereIn("order_code", orderCodes).
|
|
Group("order_code").
|
|
Scan(&rows); err != nil {
|
|
return nil, gerror.Wrap(err, "count changed order subscriptions failed")
|
|
}
|
|
counts := make(map[string]int, len(rows))
|
|
for _, row := range rows {
|
|
counts[strings.TrimSpace(row.OrderCode)] = row.Count
|
|
}
|
|
return counts, nil
|
|
}
|
|
|
|
func (r *Repository) UpdateChangeCard(ctx context.Context, tx gdb.TX, iccid, packageCode string, packageStatus int, packageFlowMb float64, beginAt, endAt *gtime.Time, userID int64) error {
|
|
result, err := tx.Ctx(ctx).Model(TableSimCardInfo).
|
|
Where("iccid", strings.TrimSpace(iccid)).
|
|
Data(changeCardUpdateDO{
|
|
CurrentPackageCode: packageCode,
|
|
PackageStatus: packageStatus,
|
|
FlowPkgBeginAt: beginAt,
|
|
FlowPkgPresentAt: gdb.Raw("NULL"),
|
|
FlowPkgRealPresentAt: gdb.Raw("NULL"),
|
|
FlowPkgEndAt: endAt,
|
|
DataServiceStatus: 1,
|
|
PackageTotalFlowMb: packageFlowMb,
|
|
UsedFlowMb: 0,
|
|
TrafficCutoffStatus: 0,
|
|
TrafficCutoffThreshold: 0,
|
|
SmsServiceStatus: 0,
|
|
PackageTotalSms: 0,
|
|
UsedSms: 0,
|
|
VoiceServiceStatus: 0,
|
|
PackageTotalVoice: 0,
|
|
UsedVoice: 0,
|
|
RoamServiceStatus: 0,
|
|
PackageTotalRoamFlowMb: 0,
|
|
UsedRoamFlowMb: 0,
|
|
UpdatedBy: userID,
|
|
UpdatedAt: gtime.Now(),
|
|
}).
|
|
Update()
|
|
if err != nil {
|
|
return gerror.Wrap(err, "update changed card package failed")
|
|
}
|
|
affected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return gerror.Wrap(err, "read changed card update count failed")
|
|
}
|
|
if affected != 1 {
|
|
return gerror.Newf("changed card was modified concurrently: %s", iccid)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) MarkChangeDetailsSuccess(ctx context.Context, tx gdb.TX, batchNo string, details []*BatchDetailRow, snapshots map[int]*gjson.Json, userID int64) error {
|
|
for _, detail := range details {
|
|
if detail == nil {
|
|
continue
|
|
}
|
|
data := do.BatchDetail{
|
|
DetailStatus: consts.BatchDetailStatusSuccess,
|
|
ErrorCode: "",
|
|
ErrorReason: "",
|
|
Snapshot: snapshots[detail.RowNo],
|
|
UpdatedBy: userID,
|
|
}
|
|
result, err := tx.Ctx(ctx).Model(TableBatchDetail).
|
|
Where("batch_no", batchNo).
|
|
Where("batch_type", consts.BatchTypeChange).
|
|
Where("row_no", detail.RowNo).
|
|
Data(data).
|
|
Update()
|
|
if err != nil {
|
|
return gerror.Wrap(err, "mark change detail success failed")
|
|
}
|
|
affected, affectedErr := result.RowsAffected()
|
|
if affectedErr != nil || affected != 1 {
|
|
return gerror.Newf("change detail disappeared during success update: row %d", detail.RowNo)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func appendChangeRemark(old, batchNo string) string {
|
|
addition := "changeBatchNo=" + strings.TrimSpace(batchNo)
|
|
old = strings.TrimSpace(old)
|
|
if old == "" {
|
|
return addition
|
|
}
|
|
return old + "; " + addition
|
|
}
|