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>
155 lines
5.0 KiB
Go
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
|
|
}
|