按 PRD 2.3/2.4/2.5 落地套餐退款的方式矩阵与原路渠道退款: - 退款申请派生并冻结权威实收金额(线上取原成功支付记录,钱包/线下取订单实际收款), 提交人不可填写或修改;按来源支付方式生成可选方式矩阵并在创建、提交、执行前重复校验。 - 审批切换为「每次提交一条不可变审批尝试记录 + 独立企业微信审批实例」,业务标识取尝试 记录主键;终态消费按尝试记录优先、退款申请兜底双读,兼容存量无实例与已关联实例申请。 新增活动退款部分唯一索引 (order_id) WHERE status IN (1,5,6)。 - 本地人工终审保持既有开关,补齐通过入口的 approval_instance_id IS NULL 守卫,使三个 入口一致拒绝已关联审批实例的申请;重提按尝试模式重写(仅已拒绝/已退回/原路失败且无异常)。 - 权益时点:企微通过事务写退款终态、按方式确定的订单态、钱包回款、员工账单冲销与可靠 失效事实;套餐失效/接续/停机仍由既有可靠机制最终一致执行,不把外部调用放入资金事务。 订单支付状态按方式置位:凭证退款与退回原钱包在企微通过时置已退款,原路须渠道明确成功。 - 按官方契约实现微信直连 v3、微信 v2(双向证书)、富友(/commonRefund 与 /refundQuery)、 支付宝四类原路退款;能力只由服务商类型与退款必需凭证完整性决定,无人工开关。 渠道请求号在提交时冻结到尝试记录,并以 channel_submitted_at 条件认领保证资金动作至多 提交一次(重复投递只查询不二次提交);不向任何渠道传递退款结果通知地址。 - 新增 refund:channel:recovery 恢复任务只查询回填;本地查询窗口超期(富友 72 小时、 微信 v2 7 天)转原路退款失败、渠道状态已失败、分类超时未知并置异常转人工,不放行自动 重提以避免重复退款。 - 同步退款 DTO/导出/审计资源与审计查询关联、商户凭证文档,并修正 fuiou 集成契约文档。 迁移 000218(退款尝试与渠道退款事实)、000219(微信 v2 客户端证书凭证)成对提供, 未修改既有迁移;测试库 junhong_cmp_test 完成 up/down/up 与行为核对,未调用真实渠道。
236 lines
16 KiB
Go
236 lines
16 KiB
Go
package dto
|
||
|
||
import (
|
||
"fmt"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
)
|
||
|
||
// CreateWechatConfigRequest 创建微信参数配置请求
|
||
type CreateWechatConfigRequest struct {
|
||
Name string `json:"name" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"配置名称"`
|
||
Description string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"配置描述"`
|
||
ProviderType string `json:"provider_type" validate:"required,oneof=wechat wechat_v2 fuiou" required:"true" description:"支付渠道类型 (wechat:微信直连v3, wechat_v2:微信直连v2, fuiou:富友)"`
|
||
|
||
OaAppID string `json:"oa_app_id" validate:"omitempty,max=100" maxLength:"100" description:"公众号AppID"`
|
||
OaAppSecret string `json:"oa_app_secret" validate:"omitempty,max=200" maxLength:"200" description:"公众号AppSecret"`
|
||
OaToken string `json:"oa_token" validate:"omitempty,max=200" maxLength:"200" description:"公众号Token"`
|
||
OaAesKey string `json:"oa_aes_key" validate:"omitempty,max=200" maxLength:"200" description:"公众号AES加密Key"`
|
||
OaOAuthRedirectURL string `json:"oa_oauth_redirect_url" validate:"omitempty,max=500" maxLength:"500" description:"OAuth回调地址"`
|
||
|
||
MiniappAppID string `json:"miniapp_app_id" validate:"omitempty,max=100" maxLength:"100" description:"小程序AppID"`
|
||
MiniappAppSecret string `json:"miniapp_app_secret" validate:"omitempty,max=200" maxLength:"200" description:"小程序AppSecret"`
|
||
|
||
WxMchID string `json:"wx_mch_id" validate:"omitempty,max=100" maxLength:"100" description:"微信商户号"`
|
||
WxAPIV3Key string `json:"wx_api_v3_key" validate:"omitempty,max=200" maxLength:"200" description:"微信APIv3密钥"`
|
||
WxAPIV2Key string `json:"wx_api_v2_key" validate:"omitempty,max=200" maxLength:"200" description:"微信APIv2密钥"`
|
||
WxCertContent string `json:"wx_cert_content" validate:"omitempty" description:"微信支付证书内容(PEM格式)"`
|
||
WxKeyContent string `json:"wx_key_content" validate:"omitempty" description:"微信支付密钥内容(PEM格式)"`
|
||
WxSerialNo string `json:"wx_serial_no" validate:"omitempty,max=200" maxLength:"200" description:"微信证书序列号"`
|
||
WxNotifyURL string `json:"wx_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"微信支付回调地址"`
|
||
// v2 退款接口为双向证书接口,支付与查单不依赖该证书。
|
||
WxClientCertContent string `json:"wx_client_cert_content" validate:"omitempty" description:"微信支付API客户端证书内容(PEM格式,v2 退款双向证书所需)"`
|
||
WxClientKeyContent string `json:"wx_client_key_content" validate:"omitempty" description:"微信支付API客户端证书私钥内容(PEM格式,v2 退款双向证书所需)"`
|
||
|
||
FyInsCd string `json:"fy_ins_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友机构号"`
|
||
FyMchntCd string `json:"fy_mchnt_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友商户号"`
|
||
FyTermID string `json:"fy_term_id" validate:"omitempty,max=50" maxLength:"50" description:"富友终端号"`
|
||
FyPrivateKey string `json:"fy_private_key" validate:"omitempty" description:"富友私钥(PEM格式)"`
|
||
FyPublicKey string `json:"fy_public_key" validate:"omitempty" description:"富友公钥(PEM格式)"`
|
||
FyAPIURL string `json:"fy_api_url" validate:"omitempty,max=500" maxLength:"500" description:"富友API地址"`
|
||
FyNotifyURL string `json:"fy_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"富友支付回调地址"`
|
||
|
||
// 支付宝配置字段(并存能力,不受 provider_type 限制)
|
||
AliAppID string `json:"ali_app_id" validate:"omitempty,max=100" maxLength:"100" description:"支付宝应用ID"`
|
||
AliPrivateKey string `json:"ali_private_key" validate:"omitempty" description:"支付宝应用私钥(PEM格式)"`
|
||
AliPublicKey string `json:"ali_public_key" validate:"omitempty" description:"支付宝公钥(PEM格式,用于验签)"`
|
||
AliNotifyURL string `json:"ali_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"支付宝异步通知地址"`
|
||
AliReturnURL string `json:"ali_return_url" validate:"omitempty,max=500" maxLength:"500" description:"支付宝同步跳转地址"`
|
||
AliProduction bool `json:"ali_production" description:"是否生产环境 (true:生产, false:沙箱)"`
|
||
AliPayExpireMinutes int `json:"ali_pay_expire_minutes" validate:"omitempty,min=1,max=1440" minimum:"1" maximum:"1440" description:"支付过期分钟数 (默认30分钟)"`
|
||
}
|
||
|
||
// UpdateWechatConfigRequest 更新微信参数配置请求
|
||
type UpdateWechatConfigRequest struct {
|
||
Name *string `json:"name" validate:"omitempty,min=1,max=100" minLength:"1" maxLength:"100" description:"配置名称"`
|
||
Description *string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"配置描述"`
|
||
ProviderType *string `json:"provider_type" validate:"omitempty,oneof=wechat wechat_v2 fuiou" description:"支付渠道类型 (wechat:微信直连v3, wechat_v2:微信直连v2, fuiou:富友)"`
|
||
|
||
OaAppID *string `json:"oa_app_id" validate:"omitempty,max=100" maxLength:"100" description:"公众号AppID"`
|
||
OaAppSecret *string `json:"oa_app_secret" validate:"omitempty,max=200" maxLength:"200" description:"公众号AppSecret"`
|
||
OaToken *string `json:"oa_token" validate:"omitempty,max=200" maxLength:"200" description:"公众号Token"`
|
||
OaAesKey *string `json:"oa_aes_key" validate:"omitempty,max=200" maxLength:"200" description:"公众号AES加密Key"`
|
||
OaOAuthRedirectURL *string `json:"oa_oauth_redirect_url" validate:"omitempty,max=500" maxLength:"500" description:"OAuth回调地址"`
|
||
|
||
MiniappAppID *string `json:"miniapp_app_id" validate:"omitempty,max=100" maxLength:"100" description:"小程序AppID"`
|
||
MiniappAppSecret *string `json:"miniapp_app_secret" validate:"omitempty,max=200" maxLength:"200" description:"小程序AppSecret"`
|
||
|
||
WxMchID *string `json:"wx_mch_id" validate:"omitempty,max=100" maxLength:"100" description:"微信商户号"`
|
||
WxAPIV3Key *string `json:"wx_api_v3_key" validate:"omitempty,max=200" maxLength:"200" description:"微信APIv3密钥"`
|
||
WxAPIV2Key *string `json:"wx_api_v2_key" validate:"omitempty,max=200" maxLength:"200" description:"微信APIv2密钥"`
|
||
WxCertContent *string `json:"wx_cert_content" validate:"omitempty" description:"微信支付证书内容(PEM格式)"`
|
||
WxKeyContent *string `json:"wx_key_content" validate:"omitempty" description:"微信支付密钥内容(PEM格式)"`
|
||
WxSerialNo *string `json:"wx_serial_no" validate:"omitempty,max=200" maxLength:"200" description:"微信证书序列号"`
|
||
WxNotifyURL *string `json:"wx_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"微信支付回调地址"`
|
||
// v2 退款接口为双向证书接口,支付与查单不依赖该证书。
|
||
WxClientCertContent *string `json:"wx_client_cert_content" validate:"omitempty" description:"微信支付API客户端证书内容(PEM格式,v2 退款双向证书所需)"`
|
||
WxClientKeyContent *string `json:"wx_client_key_content" validate:"omitempty" description:"微信支付API客户端证书私钥内容(PEM格式,v2 退款双向证书所需)"`
|
||
|
||
FyInsCd *string `json:"fy_ins_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友机构号"`
|
||
FyMchntCd *string `json:"fy_mchnt_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友商户号"`
|
||
FyTermID *string `json:"fy_term_id" validate:"omitempty,max=50" maxLength:"50" description:"富友终端号"`
|
||
FyPrivateKey *string `json:"fy_private_key" validate:"omitempty" description:"富友私钥(PEM格式)"`
|
||
FyPublicKey *string `json:"fy_public_key" validate:"omitempty" description:"富友公钥(PEM格式)"`
|
||
FyAPIURL *string `json:"fy_api_url" validate:"omitempty,max=500" maxLength:"500" description:"富友API地址"`
|
||
FyNotifyURL *string `json:"fy_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"富友支付回调地址"`
|
||
|
||
// 支付宝配置字段(并存能力,不受 provider_type 限制)
|
||
AliAppID *string `json:"ali_app_id" validate:"omitempty,max=100" maxLength:"100" description:"支付宝应用ID"`
|
||
AliPrivateKey *string `json:"ali_private_key" validate:"omitempty" description:"支付宝应用私钥(PEM格式)"`
|
||
AliPublicKey *string `json:"ali_public_key" validate:"omitempty" description:"支付宝公钥(PEM格式,用于验签)"`
|
||
AliNotifyURL *string `json:"ali_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"支付宝异步通知地址"`
|
||
AliReturnURL *string `json:"ali_return_url" validate:"omitempty,max=500" maxLength:"500" description:"支付宝同步跳转地址"`
|
||
AliProduction *bool `json:"ali_production" description:"是否生产环境 (true:生产, false:沙箱)"`
|
||
AliPayExpireMinutes *int `json:"ali_pay_expire_minutes" validate:"omitempty,min=1,max=1440" minimum:"1" maximum:"1440" description:"支付过期分钟数"`
|
||
}
|
||
|
||
// UpdateWechatConfigParams 更新微信参数配置聚合参数 (用于文档生成)
|
||
type UpdateWechatConfigParams struct {
|
||
IDReq
|
||
UpdateWechatConfigRequest
|
||
}
|
||
|
||
// WechatConfigListRequest 微信参数配置列表查询请求
|
||
type WechatConfigListRequest struct {
|
||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||
ProviderType *string `json:"provider_type" query:"provider_type" validate:"omitempty,oneof=wechat wechat_v2 fuiou" description:"支付渠道类型 (wechat:微信直连v3, wechat_v2:微信直连v2, fuiou:富友)"`
|
||
IsActive *bool `json:"is_active" query:"is_active" description:"是否激活 (true:已激活, false:未激活)"`
|
||
}
|
||
|
||
// WechatConfigResponse 微信参数配置响应
|
||
type WechatConfigResponse struct {
|
||
ID uint `json:"id" description:"配置ID"`
|
||
Name string `json:"name" description:"配置名称"`
|
||
Description string `json:"description" description:"配置描述"`
|
||
ProviderType string `json:"provider_type" description:"支付渠道类型 (wechat:微信直连v3, wechat_v2:微信直连v2, fuiou:富友)"`
|
||
IsActive bool `json:"is_active" description:"是否激活"`
|
||
|
||
OaAppID string `json:"oa_app_id" description:"公众号AppID"`
|
||
OaAppSecret string `json:"oa_app_secret" description:"公众号AppSecret(已脱敏)"`
|
||
OaToken string `json:"oa_token" description:"公众号Token(已脱敏)"`
|
||
OaAesKey string `json:"oa_aes_key" description:"公众号AES加密Key(已脱敏)"`
|
||
OaOAuthRedirectURL string `json:"oa_oauth_redirect_url" description:"OAuth回调地址"`
|
||
|
||
MiniappAppID string `json:"miniapp_app_id" description:"小程序AppID"`
|
||
MiniappAppSecret string `json:"miniapp_app_secret" description:"小程序AppSecret(已脱敏)"`
|
||
|
||
WxMchID string `json:"wx_mch_id" description:"微信商户号"`
|
||
WxAPIV3Key string `json:"wx_api_v3_key" description:"微信APIv3密钥(已脱敏)"`
|
||
WxAPIV2Key string `json:"wx_api_v2_key" description:"微信APIv2密钥(已脱敏)"`
|
||
WxCertContent string `json:"wx_cert_content" description:"微信支付证书内容(配置状态)"`
|
||
WxKeyContent string `json:"wx_key_content" description:"微信支付密钥内容(配置状态)"`
|
||
WxSerialNo string `json:"wx_serial_no" description:"微信证书序列号"`
|
||
WxNotifyURL string `json:"wx_notify_url" description:"微信支付回调地址"`
|
||
WxClientCertContent string `json:"wx_client_cert_content" description:"微信支付API客户端证书内容(配置状态,v2 退款双向证书所需)"`
|
||
WxClientKeyContent string `json:"wx_client_key_content" description:"微信支付API客户端证书私钥内容(配置状态,v2 退款双向证书所需)"`
|
||
|
||
FyInsCd string `json:"fy_ins_cd" description:"富友机构号"`
|
||
FyMchntCd string `json:"fy_mchnt_cd" description:"富友商户号"`
|
||
FyTermID string `json:"fy_term_id" description:"富友终端号"`
|
||
FyPrivateKey string `json:"fy_private_key" description:"富友私钥(配置状态)"`
|
||
FyPublicKey string `json:"fy_public_key" description:"富友公钥(配置状态)"`
|
||
FyAPIURL string `json:"fy_api_url" description:"富友API地址"`
|
||
FyNotifyURL string `json:"fy_notify_url" description:"富友支付回调地址"`
|
||
|
||
// 支付宝配置字段(敏感字段已脱敏)
|
||
AliAppID string `json:"ali_app_id" description:"支付宝应用ID"`
|
||
AliPrivateKey string `json:"ali_private_key" description:"支付宝应用私钥(配置状态)"`
|
||
AliPublicKey string `json:"ali_public_key" description:"支付宝公钥(配置状态)"`
|
||
AliNotifyURL string `json:"ali_notify_url" description:"支付宝异步通知地址"`
|
||
AliReturnURL string `json:"ali_return_url" description:"支付宝同步跳转地址"`
|
||
AliProduction bool `json:"ali_production" description:"是否生产环境"`
|
||
AliPayExpireMinutes int `json:"ali_pay_expire_minutes" description:"支付过期分钟数"`
|
||
|
||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||
}
|
||
|
||
// WechatConfigListResponse 微信参数配置列表响应
|
||
type WechatConfigListResponse struct {
|
||
List []*WechatConfigResponse `json:"items" description:"配置列表"`
|
||
Total int64 `json:"total" description:"总数"`
|
||
Page int `json:"page" description:"当前页"`
|
||
PageSize int `json:"size" description:"每页数量"`
|
||
}
|
||
|
||
// MaskShortSecret 对短密钥进行脱敏处理
|
||
// 长度小于 8 返回 "***",否则保留前4位和后4位
|
||
func MaskShortSecret(val string) string {
|
||
if len(val) < 8 {
|
||
return "***"
|
||
}
|
||
return fmt.Sprintf("%s***%s", val[:4], val[len(val)-4:])
|
||
}
|
||
|
||
// MaskLongSecret 对长密钥/证书进行脱敏处理
|
||
// 空值返回 "[未配置]",否则返回 "[已配置]"
|
||
func MaskLongSecret(val string) string {
|
||
if val == "" {
|
||
return "[未配置]"
|
||
}
|
||
return "[已配置]"
|
||
}
|
||
|
||
// FromWechatConfigModel 从模型转换为响应 DTO,敏感字段自动脱敏
|
||
func FromWechatConfigModel(m *model.WechatConfig) *WechatConfigResponse {
|
||
resp := &WechatConfigResponse{
|
||
ID: m.ID,
|
||
Name: m.Name,
|
||
ProviderType: m.ProviderType,
|
||
IsActive: m.IsActive,
|
||
|
||
OaAppID: m.OaAppID,
|
||
OaAppSecret: MaskShortSecret(m.OaAppSecret),
|
||
OaToken: MaskShortSecret(m.OaToken),
|
||
OaAesKey: MaskShortSecret(m.OaAesKey),
|
||
OaOAuthRedirectURL: m.OaOAuthRedirectURL,
|
||
|
||
MiniappAppID: m.MiniappAppID,
|
||
MiniappAppSecret: MaskShortSecret(m.MiniappAppSecret),
|
||
|
||
WxMchID: m.WxMchID,
|
||
WxAPIV3Key: MaskShortSecret(m.WxAPIV3Key),
|
||
WxAPIV2Key: MaskShortSecret(m.WxAPIV2Key),
|
||
WxCertContent: MaskLongSecret(m.WxCertContent),
|
||
WxKeyContent: MaskLongSecret(m.WxKeyContent),
|
||
WxSerialNo: m.WxSerialNo,
|
||
WxNotifyURL: m.WxNotifyURL,
|
||
|
||
FyInsCd: m.FyInsCd,
|
||
FyMchntCd: m.FyMchntCd,
|
||
FyTermID: m.FyTermID,
|
||
FyPrivateKey: MaskLongSecret(m.FyPrivateKey),
|
||
FyPublicKey: MaskLongSecret(m.FyPublicKey),
|
||
FyAPIURL: m.FyAPIURL,
|
||
FyNotifyURL: m.FyNotifyURL,
|
||
|
||
AliAppID: m.AliAppID,
|
||
AliPrivateKey: MaskLongSecret(m.AliPrivateKey),
|
||
AliPublicKey: MaskLongSecret(m.AliPublicKey),
|
||
AliNotifyURL: m.AliNotifyURL,
|
||
AliReturnURL: m.AliReturnURL,
|
||
AliProduction: m.AliProduction,
|
||
AliPayExpireMinutes: m.AliPayExpireMinutes,
|
||
|
||
CreatedAt: m.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
UpdatedAt: m.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||
}
|
||
|
||
if m.Description != nil {
|
||
resp.Description = *m.Description
|
||
}
|
||
|
||
return resp
|
||
}
|