chore: 初始开源发布

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),属既有缺陷,本仓未引入亦未修复。
This commit is contained in:
MiFaZhan
2026-10-22 16:37:56 +08:00
commit 2bec257289
361 changed files with 71524 additions and 0 deletions
@@ -0,0 +1,411 @@
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
}