Files
junhong_cmp_fiber/internal/application/packagesilence/approval.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

156 lines
7.0 KiB
Go

package packagesilence
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"time"
"github.com/bytedance/sonic"
"gorm.io/gorm"
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// 沉默期审批模板"申请类型"控件取值(技术方案 §11.3)。
const (
ApplyTypePurchaseAdjust = "购买时调整沉默期"
ApplyTypeSpecial = "特殊延期"
ApplyTypeOfflinePaid = "线下付费延期"
)
// 沉默期审批业务字段(企微场景配置把模板控件映射到这些字段)。
const (
ApprovalFieldApplyType = constants.ApprovalFieldSilenceApplyType
ApprovalFieldBatchNo = constants.ApprovalFieldSilenceBatchNo
ApprovalFieldAssetIdentifier = constants.ApprovalFieldSilenceAssetIdentifier
ApprovalFieldPackageName = constants.ApprovalFieldSilencePackageName
ApprovalFieldOrderNo = constants.ApprovalFieldSilenceOrderNo
ApprovalFieldDefaultDays = constants.ApprovalFieldSilenceDefaultDays
ApprovalFieldRequestedDays = constants.ApprovalFieldSilenceRequestedDays
ApprovalFieldOriginalDeadline = constants.ApprovalFieldSilenceOriginalDeadline
ApprovalFieldInputDays = constants.ApprovalFieldSilenceInputDays
ApprovalFieldBilledDays = constants.ApprovalFieldSilenceBilledDays
ApprovalFieldNewDeadline = constants.ApprovalFieldSilenceNewDeadline
ApprovalFieldExtensionType = constants.ApprovalFieldSilenceExtensionType
ApprovalFieldItemCount = constants.ApprovalFieldSilenceItemCount
ApprovalFieldAmount = constants.ApprovalFieldSilenceAmount
ApprovalFieldAmountCent = constants.ApprovalFieldSilenceAmountCent
ApprovalFieldReason = constants.ApprovalFieldSilenceReason
ApprovalFieldBasis = constants.ApprovalFieldSilenceBasis
ApprovalFieldEvidenceKeys = constants.ApprovalFieldSilenceEvidenceKeys
ApprovalFieldPaymentMethod = constants.ApprovalFieldSilencePaymentMethod
ApprovalFieldTradeNo = constants.ApprovalFieldSilenceTradeNo
ApprovalFieldVoucherKeys = constants.ApprovalFieldSilenceVoucherKeys
ApprovalFieldSubmitterID = constants.ApprovalFieldSubmitterID
ApprovalFieldSubmitterName = constants.ApprovalFieldSubmitterName
)
// ApprovalFields 返回沉默期审批场景允许映射的业务字段(企微场景配置校验用)。
func ApprovalFields() []string {
return []string{
ApprovalFieldApplyType, ApprovalFieldBatchNo, ApprovalFieldAssetIdentifier, ApprovalFieldPackageName,
ApprovalFieldOrderNo, ApprovalFieldDefaultDays, ApprovalFieldRequestedDays, ApprovalFieldOriginalDeadline,
ApprovalFieldInputDays, ApprovalFieldBilledDays, ApprovalFieldNewDeadline, ApprovalFieldExtensionType,
ApprovalFieldItemCount, ApprovalFieldAmount, ApprovalFieldAmountCent, ApprovalFieldReason, ApprovalFieldBasis,
ApprovalFieldEvidenceKeys, ApprovalFieldPaymentMethod, ApprovalFieldTradeNo, ApprovalFieldVoucherKeys,
ApprovalFieldSubmitterID, ApprovalFieldSubmitterName,
}
}
// approvalSubmitter 是审批发起人快照。
type approvalSubmitter struct {
ID uint
Name string
UserType int
}
func loadSubmitter(ctx context.Context, db *gorm.DB, accountID uint) (approvalSubmitter, error) {
var account model.Account
if err := db.WithContext(ctx).Select("id", "username", "user_type").Where("id = ?", accountID).First(&account).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return approvalSubmitter{}, errors.New(errors.CodeForbidden, "审批发起人账号不存在")
}
return approvalSubmitter{}, errors.Wrap(errors.CodeDatabaseError, err, "查询审批发起人失败")
}
return approvalSubmitter{ID: account.ID, Name: account.Username, UserType: account.UserType}, nil
}
// prepareApproval 在业务事务前确认企微审批场景与发起人企微成员绑定可用;不可用时拒绝提交。
func prepareApproval(ctx context.Context, port approvalapp.Port, submitterID uint, correlationID string) (approvalapp.Preparation, error) {
if port == nil {
return approvalapp.Preparation{}, errors.New(errors.CodeServiceUnavailable, "沉默期审批能力未配置")
}
return port.Prepare(ctx, approvalapp.PrepareRequest{
BusinessType: constants.ApprovalBusinessTypePackageSilence, SubmitterAccountID: submitterID,
CorrelationID: correlationID,
})
}
// createApprovalInTx 在业务事务内创建审批实例并关联到批次。
func createApprovalInTx(ctx context.Context, tx *gorm.DB, port approvalapp.Port, preparation approvalapp.Preparation,
batch *model.PackageSilenceExtensionBatch, submitter approvalSubmitter, applyType string, fields map[string]any) (uint, error) {
submitterSnapshot, err := sonic.Marshal(map[string]any{
"account_id": submitter.ID, "account_name": submitter.Name, "user_type": submitter.UserType,
})
if err != nil {
return 0, errors.Wrap(errors.CodeInternalError, err, "编码沉默期审批发起人快照失败")
}
request := map[string]any{
ApprovalFieldApplyType: applyType, ApprovalFieldBatchNo: batch.BatchNo,
ApprovalFieldSubmitterID: submitter.ID, ApprovalFieldSubmitterName: submitter.Name,
}
for key, value := range fields {
request[key] = value
}
requestSnapshot, err := sonic.Marshal(request)
if err != nil {
return 0, errors.Wrap(errors.CodeInternalError, err, "编码沉默期审批业务快照失败")
}
reference, err := port.CreateInTx(ctx, tx, approvalapp.CreateRequest{
Preparation: preparation, BusinessType: constants.ApprovalBusinessTypePackageSilence,
BusinessID: batch.ID, SubmitterAccountID: submitter.ID,
SubmitterSnapshot: submitterSnapshot, RequestSnapshot: requestSnapshot, CorrelationID: batch.BatchNo,
})
if err != nil {
return 0, err
}
result := tx.WithContext(ctx).Model(&model.PackageSilenceExtensionBatch{}).
Where("id = ? AND approval_instance_id IS NULL", batch.ID).
Updates(map[string]any{"approval_instance_id": reference.InstanceID, "approval_status": constants.SilenceApprovalStatusPending})
if result.Error != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, result.Error, "关联沉默期审批实例失败")
}
if result.RowsAffected != 1 {
return 0, errors.New(errors.CodeConflict, "沉默期审批实例关联已变化")
}
batch.ApprovalInstanceID = &reference.InstanceID
batch.ApprovalStatus = constants.SilenceApprovalStatusPending
return reference.InstanceID, nil
}
// generateNo 生成带前缀的单号:前缀 + 年月日时分秒 + 6 位随机数。
func generateNo(prefix string, now time.Time) string {
value, err := rand.Int(rand.Reader, big.NewInt(1000000))
random := int64(0)
if err == nil {
random = value.Int64()
}
return fmt.Sprintf("%s%s%06d", prefix, now.In(time.FixedZone("CST", 8*3600)).Format("20060102150405"), random)
}
func formatAmount(cents int64) string {
return fmt.Sprintf("%d.%02d", cents/100, cents%100)
}
func formatDate(t *time.Time) string {
if t == nil {
return ""
}
return t.In(time.FixedZone("CST", 8*3600)).Format("2006-01-02")
}