Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕 - 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败 - 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影 - 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序 - 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列 - 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名 - 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
556 lines
36 KiB
Go
556 lines
36 KiB
Go
package wecom
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
stdErrors "errors"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/bytedance/sonic"
|
||
"gorm.io/gorm"
|
||
|
||
systemconfigapp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig"
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||
)
|
||
|
||
// TemplateControl 是模板详情中可供业务映射的最小控件结构。
|
||
type TemplateControl struct {
|
||
ID string `json:"id"`
|
||
Type string `json:"type"`
|
||
Title string `json:"title"`
|
||
Required bool `json:"required"`
|
||
OptionKeys []string `json:"option_keys"`
|
||
}
|
||
|
||
// TemplateDefinition 是企微 Adapter 返回的模板最小结构。
|
||
type TemplateDefinition struct {
|
||
TemplateID string `json:"template_id"`
|
||
Name string `json:"name"`
|
||
Controls []TemplateControl `json:"controls"`
|
||
}
|
||
|
||
// TemplateProvider 定义审批模板详情外部端口。
|
||
type TemplateProvider interface {
|
||
GetTemplateDetail(ctx context.Context, applicationID uint, templateID string) (TemplateDefinition, error)
|
||
}
|
||
|
||
// SceneRepository 定义审批场景当前配置持久化边界。
|
||
type SceneRepository interface {
|
||
FindForUpdate(ctx context.Context, tx *gorm.DB, businessType string) (*model.WeComApprovalScene, error)
|
||
Create(ctx context.Context, tx *gorm.DB, scene *model.WeComApprovalScene) error
|
||
Update(ctx context.Context, tx *gorm.DB, scene *model.WeComApprovalScene) error
|
||
List(ctx context.Context, page, pageSize int) ([]model.WeComApprovalScene, int64, error)
|
||
}
|
||
|
||
// SceneService 保存经企微模板详情校验的业务场景当前映射。
|
||
type SceneService struct {
|
||
db *gorm.DB
|
||
provider TemplateProvider
|
||
repo SceneRepository
|
||
audit systemconfigapp.AuditWriter
|
||
now func() time.Time
|
||
}
|
||
|
||
// NewSceneService 创建企业微信审批场景配置用例。
|
||
func NewSceneService(db *gorm.DB, provider TemplateProvider, repo SceneRepository, audit systemconfigapp.AuditWriter) *SceneService {
|
||
return &SceneService{db: db, provider: provider, repo: repo, audit: audit, now: time.Now}
|
||
}
|
||
|
||
// InspectTemplate 实时读取企微模板详情,供管理员配置控件映射。
|
||
func (s *SceneService) InspectTemplate(ctx context.Context, applicationID uint, request dto.InspectWeComTemplateRequest) (*dto.WeComTemplateDetailResponse, error) {
|
||
if s == nil || s.provider == nil {
|
||
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信模板服务未配置")
|
||
}
|
||
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
|
||
return nil, errors.New(errors.CodeForbidden)
|
||
}
|
||
templateID := strings.TrimSpace(request.TemplateID)
|
||
if applicationID == 0 || templateID == "" {
|
||
return nil, errors.New(errors.CodeInvalidParam)
|
||
}
|
||
definition, err := s.provider.GetTemplateDetail(ctx, applicationID, templateID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
controls := make([]dto.WeComTemplateControlResponse, 0, len(definition.Controls))
|
||
for _, control := range definition.Controls {
|
||
controls = append(controls, dto.WeComTemplateControlResponse{
|
||
ID: control.ID, Type: control.Type, Title: control.Title,
|
||
Required: control.Required, OptionKeys: control.OptionKeys,
|
||
})
|
||
}
|
||
return &dto.WeComTemplateDetailResponse{
|
||
TemplateID: definition.TemplateID, Name: definition.Name, Controls: controls,
|
||
}, nil
|
||
}
|
||
|
||
// ListBusinessFields 返回指定审批场景允许映射的业务快照字段。
|
||
func (s *SceneService) ListBusinessFields(ctx context.Context, businessType string) (*dto.WeComBusinessFieldListResponse, error) {
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
|
||
return nil, errors.New(errors.CodeForbidden)
|
||
}
|
||
businessType = strings.TrimSpace(businessType)
|
||
items, ok := sceneBusinessFields(businessType)
|
||
if !ok {
|
||
return nil, errors.New(errors.CodeInvalidParam, "不支持的审批业务类型")
|
||
}
|
||
return &dto.WeComBusinessFieldListResponse{
|
||
BusinessType: businessType, BusinessTypeName: approvalBusinessTypeName(businessType), Items: items,
|
||
}, nil
|
||
}
|
||
|
||
// Save 校验模板控件后创建或替换指定稳定业务场景映射。
|
||
func (s *SceneService) Save(ctx context.Context, businessType string, request dto.SaveWeComApprovalSceneRequest) (*dto.WeComApprovalSceneResponse, error) {
|
||
if s == nil || s.db == nil || s.provider == nil || s.repo == nil || s.audit == nil {
|
||
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信审批场景服务未配置")
|
||
}
|
||
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin {
|
||
return nil, errors.New(errors.CodeForbidden)
|
||
}
|
||
businessType = strings.TrimSpace(businessType)
|
||
if !validApprovalBusinessType(businessType) {
|
||
return nil, errors.New(errors.CodeInvalidParam, "不支持的审批业务类型")
|
||
}
|
||
if request.ApplicationID == 0 || strings.TrimSpace(request.TemplateID) == "" || len(request.ControlMapping) == 0 ||
|
||
(request.Status != constants.StatusDisabled && request.Status != constants.StatusEnabled) {
|
||
return nil, errors.New(errors.CodeInvalidParam)
|
||
}
|
||
definition, err := s.provider.GetTemplateDetail(ctx, request.ApplicationID, strings.TrimSpace(request.TemplateID))
|
||
if err != nil {
|
||
s.recordFailure(ctx, businessType, request, constants.AuditResultFailed, errors.CodeInternalError, "校验企业微信审批模板失败")
|
||
return nil, err
|
||
}
|
||
if err := validateSceneMapping(businessType, request.ControlMapping, definition.Controls); err != nil {
|
||
s.recordFailure(ctx, businessType, request, constants.AuditResultDenied, errors.CodeInvalidParam, "拒绝保存非法企业微信审批场景映射")
|
||
return nil, err
|
||
}
|
||
request.ControlMapping = normalizeSceneMapping(request.ControlMapping)
|
||
mappingJSON, err := sonic.Marshal(request.ControlMapping)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "编码企业微信控件映射失败")
|
||
}
|
||
snapshotJSON, err := sonic.Marshal(definition)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "编码企业微信模板快照失败")
|
||
}
|
||
fingerprintBytes := sha256.Sum256(snapshotJSON)
|
||
fingerprint := hex.EncodeToString(fingerprintBytes[:])
|
||
operatorID := middleware.GetUserIDFromContext(ctx)
|
||
if operatorID == 0 {
|
||
return nil, errors.New(errors.CodeUnauthorized)
|
||
}
|
||
now := s.now().UTC()
|
||
var saved *model.WeComApprovalScene
|
||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
if err := tx.Exec("SELECT pg_advisory_xact_lock(hashtext(?))", "wecom-scene:"+businessType).Error; err != nil {
|
||
return err
|
||
}
|
||
existing, err := s.repo.FindForUpdate(ctx, tx, businessType)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
before := map[string]any{"configured": false, "business_type": businessType}
|
||
if existing == nil {
|
||
existing = &model.WeComApprovalScene{BusinessType: businessType, CreatedBy: operatorID, CreatedAt: now}
|
||
} else {
|
||
before = sceneAuditSnapshot(existing)
|
||
}
|
||
existing.ApplicationID = request.ApplicationID
|
||
existing.TemplateID = definition.TemplateID
|
||
existing.TemplateName = definition.Name
|
||
existing.ControlMapping = mappingJSON
|
||
existing.TemplateSnapshot = snapshotJSON
|
||
existing.TemplateFingerprint = fingerprint
|
||
existing.Status = request.Status
|
||
existing.LastVerifiedAt = now
|
||
existing.UpdatedBy = operatorID
|
||
existing.UpdatedAt = now
|
||
if existing.ID == 0 {
|
||
if err := s.repo.Create(ctx, tx, existing); err != nil {
|
||
return err
|
||
}
|
||
} else if err := s.repo.Update(ctx, tx, existing); err != nil {
|
||
return err
|
||
}
|
||
requestID := ""
|
||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||
requestID = *value
|
||
}
|
||
resourceID := strings.TrimSpace(existing.BusinessType)
|
||
if existing.ID != 0 {
|
||
resourceID = fmt.Sprintf("%d", existing.ID)
|
||
}
|
||
if err := s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{
|
||
OperatorID: operatorID, OperationType: constants.AuditOperationWeComApprovalSceneSave, Description: "保存企业微信审批模板映射",
|
||
ConfigKey: "wecom.approval_scene." + businessType, BeforeData: before,
|
||
AfterData: sceneAuditSnapshot(existing), RequestID: requestID, CorrelationID: requestID,
|
||
ResourceID: &resourceID, DisplayName: existing.TemplateName, Identity: sceneAuditIdentity(existing),
|
||
}); err != nil {
|
||
return err
|
||
}
|
||
saved = existing
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
s.recordFailure(ctx, businessType, request, constants.AuditResultFailed, errors.CodeDatabaseError, "保存企业微信审批场景失败")
|
||
var appErr *errors.AppError
|
||
if stdErrors.As(err, &appErr) {
|
||
return nil, appErr
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "保存企业微信审批场景失败")
|
||
}
|
||
return sceneResponse(*saved)
|
||
}
|
||
|
||
// List 分页查询企业微信审批场景当前配置。
|
||
func (s *SceneService) List(ctx context.Context, request dto.WeComApprovalSceneListRequest) (*dto.WeComApprovalSceneListResponse, error) {
|
||
if s == nil || s.repo == nil {
|
||
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信审批场景服务未配置")
|
||
}
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
|
||
return nil, errors.New(errors.CodeForbidden)
|
||
}
|
||
if request.Page <= 0 {
|
||
request.Page = constants.DefaultPage
|
||
}
|
||
if request.PageSize <= 0 {
|
||
request.PageSize = constants.DefaultPageSize
|
||
}
|
||
if request.PageSize > constants.MaxPageSize {
|
||
return nil, errors.New(errors.CodeInvalidParam)
|
||
}
|
||
scenes, total, err := s.repo.List(ctx, request.Page, request.PageSize)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
items := make([]dto.WeComApprovalSceneResponse, 0, len(scenes))
|
||
for _, scene := range scenes {
|
||
item, err := sceneResponse(scene)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, *item)
|
||
}
|
||
return &dto.WeComApprovalSceneListResponse{Items: items, Total: total, Page: request.Page, PageSize: request.PageSize}, nil
|
||
}
|
||
|
||
func validateSceneMapping(businessType string, mapping []dto.WeComControlMappingItem, controls []TemplateControl) error {
|
||
controlByID := make(map[string]TemplateControl, len(controls))
|
||
for _, control := range controls {
|
||
controlByID[control.ID] = control
|
||
}
|
||
mappedControls := make(map[string]struct{}, len(mapping))
|
||
businessFields := make(map[string]struct{}, len(mapping))
|
||
for _, item := range mapping {
|
||
item.BusinessField = strings.TrimSpace(item.BusinessField)
|
||
item.ControlID = strings.TrimSpace(item.ControlID)
|
||
if item.BusinessField == "" || item.ControlID == "" {
|
||
return errors.New(errors.CodeInvalidParam, "企业微信控件映射字段不能为空")
|
||
}
|
||
if !allowedSceneBusinessField(businessType, item.BusinessField) {
|
||
return errors.New(errors.CodeInvalidParam, "企业微信控件映射包含当前业务不支持的字段")
|
||
}
|
||
if _, exists := businessFields[item.BusinessField]; exists {
|
||
return errors.New(errors.CodeInvalidParam, "企业微信业务字段映射重复")
|
||
}
|
||
if _, exists := mappedControls[item.ControlID]; exists {
|
||
return errors.New(errors.CodeInvalidParam, "企业微信模板控件不能重复映射")
|
||
}
|
||
control, exists := controlByID[item.ControlID]
|
||
if !exists || !strings.EqualFold(control.Type, strings.TrimSpace(item.ControlType)) {
|
||
return errors.New(errors.CodeInvalidParam, "企业微信模板控件 ID 或类型已失效")
|
||
}
|
||
validOptions := make(map[string]struct{}, len(control.OptionKeys))
|
||
for _, key := range control.OptionKeys {
|
||
validOptions[key] = struct{}{}
|
||
}
|
||
for _, key := range item.OptionMapping {
|
||
if _, exists := validOptions[key]; !exists {
|
||
return errors.New(errors.CodeInvalidParam, "企业微信模板选择项 key 已失效")
|
||
}
|
||
}
|
||
businessFields[item.BusinessField] = struct{}{}
|
||
mappedControls[item.ControlID] = struct{}{}
|
||
}
|
||
for _, control := range controls {
|
||
if control.Required {
|
||
if _, exists := mappedControls[control.ID]; !exists {
|
||
return errors.New(errors.CodeInvalidParam, "企业微信模板存在未映射的必填控件: "+control.Title)
|
||
}
|
||
}
|
||
}
|
||
// 本变更新增字段必须配置映射:缺失即明确失败,避免审批材料被静默丢弃后审批人看不到关键依据。
|
||
// 既有未映射的可选控件不受影响,保持既有静默跳过行为。
|
||
wanted, ok := sceneBusinessFields(businessType)
|
||
if !ok {
|
||
return errors.New(errors.CodeInvalidParam, "企业微信审批业务类型无效")
|
||
}
|
||
for _, field := range wanted {
|
||
if !field.RequiredMapping {
|
||
continue
|
||
}
|
||
if _, exists := businessFields[field.Code]; !exists {
|
||
return errors.New(errors.CodeInvalidParam, "企业微信场景缺少必须映射的业务字段: "+field.Name)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// RequiredSceneField 描述本变更新增、必须配置控件映射的业务字段。
|
||
type RequiredSceneField struct {
|
||
// Code 是 control_mapping.business_field 应填写的稳定字段编码。
|
||
Code string
|
||
// Name 是字段中文名称,用于在缺失时指出具体缺哪个字段/控件。
|
||
Name string
|
||
}
|
||
|
||
// RequiredSceneFields 返回指定业务类型下必须配置控件映射的业务字段,顺序与场景白名单一致。
|
||
//
|
||
// 这是「必须映射字段」的唯一判定来源:场景映射校验(保存时)与表单构建(提交时)都从
|
||
// 这里取集合,MUST NOT 再维护第二份清单。既有未标注必须映射的可选字段不在返回结果中,
|
||
// 因此其缺失映射仍保持既有静默跳过行为。
|
||
func RequiredSceneFields(businessType string) []RequiredSceneField {
|
||
fields, ok := sceneBusinessFields(businessType)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
result := make([]RequiredSceneField, 0, len(fields))
|
||
for _, field := range fields {
|
||
if field.RequiredMapping {
|
||
result = append(result, RequiredSceneField{Code: field.Code, Name: field.Name})
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// approvalBusinessTypes 返回全部已登记审批业务场景,供只读字段汇总复用。
|
||
func approvalBusinessTypes() []string {
|
||
return []string{
|
||
constants.ApprovalBusinessTypeRefund,
|
||
constants.ApprovalBusinessTypeOfflineRecharge,
|
||
constants.ApprovalBusinessTypeEmployeeCollection,
|
||
constants.ApprovalBusinessTypeAgentDistribution,
|
||
constants.ApprovalBusinessTypeWithdrawalQualification,
|
||
constants.ApprovalBusinessTypeCommissionWithdrawal,
|
||
}
|
||
}
|
||
|
||
func allowedSceneBusinessField(businessType, businessField string) bool {
|
||
fields, ok := sceneBusinessFields(businessType)
|
||
if !ok {
|
||
return false
|
||
}
|
||
for _, field := range fields {
|
||
if field.Code == businessField {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func sceneBusinessFields(businessType string) ([]dto.WeComBusinessFieldResponse, bool) {
|
||
switch businessType {
|
||
case constants.ApprovalBusinessTypeOfflineRecharge:
|
||
return []dto.WeComBusinessFieldResponse{
|
||
{Code: constants.ApprovalFieldRechargeNo, Name: "充值单号", ValueType: constants.ApprovalFieldValueTypeString, Description: "员工线下代充值单号"},
|
||
{Code: constants.ApprovalFieldShopID, Name: "目标店铺 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本次充值目标店铺的系统 ID"},
|
||
{Code: constants.ApprovalFieldShopName, Name: "目标店铺名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次充值目标店铺名称快照"},
|
||
{Code: constants.ApprovalFieldAmount, Name: "充值金额", ValueType: constants.ApprovalFieldValueTypeMoney, Description: "以元为单位且保留两位小数的充值金额"},
|
||
{Code: constants.ApprovalFieldAmountCent, Name: "充值金额(分)", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "以分为单位的充值金额整数"},
|
||
{Code: constants.ApprovalFieldPaymentVoucherKey, Name: "付款凭证", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "提交时上传到企微文件控件的付款凭证列表"},
|
||
{Code: constants.ApprovalFieldRemark, Name: "备注", ValueType: constants.ApprovalFieldValueTypeString, Description: "员工提交线下代充值时填写的备注"},
|
||
{Code: constants.ApprovalFieldSubmitterID, Name: "提交人账号 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本系统真实业务提交人账号 ID"},
|
||
{Code: constants.ApprovalFieldSubmitterName, Name: "提交人名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "本系统真实业务提交人名称快照"},
|
||
{Code: constants.ApprovalFieldOfflinePaymentMethod, Name: "线下收款方式", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次充值使用的线下收款方式名称快照"},
|
||
{Code: constants.ApprovalFieldOfflinePaymentMethodCode, Name: "线下收款方式编码", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次充值使用的线下收款方式稳定编码快照"},
|
||
{Code: constants.ApprovalFieldExternalTransactionNo, Name: "交易流水号", ValueType: constants.ApprovalFieldValueTypeString, Description: "人工确认的第三方交易流水号,用于审批人核验;与在线渠道交易号无关"},
|
||
{Code: constants.ApprovalFieldOtherVoucherKey, Name: "其他凭证", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "提交时上传到企微文件控件的其他凭证列表"},
|
||
}, true
|
||
case constants.ApprovalBusinessTypeRefund:
|
||
return []dto.WeComBusinessFieldResponse{
|
||
{Code: constants.ApprovalFieldRefundNo, Name: "退款单号", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次退款申请单号"},
|
||
{Code: constants.ApprovalFieldOrderID, Name: "订单 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "退款关联订单的系统 ID"},
|
||
{Code: constants.ApprovalFieldOrderNo, Name: "订单号", ValueType: constants.ApprovalFieldValueTypeString, Description: "退款关联订单号"},
|
||
{Code: constants.ApprovalFieldAssetIdentifier, Name: "资产标识", ValueType: constants.ApprovalFieldValueTypeString, Description: "退款关联卡或设备的业务标识"},
|
||
{Code: constants.ApprovalFieldAssetType, Name: "资产类型", ValueType: constants.ApprovalFieldValueTypeString, Description: "退款关联资产类型编码"},
|
||
{Code: constants.ApprovalFieldActualReceivedAmount, Name: "订单实收金额", ValueType: constants.ApprovalFieldValueTypeMoney, Description: "以元为单位且保留两位小数的订单实收金额"},
|
||
{Code: constants.ApprovalFieldRequestedRefundAmount, Name: "申请退款金额", ValueType: constants.ApprovalFieldValueTypeMoney, Description: "以元为单位且保留两位小数的本次申请退款金额"},
|
||
{Code: constants.ApprovalFieldRefundVoucherKey, Name: "退款凭证", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "提交时上传到企微文件控件的退款凭证列表"},
|
||
{Code: constants.ApprovalFieldRefundReason, Name: "退款原因", ValueType: constants.ApprovalFieldValueTypeString, Description: "业务提交人填写的退款原因"},
|
||
{Code: constants.ApprovalFieldPackageUsageID, Name: "套餐使用记录 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "退款关联套餐使用记录 ID;无关联记录时可能为空"},
|
||
{Code: constants.ApprovalFieldRefundAssetType, Name: "资产类型", ValueType: constants.ApprovalFieldValueTypeString, Description: "退款关联资产类型:card 单卡、device 设备;沿用退款申请的下单快照推导", RequiredMapping: true},
|
||
{Code: constants.ApprovalFieldRefundDeviceType, Name: "设备类型", ValueType: constants.ApprovalFieldValueTypeString, Description: "退款申请关联设备的类型;资产不是设备或无关联设备时为空字符串", RequiredMapping: true},
|
||
{Code: constants.ApprovalFieldRefundDeviceModel, Name: "设备型号", ValueType: constants.ApprovalFieldValueTypeString, Description: "退款申请关联设备的型号;资产不是设备或无关联设备时为空字符串", RequiredMapping: true},
|
||
{Code: constants.ApprovalFieldRefundPackageUsedMB, Name: "当前退款套餐已用量(MB)", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "当前退款套餐已用量(MB,真实流量);解析不到套餐时为 0", RequiredMapping: true},
|
||
{Code: constants.ApprovalFieldRefundPackageTotalMB, Name: "当前退款套餐总量(MB)", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "当前退款套餐总量(MB,真实流量);解析不到套餐时为 0", RequiredMapping: true},
|
||
{Code: constants.ApprovalFieldRefundChannelTradeNo, Name: "原支付渠道交易流水号", ValueType: constants.ApprovalFieldValueTypeString, Description: "创建申请时冻结的原成功支付记录渠道交易流水号;线下订单为空字符串", RequiredMapping: true},
|
||
{Code: constants.ApprovalFieldRefundApplicantRemark, Name: "申请人备注", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次提交填写的申请人备注快照;未填写时为空字符串。与审批备注不是同一字段", RequiredMapping: true},
|
||
{Code: constants.ApprovalFieldSubmitterID, Name: "提交人账号 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本系统真实业务提交人账号 ID"},
|
||
{Code: constants.ApprovalFieldSubmitterName, Name: "提交人名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "本系统真实业务提交人名称快照"},
|
||
}, true
|
||
case constants.ApprovalBusinessTypeEmployeeCollection:
|
||
return []dto.WeComBusinessFieldResponse{
|
||
{Code: constants.ApprovalFieldCollectionApplicationID, Name: "核销申请 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "员工代收款核销申请的系统 ID"},
|
||
{Code: constants.ApprovalFieldCollectionPaymentMethod, Name: "线下收款方式", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次外部付款使用的线下收款方式名称快照"},
|
||
{Code: constants.ApprovalFieldCollectionPaidAmount, Name: "付款金额", ValueType: constants.ApprovalFieldValueTypeMoney, Description: "以元为单位且保留两位小数的人工确认付款金额"},
|
||
{Code: constants.ApprovalFieldCollectionPaidAmountCent, Name: "付款金额(分)", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "以分为单位的人工确认付款金额整数"},
|
||
{Code: constants.ApprovalFieldCollectionPayerName, Name: "付款方", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次外部付款的付款方名称"},
|
||
{Code: constants.ApprovalFieldCollectionPaidAt, Name: "付款时间", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次外部付款时间,RFC3339 格式"},
|
||
{Code: constants.ApprovalFieldCollectionExternalTransactionNo, Name: "外部交易流水号", ValueType: constants.ApprovalFieldValueTypeString, Description: "人工确认的第三方交易流水号,用于审批人核验"},
|
||
{Code: constants.ApprovalFieldPaymentVoucherKey, Name: "付款凭证", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "提交时上传到企微文件控件的付款凭证列表"},
|
||
{Code: constants.ApprovalFieldRemark, Name: "备注", ValueType: constants.ApprovalFieldValueTypeString, Description: "申请人填写的核销备注"},
|
||
{Code: constants.ApprovalFieldCollectionBillCount, Name: "分摊账单数量", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本次核销分摊的账单数量"},
|
||
{Code: constants.ApprovalFieldCollectionBillSummary, Name: "账单分摊摘要", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次各账单应收金额与分摊金额摘要"},
|
||
{Code: constants.ApprovalFieldSubmitterID, Name: "提交人账号 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本系统真实业务提交人账号 ID"},
|
||
{Code: constants.ApprovalFieldSubmitterName, Name: "提交人名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "本系统真实业务提交人名称快照"},
|
||
}, true
|
||
case constants.ApprovalBusinessTypeAgentDistribution:
|
||
return []dto.WeComBusinessFieldResponse{
|
||
{Code: constants.ApprovalFieldDistributionCode, Name: "分销码", ValueType: constants.ApprovalFieldValueTypeString, Description: "注册使用的上级店铺分销码脱敏值,仅用于审批人核对来源,不代表新建店铺的码"},
|
||
{Code: constants.ApprovalFieldDistributionParentShopID, Name: "上级店铺 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "分销码所属上级店铺的系统 ID"},
|
||
{Code: constants.ApprovalFieldDistributionParentShopName, Name: "上级店铺名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "分销码所属上级店铺名称快照"},
|
||
{Code: constants.ApprovalFieldDistributionShopName, Name: "申请店铺名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "扫码注册申请的店铺名称快照"},
|
||
{Code: constants.ApprovalFieldDistributionShopCode, Name: "申请店铺编号", ValueType: constants.ApprovalFieldValueTypeString, Description: "扫码注册申请的店铺编号快照,通过时按既有唯一约束校验"},
|
||
{Code: constants.ApprovalFieldDistributionUsername, Name: "代理账号用户名", ValueType: constants.ApprovalFieldValueTypeString, Description: "扫码注册申请的代理主账号用户名快照"},
|
||
{Code: constants.ApprovalFieldDistributionPhoneMasked, Name: "注册手机号", ValueType: constants.ApprovalFieldValueTypeString, Description: "脱敏后的注册手机号,禁止写入完整手机号"},
|
||
{Code: constants.ApprovalFieldDistributionContactName, Name: "联系人姓名", ValueType: constants.ApprovalFieldValueTypeString, Description: "扫码注册填写的联系人姓名"},
|
||
{Code: constants.ApprovalFieldDistributionRegion, Name: "注册地址摘要", ValueType: constants.ApprovalFieldValueTypeString, Description: "省市区与详细地址拼接的注册地址摘要"},
|
||
{Code: constants.ApprovalFieldDistributionBusinessOwner, Name: "业务员", ValueType: constants.ApprovalFieldValueTypeString, Description: "上级代理店铺当前启用业务员的名称快照,用于确认将同步给新代理的初始业务员;上级无业务员时为空字符串", RequiredMapping: true},
|
||
{Code: constants.ApprovalFieldDistributionBusinessOwnerAbsence, Name: "无业务员标记", ValueType: constants.ApprovalFieldValueTypeString, Description: "上级店铺当前无业务员时提交「无」,有业务员时为空字符串,MUST NOT 用提交人或上级主账号填充", RequiredMapping: true},
|
||
{Code: constants.ApprovalFieldSubmitterID, Name: "提交人账号 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本系统真实业务提交人账号 ID"},
|
||
{Code: constants.ApprovalFieldSubmitterName, Name: "提交人名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "本系统真实业务提交人名称快照"},
|
||
}, true
|
||
case constants.ApprovalBusinessTypeWithdrawalQualification:
|
||
return []dto.WeComBusinessFieldResponse{
|
||
{Code: constants.ApprovalFieldQualificationShopID, Name: "店铺 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "提现资料资格所属代理店铺的系统 ID"},
|
||
{Code: constants.ApprovalFieldQualificationShopName, Name: "店铺名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "提现资料资格所属代理店铺名称快照"},
|
||
{Code: constants.ApprovalFieldQualificationSubjectType, Name: "签约主体类型", ValueType: constants.ApprovalFieldValueTypeString, Description: "签约主体类型中文名:企业或个人"},
|
||
{Code: constants.ApprovalFieldQualificationSubjectCodeMasked, Name: "签约主体代码", ValueType: constants.ApprovalFieldValueTypeString, Description: "脱敏后的统一社会信用代码或身份证号,禁止写入完整证件号"},
|
||
{Code: constants.ApprovalFieldQualificationLegalPersonMasked, Name: "法人身份证号", ValueType: constants.ApprovalFieldValueTypeString, Description: "脱敏后的法人身份证号,禁止写入完整证件号"},
|
||
{Code: constants.ApprovalFieldQualificationContractKey, Name: "合同附件", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "合同对象存储 Key 列表(单个对象)"},
|
||
{Code: constants.ApprovalFieldQualificationIDCardFrontKey, Name: "法人身份证正面", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "法人身份证正面对象存储 Key 列表(单个对象)"},
|
||
{Code: constants.ApprovalFieldQualificationIDCardBackKey, Name: "法人身份证反面", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "法人身份证反面对象存储 Key 列表(单个对象)"},
|
||
{Code: constants.ApprovalFieldQualificationBusinessLicenseKey, Name: "营业执照", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "营业执照对象存储 Key 列表(单个对象,可选)"},
|
||
{Code: constants.ApprovalFieldQualificationShopFrontKey, Name: "门头照", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "门头照对象存储 Key 列表(单个对象,可选)"},
|
||
{Code: constants.ApprovalFieldQualificationInvoiceKey, Name: "发票", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "发票对象存储 Key 列表(单个对象,仅企业可选)"},
|
||
{Code: constants.ApprovalFieldQualificationInvoiceTitle, Name: "发票抬头", ValueType: constants.ApprovalFieldValueTypeString, Description: "发票抬头,仅企业填写且必须与合同主体一致"},
|
||
{Code: constants.ApprovalFieldSubmitterID, Name: "提交人账号 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本系统真实业务提交人账号 ID"},
|
||
{Code: constants.ApprovalFieldSubmitterName, Name: "提交人名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "本系统真实业务提交人名称快照"},
|
||
}, true
|
||
case constants.ApprovalBusinessTypeCommissionWithdrawal:
|
||
return []dto.WeComBusinessFieldResponse{
|
||
{Code: constants.ApprovalFieldWithdrawalNo, Name: "提现单号", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次提现申请单号"},
|
||
{Code: constants.ApprovalFieldWithdrawalAttemptNo, Name: "提交次序", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本次为第几次提交,重提时递增"},
|
||
{Code: constants.ApprovalFieldWithdrawalShopID, Name: "店铺 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "发起提现的代理店铺系统 ID"},
|
||
{Code: constants.ApprovalFieldWithdrawalShopName, Name: "店铺名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "发起提现的代理店铺名称快照"},
|
||
{Code: constants.ApprovalFieldWithdrawalAmount, Name: "提现金额", ValueType: constants.ApprovalFieldValueTypeMoney, Description: "以元为单位且保留两位小数的提现金额"},
|
||
{Code: constants.ApprovalFieldWithdrawalAmountCent, Name: "提现金额(分)", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "以分为单位的提现金额整数"},
|
||
{Code: constants.ApprovalFieldWithdrawalFee, Name: "手续费", ValueType: constants.ApprovalFieldValueTypeMoney, Description: "以元为单位且保留两位小数的本次手续费"},
|
||
{Code: constants.ApprovalFieldWithdrawalActualAmount, Name: "实际到账金额", ValueType: constants.ApprovalFieldValueTypeMoney, Description: "以元为单位且保留两位小数的实际到账金额"},
|
||
{Code: constants.ApprovalFieldWithdrawalMethod, Name: "收款方式", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次收款方式名称快照"},
|
||
{Code: constants.ApprovalFieldWithdrawalAccountName, Name: "收款人姓名", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次收款人姓名"},
|
||
{Code: constants.ApprovalFieldWithdrawalAccountNumber, Name: "收款账号", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次收款账号,供审批人核验打款"},
|
||
{Code: constants.ApprovalFieldWithdrawalInvoiceKey, Name: "申请级发票", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "本次申请级发票对象存储 Key 列表,无发票时为空数组"},
|
||
{Code: constants.ApprovalFieldSubmitterID, Name: "提交人账号 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本系统真实业务提交人账号 ID"},
|
||
{Code: constants.ApprovalFieldSubmitterName, Name: "提交人名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "本系统真实业务提交人名称快照"},
|
||
}, true
|
||
default:
|
||
return nil, false
|
||
}
|
||
}
|
||
|
||
func normalizeSceneMapping(mapping []dto.WeComControlMappingItem) []dto.WeComControlMappingItem {
|
||
result := make([]dto.WeComControlMappingItem, 0, len(mapping))
|
||
for _, item := range mapping {
|
||
item.BusinessField = strings.TrimSpace(item.BusinessField)
|
||
item.ControlID = strings.TrimSpace(item.ControlID)
|
||
item.ControlType = strings.TrimSpace(item.ControlType)
|
||
if item.OptionMapping == nil {
|
||
item.OptionMapping = map[string]string{}
|
||
}
|
||
result = append(result, item)
|
||
}
|
||
return result
|
||
}
|
||
|
||
func validApprovalBusinessType(businessType string) bool {
|
||
for _, candidate := range approvalBusinessTypes() {
|
||
if candidate == businessType {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func approvalBusinessTypeName(businessType string) string {
|
||
switch businessType {
|
||
case constants.ApprovalBusinessTypeRefund:
|
||
return "退款审批"
|
||
case constants.ApprovalBusinessTypeOfflineRecharge:
|
||
return "员工线下代充值审批"
|
||
case constants.ApprovalBusinessTypeEmployeeCollection:
|
||
return "员工代收款核销审批"
|
||
case constants.ApprovalBusinessTypeAgentDistribution:
|
||
return "代理扫码分销注册审批"
|
||
case constants.ApprovalBusinessTypeWithdrawalQualification:
|
||
return "提现资料资格审批"
|
||
case constants.ApprovalBusinessTypeCommissionWithdrawal:
|
||
return "佣金提现终审"
|
||
default:
|
||
return "未知审批业务类型"
|
||
}
|
||
}
|
||
|
||
func sceneAuditSnapshot(scene *model.WeComApprovalScene) map[string]any {
|
||
return map[string]any{
|
||
"business_type": scene.BusinessType, "application_id": scene.ApplicationID,
|
||
"template_id": scene.TemplateID, "status": scene.Status, "last_verified_at": scene.LastVerifiedAt,
|
||
"template_fingerprint": scene.TemplateFingerprint,
|
||
}
|
||
}
|
||
|
||
func sceneAuditIdentity(scene *model.WeComApprovalScene) map[string]any {
|
||
if scene == nil {
|
||
return nil
|
||
}
|
||
return map[string]any{
|
||
"id": scene.ID, "business_type": scene.BusinessType, "application_id": scene.ApplicationID,
|
||
"template_id": scene.TemplateID, "template_name": scene.TemplateName, "status": scene.Status,
|
||
}
|
||
}
|
||
|
||
func (s *SceneService) recordFailure(ctx context.Context, businessType string, request dto.SaveWeComApprovalSceneRequest, result string, code int, description string) {
|
||
recordConfigFailure(ctx, s.db, s.audit, systemconfigapp.ChangeAudit{
|
||
OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: constants.AuditOperationWeComApprovalSceneSave,
|
||
Description: description, ConfigKey: "wecom.approval_scene." + businessType,
|
||
DisplayName: businessType, Identity: map[string]any{
|
||
"business_type": businessType, "application_id": request.ApplicationID,
|
||
"template_id": strings.TrimSpace(request.TemplateID), "status": request.Status,
|
||
},
|
||
Result: result, ErrorCode: fmt.Sprintf("%d", code), ErrorSummary: description,
|
||
})
|
||
}
|
||
|
||
func sceneResponse(scene model.WeComApprovalScene) (*dto.WeComApprovalSceneResponse, error) {
|
||
var mapping []dto.WeComControlMappingItem
|
||
if err := sonic.Unmarshal(scene.ControlMapping, &mapping); err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "解析企业微信控件映射失败")
|
||
}
|
||
return &dto.WeComApprovalSceneResponse{
|
||
ID: scene.ID, BusinessType: scene.BusinessType, BusinessTypeName: approvalBusinessTypeName(scene.BusinessType),
|
||
ApplicationID: scene.ApplicationID, TemplateID: scene.TemplateID, TemplateName: scene.TemplateName,
|
||
TemplateFingerprint: scene.TemplateFingerprint,
|
||
ControlMapping: mapping, Status: scene.Status, StatusName: constants.GetStatusName(scene.Status),
|
||
LastVerifiedAt: scene.LastVerifiedAt, UpdatedAt: scene.UpdatedAt,
|
||
}, nil
|
||
}
|