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>
156 lines
7.0 KiB
Go
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")
|
|
}
|