Files
junhong_cmp_fiber/internal/application/packagesilence/record.go
邹进 909f4d0ec2
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 8m39s
feat: 实名即生效套餐沉默期、延期办理与资产出库时间
- 迁移 000259~000265:套餐记录沉默期字段与扫描索引、套餐默认沉默期、延期政策/流水/订单/批次/明细 5 张表、默认政策与系统配置(总开关默认关闭)、企微审批白名单、卡与设备出库时间及出库记录
- 沉默期设置(订单发放、自动购包、钱包续费)、后台购买时调整(超默认值按政策免审或企微审批)、统一判定与激活闸门、排队与孤儿扫描跳过过期套餐、过期套餐禁止退款、每 10 分钟扫描
- 总开关专用接口与打开前 4 项检查;延期政策;延期资格与计费;H5 自助与后台二维码在线支付(商户池路由)、支付回调与查单恢复、线下登记、特殊/批量延期、异常重试与原路退款;沉默期审批场景与终态处理
- 换货沉默期流水与出库时间;卡/设备列表、详情、导出与出库记录接口;延期订单与即将过期套餐列表及导出;财务流水纳入延期支付
- 沉默期与出库时间回填工具 cmd/silence-backfill;前端接口变更说明

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:32:58 +09:00

155 lines
5.0 KiB
Go

package packagesilence
import (
"context"
"time"
"github.com/bytedance/sonic"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/model"
"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"
)
// recordInput 是一条沉默期流水与对应审计。
type recordInput struct {
Usage *model.PackageUsage // 变更后的套餐记录
EventType string
Trigger string
BeforeStatus *string
BeforeDeadline *time.Time
ChangeDays int
DefaultDays *int
IsAdjusted bool
Reason string
ApprovalID *uint
BatchID *uint
ExtensionID *uint
ExchangeID *uint
ExchangeNo string
RealnameAt *time.Time
Extra map[string]any
AuditAction string
AuditSummary string
}
// writeRecord 在调用方事务内写不可变流水与审计。审计写入失败不回滚业务(统一审计 Writer 自行留痕)。
func writeRecord(ctx context.Context, tx *gorm.DB, writer *audit.Writer, input recordInput) error {
usage := input.Usage
if usage == nil || usage.ID == 0 {
return errors.New(errors.CodeInvalidParam, "沉默期流水缺少套餐记录")
}
assetType, assetID := usageAsset(usage)
operatorID, operatorType := operatorFromContext(ctx)
extra := []byte("{}")
if len(input.Extra) > 0 {
encoded, err := sonic.Marshal(input.Extra)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err, "序列化沉默期流水扩展信息失败")
}
extra = encoded
}
record := &model.PackageSilenceRecord{
PackageUsageID: usage.ID, EventType: input.EventType, TriggerSource: input.Trigger,
AssetType: assetType, AssetID: assetID,
BeforeStatus: input.BeforeStatus, AfterStatus: usage.SilenceStatus,
BeforeDeadlineAt: input.BeforeDeadline, AfterDeadlineAt: usage.SilenceDeadlineAt,
PeriodDays: usage.SilencePeriodDays, DefaultDaysSnapshot: input.DefaultDays,
IsAdjusted: input.IsAdjusted, ChangeDays: input.ChangeDays,
OperatorID: operatorID, OperatorType: operatorType, Reason: truncate(input.Reason, 500),
ApprovalInstanceID: input.ApprovalID, BatchID: input.BatchID, ExtensionID: input.ExtensionID,
ExchangeOrderID: input.ExchangeID, ExchangeNo: input.ExchangeNo, RealnameAt: input.RealnameAt,
Extra: extra, CreatedAt: time.Now(),
}
if err := postgres.NewPackageSilenceRecordStore(tx).Create(ctx, record); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "写入沉默期流水失败")
}
if input.AuditAction == "" || writer == nil {
return nil
}
metadata := map[string]any{
"event_type": input.EventType, "trigger": input.Trigger, "record_id": record.ID,
"change_days": input.ChangeDays, "reason": truncate(input.Reason, 200),
}
for key, value := range input.Extra {
metadata[key] = value
}
return writer.AppendSilenceUsage(ctx, tx, audit.SilenceAuditInput{
ActionCode: input.AuditAction, Summary: input.AuditSummary, Usage: usage,
BeforeData: map[string]any{"silence_status": input.BeforeStatus, "silence_deadline_at": input.BeforeDeadline},
AfterData: silenceStateData(usage), Metadata: metadata,
})
}
func silenceStateData(usage *model.PackageUsage) map[string]any {
return map[string]any{
"silence_status": usage.SilenceStatus, "silence_deadline_at": usage.SilenceDeadlineAt,
"silence_period_days": usage.SilencePeriodDays, "silence_extended_days": usage.SilenceExtendedDays,
"silence_paid_extended_days": usage.SilencePaidExtendedDays, "silence_extension_count": usage.SilenceExtensionCount,
"silence_free_extension_used": usage.SilenceFreeExtensionUsed, "silence_expired_at": usage.SilenceExpiredAt,
"silence_adjust_pending": usage.SilenceAdjustPending,
}
}
// usageAsset 返回套餐记录当前载体。
func usageAsset(usage *model.PackageUsage) (string, uint) {
if usage.DeviceID > 0 {
return constants.SilenceAssetTypeDevice, usage.DeviceID
}
return constants.SilenceAssetTypeIotCard, usage.IotCardID
}
func operatorFromContext(ctx context.Context) (uint, string) {
value := auditcontext.From(ctx)
if value.ActorID == "" {
return 0, constants.SilenceOperatorSystem
}
var id uint
for _, r := range value.ActorID {
if r < '0' || r > '9' {
return 0, value.ActorKind
}
id = id*10 + uint(r-'0')
}
switch value.ActorKind {
case constants.AuditActorAccount:
return id, constants.SilenceOperatorAccount
case constants.AuditActorPersonalCustomer:
return id, constants.SilenceOperatorCustomer
default:
return id, value.ActorKind
}
}
func truncate(value string, max int) string {
runes := []rune(value)
if len(runes) <= max {
return value
}
return string(runes[:max])
}
func stringPtr(value string) *string {
return &value
}
func copyStringPtr(value *string) *string {
if value == nil {
return nil
}
copied := *value
return &copied
}
func copyTimePtr(value *time.Time) *time.Time {
if value == nil {
return nil
}
copied := *value
return &copied
}