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,529 @@
package renewchange
import (
"context"
"database/sql"
"errors"
"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/os/gtime"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
)
// Repository 提供对续费/变更相关表的 typed 访问。
// 表名/列名都是常量(见 tables.go),与 Admin 同步链路保持一致。
type Repository struct{}
func New() *Repository {
return &Repository{}
}
const simCardRenewChangeFields = "id,iccid,msisdn,imsi,dept_id,current_package_code,package_status,outbound_batch_no,flow_pkg_begin_at,flow_pkg_end_at,tenant_id,created_dept,updated_by,updated_at"
func (r *Repository) database() gdb.DB {
return db()
}
// DB 返回底层 gdb.DB,供 service 层发起事务。
func (r *Repository) DB() gdb.DB {
return r.database()
}
// LoadBatchMain 按 batchNo+batchType 加载。
func (r *Repository) LoadBatchMain(ctx context.Context, batchNo string, batchType int) (*BatchMainRow, error) {
if batchNo == "" {
return nil, gerror.New("batch number is empty")
}
m := r.database().Model(TableBatchMain).Safe().Ctx(ctx).
Where("batch_no", batchNo).
Where("batch_type", batchType)
var out BatchMainRow
if err := m.Scan(&out); err != nil {
return nil, gerror.Wrap(err, "query batch main failed")
}
if out.Id == 0 {
return nil, nil
}
return &out, nil
}
// LoadBatchDetails 加载指定批次下指定状态集合的明细。
func (r *Repository) LoadBatchDetails(ctx context.Context, batchNo string, batchType int, statuses []int) ([]*BatchDetailRow, error) {
if batchNo == "" {
return nil, nil
}
m := r.database().Model(TableBatchDetail).Safe().Ctx(ctx).
Where("batch_no", batchNo).
Where("batch_type", batchType).
OrderAsc("row_no")
if len(statuses) > 0 {
m = m.WhereIn("detail_status", statuses)
}
var out []*BatchDetailRow
if err := m.Scan(&out); err != nil {
return nil, gerror.Wrap(err, "query batch details failed")
}
return out, nil
}
func (r *Repository) LoadCardsByIdentityValues(ctx context.Context, tenantID string, values []string) ([]*SimCardInfo, error) {
uniq := uniqueStrings(values)
if len(uniq) == 0 {
return nil, nil
}
tenantID = strings.TrimSpace(tenantID)
byID := map[int64]*SimCardInfo{}
for _, column := range []string{"iccid", "msisdn", "imsi"} {
var rows []*SimCardInfo
query := r.database().Model(TableSimCardInfo).Safe().Ctx(ctx).
Fields(simCardRenewChangeFields).
WhereIn(column, uniq)
if tenantID != "" {
query = query.Where("tenant_id", tenantID)
}
if err := query.Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "query sim cards by identity failed")
}
for _, row := range rows {
if row != nil && row.Id > 0 {
byID[row.Id] = row
}
}
}
out := make([]*SimCardInfo, 0, len(byID))
for _, row := range byID {
out = append(out, row)
}
return out, nil
}
// UpdateBatchMainStatus 在事务内更新主表。
func (r *Repository) UpdateBatchMainStatus(ctx context.Context, tx gdb.TX, batchNo string, batchType int, data *BatchMainRow) error {
updateData := map[string]any{
"batch_type": data.BatchType,
"batch_async_status": data.BatchAsyncStatus,
"run_result": data.RunResult,
"total_count": data.TotalCount,
"valid_count": data.ValidCount,
"success_count": data.SuccessCount,
"fail_count": data.FailCount,
"fail_reason": data.FailReason,
"batch_info": data.BatchInfo,
"end_time": data.EndTime,
"updated_by": data.UpdatedBy,
"updated_at": gtime.Now(),
"order_sn": data.OrderSn,
"target_dept_id": data.TargetDeptId,
}
if data.StartTime != nil && !data.StartTime.IsZero() {
updateData["start_time"] = data.StartTime
}
_, err := tx.Ctx(ctx).Model(TableBatchMain).
Where("batch_no", batchNo).
Where("batch_type", batchType).
Data(updateData).
Update()
return gerror.Wrap(err, "update batch main failed")
}
// ConvertBatchDetails 将指定状态的明细转换为目标类型与状态。
func (r *Repository) ConvertBatchDetails(ctx context.Context, tx gdb.TX, batchNo string, fromTypes []int, toType, toStatus int, data map[string]any) (int64, error) {
if data == nil {
data = map[string]any{}
}
data["batch_type"] = toType
data["detail_status"] = toStatus
data["updated_at"] = gtime.Now()
result, err := tx.Ctx(ctx).Model(TableBatchDetail).
Where("batch_no", batchNo).
WhereIn("batch_type", fromTypes).
Data(data).
Update()
if err != nil {
return 0, gerror.Wrap(err, "convert batch details failed")
}
affected, err := result.RowsAffected()
if err != nil {
return 0, err
}
return affected, nil
}
// UpdateBatchDetailSnapshot 覆盖指定明细的 snapshot 与状态。
func (r *Repository) UpdateBatchDetailSnapshot(ctx context.Context, tx gdb.TX, batchNo string, batchType, rowNo int, status int, snapshot *gjson.Json, reason string, userID int64) error {
data := map[string]any{
"detail_status": status,
"snapshot": snapshot,
"updated_at": gtime.Now(),
"updated_by": userID,
}
if reason != "" {
data["error_reason"] = reason
}
_, err := tx.Ctx(ctx).Model(TableBatchDetail).
Where("batch_no", batchNo).
Where("batch_type", batchType).
Where("row_no", rowNo).
Data(data).
Update()
return gerror.Wrap(err, "update batch detail snapshot failed")
}
// UpsertBatchDetail 按 (batch_no, batch_type, row_no) 写入明细(覆盖式)。
func (r *Repository) UpsertBatchDetail(ctx context.Context, tx gdb.TX, detail *BatchDetailRow) error {
_, err := tx.Ctx(ctx).Model(TableBatchDetail).
Where("batch_no", detail.BatchNo).
Where("batch_type", detail.BatchType).
Where("row_no", detail.RowNo).
Data(map[string]any{
"detail_status": detail.DetailStatus,
"blank_out_status": detail.BlankOutStatus,
"item_id": detail.ItemId,
"error_code": detail.ErrorCode,
"error_reason": detail.ErrorReason,
"snapshot": detail.Snapshot,
"updated_at": gtime.Now(),
"updated_by": detail.UpdatedBy,
}).
Update()
if err != nil {
return gerror.Wrap(err, "update batch detail failed")
}
return nil
}
// LoadCardsByIccids 加载正式卡。
func (r *Repository) LoadCardsByIccids(ctx context.Context, iccids []string) (map[string]*SimCardInfo, error) {
out := make(map[string]*SimCardInfo, len(iccids))
uniq := uniqueStrings(iccids)
if len(uniq) == 0 {
return out, nil
}
var rows []*SimCardInfo
if err := r.database().Model(TableSimCardInfo).Safe().Ctx(ctx).
Fields(simCardRenewChangeFields).
WhereIn("iccid", uniq).
Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "query sim cards failed")
}
for _, row := range rows {
if row == nil {
continue
}
row.Iccid = strings.TrimSpace(row.Iccid)
if row.Iccid == "" {
continue
}
out[row.Iccid] = row
}
return out, nil
}
// LockCardsByIccids 在事务内按 ICCID 锁行加载正式卡。
func (r *Repository) LockCardsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*SimCardInfo, error) {
out := make(map[string]*SimCardInfo, len(iccids))
uniq := uniqueStrings(iccids)
if len(uniq) == 0 {
return out, nil
}
var rows []*SimCardInfo
if err := tx.Ctx(ctx).Model(TableSimCardInfo).
Fields(simCardRenewChangeFields).
WhereIn("iccid", uniq).
LockUpdate().
Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "lock sim cards failed")
}
for _, row := range rows {
if row == nil {
continue
}
row.Iccid = strings.TrimSpace(row.Iccid)
if row.Iccid == "" {
continue
}
out[row.Iccid] = row
}
return out, nil
}
// LoadActiveBindRowsByIccids loads every pending/effective formal package
// subscription for prepare validation. Add-on package rows (103/104) are
// intentionally excluded from current-subscription selection.
func (r *Repository) LoadActiveBindRowsByIccids(ctx context.Context, iccids []string) ([]*SimCardBindPkg, error) {
uniq := uniqueStrings(iccids)
if len(uniq) == 0 {
return nil, nil
}
var rows []*SimCardBindPkg
if err := r.database().Ctx(ctx).Model(TableSimCardBindPkg).
WhereIn("iccid", uniq).
WhereIn("package_type", consts.FormalPackageTypes()).
WhereIn("effective_status", []int{consts.EffectiveStatusPending, consts.EffectiveStatusEffective}).
OrderDesc("effective_status").
OrderAsc("effective_time").
OrderAsc("id").
Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "load active binds failed")
}
return rows, nil
}
// LoadRenewBindRowsByIccids includes expired formal-package history so renewal
// can restart after a card has naturally expired. Add-on package rows (103/104)
// never participate in renewal selection.
func (r *Repository) LoadRenewBindRowsByIccids(ctx context.Context, iccids []string) ([]*SimCardBindPkg, error) {
uniq := uniqueStrings(iccids)
if len(uniq) == 0 {
return nil, nil
}
var rows []*SimCardBindPkg
if err := r.database().Ctx(ctx).Model(TableSimCardBindPkg).
WhereIn("iccid", uniq).
WhereIn("package_type", consts.FormalPackageTypes()).
WhereIn("effective_status", []int{
consts.EffectiveStatusPending,
consts.EffectiveStatusEffective,
consts.EffectiveStatusExpired,
}).
OrderDesc("effective_status").
OrderAsc("effective_time").
OrderAsc("id").
Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "load renew binds failed")
}
return rows, nil
}
// LockActiveBindRowsByIccids is the transactional counterpart used by change
// execution before any business write. It locks formal package rows only.
func (r *Repository) LockActiveBindRowsByIccids(ctx context.Context, tx gdb.TX, iccids []string) ([]*SimCardBindPkg, error) {
uniq := uniqueStrings(iccids)
if len(uniq) == 0 {
return nil, nil
}
var rows []*SimCardBindPkg
if err := tx.Ctx(ctx).Model(TableSimCardBindPkg).
WhereIn("iccid", uniq).
WhereIn("package_type", consts.FormalPackageTypes()).
WhereIn("effective_status", []int{consts.EffectiveStatusPending, consts.EffectiveStatusEffective}).
OrderDesc("effective_status").
OrderAsc("effective_time").
OrderAsc("id").
LockUpdate().
Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "lock active binds failed")
}
return rows, nil
}
// LockRenewBindRowsByIccids includes expired formal-package history for renewal
// execution. Change execution keeps using LockActiveBindRowsByIccids.
func (r *Repository) LockRenewBindRowsByIccids(ctx context.Context, tx gdb.TX, iccids []string) ([]*SimCardBindPkg, error) {
uniq := uniqueStrings(iccids)
if len(uniq) == 0 {
return nil, nil
}
var rows []*SimCardBindPkg
if err := tx.Ctx(ctx).Model(TableSimCardBindPkg).
WhereIn("iccid", uniq).
WhereIn("package_type", consts.FormalPackageTypes()).
WhereIn("effective_status", []int{
consts.EffectiveStatusPending,
consts.EffectiveStatusEffective,
consts.EffectiveStatusExpired,
}).
OrderDesc("effective_status").
OrderAsc("effective_time").
OrderAsc("id").
LockUpdate().
Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "lock renew binds failed")
}
return rows, nil
}
// LockPendingBindsByIccids 锁行加载 pending 状态的未来订购。
func (r *Repository) LockPendingBindsByIccids(ctx context.Context, tx gdb.TX, iccids []string) ([]*SimCardBindPkg, error) {
uniq := uniqueStrings(iccids)
if len(uniq) == 0 {
return nil, nil
}
var rows []*SimCardBindPkg
if err := tx.Ctx(ctx).Model(TableSimCardBindPkg).
WhereIn("iccid", uniq).
WhereIn("package_type", consts.FormalPackageTypes()).
Where("effective_status", consts.EffectiveStatusPending).
OrderAsc("effective_time").
OrderAsc("id").
LockUpdate().
Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "lock pending binds failed")
}
return rows, nil
}
// DeletePendingBindsBySubscriptionNos 删除(作废)一批订阅号。
func (r *Repository) DeletePendingBindsBySubscriptionNos(ctx context.Context, tx gdb.TX, subscriptionNos []string) (int64, error) {
uniq := uniqueStrings(subscriptionNos)
if len(uniq) == 0 {
return 0, nil
}
result, err := tx.Ctx(ctx).Model(TableSimCardBindPkg).
WhereIn("subscription_no", uniq).
Delete()
if err != nil {
return 0, gerror.Wrap(err, "delete pending binds failed")
}
affected, err := result.RowsAffected()
if err != nil {
return 0, err
}
return affected, nil
}
// LoadPackageByCode 按 packageCode 读取套餐。
func (r *Repository) LoadPackageByCode(ctx context.Context, tenantID, packageCode string) (*FlowPackageInfo, error) {
if packageCode == "" {
return nil, errPkgNotFound
}
var row FlowPackageInfo
err := r.database().Model(TableSimFlowPackageInfo).Safe().Ctx(ctx).
Where("tenant_id", tenantID).
Where("package_code", strings.TrimSpace(packageCode)).
Scan(&row)
if err != nil {
return nil, gerror.Wrap(err, "query package failed")
}
if row.Id == 0 {
return nil, errPkgNotFound
}
return &row, nil
}
// LoadDeptName 按 deptId 取客户名。
func (r *Repository) LoadDeptName(ctx context.Context, tenantID string, deptID int64) (string, error) {
row, err := r.LoadDept(ctx, tenantID, deptID)
if err != nil {
return "", err
}
return strings.TrimSpace(row.DeptName), nil
}
// LoadDept returns the current department and its parent. Renewals use the
// parent department as the price owner for the current card owner.
func (r *Repository) LoadDept(ctx context.Context, tenantID string, deptID int64) (*SysDeptRow, error) {
if deptID <= 0 {
return nil, errDeptNotFound
}
var row SysDeptRow
err := r.database().Model(TableSysDept).Safe().Ctx(ctx).
Where("tenant_id", tenantID).
Where("dept_id", deptID).
Scan(&row)
if err != nil {
return nil, gerror.Wrap(err, "query dept failed")
}
if row.DeptId == 0 {
return nil, errDeptNotFound
}
return &row, nil
}
// LoadPriceOverride 读取代理续费/变更价格覆盖。
func (r *Repository) LoadPriceOverride(ctx context.Context, tenantID string, deptID, targetAgentID int64, scopeType int, packageCode string) (*FlowPackagePrice, error) {
var row FlowPackagePrice
err := r.database().Model(TableSimFlowPackagePrice).Safe().Ctx(ctx).
Where("tenant_id", tenantID).
Where("dept_id", deptID).
Where("agent_scope_type", scopeType).
Where("target_agent_id", targetAgentID).
Where("package_code", strings.TrimSpace(packageCode)).
Where("enabled", 1).
Scan(&row)
return normalizePriceOverrideResult(&row, err)
}
func normalizePriceOverrideResult(row *FlowPackagePrice, err error) (*FlowPackagePrice, error) {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, gerror.Wrap(err, "query price override failed")
}
if row == nil || row.Id == 0 {
return nil, nil
}
return row, nil
}
// UpdateCardPackageFields 续费/变更后更新卡的当前套餐与流量状态。
func (r *Repository) UpdateCardPackageFields(ctx context.Context, tx gdb.TX, iccid string, fields map[string]any, userID int64) error {
if iccid == "" {
return gerror.New("iccid is empty")
}
fields["updated_by"] = userID
fields["updated_at"] = gtime.Now()
_, err := tx.Ctx(ctx).Model(TableSimCardInfo).
Where("iccid", iccid).
Data(fields).
Update()
return gerror.Wrap(err, "update sim card failed")
}
// InsertOrder 插入订单(返回 orderSn)。
// InsertOrderItem 插入订单项。
// UpdateOrderReturnStatus 更新订单退订状态。
// UpdateOrderItemExtra 合并并写入订单项 extraSkuData(JSON 字符串)。
// ActiveBindCountByOrderCodes 统计订单当前未作废的订购数。
func (r *Repository) ActiveBindCountByOrderCodes(ctx context.Context, tx gdb.TX, orderSNs []string) (map[string]int, error) {
out := make(map[string]int, len(orderSNs))
uniq := uniqueStrings(orderSNs)
if len(uniq) == 0 {
return out, nil
}
type row struct {
OrderCode string `orm:"order_code"`
Count int `orm:"count"`
}
var rows []row
if err := tx.Ctx(ctx).Model(TableSimCardBindPkg).
Fields("order_code", "COUNT(1) AS count").
WhereIn("order_code", uniq).
Group("order_code").
Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "count active binds failed")
}
for _, r := range rows {
out[strings.TrimSpace(r.OrderCode)] = r.Count
}
return out, nil
}
// InsertBind 插入新的订购记录。
func (r *Repository) InsertBind(ctx context.Context, tx gdb.TX, bind *SimCardBindPkg) error {
_, err := tx.Ctx(ctx).Model(TableSimCardBindPkg).Data(bind).Insert()
return gerror.Wrap(err, "insert bind failed")
}
// InsertSysOss 插入 OSS 记录,返回 ID。
func (r *Repository) InsertSysOss(ctx context.Context, tx gdb.TX, row *SysOssRow) (int64, error) {
id, err := tx.Ctx(ctx).Model(TableSysOss).Data(row).InsertAndGetId()
if err != nil {
return 0, gerror.Wrap(err, "insert sys oss failed")
}
return id, nil
}
// InsertBatchDownload 插入 t_resource_batch_download 记录,返回 ID。
func (r *Repository) InsertBatchDownload(ctx context.Context, tx gdb.TX, row *ResourceBatchDownloadRow) (int64, error) {
id, err := tx.Ctx(ctx).Model(TableTResourceBatchDl).Data(row).InsertAndGetId()
if err != nil {
return 0, gerror.Wrap(err, "insert batch download failed")
}
return id, nil
}