Files
邹进 4967f4df43
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat: 卡实名时间以上游 archiveTime 为准并记录实名时间历史,新增二次实名批次
2026-11-08 19:12:46 +09:00

270 lines
9.9 KiB
Go

package cardrealname
import (
"context"
"github.com/bytedance/sonic"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
// Query 提供单卡实名记录与二次实名批次查询。
type Query struct {
db *gorm.DB
}
// NewQuery 创建实名记录查询。
func NewQuery(db *gorm.DB) *Query {
return &Query{db: db}
}
// CardHistory 返回单卡当前实名时间与实名时间历史(按时间倒序分页);卡按当前账号数据范围可见。
func (q *Query) CardHistory(ctx context.Context, req dto.CardRealnameHistoryListRequest) (*dto.CardRealnameHistoryResponse, error) {
var card model.IotCard
if err := middleware.ApplyShopFilter(ctx, q.db.WithContext(ctx).Select("id", "first_realname_at").Where("id = ?", req.ID)).
First(&card).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "IoT 卡不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询 IoT 卡失败")
}
page, size := normalizePage(req.Page, req.PageSize)
resp := &dto.CardRealnameHistoryResponse{Current: dto.CardRealnameCurrentResponse{CardID: card.ID}, Page: page, PageSize: size}
var record model.IotCardRealname
err := q.db.WithContext(ctx).Where("card_id = ?", card.ID).Take(&record).Error
switch {
case err == nil:
resp.Current = dto.CardRealnameCurrentResponse{
CardID: card.ID, RealnameAt: record.RealnameAt, RealnameAtSource: record.RealnameAtSource,
RealnameAtSourceName: constants.RealnameAtSourceName(record.RealnameAtSource),
UpstreamRealnameAt: record.UpstreamRealnameAt, FirstObservedAt: record.FirstObservedAt,
ResetPending: record.ResetStatus == constants.RealnameResetStatusPending, ResetAt: record.ResetAt,
ResetBaselineAt: record.ResetBaselineAt, ResetBatchID: record.ResetBatchID,
}
case err == gorm.ErrRecordNotFound:
resp.Current.RealnameAt = card.FirstRealnameAt
default:
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡当前实名记录失败")
}
base := q.db.WithContext(ctx).Model(&model.IotCardRealnameHistory{}).Where("card_id = ?", card.ID)
if err := base.Count(&resp.Total).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "统计卡实名时间历史失败")
}
var rows []model.IotCardRealnameHistory
if err := base.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&rows).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡实名时间历史失败")
}
operators, err := q.operatorNames(ctx, rows)
if err != nil {
return nil, err
}
resp.Items = make([]dto.CardRealnameHistoryItem, 0, len(rows))
for _, row := range rows {
resp.Items = append(resp.Items, dto.CardRealnameHistoryItem{
ID: row.ID, CardID: row.CardID, ICCID: row.ICCID, BeforeAt: row.BeforeAt, AfterAt: row.AfterAt,
AfterSource: row.AfterSource, AfterSourceName: constants.RealnameAtSourceName(row.AfterSource),
ChangeType: row.ChangeType, ChangeTypeName: constants.RealnameChangeTypeName(row.ChangeType),
ObservationSource: row.ObservationSource, ObservationSourceName: observationSourceName(row.ObservationSource),
ResetBatchID: row.ResetBatchID, OperatorID: row.OperatorID, OperatorName: operators[row.OperatorID],
ObservedAt: row.ObservedAt, CreatedAt: row.CreatedAt,
})
}
return resp, nil
}
// ListResetBatches 分页查询二次实名批次(不含逐行结果)。
func (q *Query) ListResetBatches(ctx context.Context, req dto.RealnameResetBatchListRequest) ([]dto.RealnameResetBatchResponse, int64, int, int, error) {
if err := requirePlatform(ctx); err != nil {
return nil, 0, 0, 0, err
}
page, size := normalizePage(req.Page, req.PageSize)
base := q.db.WithContext(ctx).Model(&model.IotCardRealnameResetBatch{})
if req.BatchNo != "" {
base = base.Where("batch_no = ?", req.BatchNo)
}
var total int64
if err := base.Count(&total).Error; err != nil {
return nil, 0, 0, 0, errors.Wrap(errors.CodeDatabaseError, err, "统计二次实名批次失败")
}
var rows []model.IotCardRealnameResetBatch
if err := base.Omit("items").Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&rows).Error; err != nil {
return nil, 0, 0, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询二次实名批次失败")
}
pending, err := q.pendingCounts(ctx, batchIDs(rows))
if err != nil {
return nil, 0, 0, 0, err
}
list := make([]dto.RealnameResetBatchResponse, 0, len(rows))
for _, row := range rows {
item := batchResponse(row)
item.PendingCount = pending[row.ID]
list = append(list, item)
}
return list, total, page, size, nil
}
// GetResetBatch 查询二次实名批次详情,逐行结果按卡当前状态刷新为等待 / 已完成 / 已撤销。
func (q *Query) GetResetBatch(ctx context.Context, id uint) (*dto.RealnameResetBatchResponse, error) {
if err := requirePlatform(ctx); err != nil {
return nil, err
}
var row model.IotCardRealnameResetBatch
if err := q.db.WithContext(ctx).Where("id = ?", id).Take(&row).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "二次实名批次不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询二次实名批次失败")
}
var items []dto.RealnameResetItem
if len(row.Items) > 0 {
if err := sonic.Unmarshal(row.Items, &items); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "解析二次实名批次结果失败")
}
}
finals, err := q.finalResults(ctx, row.ID)
if err != nil {
return nil, err
}
pending := 0
for index := range items {
if items[index].Result == constants.RealnameResetItemPending {
if final, ok := finals[items[index].CardID]; ok {
items[index].Result = final
} else {
pending++
}
}
items[index].ResultName = constants.RealnameResetItemName(items[index].Result)
}
resp := batchResponse(row)
resp.PendingCount = pending
resp.Items = items
return &resp, nil
}
// finalResults 返回批次内已完成或已撤销的卡(以历史为准,同卡取最新一条)。
func (q *Query) finalResults(ctx context.Context, batchID uint) (map[uint]string, error) {
var rows []model.IotCardRealnameHistory
if err := q.db.WithContext(ctx).Select("card_id", "change_type").
Where("reset_batch_id = ? AND change_type IN ?", batchID,
[]string{constants.RealnameChangeResetCompleted, constants.RealnameChangeResetReverted}).
Order("id").Find(&rows).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询二次实名结果失败")
}
result := make(map[uint]string, len(rows))
for _, row := range rows {
if row.ChangeType == constants.RealnameChangeResetCompleted {
result[row.CardID] = constants.RealnameResetItemCompleted
} else {
result[row.CardID] = constants.RealnameResetItemReverted
}
}
return result, nil
}
func (q *Query) pendingCounts(ctx context.Context, ids []uint) (map[uint]int, error) {
result := make(map[uint]int, len(ids))
if len(ids) == 0 {
return result, nil
}
var rows []struct {
BatchID uint `gorm:"column:reset_batch_id"`
Count int `gorm:"column:count"`
}
if err := q.db.WithContext(ctx).Model(&model.IotCardRealname{}).Select("reset_batch_id, COUNT(*) AS count").
Where("reset_batch_id IN ? AND reset_status = ?", ids, constants.RealnameResetStatusPending).
Group("reset_batch_id").Scan(&rows).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "统计等待二次实名卡数失败")
}
for _, row := range rows {
result[row.BatchID] = row.Count
}
return result, nil
}
func (q *Query) operatorNames(ctx context.Context, rows []model.IotCardRealnameHistory) (map[uint]string, error) {
ids := make([]uint, 0)
for _, row := range rows {
if row.OperatorID > 0 {
ids = append(ids, row.OperatorID)
}
}
result := make(map[uint]string, len(ids))
if len(ids) == 0 {
return result, nil
}
var accounts []model.Account
if err := q.db.WithContext(ctx).Select("id", "username").Where("id IN ?", ids).Find(&accounts).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询操作人失败")
}
for _, account := range accounts {
result[account.ID] = account.Username
}
return result, nil
}
func batchResponse(row model.IotCardRealnameResetBatch) dto.RealnameResetBatchResponse {
return dto.RealnameResetBatchResponse{
ID: row.ID, BatchNo: row.BatchNo, Reason: row.Reason, ImportFileKey: row.ImportFileKey,
TotalCount: row.TotalCount, SucceededCount: row.SucceededCount, SkippedCount: row.SkippedCount,
CompletedCount: row.CompletedCount, RevertedCount: row.RevertedCount,
OperatorID: row.OperatorID, OperatorName: row.OperatorName, CreatedAt: row.CreatedAt,
}
}
func batchIDs(rows []model.IotCardRealnameResetBatch) []uint {
ids := make([]uint, 0, len(rows))
for _, row := range rows {
ids = append(ids, row.ID)
}
return ids
}
func requirePlatform(ctx context.Context) error {
switch middleware.GetUserTypeFromContext(ctx) {
case constants.UserTypeSuperAdmin, constants.UserTypePlatform:
return nil
default:
return errors.New(errors.CodeForbidden, "仅平台账号可查看二次实名批次")
}
}
func normalizePage(page, size int) (int, int) {
if page < 1 {
page = 1
}
if size < 1 {
size = 20
}
if size > 100 {
size = 100
}
return page, size
}
func observationSourceName(source string) string {
switch source {
case constants.CardObservationSourcePolling:
return "轮询"
case constants.CardObservationSourceManualSync:
return "手动刷新"
case constants.CardObservationSourceManualOverride:
return "人工"
case constants.CardObservationSourceCarrierCallback:
return "运营商回调"
case constants.CardObservationSourceBusinessEvent:
return "业务观测"
case "migration":
return "上线迁移"
case "package_silence":
return "沉默期判定"
default:
return source
}
}