Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 8m39s
- 迁移 000259~000265:套餐记录沉默期字段与扫描索引、套餐默认沉默期、延期政策/流水/订单/批次/明细 5 张表、默认政策与系统配置(总开关默认关闭)、企微审批白名单、卡与设备出库时间及出库记录 - 沉默期设置(订单发放、自动购包、钱包续费)、后台购买时调整(超默认值按政策免审或企微审批)、统一判定与激活闸门、排队与孤儿扫描跳过过期套餐、过期套餐禁止退款、每 10 分钟扫描 - 总开关专用接口与打开前 4 项检查;延期政策;延期资格与计费;H5 自助与后台二维码在线支付(商户池路由)、支付回调与查单恢复、线下登记、特殊/批量延期、异常重试与原路退款;沉默期审批场景与终态处理 - 换货沉默期流水与出库时间;卡/设备列表、详情、导出与出库记录接口;延期订单与即将过期套餐列表及导出;财务流水纳入延期支付 - 沉默期与出库时间回填工具 cmd/silence-backfill;前端接口变更说明 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
471 lines
23 KiB
Go
471 lines
23 KiB
Go
package packagesilence
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
|
||
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
|
||
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
|
||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||
)
|
||
|
||
// DecisionHandler 消费沉默期审批场景 package_silence_approval 的标准终态(技术方案 §11.3)。
|
||
// 审批实例的业务 ID 为批次 ID,按批次路径分发:adjust 购买时调整、offline 线下付费延期、special 特殊延期。
|
||
// 回调不依赖表单文本;以批次与明细状态条件更新保证重复事件只处理一次。总开关关闭时照常处理。
|
||
type DecisionHandler struct {
|
||
extensions *ExtensionService
|
||
}
|
||
|
||
// NewDecisionHandler 创建沉默期审批终态消费者。
|
||
func NewDecisionHandler(extensions *ExtensionService) *DecisionHandler {
|
||
return &DecisionHandler{extensions: extensions}
|
||
}
|
||
|
||
var _ approvalapp.BusinessDecisionHandler = (*DecisionHandler)(nil)
|
||
|
||
// Handle 幂等消费标准审批终态。
|
||
func (h *DecisionHandler) Handle(ctx context.Context, event approvalapp.TerminalDecisionEvent) error {
|
||
if h == nil || h.extensions == nil {
|
||
return errors.New(errors.CodeInternalError, "沉默期审批终态能力未配置")
|
||
}
|
||
if event.BusinessType != constants.ApprovalBusinessTypePackageSilence || event.BusinessID == 0 || event.InstanceID == 0 {
|
||
return errors.New(errors.CodeInvalidParam, "沉默期审批终态参数无效")
|
||
}
|
||
ctx = auditcontext.With(ctx, auditcontext.Context{
|
||
ActorKind: constants.AuditActorExternalSystem, ActorID: "wecom_approval", ActorName: "企业微信审批",
|
||
Source: constants.AuditSourceCallback, CorrelationID: event.CorrelationID, ParentEventID: event.EventID,
|
||
})
|
||
approved := event.Decision == constants.ApprovalDecisionApproved
|
||
if event.Decision == constants.ApprovalDecisionRevokedAfterApproved {
|
||
// 通过后撤销:延期已按通过执行,不回滚。
|
||
return nil
|
||
}
|
||
s := h.extensions
|
||
var batch *model.PackageSilenceExtensionBatch
|
||
var postUsageIDs []uint
|
||
executeSpecial := false
|
||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
var err error
|
||
batch, err = lockBatchForDecision(ctx, tx, event.BusinessID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if batch.ApprovalInstanceID == nil || *batch.ApprovalInstanceID != event.InstanceID {
|
||
return errors.New(errors.CodeConflict, "沉默期批次关联的审批实例不一致")
|
||
}
|
||
if batch.Status != constants.SilenceBatchStatusPendingApproval {
|
||
return nil
|
||
}
|
||
now := s.now()
|
||
approvalStatus := constants.SilenceApprovalStatusRejected
|
||
if approved {
|
||
approvalStatus = constants.SilenceApprovalStatusApproved
|
||
} else if event.Decision == constants.ApprovalDecisionCancelled || event.Decision == constants.ApprovalDecisionDeleted {
|
||
approvalStatus = constants.SilenceApprovalStatusCanceled
|
||
}
|
||
switch batch.Path {
|
||
case constants.SilencePathAdjust:
|
||
usageID, err := s.applyAdjustDecisionInTx(ctx, tx, batch, approved, approvalStatus, now)
|
||
if usageID > 0 {
|
||
postUsageIDs = append(postUsageIDs, usageID)
|
||
}
|
||
return err
|
||
case constants.SilencePathOffline:
|
||
usageID, err := s.applyOfflineDecisionInTx(ctx, tx, batch, approved, approvalStatus, now)
|
||
if usageID > 0 {
|
||
postUsageIDs = append(postUsageIDs, usageID)
|
||
}
|
||
return err
|
||
case constants.SilencePathSpecial:
|
||
executeSpecial, err = s.applySpecialDecisionInTx(ctx, tx, batch, approved, approvalStatus, now)
|
||
return err
|
||
default:
|
||
return errors.New(errors.CodeInvalidStatus, "沉默期批次路径不支持审批")
|
||
}
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if executeSpecial {
|
||
return s.executeBatch(ctx, batch.ID)
|
||
}
|
||
if batch != nil && batch.Path == constants.SilencePathAdjust {
|
||
for _, id := range postUsageIDs {
|
||
if _, err := s.resolver.ResolveAndActivate(ctx, id, ResolveOptions{Trigger: constants.SilenceTriggerAdjustApproval, AllowExpire: s.sw.Enabled(ctx)}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
s.afterExecuted(ctx, postUsageIDs)
|
||
return nil
|
||
}
|
||
|
||
// applyAdjustDecisionInTx 处理购买时调整审批终态(技术方案 §4.3):通过按 起算日期 + 审批天数 重算截止日;
|
||
// 驳回或撤回保持默认值;两种情况都清除审批中标记,提交后立即重新判定。
|
||
func (s *ExtensionService) applyAdjustDecisionInTx(ctx context.Context, tx *gorm.DB, batch *model.PackageSilenceExtensionBatch, approved bool, approvalStatus string, now time.Time) (uint, error) {
|
||
if batch.AdjustPackageUsageID == nil {
|
||
return 0, errors.New(errors.CodeInvalidStatus, "调整申请缺少套餐记录")
|
||
}
|
||
var usage model.PackageUsage
|
||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", *batch.AdjustPackageUsageID).First(&usage).Error; err != nil {
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "锁定套餐记录失败")
|
||
}
|
||
batchStatus := constants.SilenceBatchStatusRejected
|
||
values := map[string]any{"silence_adjust_pending": false, "updated_at": now}
|
||
beforeDeadline := copyTimePtr(usage.SilenceDeadlineAt)
|
||
days := usage.SilencePeriodDays
|
||
if approved && batch.AdjustRequestedDays != nil && usage.SilenceStartAt != nil {
|
||
deadline := packagedomain.SilenceDeadline(*usage.SilenceStartAt, *batch.AdjustRequestedDays)
|
||
values["silence_period_days"] = *batch.AdjustRequestedDays
|
||
values["silence_deadline_at"] = deadline
|
||
usage.SilenceDeadlineAt = &deadline
|
||
days = batch.AdjustRequestedDays
|
||
batchStatus = constants.SilenceBatchStatusCompleted
|
||
}
|
||
if err := tx.WithContext(ctx).Model(&model.PackageUsage{}).Where("id = ? AND silence_adjust_pending = ?", usage.ID, true).Updates(values).Error; err != nil {
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新套餐沉默期调整结果失败")
|
||
}
|
||
usage.SilenceAdjustPending = false
|
||
usage.SilencePeriodDays = days
|
||
if _, err := postgres.NewPackageSilenceBatchStore(tx).TransitionStatus(ctx, batch.ID, []string{constants.SilenceBatchStatusPendingApproval}, batchStatus, map[string]any{
|
||
"approval_status": approvalStatus, "approval_finished_at": now, "completed_at": now,
|
||
"succeeded_count": boolToInt(approved), "failed_count": boolToInt(!approved),
|
||
}); err != nil {
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新调整申请状态失败")
|
||
}
|
||
action, summary := constants.AuditActionUsageSilenceAdjustRejected, "购买时调整沉默期审批未通过,保持默认值"
|
||
if approved {
|
||
action, summary = constants.AuditActionUsageSilenceAdjustApproved, "购买时调整沉默期审批通过,重算截止日"
|
||
}
|
||
approvalID, batchID := *batch.ApprovalInstanceID, batch.ID
|
||
changeDays := 0
|
||
if batch.AdjustRequestedDays != nil && batch.AdjustDefaultDays != nil {
|
||
changeDays = *batch.AdjustRequestedDays - *batch.AdjustDefaultDays
|
||
}
|
||
err := writeRecord(ctx, tx, s.audit, recordInput{
|
||
Usage: &usage, EventType: constants.SilenceRecordEventAdjust, Trigger: constants.SilenceTriggerAdjustApproval,
|
||
BeforeStatus: copyStringPtr(usage.SilenceStatus), BeforeDeadline: beforeDeadline, DefaultDays: batch.AdjustDefaultDays,
|
||
IsAdjusted: approved, ChangeDays: changeDays, Reason: batch.Reason, ApprovalID: &approvalID, BatchID: &batchID,
|
||
Extra: map[string]any{"stage": approvalStatus, "requested_days": batch.AdjustRequestedDays},
|
||
AuditAction: action, AuditSummary: summary,
|
||
})
|
||
return usage.ID, err
|
||
}
|
||
|
||
// applyOfflineDecisionInTx 处理线下付费延期审批:通过执行延期,驳回或撤回订单 rejected、不延期、系统不退款。
|
||
func (s *ExtensionService) applyOfflineDecisionInTx(ctx context.Context, tx *gorm.DB, batch *model.PackageSilenceExtensionBatch, approved bool, approvalStatus string, now time.Time) (uint, error) {
|
||
var order model.PackageSilenceExtensionOrder
|
||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("batch_id = ?", batch.ID).First(&order).Error; err != nil {
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "锁定线下延期订单失败")
|
||
}
|
||
if _, err := postgres.NewPackageSilenceBatchStore(tx).TransitionStatus(ctx, batch.ID, []string{constants.SilenceBatchStatusPendingApproval},
|
||
map[bool]string{true: constants.SilenceBatchStatusExecuting, false: constants.SilenceBatchStatusRejected}[approved],
|
||
map[string]any{"approval_status": approvalStatus, "approval_finished_at": now}); err != nil {
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新线下延期批次失败")
|
||
}
|
||
var usage model.PackageUsage
|
||
_ = tx.WithContext(ctx).Where("id = ?", order.PackageUsageID).First(&usage).Error
|
||
if !approved {
|
||
if _, err := postgres.NewPackageSilenceOrderStore(tx).TransitionStatus(ctx, order.ID, []string{constants.SilenceOrderStatusPendingApproval}, constants.SilenceOrderStatusRejected, nil); err != nil {
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新线下延期订单为已驳回失败")
|
||
}
|
||
if err := tx.Model(&model.PackageSilenceExtension{}).Where("batch_id = ? AND status = ?", batch.ID, constants.SilenceExtensionStatusPendingApproval).
|
||
Updates(map[string]any{"status": constants.SilenceExtensionStatusRejected, "updated_at": now}).Error; err != nil {
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新线下延期明细为已驳回失败")
|
||
}
|
||
if err := tx.Model(&model.PackageSilenceExtensionBatch{}).Where("id = ?", batch.ID).Updates(map[string]any{"failed_count": 1, "completed_at": now}).Error; err != nil {
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "更新线下延期批次计数失败")
|
||
}
|
||
before := order.Status
|
||
order.Status = constants.SilenceOrderStatusRejected
|
||
if usage.ID > 0 && s.audit != nil {
|
||
_ = s.audit.AppendSilenceUsage(ctx, tx, extensionAuditInput(&usage, batch, constants.AuditActionUsageSilenceExtensionRejected, "线下付费延期审批未通过"))
|
||
}
|
||
return 0, s.writeOrderAudit(ctx, tx, &order, before, "线下付费延期审批未通过,订单已驳回")
|
||
}
|
||
if usage.ID > 0 && s.audit != nil {
|
||
_ = s.audit.AppendSilenceUsage(ctx, tx, extensionAuditInput(&usage, batch, constants.AuditActionUsageSilenceExtensionApproved, "线下付费延期审批通过"))
|
||
}
|
||
ok, err := s.executeOrderInTx(ctx, tx, &order)
|
||
if err != nil || !ok {
|
||
return 0, err
|
||
}
|
||
return order.PackageUsageID, nil
|
||
}
|
||
|
||
// applySpecialDecisionInTx 处理特殊延期审批:通过时明细置为已批准、提交后逐条执行;驳回或撤回不产生任何变化。
|
||
func (s *ExtensionService) applySpecialDecisionInTx(ctx context.Context, tx *gorm.DB, batch *model.PackageSilenceExtensionBatch, approved bool, approvalStatus string, now time.Time) (bool, error) {
|
||
batchStatus, detailStatus := constants.SilenceBatchStatusRejected, constants.SilenceExtensionStatusRejected
|
||
action, summary := constants.AuditActionUsageSilenceExtensionRejected, "特殊延期审批未通过"
|
||
if approved {
|
||
batchStatus, detailStatus = constants.SilenceBatchStatusExecuting, constants.SilenceExtensionStatusApproved
|
||
action, summary = constants.AuditActionUsageSilenceExtensionApproved, "特殊延期审批通过"
|
||
}
|
||
if _, err := postgres.NewPackageSilenceBatchStore(tx).TransitionStatus(ctx, batch.ID, []string{constants.SilenceBatchStatusPendingApproval}, batchStatus,
|
||
map[string]any{"approval_status": approvalStatus, "approval_finished_at": now}); err != nil {
|
||
return false, errors.Wrap(errors.CodeDatabaseError, err, "更新特殊延期批次失败")
|
||
}
|
||
var extensions []model.PackageSilenceExtension
|
||
if err := tx.WithContext(ctx).Where("batch_id = ? AND status = ?", batch.ID, constants.SilenceExtensionStatusPendingApproval).Find(&extensions).Error; err != nil {
|
||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询特殊延期明细失败")
|
||
}
|
||
if err := tx.Model(&model.PackageSilenceExtension{}).Where("batch_id = ? AND status = ?", batch.ID, constants.SilenceExtensionStatusPendingApproval).
|
||
Updates(map[string]any{"status": detailStatus, "updated_at": now}).Error; err != nil {
|
||
return false, errors.Wrap(errors.CodeDatabaseError, err, "更新特殊延期明细失败")
|
||
}
|
||
if !approved {
|
||
if err := tx.Model(&model.PackageSilenceExtensionBatch{}).Where("id = ?", batch.ID).
|
||
Updates(map[string]any{"failed_count": len(extensions), "completed_at": now}).Error; err != nil {
|
||
return false, errors.Wrap(errors.CodeDatabaseError, err, "更新特殊延期批次计数失败")
|
||
}
|
||
}
|
||
for index := range extensions {
|
||
var usage model.PackageUsage
|
||
if err := tx.WithContext(ctx).Where("id = ?", extensions[index].PackageUsageID).First(&usage).Error; err == nil && s.audit != nil {
|
||
_ = s.audit.AppendSilenceUsage(ctx, tx, extensionAuditInput(&usage, batch, action, summary))
|
||
}
|
||
}
|
||
return approved, nil
|
||
}
|
||
|
||
// HandleException 后台处理异常明细(权限 package_silence:exception):重试执行或关闭;不得直接改库时间。
|
||
// 在线订单关闭时按冻结商户原路退款。
|
||
func (s *ExtensionService) HandleException(ctx context.Context, extensionID uint, req dto.SilenceExceptionHandleRequest) (*dto.SilenceExtensionResponse, error) {
|
||
if err := s.requirePermission(ctx, constants.PermissionPackageSilenceException); err != nil {
|
||
return nil, err
|
||
}
|
||
reason := strings.TrimSpace(req.Reason)
|
||
if reason == "" {
|
||
return nil, errors.New(errors.CodeInvalidParam, "处理原因不能为空")
|
||
}
|
||
extension, err := postgres.NewPackageSilenceExtensionStore(s.db).GetByID(ctx, extensionID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeNotFound, "延期明细不存在")
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询延期明细失败")
|
||
}
|
||
if _, err := s.loadUsage(ctx, s.db, extension.PackageUsageID, Scope{}); err != nil {
|
||
return nil, err
|
||
}
|
||
if extension.Status != constants.SilenceExtensionStatusException {
|
||
return nil, errors.New(errors.CodeInvalidStatus, "只能处理执行异常的明细")
|
||
}
|
||
operatorID := middleware.GetUserIDFromContext(ctx)
|
||
now := s.now()
|
||
executed, refundOrderID := false, uint(0)
|
||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
locked, err := postgres.NewPackageSilenceExtensionStore(tx).LockByID(ctx, extensionID)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "锁定延期明细失败")
|
||
}
|
||
if locked.Status != constants.SilenceExtensionStatusException {
|
||
return errors.New(errors.CodeInvalidStatus, "只能处理执行异常的明细")
|
||
}
|
||
var order *model.PackageSilenceExtensionOrder
|
||
if locked.ExtensionOrderID != nil {
|
||
if order, err = postgres.NewPackageSilenceOrderStore(tx).LockByID(ctx, *locked.ExtensionOrderID); err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "锁定延期订单失败")
|
||
}
|
||
}
|
||
if err := tx.Model(&model.PackageSilenceExtension{}).Where("id = ?", locked.ID).
|
||
Updates(map[string]any{"handled_by": operatorID, "handled_at": now}).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "记录异常处理人失败")
|
||
}
|
||
if req.Action == "close" {
|
||
if _, err := postgres.NewPackageSilenceExtensionStore(tx).TransitionStatus(ctx, locked.ID, []string{constants.SilenceExtensionStatusException},
|
||
constants.SilenceExtensionStatusClosed, map[string]any{"failure_summary": truncate("人工关闭:"+reason, 500)}); err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "关闭异常明细失败")
|
||
}
|
||
if order != nil && order.PaymentMethod != model.PaymentByOffline && order.Status == constants.SilenceOrderStatusException {
|
||
refundOrderID = order.ID
|
||
}
|
||
if order != nil && order.PaymentMethod == model.PaymentByOffline && order.Status == constants.SilenceOrderStatusException {
|
||
if _, err := postgres.NewPackageSilenceOrderStore(tx).TransitionStatus(ctx, order.ID, []string{constants.SilenceOrderStatusException},
|
||
constants.SilenceOrderStatusClosed, map[string]any{"closed_at": now, "close_reason": truncate("人工关闭:"+reason, 200)}); err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "关闭线下延期订单失败")
|
||
}
|
||
}
|
||
return refreshBatchCountsInTx(ctx, tx, locked.BatchID, now)
|
||
}
|
||
// 重试:按原明细与快照重新执行
|
||
batch, err := postgres.NewPackageSilenceBatchStore(tx).GetByID(ctx, locked.BatchID)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询延期批次失败")
|
||
}
|
||
if order != nil {
|
||
if order.Status != constants.SilenceOrderStatusException {
|
||
return errors.New(errors.CodeInvalidStatus, "延期订单不是异常状态,不能重试")
|
||
}
|
||
if order.RefundNo != nil {
|
||
return errors.New(errors.CodeInvalidStatus, "延期订单已发起原路退款,不能重试")
|
||
}
|
||
ok, err := s.executeOrderInTx(ctx, tx, order)
|
||
executed = ok
|
||
return err
|
||
}
|
||
from := []string{constants.SilenceExtensionStatusException}
|
||
outcome, err := s.executeExtensionInTx(ctx, tx, locked, executeKind{free: batch.ExtensionType == constants.SilenceExtensionTypeFree},
|
||
paidSnapshot{freeLimit: batch.FreeExtensionLimitSnapshot}, from)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !outcome.executed {
|
||
return errors.NewWithData(errors.CodeInvalidStatus, map[string]any{"failure_code": outcome.failureCode}, "重试仍不满足延期条件:"+outcome.failureText)
|
||
}
|
||
executed = true
|
||
return refreshBatchCountsInTx(ctx, tx, locked.BatchID, now)
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if executed {
|
||
s.afterExecuted(ctx, []uint{extension.PackageUsageID})
|
||
}
|
||
if refundOrderID > 0 {
|
||
if err := s.refundOrder(ctx, refundOrderID, "异常明细人工关闭,原路退款:"+reason); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
current, err := postgres.NewPackageSilenceExtensionStore(s.db).GetByID(ctx, extensionID)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询延期明细失败")
|
||
}
|
||
resp := extensionDTO(current)
|
||
return &resp, nil
|
||
}
|
||
|
||
// BatchDetail 查询延期批次与明细。
|
||
func (s *ExtensionService) BatchDetail(ctx context.Context, batchID uint) (*dto.SilenceBatchResponse, error) {
|
||
batch, err := postgres.NewPackageSilenceBatchStore(s.db).GetByID(ctx, batchID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeNotFound, "延期批次不存在")
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询延期批次失败")
|
||
}
|
||
extensions, err := postgres.NewPackageSilenceExtensionStore(s.db).ListByBatch(ctx, batchID)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询延期明细失败")
|
||
}
|
||
if len(extensions) > 0 {
|
||
if _, err := s.loadUsage(ctx, s.db, extensions[0].PackageUsageID, Scope{}); err != nil {
|
||
return nil, err
|
||
}
|
||
} else if batch.AdjustPackageUsageID != nil {
|
||
if _, err := s.loadUsage(ctx, s.db, *batch.AdjustPackageUsageID, Scope{}); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
resp := BatchDTO(batch)
|
||
for index := range extensions {
|
||
resp.Items = append(resp.Items, extensionDTO(&extensions[index]))
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// Records 按套餐记录查询沉默期流水(后台与 H5)。
|
||
func (s *ExtensionService) Records(ctx context.Context, usageID uint, scope Scope) ([]dto.SilenceRecordResponse, error) {
|
||
if _, err := s.loadUsage(ctx, s.db, usageID, scope); err != nil {
|
||
return nil, err
|
||
}
|
||
records, err := postgres.NewPackageSilenceRecordStore(s.db).ListByUsage(ctx, usageID)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询沉默期流水失败")
|
||
}
|
||
items := make([]dto.SilenceRecordResponse, 0, len(records))
|
||
for index := range records {
|
||
record := &records[index]
|
||
items = append(items, dto.SilenceRecordResponse{
|
||
ID: record.ID, PackageUsageID: record.PackageUsageID, EventType: record.EventType,
|
||
EventName: RecordEventName(record.EventType), TriggerSource: record.TriggerSource, AssetType: record.AssetType,
|
||
AssetID: record.AssetID, BeforeStatus: record.BeforeStatus, AfterStatus: record.AfterStatus,
|
||
BeforeDeadlineAt: record.BeforeDeadlineAt, AfterDeadlineAt: record.AfterDeadlineAt, PeriodDays: record.PeriodDays,
|
||
DefaultDays: record.DefaultDaysSnapshot, IsAdjusted: record.IsAdjusted, ChangeDays: record.ChangeDays,
|
||
OperatorID: record.OperatorID, OperatorType: record.OperatorType, Reason: record.Reason,
|
||
ApprovalInstanceID: record.ApprovalInstanceID, BatchID: record.BatchID, ExtensionID: record.ExtensionID,
|
||
ExchangeNo: record.ExchangeNo, CreatedAt: record.CreatedAt,
|
||
})
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
// RecordEventName 返回流水事件名称。
|
||
func RecordEventName(event string) string {
|
||
switch event {
|
||
case constants.SilenceRecordEventSet:
|
||
return "设置沉默期"
|
||
case constants.SilenceRecordEventAdjust:
|
||
return "购买时调整"
|
||
case constants.SilenceRecordEventExpired:
|
||
return "沉默期过期"
|
||
case constants.SilenceRecordEventEnded:
|
||
return "期内实名结束"
|
||
case constants.SilenceRecordEventExtend:
|
||
return "延期"
|
||
case constants.SilenceRecordEventExchange:
|
||
return "换货迁移"
|
||
case constants.SilenceRecordEventBackfill:
|
||
return "历史回填"
|
||
case constants.SilenceRecordEventRealnameBackfill:
|
||
return "补写实名时间"
|
||
default:
|
||
return event
|
||
}
|
||
}
|
||
|
||
// BatchDTO 构造批次响应(不含明细)。
|
||
func BatchDTO(batch *model.PackageSilenceExtensionBatch) *dto.SilenceBatchResponse {
|
||
return &dto.SilenceBatchResponse{
|
||
ID: batch.ID, BatchNo: batch.BatchNo, Path: batch.Path, ExtensionType: batch.ExtensionType,
|
||
ApprovalRequired: batch.ApprovalRequired, ApprovalInstanceID: batch.ApprovalInstanceID,
|
||
ApprovalStatus: batch.ApprovalStatus, Status: batch.Status, Reason: batch.Reason, Basis: batch.Basis,
|
||
EvidenceRefs: nonNil([]string(batch.EvidenceRefs)), Amount: batch.Amount, RequestedBy: batch.RequestedBy,
|
||
RequestedAt: batch.RequestedAt, RequestedCount: batch.RequestedCount, SucceededCount: batch.SucceededCount,
|
||
FailedCount: batch.FailedCount, CompletedAt: batch.CompletedAt,
|
||
}
|
||
}
|
||
|
||
func extensionDTO(extension *model.PackageSilenceExtension) dto.SilenceExtensionResponse {
|
||
return dto.SilenceExtensionResponse{
|
||
ID: extension.ID, BatchID: extension.BatchID, RowNo: extension.RowNo, PackageUsageID: extension.PackageUsageID,
|
||
PackageName: extension.PackageNameSnapshot, AssetType: extension.AssetType, AssetIdentifier: extension.AssetIdentifierSnapshot,
|
||
ExtensionType: extension.ExtensionType, InputDays: extension.InputDays, Months: extension.Months,
|
||
ExtensionDays: extension.ExtensionDays, Amount: extension.Amount, ExtensionOrderID: extension.ExtensionOrderID,
|
||
BeforeDeadlineAt: extension.BeforeDeadlineAt, AfterDeadlineAt: extension.AfterDeadlineAt, Status: extension.Status,
|
||
FailureCode: extension.FailureCode, FailureSummary: extension.FailureSummary, ExecutedAt: extension.ExecutedAt,
|
||
}
|
||
}
|
||
|
||
func extensionAuditInput(usage *model.PackageUsage, batch *model.PackageSilenceExtensionBatch, action, summary string) audit.SilenceAuditInput {
|
||
return audit.SilenceAuditInput{
|
||
ActionCode: action, Summary: summary, Usage: usage,
|
||
AfterData: map[string]any{"batch_status": batch.Status},
|
||
Metadata: map[string]any{
|
||
"batch_no": batch.BatchNo, "batch_id": batch.ID, "path": batch.Path, "extension_type": batch.ExtensionType,
|
||
"approval_required": batch.ApprovalRequired, "approval_instance_id": batch.ApprovalInstanceID,
|
||
"amount": batch.Amount, "reason": truncate(batch.Reason, 200), "evidence_refs": []string(batch.EvidenceRefs),
|
||
"requested_by": batch.RequestedBy,
|
||
},
|
||
}
|
||
}
|
||
|
||
func boolToInt(value bool) int {
|
||
if value {
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|