Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
AUG26-008。
- 迁移 000214–000217:tb_shop 全局唯一且不可修改的随机分销码(含存量回填)、
tb_agent_distribution_registration 待审批注册记录、tb_withdrawal_qualification 资料版本、
tb_commission_withdrawal_request_attempt 审批尝试记录,以及提现申请的 latest_*/异常标记列;
不修改既有迁移,down 在存在本 Change 业务事实或新类型场景行时拒绝破坏性回滚。
- 公开接口 POST /api/c/v1/agent-distribution-registrations:无认证,复用既有短信验证码校验、
消费与限流;无效分销码、停用上级、验证码无效或已消费统一返回「分销码不可用」且不落库,
审批通过前不创建店铺、账号或钱包。
- 审批通过才在同一事务内建启用店铺、代理主账号、钱包、上级层级与业务员快照,驳回不建实体,
重复回调不重复建实体,提交后清理上级下级缓存。
- 提现资料资格按不可变版本保存,替换合同或法人身份证即新增版本并同事务失效旧有效版本;
超管作废原因必填;代理停用与店铺删除联动失效。
- 提现每次提交或重提新增不可变审批尝试记录并冻结金额;企业微信通过仅一次从冻结扣减、
保持状态 2 并写 paid_at(不使用状态 4),驳回/cancelled/deleted 仅一次释放,
通过后撤销不回滚、不重新冻结、只写正交异常标记;加锁顺序统一为申请→尝试→钱包。
- 本地人工终审对已关联审批实例的申请返回状态冲突,approval_instance_id 为空的存量申请保持既有行为,
不新增任何配置开关。
- 补齐审批业务类型注册点全集:业务类型与场景字段常量、场景 DTO 两处枚举与中文描述、
场景字段白名单/合法类型/中文名、数据库 CHECK、Worker 决策消费者与装配、审批审计资源映射,
以及三个新审计资源与 13 个审计动作;失败/拒绝审计改为必达。
- 新增后台路由与 OpenAPI:资格提交/查询/作废、提现申请/重提/详情、店铺详情返回只读分销码。
- 归档本 Change:主 Spec 新增 agent-distribution-withdrawal 能力(5 个 Requirement)。
验证(junhong_cmp_test + Redis DB 6,显式 DB_*,未重置整库):
- 迁移 up → version 217 且 dirty=false → down 3 → up 回 217,fixture 复核残留为 0。
- 受控状态机脚手架 227 项通过 / 0 项失败,覆盖 18 组场景(幂等与乱序回调、资金冻结/释放/重提、
退款回扣 × 在途提现并发、负向场景拒绝审计与 14 个动作码审计真实落库)。
- gofmt 空、go build/go vet 通过、gendocs 与工作区逐字节一致、context-health 通过、
openspec validate --strict 通过、doctor healthy;自动化测试按项目决策为 N/A。
运行期前置(未完成,非代码交付物):由超管经 PUT /api/admin/wecom/scenes/{business_type} 为
agent_distribution_approval、withdrawal_qualification_approval、commission_withdrawal_approval
配置启用场景与模板控件映射;未配置时相应提交失败关闭。
499 lines
31 KiB
Go
499 lines
31 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)
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
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.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.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 {
|
||
switch businessType {
|
||
case constants.ApprovalBusinessTypeRefund,
|
||
constants.ApprovalBusinessTypeOfflineRecharge,
|
||
constants.ApprovalBusinessTypeEmployeeCollection,
|
||
constants.ApprovalBusinessTypeAgentDistribution,
|
||
constants.ApprovalBusinessTypeWithdrawalQualification,
|
||
constants.ApprovalBusinessTypeCommissionWithdrawal:
|
||
return true
|
||
default:
|
||
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
|
||
}
|