Files
junhong_cmp_fiber/internal/infrastructure/wecom/approval_form.go
break b063617153
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
完善退款审批材料与恢复流程
2026-09-20 17:59:52 +08:00

226 lines
9.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package wecom
import (
"context"
"fmt"
"strings"
"github.com/bytedance/sonic"
wecomapp "github.com/break/junhong_cmp_fiber/internal/application/wecom"
"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"
)
type approvalAttachmentPort interface {
Upload(ctx context.Context, applicationID, instanceID uint, reference ApprovalFileReference) (string, error)
}
type approvalFormBuildResult struct {
Contents []map[string]any
}
type attachmentPreparationError struct {
err error
}
func (e *attachmentPreparationError) Error() string {
return e.err.Error()
}
func (e *attachmentPreparationError) Unwrap() error {
return e.err
}
// buildApprovalForm 按后台已校验映射将渠道无关业务快照组装为企微控件值。
func buildApprovalForm(ctx context.Context, businessType string, applicationID, instanceID uint, mappingJSON, templateJSON, snapshotJSON []byte, attachments approvalAttachmentPort) (approvalFormBuildResult, error) {
var mappings []dto.WeComControlMappingItem
if err := sonic.Unmarshal(mappingJSON, &mappings); err != nil || len(mappings) == 0 {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "企业微信审批控件映射无效")
}
var snapshot map[string]any
if err := sonic.Unmarshal(snapshotJSON, &snapshot); err != nil || snapshot == nil {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "审批业务快照无效")
}
var template wecomapp.TemplateDefinition
if err := sonic.Unmarshal(templateJSON, &template); err != nil {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "企业微信审批模板快照无效")
}
requiredControls := make(map[string]bool, len(template.Controls))
for _, control := range template.Controls {
requiredControls[control.ID] = control.Required
}
mappingFields := make(map[string]dto.WeComControlMappingItem, len(mappings))
for _, item := range mappings {
mappingFields[strings.TrimSpace(item.BusinessField)] = item
}
requiredFields := wecomapp.RequiredSceneFields(businessType)
if businessType == constants.ApprovalBusinessTypeRefund {
requiredFields = append(requiredFields, wecomapp.RequiredRefundFields(snapshot)...)
}
for _, field := range requiredFields {
mapping, exists := mappingFields[field.Code]
if !exists {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "企业微信审批场景缺少必须配置的控件映射: "+field.Name+"(业务字段 "+field.Code+"")
}
if _, exists := lookupSnapshotValue(snapshot, field.Code); !exists {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "审批业务快照缺少必须提交的字段: "+field.Name)
}
if businessType == constants.ApprovalBusinessTypeRefund && field.Code == constants.ApprovalFieldRefundMethodCode {
methodCode, _ := snapshot[field.Code].(string)
methodCode = strings.TrimSpace(methodCode)
if methodCode == "" || (methodCode != constants.RefundMethodOriginalRoute && methodCode != constants.RefundMethodCustomerAccount && methodCode != constants.RefundMethodAssetWallet && methodCode != constants.RefundMethodAgentWallet) {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "审批业务快照缺少有效退款方式编码")
}
}
if field.Code == constants.ApprovalFieldCustomerAccountInfo {
value, _ := snapshot[field.Code].(string)
if strings.TrimSpace(value) == "" {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "审批业务快照缺少非空客户收款信息")
}
if !strings.EqualFold(mapping.ControlType, "textarea") {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "客户收款信息必须映射到多行文本控件")
}
}
if field.Code == constants.ApprovalFieldCustomerVoucherKey {
refs, err := parseApprovalFileReferences(snapshot[field.Code])
if err != nil || len(refs) == 0 {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "审批业务快照缺少客户收款凭证")
}
if !strings.EqualFold(mapping.ControlType, "file") {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "客户收款凭证必须映射到附件控件")
}
}
}
fieldNames := make(map[string]string, len(requiredFields))
for _, field := range requiredFields {
fieldNames[field.Code] = field.Name
}
contents := make([]map[string]any, 0, len(mappings))
attachmentCount := 0
for _, mapping := range mappings {
raw, exists := lookupSnapshotValue(snapshot, mapping.BusinessField)
if !exists || raw == nil {
if requiredControls[mapping.ControlID] {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "审批业务快照缺少模板必填字段: "+mapping.BusinessField)
}
if name, required := fieldNames[strings.TrimSpace(mapping.BusinessField)]; required {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "审批业务快照缺少必须提交的字段: "+name+",请确认已配置对应控件与业务字段映射")
}
continue
}
value, count, err := buildControlValue(ctx, applicationID, instanceID, mapping, raw, attachments)
if err != nil {
return approvalFormBuildResult{}, err
}
attachmentCount += count
if attachmentCount > constants.WeComApprovalMaxAttachmentCount {
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidParam, "单张企业微信审批单最多支持 6 个附件")
}
contents = append(contents, map[string]any{
"control": mapping.ControlType, "id": mapping.ControlID, "value": value,
})
}
return approvalFormBuildResult{Contents: contents}, nil
}
// lookupSnapshotValue 支持用点分路径读取嵌套业务快照,避免模板映射绑定具体 DTO。
func lookupSnapshotValue(snapshot map[string]any, path string) (any, bool) {
segments := strings.Split(strings.TrimSpace(path), ".")
var current any = snapshot
for _, segment := range segments {
object, ok := current.(map[string]any)
if !ok {
return nil, false
}
current, ok = object[segment]
if !ok {
return nil, false
}
}
return current, true
}
// buildControlValue 按企微控件类型生成官方要求的 value 结构,复杂结构允许业务快照直接传入对象。
func buildControlValue(ctx context.Context, applicationID, instanceID uint, mapping dto.WeComControlMappingItem, raw any, attachments approvalAttachmentPort) (map[string]any, int, error) {
if object, ok := raw.(map[string]any); ok && !strings.EqualFold(mapping.ControlType, "File") {
return object, 0, nil
}
switch strings.ToLower(strings.TrimSpace(mapping.ControlType)) {
case "text", "textarea":
return map[string]any{"text": fmt.Sprint(raw)}, 0, nil
case "number":
return map[string]any{"new_number": fmt.Sprint(raw)}, 0, nil
case "money":
return map[string]any{"new_money": fmt.Sprint(raw)}, 0, nil
case "date":
return map[string]any{"date": map[string]any{"type": "day", "s_timestamp": fmt.Sprint(raw)}}, 0, nil
case "selector":
businessValue := fmt.Sprint(raw)
key := mapping.OptionMapping[businessValue]
if key == "" {
return nil, 0, errors.New(errors.CodeInvalidStatus, "审批业务枚举值没有企微选项映射: "+mapping.BusinessField)
}
return map[string]any{"selector": map[string]any{"type": "single", "options": []map[string]string{{"key": key}}}}, 0, nil
case "contact":
return map[string]any{"members": []map[string]string{{"userid": fmt.Sprint(raw)}}}, 0, nil
case "department":
return map[string]any{"departments": []map[string]string{{"openapi_id": fmt.Sprint(raw)}}}, 0, nil
case "file":
if attachments == nil {
return nil, 0, errors.New(errors.CodeServiceUnavailable, "企业微信审批附件上传服务未配置")
}
references, err := parseApprovalFileReferences(raw)
if err != nil {
return nil, 0, err
}
files := make([]map[string]string, 0, len(references))
for _, reference := range references {
mediaID, err := attachments.Upload(ctx, applicationID, instanceID, reference)
if err != nil {
return nil, 0, &attachmentPreparationError{err: err}
}
files = append(files, map[string]string{"file_id": mediaID})
}
return map[string]any{"files": files}, len(files), nil
default:
return nil, 0, errors.New(errors.CodeInvalidStatus, "暂不支持的企业微信审批控件类型: "+mapping.ControlType)
}
}
// parseApprovalFileReferences 兼容现有退款和线下充值的字符串 Key 列表,以及带文件名的结构化引用。
func parseApprovalFileReferences(raw any) ([]ApprovalFileReference, error) {
if storageKey, ok := raw.(string); ok && strings.TrimSpace(storageKey) != "" {
return []ApprovalFileReference{{StorageKey: strings.TrimSpace(storageKey)}}, nil
}
bytes, err := sonic.Marshal(raw)
if err != nil {
return nil, errors.Wrap(errors.CodeInvalidParam, err, "编码审批附件引用失败")
}
var references []ApprovalFileReference
if err := sonic.Unmarshal(bytes, &references); err != nil {
var storageKeys []string
if keyErr := sonic.Unmarshal(bytes, &storageKeys); keyErr == nil && len(storageKeys) > 0 {
references = make([]ApprovalFileReference, 0, len(storageKeys))
for _, storageKey := range storageKeys {
if strings.TrimSpace(storageKey) != "" {
references = append(references, ApprovalFileReference{StorageKey: strings.TrimSpace(storageKey)})
}
}
if len(references) > 0 {
return references, nil
}
}
var single ApprovalFileReference
if singleErr := sonic.Unmarshal(bytes, &single); singleErr != nil {
return nil, errors.New(errors.CodeInvalidParam, "审批附件必须提供对象存储引用")
}
references = []ApprovalFileReference{single}
}
if len(references) == 0 {
return nil, errors.New(errors.CodeInvalidParam, "审批附件不能为空")
}
return references, nil
}