Files
iotlcm/worker-svr/internal/repository/renewchange/repository.go

530 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}