diff --git a/README.md b/README.md index a39f728..3d4cbb7 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ default: - **套餐系统升级**:完整的套餐生命周期管理,支持主套餐排队激活、加油包绑定主套餐、囤货待实名激活、流量按优先级扣减、自然月/按天有效期计算、日/月/年流量重置、客户端流量查询和套餐流量详单;详见 [套餐系统升级文档](docs/package-system-upgrade/) - **套餐生效条件覆盖与购买快照**:支持代理分配覆盖套餐生效条件,并将生效条件、周期类型和购买时长固化到套餐使用记录;激活与排队接续只消费购买快照。详见 [功能总结](docs/ur55-package-expiry-base/功能总结.md) - **UR#33 套餐临期列表与 C 端节点提醒**:平台和代理可按统一最终到期口径分页查看 0~15 天临期卡/设备及分类数量,返回高亮和优先标识;每日 15/7/3 天节点通过公共 Outbox 向关联个人客户幂等发送站内通知。详见 [功能总结](docs/ur33-package-expiry-reminder/功能总结.md) -- **企业微信最小审批 Adapter、退款与员工线下代充值**:超级管理员可维护自建应用连接参数、默认发起人和后台模板映射,可按已知模板 ID 实时读取控件后配置映射;通讯录按应用可见部门同步,不依赖根部门权限。管理接口和数据库均使用明文连接凭据。Adapter 支持成员显式绑定、异步提交、加密回调、权威详情同步、未终态轮询和结果未知时间窗恢复;退款和员工线下代充值均原子创建业务单与审批事实,approved 复用既有资金用例幂等执行,其他终态不产生资金副作用。未知结果只有唯一审批单号候选才关联,绝不盲目重提。详见 [功能总结](docs/wecom-application-connection/功能总结.md) +- **企业微信最小审批 Adapter、退款与员工线下代充值**:超级管理员可维护自建应用连接参数、默认发起人和后台模板映射,可按已知模板 ID 实时读取控件,并按业务类型查询系统可映射字段;通讯录按应用可见部门同步,不依赖根部门权限。管理接口和数据库均使用明文连接凭据。Adapter 支持成员显式绑定、异步提交、加密回调、权威详情同步、未终态轮询和结果未知时间窗恢复;退款和员工线下代充值均原子创建业务单与审批事实,approved 复用既有资金用例幂等执行,其他终态不产生资金副作用。未知结果只有唯一审批单号候选才关联,绝不盲目重提。详见 [功能总结](docs/wecom-application-connection/功能总结.md) - **代理钱包桌面扫码充值**:代理可使用微信 Native 或支付宝当面付为当前店铺主钱包在线自充;充值接口通过 `recharge_source` 区分平台线下代充与代理在线自充,支付确认后由 Outbox/Worker 幂等入账,并保留创建支付商户身份供未来对账。详见 [功能总结](docs/feature-034-agent-wallet-qr-recharge/功能总结.md) - **套餐价格回退与平台赠送策略**:新增价格配置状态、普通套餐成本价回退、赠送套餐独立语义、平台后台赠送订单发放和历史 0 价复核清单;详见 [功能总结](docs/package-price-fallback-and-platform-gift-policy/功能总结.md) 与 [最终验收清单](docs/package-price-fallback-and-platform-gift-policy/最终验收清单.md) - **分佣验证指引**:对代理分佣的冻结、解冻、提现校验流程进行了结构化说明与流程图,详见 [分佣逻辑正确与否验证](docs/优化说明/分佣逻辑正确与否验证.md) diff --git a/docs/admin-openapi.yaml b/docs/admin-openapi.yaml index c76a3cc..c419bfa 100644 --- a/docs/admin-openapi.yaml +++ b/docs/admin-openapi.yaml @@ -11006,6 +11006,36 @@ components: format: date-time type: string type: object + DtoWeComBusinessFieldListResponse: + properties: + business_type: + description: 业务类型 + type: string + business_type_name: + description: 业务类型名称(中文) + type: string + items: + description: 当前业务类型允许写入 control_mapping.business_field 的字段 + items: + $ref: '#/components/schemas/DtoWeComBusinessFieldResponse' + nullable: true + type: array + type: object + DtoWeComBusinessFieldResponse: + properties: + code: + description: control_mapping.business_field 应填写的稳定字段编码 + type: string + description: + description: 字段取值说明 + type: string + name: + description: 字段中文名称 + type: string + value_type: + description: 字段值类型 (string:字符串, integer:整数, money:两位小数的元金额字符串, file_list:对象存储文件引用列表) + type: string + type: object DtoWeComConnectionTestResponse: properties: success: @@ -11015,7 +11045,7 @@ components: DtoWeComControlMappingItem: properties: business_field: - description: 稳定业务字段编码 + description: 系统业务字段编码;按 business_type 调用 GET /api/admin/wecom/scenes/{business_type}/fields 获取可选值 type: string control_id: description: 企微模板控件 ID @@ -28890,12 +28920,12 @@ paths: /api/admin/wecom/scenes/{business_type}: put: parameters: - - description: 业务类型 (refund_approval:退款审批, offline_recharge_approval:员工线下代充值审批) + - description: 业务类型 (refund_approval:退款审批, offline_recharge_approval:员工线下代充值审批);可先调用 GET /api/admin/wecom/scenes/{business_type}/fields 查询允许映射的业务字段 in: path name: business_type required: true schema: - description: 业务类型 (refund_approval:退款审批, offline_recharge_approval:员工线下代充值审批) + description: 业务类型 (refund_approval:退款审批, offline_recharge_approval:员工线下代充值审批);可先调用 GET /api/admin/wecom/scenes/{business_type}/fields 查询允许映射的业务字段 type: string requestBody: content: @@ -28958,6 +28988,72 @@ paths: summary: 保存并校验企业微信审批场景模板映射 tags: - 企业微信审批 + /api/admin/wecom/scenes/{business_type}/fields: + get: + parameters: + - description: 业务类型 (refund_approval:退款审批, offline_recharge_approval:员工线下代充值审批) + in: path + name: business_type + required: true + schema: + description: 业务类型 (refund_approval:退款审批, offline_recharge_approval:员工线下代充值审批) + type: string + responses: + "200": + content: + application/json: + schema: + properties: + code: + description: 响应码 + example: 0 + type: integer + data: + $ref: '#/components/schemas/DtoWeComBusinessFieldListResponse' + msg: + description: 响应消息 + example: success + type: string + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - msg + - data + - timestamp + type: object + description: 成功 + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 请求参数错误 + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 未认证或认证已过期 + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 无权访问 + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 服务器内部错误 + security: + - BearerAuth: [] + summary: 查询企业微信审批场景可映射字段 + tags: + - 企业微信审批 /api/auth/login: post: requestBody: diff --git a/docs/wecom-application-connection/功能总结.md b/docs/wecom-application-connection/功能总结.md index d81cbe5..41d9811 100644 --- a/docs/wecom-application-connection/功能总结.md +++ b/docs/wecom-application-connection/功能总结.md @@ -11,6 +11,7 @@ - `POST /api/admin/wecom/applications/:id/members/sync`:拉取应用可见范围内的成员并原子刷新本地选择快照。 - `GET /api/admin/wecom/applications/:id/members`:按姓名或 userid 搜索并分页选择最近同步的可见成员。 - `POST /api/admin/wecom/applications/:id/templates/inspect`:输入企微后台已知模板 ID,实时读取模板名称、控件 ID、类型、必填标识和选项 key,供保存映射前选择控件。 +- `GET /api/admin/wecom/scenes/:business_type/fields`:返回指定业务类型允许映射的系统字段编码、中文名、值类型和取值说明。 - `PUT /api/admin/accounts/:id/wecom-binding`:把系统账号绑定到管理员明确选择的 `(corp_id, userid)`,同时保存姓名快照。 - `PUT /api/admin/wecom/scenes/:business_type`:配置已知 `template_id` 和业务字段控件映射,保存前实时读取模板详情校验。 - `GET /api/admin/wecom/scenes`:分页查询退款、员工线下代充值两个稳定业务场景的当前模板映射。 @@ -50,6 +51,7 @@ ## 审批场景与模板映射 - 企微没有按应用枚举全部审批模板的接口;管理员需先从企微后台取得模板 ID,再调用模板检查接口读取可映射控件。 +- 前端保存映射前先查询场景字段接口,以返回的 `code` 填写 `control_mapping.business_field`;字段接口与保存校验复用同一份定义。 - 稳定业务类型固定为 `refund_approval` 和 `offline_recharge_approval`,业务代码不直接写死企微模板 ID。 - 模板必须先在企业微信后台创建;本系统不创建模板,也不保存审批节点或审批人规则。 - 保存映射时调用模板详情,逐项校验控件 ID、控件类型和选择项 key,并要求模板所有必填控件都有映射;模板结构已变化时拒绝覆盖当前有效配置。 diff --git a/internal/application/wecom/scene.go b/internal/application/wecom/scene.go index 6a2b679..4e71356 100644 --- a/internal/application/wecom/scene.go +++ b/internal/application/wecom/scene.go @@ -90,6 +90,22 @@ func (s *SceneService) InspectTemplate(ctx context.Context, applicationID uint, }, 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 { @@ -269,40 +285,50 @@ func validateSceneMapping(businessType string, mapping []dto.WeComControlMapping } func allowedSceneBusinessField(businessType, businessField string) bool { - allowed := map[string]struct{}{} - switch businessType { - case constants.ApprovalBusinessTypeOfflineRecharge: - allowed = map[string]struct{}{ - constants.ApprovalFieldRechargeNo: {}, - constants.ApprovalFieldShopID: {}, - constants.ApprovalFieldShopName: {}, - constants.ApprovalFieldAmount: {}, - constants.ApprovalFieldAmountCent: {}, - constants.ApprovalFieldPaymentVoucherKey: {}, - constants.ApprovalFieldRemark: {}, - constants.ApprovalFieldSubmitterID: {}, - constants.ApprovalFieldSubmitterName: {}, - } - case constants.ApprovalBusinessTypeRefund: - allowed = map[string]struct{}{ - constants.ApprovalFieldRefundNo: {}, - constants.ApprovalFieldOrderID: {}, - constants.ApprovalFieldOrderNo: {}, - constants.ApprovalFieldAssetIdentifier: {}, - constants.ApprovalFieldAssetType: {}, - constants.ApprovalFieldActualReceivedAmount: {}, - constants.ApprovalFieldRequestedRefundAmount: {}, - constants.ApprovalFieldRefundVoucherKey: {}, - constants.ApprovalFieldRefundReason: {}, - constants.ApprovalFieldPackageUsageID: {}, - constants.ApprovalFieldSubmitterID: {}, - constants.ApprovalFieldSubmitterName: {}, - } - default: + fields, ok := sceneBusinessFields(businessType) + if !ok { return false } - _, exists := allowed[businessField] - return exists + 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: "本系统真实业务提交人名称快照"}, + }, 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 + default: + return nil, false + } } func normalizeSceneMapping(mapping []dto.WeComControlMappingItem) []dto.WeComControlMappingItem { @@ -323,6 +349,13 @@ func validApprovalBusinessType(businessType string) bool { return businessType == constants.ApprovalBusinessTypeRefund || businessType == constants.ApprovalBusinessTypeOfflineRecharge } +func approvalBusinessTypeName(businessType string) string { + if businessType == constants.ApprovalBusinessTypeRefund { + return "退款审批" + } + return "员工线下代充值审批" +} + func sceneAuditSnapshot(scene *model.WeComApprovalScene) map[string]any { return map[string]any{ "business_type": scene.BusinessType, "application_id": scene.ApplicationID, @@ -336,12 +369,8 @@ func sceneResponse(scene model.WeComApprovalScene) (*dto.WeComApprovalSceneRespo if err := sonic.Unmarshal(scene.ControlMapping, &mapping); err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "解析企业微信控件映射失败") } - name := "员工线下代充值审批" - if scene.BusinessType == constants.ApprovalBusinessTypeRefund { - name = "退款审批" - } return &dto.WeComApprovalSceneResponse{ - ID: scene.ID, BusinessType: scene.BusinessType, BusinessTypeName: name, + 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), diff --git a/internal/handler/admin/wecom.go b/internal/handler/admin/wecom.go index a80cf79..615e949 100644 --- a/internal/handler/admin/wecom.go +++ b/internal/handler/admin/wecom.go @@ -180,6 +180,19 @@ func (h *WeComHandler) InspectTemplate(c *fiber.Ctx) error { return response.Success(c, result) } +// ListSceneFields 查询审批场景允许映射的业务字段。 +// GET /api/admin/wecom/scenes/:business_type/fields +func (h *WeComHandler) ListSceneFields(c *fiber.Ctx) error { + if h == nil || h.scenes == nil { + return errors.New(errors.CodeServiceUnavailable, "企业微信审批场景服务未配置") + } + result, err := h.scenes.ListBusinessFields(c.UserContext(), c.Params("business_type")) + if err != nil { + return err + } + return response.Success(c, result) +} + // SaveScene 保存并校验稳定业务类型的企微模板控件映射。 // PUT /api/admin/wecom/scenes/:business_type func (h *WeComHandler) SaveScene(c *fiber.Ctx) error { diff --git a/internal/model/dto/wecom_scene_dto.go b/internal/model/dto/wecom_scene_dto.go index 190b5a7..03a5fa8 100644 --- a/internal/model/dto/wecom_scene_dto.go +++ b/internal/model/dto/wecom_scene_dto.go @@ -4,7 +4,7 @@ import "time" // WeComControlMappingItem 描述一个业务字段到企微模板控件的显式映射。 type WeComControlMappingItem struct { - BusinessField string `json:"business_field" validate:"required,min=1,max=64" description:"稳定业务字段编码"` + BusinessField string `json:"business_field" validate:"required,min=1,max=64" description:"系统业务字段编码;按 business_type 调用 GET /api/admin/wecom/scenes/{business_type}/fields 获取可选值"` ControlID string `json:"control_id" validate:"required,min=1,max=128" description:"企微模板控件 ID"` ControlType string `json:"control_type" validate:"required,min=1,max=64" description:"企微模板控件类型,必须与模板详情一致"` OptionMapping map[string]string `json:"option_mapping" description:"业务枚举值到企微选择项 key 的映射;非选择控件传空对象"` @@ -21,6 +21,26 @@ type InspectWeComTemplateParams struct { InspectWeComTemplateRequest } +// WeComBusinessFieldListParams 查询企业微信审批场景可映射字段路径参数。 +type WeComBusinessFieldListParams struct { + BusinessType string `path:"business_type" required:"true" description:"业务类型 (refund_approval:退款审批, offline_recharge_approval:员工线下代充值审批)"` +} + +// WeComBusinessFieldResponse 企业微信审批场景可映射业务字段响应。 +type WeComBusinessFieldResponse struct { + Code string `json:"code" description:"control_mapping.business_field 应填写的稳定字段编码"` + Name string `json:"name" description:"字段中文名称"` + ValueType string `json:"value_type" description:"字段值类型 (string:字符串, integer:整数, money:两位小数的元金额字符串, file_list:对象存储文件引用列表)"` + Description string `json:"description" description:"字段取值说明"` +} + +// WeComBusinessFieldListResponse 企业微信审批场景可映射业务字段列表响应。 +type WeComBusinessFieldListResponse struct { + BusinessType string `json:"business_type" description:"业务类型"` + BusinessTypeName string `json:"business_type_name" description:"业务类型名称(中文)"` + Items []WeComBusinessFieldResponse `json:"items" description:"当前业务类型允许写入 control_mapping.business_field 的字段"` +} + // WeComTemplateControlResponse 企业微信审批模板控件响应。 type WeComTemplateControlResponse struct { ID string `json:"id" description:"企微模板控件 ID"` @@ -47,7 +67,7 @@ type SaveWeComApprovalSceneRequest struct { // SaveWeComApprovalSceneParams 保存企业微信审批场景路径与请求参数。 type SaveWeComApprovalSceneParams struct { - BusinessType string `path:"business_type" required:"true" description:"业务类型 (refund_approval:退款审批, offline_recharge_approval:员工线下代充值审批)"` + BusinessType string `path:"business_type" required:"true" description:"业务类型 (refund_approval:退款审批, offline_recharge_approval:员工线下代充值审批);可先调用 GET /api/admin/wecom/scenes/{business_type}/fields 查询允许映射的业务字段"` SaveWeComApprovalSceneRequest } diff --git a/internal/routes/wecom.go b/internal/routes/wecom.go index 27a7a93..f1e36c6 100644 --- a/internal/routes/wecom.go +++ b/internal/routes/wecom.go @@ -70,6 +70,13 @@ func registerWeComRoutes(router fiber.Router, handler *admin.WeComHandler, doc * Output: new(dto.WeComTemplateDetailResponse), Auth: true, }) + Register(group, doc, groupPath, "GET", "/scenes/:business_type/fields", handler.ListSceneFields, RouteSpec{ + Summary: "查询企业微信审批场景可映射字段", + Tags: []string{"企业微信审批"}, + Input: new(dto.WeComBusinessFieldListParams), + Output: new(dto.WeComBusinessFieldListResponse), + Auth: true, + }) Register(group, doc, groupPath, "PUT", "/scenes/:business_type", handler.SaveScene, RouteSpec{ Summary: "保存并校验企业微信审批场景模板映射", Tags: []string{"企业微信审批"}, diff --git a/pkg/constants/approval.go b/pkg/constants/approval.go index 11d3953..47fef1f 100644 --- a/pkg/constants/approval.go +++ b/pkg/constants/approval.go @@ -50,6 +50,17 @@ const ( ApprovalFieldPackageUsageID = "package_usage_id" ) +const ( + // ApprovalFieldValueTypeString 表示业务字段值为普通字符串。 + ApprovalFieldValueTypeString = "string" + // ApprovalFieldValueTypeInteger 表示业务字段值为整数。 + ApprovalFieldValueTypeInteger = "integer" + // ApprovalFieldValueTypeMoney 表示业务字段值为两位小数的元金额字符串。 + ApprovalFieldValueTypeMoney = "money" + // ApprovalFieldValueTypeFileList 表示业务字段值为对象存储文件引用列表。 + ApprovalFieldValueTypeFileList = "file_list" +) + const ( // ApprovalStatusSubmitting 表示审批申请等待渠道提交。 ApprovalStatusSubmitting = 0