多笔立减金,错误映射

This commit is contained in:
ziming 2026-03-13 15:37:51 +08:00
parent 9bd8646e57
commit bbfc91da57
22 changed files with 1076 additions and 243 deletions

View File

@ -209,6 +209,8 @@ message CmbNotifyRequest {
//
string ext = 13 [json_name = "ext"];
string attach = 14 [json_name = "attach"];
//
string transactionId = 15 [json_name = "transactionId"];
}
message CmbNotifyReply {

View File

@ -0,0 +1,60 @@
package businesserr
// ThirdErrCode 表示发券接口失败时返回的第三方业务错误码
// 格式为 tecxxx用于统一抽象不同支付平台的错误
type ThirdErrCode string
// 定义具体的第三方错误码常量(微信/支付宝的都有)
const (
ThirdErrCodeDefault ThirdErrCode = "tec999" // 默认错误码
ThirdErrCodeStockNotEnough ThirdErrCode = "tec001" // 库存不足
ThirdErrCodeAdvanceFundingNotEnough ThirdErrCode = "tec002" // 垫资不足
ThirdErrCodeBatchNotStarted ThirdErrCode = "tec003" // 批次未开始
ThirdErrCodeBatchEnded ThirdErrCode = "tec004" // 批次已结束
ThirdErrCodeBatchOffline ThirdErrCode = "tec005" // 批次已下线
ThirdErrCodePaymentPlatformError ThirdErrCode = "tec006" // 微信/支付宝异常
ThirdErrCodeUserNotRealNameVerified ThirdErrCode = "tec007" // 用户没有实名认证
ThirdErrCodeUserNotFound ThirdErrCode = "tec008" // 用户账号不存在
ThirdErrCodeUserAccountFrozen ThirdErrCode = "tec009" // 用户账户被冻结
ThirdErrCodeUserHighRiskOrCheater ThirdErrCode = "tec010" // 用户为作弊用户或高风险用户
ThirdErrCodeUserNoMobileBound ThirdErrCode = "tec011" // 用户没有绑定手机号
ThirdErrCodeUserAccountAbnormal ThirdErrCode = "tec012" // 用户账号异常(没有明确的原因)
ThirdErrCodeUserParticipationExceeded ThirdErrCode = "tec013" // 用户参与次数超限
ThirdErrCodeAppIDOpenIDMismatch ThirdErrCode = "tec014" // appid与openid不匹配
ThirdErrCodeDailyLimit ThirdErrCode = "tec015" // 超批次当天限额
ThirdErrCodeCallHigh ThirdErrCode = "tec016" // 调用频率过高
)
// CmbAPIError 定义 API 错误结构体
type CmbAPIError struct {
StatusCode int `json:"status_code"`
ErrorCode ErrCode `json:"error_code"`
Description string `json:"description"`
Hint string `json:"hint"` // 解决方案
ThirdErrCode ThirdErrCode `json:"third_err_code"`
}
var CmbAPIPublicErrorMap = map[ErrCode][]CmbAPIError{
BATCH_NOT_STARTED: {
{
StatusCode: 400,
ErrorCode: BATCH_NOT_STARTED,
Description: "批次未开始",
Hint: "批次未开始",
ThirdErrCode: ThirdErrCodeDefault,
},
},
BATCH_ENDED: {
{
StatusCode: 400,
ErrorCode: BATCH_ENDED,
Description: "批次已结束",
Hint: "批次已结束",
ThirdErrCode: ThirdErrCodeDefault,
},
},
}

View File

@ -1,19 +1,18 @@
package wechatrepoimpl
package businesserr
import (
"fmt"
"strings"
"github.com/go-kratos/kratos/v2/errors"
"strings"
err2 "voucher/api/err"
)
// ErrCode 定义错误码类型
// https://pay.weixin.qq.com/doc/v3/merchant/4012463767
type ErrCode string
// 定义错误码常量
// https://pay.weixin.qq.com/doc/v3/merchant/4012463767
const (
SYSTEM_ERROR ErrCode = "SYSTEM_ERROR"
SIGN_ERROR ErrCode = "SIGN_ERROR"
APPID_MCHID_NOT_MATCH ErrCode = "APPID_MCHID_NOT_MATCH"
INVALID_REQUEST ErrCode = "INVALID_REQUEST"
PARAM_ERROR ErrCode = "PARAM_ERROR"
@ -26,196 +25,255 @@ const (
FREQUENCY_LIMITED ErrCode = "FREQUENCY_LIMITED"
)
// APIError 定义 API 错误结构体
type APIError struct {
StatusCode int `json:"status_code"`
ErrorCode ErrCode `json:"error_code"`
Description string `json:"description"`
Hint string `json:"hint"`
}
const (
BATCH_NOT_STARTED ErrCode = "BATCH_NOT_STARTED"
BATCH_ENDED ErrCode = "BATCH_ENDED"
)
// CmbAPIErrorMap 定义错误映射,方便根据错误码获取错误信息
var CmbAPIErrorMap = map[ErrCode][]CmbAPIError{
SYSTEM_ERROR: {
{
StatusCode: 500,
ErrorCode: SYSTEM_ERROR,
Description: "系统异常,请稍后重试",
Hint: "请稍后重试",
ThirdErrCode: ThirdErrCodeDefault,
},
},
SIGN_ERROR: {
{
StatusCode: 401,
ErrorCode: SIGN_ERROR,
Description: "验证不通过",
Hint: "请参阅 签名常见问题",
ThirdErrCode: ThirdErrCodeDefault,
},
},
// 定义错误映射,方便根据错误码获取错误信息
var _ = map[ErrCode][]APIError{
APPID_MCHID_NOT_MATCH: {
{
StatusCode: 400,
ErrorCode: APPID_MCHID_NOT_MATCH,
Description: "商户号与AppID不匹配",
Hint: "调用接口的商户号需与接口传入的AppID有绑定关系请参考常见问题Q4",
StatusCode: 400,
ErrorCode: APPID_MCHID_NOT_MATCH,
Description: "商户号与AppID不匹配",
Hint: "调用接口的商户号需与接口传入的AppID有绑定关系请参考常见问题Q4",
ThirdErrCode: ThirdErrCodeDefault,
},
},
INVALID_REQUEST: {
{
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "OpenID与AppID不匹配",
Hint: "OpenID与AppID需有对应关系",
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "HTTP 请求不符合微信支付 APIv3 接口规则",
Hint: "请参阅 接口规则",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "非法的商户号",
Hint: "请检查商户号准确性",
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "OpenID与AppID不匹配",
Hint: "OpenID与AppID需有对应关系",
ThirdErrCode: ThirdErrCodeAppIDOpenIDMismatch,
},
{
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "调用频率过高",
Hint: "请降低API调用频率",
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "非法的商户号",
Hint: "请检查商户号准确性",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "活动已结束或未激活",
Hint: "请检查批次状态",
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "调用频率过高",
Hint: "请降低API调用频率",
ThirdErrCode: ThirdErrCodeCallHigh,
},
{
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "批次信息获取失败,请确认参数是否有误",
Hint: "请检查创建商户号与批次号的对应关系",
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "活动已结束或未激活",
Hint: "请检查批次状态",
ThirdErrCode: ThirdErrCodeBatchEnded, //前置判断时间处理一下
},
{
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "批次信息获取失败,请确认参数是否有误",
Hint: "请检查创建商户号与批次号的对应关系",
ThirdErrCode: ThirdErrCodeDefault,
},
},
PARAM_ERROR: {
{
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "AppID必填",
Hint: "请输入AppID",
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "参数错误",
Hint: "请根据错误提示正确传入参数",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "AppID必填",
Hint: "请输入AppID",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "OpenID必填",
Hint: "请输入OpenID",
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "OpenID必填",
Hint: "请输入OpenID",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "批次号必填",
Hint: "请输入批次号",
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "批次号必填",
Hint: "请输入批次号",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "商户号必填",
Hint: "请输入商户号",
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "商户号必填",
Hint: "请输入商户号",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "非法的批次状态",
Hint: "请检查批次状态,仅支持发放状态为“运营中”的代金券批次",
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "非法的批次状态",
Hint: "请检查批次状态,仅支持发放状态为“运营中”的代金券批次",
ThirdErrCode: ThirdErrCodeDefault,
},
},
MCH_NOT_EXISTS: {
{
StatusCode: 403,
ErrorCode: MCH_NOT_EXISTS,
Description: "商户号不合法",
Hint: "请检查商户号准确性",
StatusCode: 403,
ErrorCode: MCH_NOT_EXISTS,
Description: "商户号不合法",
Hint: "请检查商户号准确性",
ThirdErrCode: ThirdErrCodeDefault,
},
},
NOT_ENOUGH: {
{
StatusCode: 403,
ErrorCode: NOT_ENOUGH,
Description: "批次预算不足",
Hint: "批次预算已发放完,请补充批次预算",
StatusCode: 403,
ErrorCode: NOT_ENOUGH,
Description: "批次预算不足",
Hint: "批次预算已发放完,请补充批次预算",
ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough,
},
{
StatusCode: 403,
ErrorCode: NOT_ENOUGH,
Description: "发券超过单天限额",
Hint: "已超过该批次设置的单天发放限制额度,无法发放",
StatusCode: 403,
ErrorCode: NOT_ENOUGH,
Description: "发券超过单天限额",
Hint: "已超过该批次设置的单天发放限制额度,无法发放",
ThirdErrCode: ThirdErrCodeDailyLimit,
},
{
StatusCode: 403,
ErrorCode: NOT_ENOUGH,
Description: "账户余额不足,请充值",
Hint: "商户号余额不足,无法继续发券,请充值",
StatusCode: 403,
ErrorCode: NOT_ENOUGH,
Description: "账户余额不足,请充值",
Hint: "商户号余额不足,无法继续发券,请充值",
ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough,
},
{
StatusCode: 403,
ErrorCode: NOT_ENOUGH,
Description: "批次预算耗尽",
Hint: "该批次的预算已经耗尽",
StatusCode: 403,
ErrorCode: NOT_ENOUGH,
Description: "批次预算耗尽",
Hint: "该批次的预算已经耗尽",
ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough,
},
},
REQUEST_BLOCKED: {
{
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "商户无权发券",
Hint: "该批次不支持其他商户发放请参考常见问题Q1",
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "商户无权发券",
Hint: "该批次不支持其他商户发放请参考常见问题Q1",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "批次不支持跨商户发券",
Hint: "该批次不支持其他商户发放请参考常见问题Q1",
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "批次不支持跨商户发券",
Hint: "该批次不支持其他商户发放请参考常见问题Q1",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "用户被限领拦截",
Hint: "该用户已达到该批次的领取上限请参考常见问题Q6",
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "用户被限领拦截",
Hint: "该用户已达到该批次的领取上限请参考常见问题Q6",
ThirdErrCode: ThirdErrCodeUserParticipationExceeded,
},
{
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "不能在API渠道发放",
Hint: "请检查批次信息,仅支持发放微信支付代金券,不支持发放立减与折扣",
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "不能在API渠道发放",
Hint: "请检查批次信息,仅支持发放微信支付代金券,不支持发放立减与折扣",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "不支持指定面额发券",
Hint: "仅在发券时指定面额及门槛的场景才生效,常规发券场景请勿传入该信息",
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "不支持指定面额发券",
Hint: "仅在发券时指定面额及门槛的场景才生效,常规发券场景请勿传入该信息",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "仅在广告场景下发放批次",
Hint: "该批次已在朋友圈广告发放,不支持在其他渠道发放",
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "仅在广告场景下发放批次",
Hint: "该批次已在朋友圈广告发放,不支持在其他渠道发放",
ThirdErrCode: ThirdErrCodeDefault,
},
},
RULE_LIMIT: {
{
StatusCode: 403,
ErrorCode: RULE_LIMIT,
Description: "用户已达最大领券次数",
Hint: "该用户已达到该批次的领取上限请参考常见问题Q6",
StatusCode: 403,
ErrorCode: RULE_LIMIT,
Description: "用户已达最大领券次数",
Hint: "该用户已达到该批次的领取上限请参考常见问题Q6",
ThirdErrCode: ThirdErrCodeUserParticipationExceeded,
},
{
StatusCode: 403,
ErrorCode: RULE_LIMIT,
Description: "被自然人规则拦截",
Hint: "该自然人已达到该批次的领取上限请参考常见问题Q6",
StatusCode: 403,
ErrorCode: RULE_LIMIT,
Description: "被自然人规则拦截",
Hint: "该自然人已达到该批次的领取上限请参考常见问题Q6",
ThirdErrCode: ThirdErrCodeUserAccountAbnormal,
},
},
USER_ACCOUNT_ABNORMAL: {
{
StatusCode: 403,
ErrorCode: USER_ACCOUNT_ABNORMAL,
Description: "用户非法",
Hint: "用户命中微信支付风控模型请参考常见问题Q5",
StatusCode: 403,
ErrorCode: USER_ACCOUNT_ABNORMAL,
Description: "用户非法",
Hint: "用户命中微信支付风控模型请参考常见问题Q5",
ThirdErrCode: ThirdErrCodeUserAccountFrozen,
},
},
RESOURCE_NOT_EXISTS: {
{
StatusCode: 404,
ErrorCode: RESOURCE_NOT_EXISTS,
Description: "批次不存在",
Hint: "请检查批次及制券商户号信息",
StatusCode: 404,
ErrorCode: RESOURCE_NOT_EXISTS,
Description: "批次不存在",
Hint: "请检查批次及制券商户号信息",
ThirdErrCode: ThirdErrCodeDefault,
},
},
FREQUENCY_LIMITED: {
{
StatusCode: 429,
ErrorCode: FREQUENCY_LIMITED,
Description: "当前请求人数过多,请稍后重试",
Hint: "请降低API调用频率",
StatusCode: 429,
ErrorCode: FREQUENCY_LIMITED,
Description: "当前请求人数过多,请稍后重试",
Hint: "请降低API调用频率",
ThirdErrCode: ThirdErrCodeCallHigh,
},
},
}
@ -284,13 +342,13 @@ var WechatError = map[string]*errors.Error{
ErrorWechatAccountFail: err2.ErrorWechatAccountFail(ErrorWechatAccountFail),
}
type ErrBody struct {
type WechatErrBody struct {
Code ErrCode `json:"Code"`
Message string `json:"Message"`
}
// GetWechatError 根据错误描述获取具体的错误处理
func (s ErrBody) GetWechatError() *errors.Error {
func (s WechatErrBody) GetWechatError() *errors.Error {
lowerDesc := strings.ToLower(s.Message)
for desc, err := range WechatError {
if strings.ToLower(desc) == lowerDesc {

View File

@ -0,0 +1,197 @@
package businesserr
// 公共 & 业务错误码常量(统一定义,前缀 MULTI_
// https://pay.weixin.qq.com/doc/v3/merchant/4012463767
const (
MULTI_PARAM_ERROR ErrCode = "PARAM_ERROR" // 参数错误(如缺少必填字段)
MULTI_INVALID_REQUEST ErrCode = "INVALID_REQUEST" // 非法请求(如 OpenID 与 AppID 不匹配、批次状态异常等)
MULTI_SIGN_ERROR ErrCode = "SIGN_ERROR" // 签名验证失败
MULTI_SYSTEM_ERROR ErrCode = "SYSTEM_ERROR" // 系统异常
MULTI_APPID_MCHID_NOT_MATCH ErrCode = "APPID_MCHID_NOT_MATCH" // 商户号与 AppID 不匹配
MULTI_MCH_NOT_EXISTS ErrCode = "MCH_NOT_EXISTS" // 商户号不合法
MULTI_NOT_ENOUGH ErrCode = "NOT_ENOUGH" // 资源不足(预算/余额/限额耗尽)
MULTI_REQUEST_BLOCKED ErrCode = "REQUEST_BLOCKED" // 请求被拦截(跨商户、渠道限制等)
MULTI_RULE_LIMIT ErrCode = "RULE_LIMIT" // 用户或自然人达到领取上限
MULTI_USER_ACCOUNT_ABNORMAL ErrCode = "USER_ACCOUNT_ABNORMAL" // 用户账号异常(风控、未实名等)
MULTI_RESOURCE_NOT_EXISTS ErrCode = "RESOURCE_NOT_EXISTS" // 批次不存在
)
// MULTIAPIErrorMap 定义错误映射,方便根据错误码获取所有可能的错误场景
var CmbMULTIAPIErrorMap = map[ErrCode][]CmbAPIError{
MULTI_SYSTEM_ERROR: {
{
StatusCode: 500,
ErrorCode: MULTI_SYSTEM_ERROR,
Description: "系统异常,请稍后重试",
Hint: "请稍后重试",
ThirdErrCode: ThirdErrCodeDefault,
},
},
MULTI_SIGN_ERROR: {
{
StatusCode: 401,
ErrorCode: MULTI_SIGN_ERROR,
Description: "验证不通过",
Hint: "请参阅 签名常见问题",
ThirdErrCode: ThirdErrCodeDefault,
},
},
MULTI_APPID_MCHID_NOT_MATCH: {
{
StatusCode: 400,
ErrorCode: MULTI_APPID_MCHID_NOT_MATCH,
Description: "商户号与AppID不匹配",
Hint: "调用接口的商户号需与接口传入的AppID有绑定关系请参考常见问题Q4",
ThirdErrCode: ThirdErrCodeDefault,
},
},
MULTI_INVALID_REQUEST: {
{
StatusCode: 400,
ErrorCode: MULTI_INVALID_REQUEST,
Description: "HTTP 请求不符合微信支付 APIv3 接口规则",
Hint: "请参阅 接口规则",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: MULTI_INVALID_REQUEST,
Description: "非法的商户号",
Hint: "请检查商户号准确性",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: MULTI_INVALID_REQUEST,
Description: "OpenID与AppID不匹配",
Hint: "OpenID与AppID需有对应关系",
ThirdErrCode: ThirdErrCodeAppIDOpenIDMismatch,
},
},
MULTI_PARAM_ERROR: {
{
StatusCode: 400,
ErrorCode: MULTI_PARAM_ERROR,
Description: "参数错误",
Hint: "请根据错误提示正确传入参数",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: MULTI_PARAM_ERROR,
Description: "AppID必填",
Hint: "请输入AppID",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: MULTI_PARAM_ERROR,
Description: "OpenID必填",
Hint: "请输入OpenID",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: MULTI_PARAM_ERROR,
Description: "批次号必填",
Hint: "请输入批次号",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: MULTI_PARAM_ERROR,
Description: "商户号必填",
Hint: "请输入商户号",
ThirdErrCode: ThirdErrCodeDefault,
},
},
MULTI_MCH_NOT_EXISTS: {
{
StatusCode: 403,
ErrorCode: MULTI_MCH_NOT_EXISTS,
Description: "商户号不合法",
Hint: "请检查商户号准确性",
ThirdErrCode: ThirdErrCodeDefault,
},
},
MULTI_NOT_ENOUGH: {
{
StatusCode: 403,
ErrorCode: MULTI_NOT_ENOUGH,
Description: "批次预算耗尽",
Hint: "该批次的预算已经耗尽",
ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough,
},
{
StatusCode: 403,
ErrorCode: MULTI_NOT_ENOUGH,
Description: "账户余额不足,请充值",
Hint: "商户号余额不足,无法继续发券,请充值",
ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough,
},
{
StatusCode: 403,
ErrorCode: MULTI_NOT_ENOUGH,
Description: "发券超过单天限额",
Hint: "已超过该批次设置的单天发放限制额度,无法发放",
ThirdErrCode: ThirdErrCodeDailyLimit,
},
},
MULTI_REQUEST_BLOCKED: {
{
StatusCode: 403,
ErrorCode: MULTI_REQUEST_BLOCKED,
Description: "参数错误,请检查批次参数",
Hint: "活动未开始或已结束",
ThirdErrCode: ThirdErrCodeBatchNotStarted, // 时间前置判断一下
},
{
StatusCode: 403,
ErrorCode: MULTI_REQUEST_BLOCKED,
Description: "仅在广告场景下发放批次",
Hint: "该批次已在朋友圈广告发放,不支持在其他渠道发放",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 403,
ErrorCode: MULTI_REQUEST_BLOCKED,
Description: "商户号收款功能受限,无法发券",
Hint: "商户号收款功能受限",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 403,
ErrorCode: MULTI_REQUEST_BLOCKED,
Description: "批次不支持跨商户发券",
Hint: "该批次不支持其他商户发放",
ThirdErrCode: ThirdErrCodeDefault,
},
},
MULTI_RULE_LIMIT: {
{
StatusCode: 403,
ErrorCode: MULTI_RULE_LIMIT,
Description: "用户已达最大领券次数",
Hint: "该用户已达到该批次的领取上限",
ThirdErrCode: ThirdErrCodeUserParticipationExceeded,
},
},
MULTI_USER_ACCOUNT_ABNORMAL: {
{
StatusCode: 403,
ErrorCode: MULTI_USER_ACCOUNT_ABNORMAL,
Description: "用户未实名",
Hint: "该用户无实名信息,无法领券。商家可联系微信支付或让用户联系微信支付客服处理。",
ThirdErrCode: ThirdErrCodeUserNotRealNameVerified,
},
},
MULTI_RESOURCE_NOT_EXISTS: {
{
StatusCode: 404,
ErrorCode: MULTI_RESOURCE_NOT_EXISTS,
Description: "批次不存在",
Hint: "请检查批次及制券商户号信息",
ThirdErrCode: ThirdErrCodeDefault,
},
},
}

View File

@ -0,0 +1,80 @@
package businesserr
// ErrCode 定义错误码类型
type ErrCode string
type BusinessErr struct {
Code ErrCode `json:"code"`
Message string `json:"message"`
}
func (e *BusinessErr) Error() string {
return e.Message
}
var (
BatchNotStartedError = &BusinessErr{Code: ErrCode("400"), Message: "批次未开始"}
BatchEndedError = &BusinessErr{Code: ErrCode("400"), Message: "批次已结束"}
)
func (err *BusinessErr) HandleThirdErrCode() CmbAPIError {
errCode := err.Code
emp, ok := CmbAPIPublicErrorMap[errCode]
if ok {
for _, e := range emp {
if e.Description == err.Message {
return e
}
}
}
em, ok := CmbAPIErrorMap[errCode]
if ok {
for _, e := range em {
if e.Description == err.Message {
return e
}
}
}
return CmbAPIError{
StatusCode: 499,
ErrorCode: errCode,
Description: err.Message,
Hint: "",
ThirdErrCode: ThirdErrCodeDefault,
}
}
func (err *BusinessErr) HandleMULTIThirdErrCode() CmbAPIError {
errCode := err.Code
emp, ok := CmbAPIPublicErrorMap[errCode]
if ok {
for _, e := range emp {
if e.Description == err.Message {
return e
}
}
}
em2, ok := CmbMULTIAPIErrorMap[errCode]
if ok {
for _, e := range em2 {
if e.Description == err.Message {
return e
}
}
}
return CmbAPIError{
StatusCode: 499,
ErrorCode: errCode,
Description: err.Message,
Hint: "",
ThirdErrCode: ThirdErrCodeDefault,
}
}

View File

@ -63,12 +63,13 @@ func (v *Cmb) bizContent(_ context.Context, order *bo.OrderBo, orderNotify *bo.O
}
req := &v1.CmbNotifyRequest{
Ticket: orderNotify.OrderNo,
Status: cmbStatus.GetValue(),
TransDate: "", // 格式yyyy-mm-dd hh:mm:ss.sss
OrgNo: v.bc.Cmb.OrgNo,
Attach: order.Attach,
Ext: "",
Ticket: orderNotify.OrderNo,
Status: cmbStatus.GetValue(),
TransDate: "", // 格式yyyy-mm-dd hh:mm:ss.sss
OrgNo: v.bc.Cmb.OrgNo,
Attach: order.Attach,
Ext: "",
TransactionId: order.OutBizNo, // 招行订单号
}
if cmbStatus == vo.CmbStatusUse {

View File

@ -3,16 +3,18 @@ package biz
import (
"context"
"fmt"
"time"
err2 "voucher/api/err"
"voucher/internal/biz/bo"
"voucher/internal/biz/businesserr"
"voucher/internal/biz/vo"
)
func (this *VoucherBiz) CmbOrder(ctx context.Context, req *bo.OrderCreateReqBo) (orderNo string, err error) {
func (this *VoucherBiz) CmbOrder(ctx context.Context, req *bo.OrderCreateReqBo) (*bo.OrderBo, error) {
order, err3 := this.GetByOutBizNo(ctx, req)
if err3 != nil {
return "", err3
return nil, err3
}
if order != nil {
@ -20,24 +22,32 @@ func (this *VoucherBiz) CmbOrder(ctx context.Context, req *bo.OrderCreateReqBo)
if order.Status.IsFail() || order.Status.IsIng() {
if err4 := this.orderRetry(ctx, order); err4 != nil {
return "", err4
return nil, err4
}
}
return order.OrderNo, err
return order, nil
}
product, err3 := this.ProductRepo.GetByProductNo(ctx, req.ProductNo)
if err3 != nil {
return "", err3
return nil, err3
}
nowTime := time.Now()
if nowTime.Before(*product.StartTime) {
return nil, businesserr.BatchNotStartedError
}
if nowTime.After(*product.EndTime) {
return nil, businesserr.BatchEndedError
}
order, err3 = this.order(ctx, req, product)
if err3 != nil {
return "", err3
return nil, err3
}
return order.OrderNo, nil
return order, nil
}
func (this *VoucherBiz) order(ctx context.Context, req *bo.OrderCreateReqBo, product *bo.ProductBo) (*bo.OrderBo, error) {
@ -130,8 +140,9 @@ func (this *VoucherBiz) fail(ctx context.Context, order *bo.OrderBo, errReq erro
return err
}
if errMsg == `Post "https://api.mch.weixin.qq.com/v3/marketing/favor/users/oW97fjrv_ntYBPjMsaaEMRSj6TPA/coupons": EOF` {
// 微信:不同微信号,领取了很多次,自然人限领,发放频率限制,微信研发排期,后续这个错误信息应该会更新,近期没那么快同步上去
// 微信:不同微信号,领取了很多次,自然人限领,发放频率限制,微信研发排期,后续这个错误信息应该会更新,近期没那么快同步上去
eqMsg := fmt.Sprintf(`Post "https://api.mch.weixin.qq.com/v3/marketing/favor/users/%s/coupons": EOF`, order.Account)
if errMsg == eqMsg {
return nil
}

View File

@ -26,7 +26,7 @@ type OrderRepo interface {
Success(ctx context.Context, id uint64, voucherNo string) error
Fail(ctx context.Context, id uint64, remark string) error
Used(ctx context.Context, id uint64) error
NotifyUsed(ctx context.Context, id uint64, transactionId string) error
NotifyUsed(ctx context.Context, id uint64, transactionId string, lastUseTime time.Time) error
MultiLastUsed(ctx context.Context, id uint64, lastUseTime time.Time) error
MultiOverUsed(ctx context.Context, id uint64, lastUseTime time.Time, remark string) error
Available(ctx context.Context, id uint64) error

View File

@ -84,7 +84,7 @@ func (this *VoucherBiz) notifyUsed(ctx context.Context, order *bo.OrderBo, req *
return nil
}
if err := this.OrderRepo.NotifyUsed(ctx, order.ID, req.PlainText.ConsumeInformation.TransactionID); err != nil {
if err := this.OrderRepo.NotifyUsed(ctx, order.ID, req.PlainText.ConsumeInformation.TransactionID, req.PlainText.ConsumeInformation.ConsumeTime); err != nil {
return err
}

View File

@ -549,7 +549,7 @@ func (p *OrderRepoImpl) MultiOverUsed(ctx context.Context, id uint64, lastUseTim
return nil
}
func (p *OrderRepoImpl) NotifyUsed(ctx context.Context, id uint64, transactionId string) error {
func (p *OrderRepoImpl) NotifyUsed(ctx context.Context, id uint64, transactionId string, lastUseTime time.Time) error {
now := time.Now()
tx := p.DB(ctx).
@ -560,7 +560,7 @@ func (p *OrderRepoImpl) NotifyUsed(ctx context.Context, id uint64, transactionId
Status: vo.OrderStatusUse.GetValue(),
TransactionId: transactionId,
Remark: "微信回调核销",
LastUseTime: &now,
LastUseTime: &lastUseTime,
UpdateTime: &now,
})

View File

@ -1,11 +1,14 @@
package wechatrepoimpl
import (
"encoding/json"
"errors"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"github.com/wechatpay-apiv3/wechatpay-go/core"
err2 "voucher/api/err"
"voucher/internal/biz/bo"
"voucher/internal/biz/businesserr"
"voucher/internal/biz/wechatrepo"
"voucher/internal/conf"
"voucher/internal/data"
@ -43,11 +46,20 @@ func (w *BankMultiActivityImpl) Order(order *bo.OrderBo) (couponId string, err e
var e *utils.ApiException
if errors.As(err, &e) {
apiErr, err3 := marketing.BuildErr(e.Body())
if err3 != nil {
return "", fmt.Errorf("ApiException analysis err: %+v", err3)
// 格式:{"code":"INVALID_REQUEST","message":"对应单号已超出重试期;请查单确认后决定是否换单请求"}
var beer *businesserr.BusinessErr
if err = json.Unmarshal(e.Body(), &beer); err != nil {
log.Errorf("微信错误返回body解析报错,body:%s,err:%s", string(e.Body()), err.Error())
return "", err2.ErrorWechatFAIL(fmt.Sprintf("微信错误返回内容解析错误:%s", err.Error()))
}
return "", err2.ErrorWechatFAIL("%s-%s", apiErr.Code, apiErr.Message)
//apiErr, err3 := marketing.BuildErr(e.Body())
//if err3 != nil {
// return "", fmt.Errorf("ApiException analysis err: %+v", err3)
//}
//return "", err2.ErrorWechatFAIL("%s-%s", apiErr.Code, apiErr.Message)
return "", beer
}
return "", err

View File

@ -9,14 +9,18 @@ import (
"github.com/wechatpay-apiv3/wechatpay-go/services/cashcoupons"
"io"
"net/http"
"strings"
"time"
err2 "voucher/api/err"
"voucher/internal/biz/bo"
"voucher/internal/biz/businesserr"
"voucher/internal/biz/vo"
"voucher/internal/biz/wechatrepo"
"voucher/internal/conf"
"voucher/internal/data"
"voucher/internal/pkg/helper"
"voucher/internal/pkg/request"
"voucher/internal/pkg/supplier/qixing"
)
// CpnRepoImpl .
@ -52,18 +56,19 @@ func (c *CpnRepoImpl) GetClient(ctx context.Context) (*core.Client, error) {
func (c *CpnRepoImpl) bodyErr(_ context.Context, result *core.APIResult) error {
// // 格式:{"code":"INVALID_REQUEST","message":"对应单号已超出重试期;请查单确认后决定是否换单请求"}
bodyBytes, err := io.ReadAll(result.Response.Body)
if err != nil {
return err2.ErrorWechatFAIL(fmt.Sprintf("读取微信错误返回body报错:%s", err.Error()))
}
var errBody ErrBody
if err = json.Unmarshal(bodyBytes, &errBody); err != nil {
var beer *businesserr.BusinessErr
if err = json.Unmarshal(bodyBytes, &beer); err != nil {
log.Errorf("微信错误返回body解析报错,body:%s,err:%s", string(bodyBytes), err.Error())
return err2.ErrorWechatFAIL(fmt.Sprintf("微信错误返回内容解析错误:%s", err.Error()))
}
return errBody.GetWechatError()
return beer
}
func (c *CpnRepoImpl) Order(ctx context.Context, order *bo.OrderBo) (string, error) {
@ -97,12 +102,85 @@ func (c *CpnRepoImpl) Order(ctx context.Context, order *bo.OrderBo) (string, err
return *resp.CouponId, nil
}
func (c *CpnRepoImpl) Query(ctx context.Context, orderWechat *bo.OrderBo) (vo.OrderStatus, error) {
func (c *CpnRepoImpl) Query(ctx context.Context, order *bo.OrderBo) (vo.OrderStatus, error) {
// todo 确认下,多笔立减金用普通立减金的接口查询也能查,结果是准确的吗
// 福州启蒙 - 启星
if order.MerchantNo == "1715349578" {
//return c.QxQuery(ctx, order)
}
return c.LsxdQuery(ctx, order)
}
func (c *CpnRepoImpl) QxQuery(ctx context.Context, order *bo.OrderBo) (vo.OrderStatus, error) {
if order.ActivityId == "" {
return 0, fmt.Errorf("商户号 %s 只支持多笔立减金查询", order.MerchantNo)
}
b := qixing.QxQueryReq{
OrderNo: order.OrderNo,
CouponId: order.VoucherNo,
OpenId: order.Account,
}
var strToBeSigned strings.Builder
kvRows := helper.SortStructJsonTag(b)
for _, kv := range kvRows {
if kv.Key == "sign" || kv.Value == "" {
continue
}
strToBeSigned.WriteString(fmt.Sprintf("%s=%s&", kv.Key, kv.Value))
}
s := strToBeSigned.String() + "config.AppKey"
b.Sign = helper.Md5(s)
body, err := json.Marshal(b)
if err != nil {
return 0, err
}
isSuccess := func(code int) bool {
return code == http.StatusOK
}
h := http.Header{
"Content-Type": []string{"application/json"},
}
_, body, err = request.Post(
ctx,
c.bc.WechatNotifyMQ.RegisterTagUrl,
body,
request.WithHeaders(h),
request.WithStatusCodeFunc(isSuccess),
request.WithTimeout(time.Second*10),
)
if err != nil {
return 0, err
}
var resp qixing.QxQueryResp
if err := json.Unmarshal(body, &resp); err != nil {
return 0, err
}
// 统一返回状态类型
return resp.Data.CouponState.GetStatus()
}
func (c *CpnRepoImpl) LsxdQuery(ctx context.Context, order *bo.OrderBo) (vo.OrderStatus, error) {
// 需要判断是多笔立减金查询还是普通立减金查询,此处需要更正查询方式
req := cashcoupons.QueryCouponRequest{
CouponId: core.String(orderWechat.VoucherNo),
Appid: core.String(orderWechat.AppID),
Openid: core.String(orderWechat.Account),
CouponId: core.String(order.VoucherNo),
Appid: core.String(order.AppID),
Openid: core.String(order.Account),
}
client, err := c.GetClient(ctx)

View File

@ -1,39 +1 @@
package wechatrepoimpl
import (
"errors"
errors2 "github.com/go-kratos/kratos/v2/errors"
"gorm.io/gorm"
"testing"
)
func TestGetErrorByDescription(t *testing.T) {
e := &ErrBody{
Code: "INVALID_REQUEST",
Message: "活动已结束或未激活",
}
t.Log(e.GetWechatError())
err := gorm.ErrRecordNotFound
t.Log(errors.Is(err, gorm.ErrRecordNotFound))
}
func TestErr(t *testing.T) {
e := &ErrBody{
Code: "INVALID_REQUEST",
Message: "活动已结束或未激活",
}
se := errors2.FromError(e.GetWechatError())
t.Log(se.Reason)
t.Log(se.Message)
e2 := errors.New("活动已结束或未激活")
se2 := errors2.FromError(e2)
t.Log(se2.Reason)
t.Log(len(se2.Reason))
t.Log(se2.Message)
}

View File

@ -0,0 +1,53 @@
package helper
import (
"fmt"
"reflect"
"sort"
)
type KeyValue struct {
Key string
Value string
}
func SortStruct(data any) []KeyValue {
v := reflect.ValueOf(data).Elem()
t := v.Type()
var kv []KeyValue
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
key := LowercaseFirstLetter(t.Field(i).Name)
value := fmt.Sprintf("%v", field.Interface())
kv = append(kv, KeyValue{Key: key, Value: value})
}
sort.SliceStable(kv, func(i, j int) bool {
return kv[i].Key < kv[j].Key
})
return kv
}
func SortStructJsonTag(data any) []KeyValue {
// 获取 data 结构体的类型和值
dataType := reflect.TypeOf(data).Elem()
dataValue := reflect.ValueOf(data).Elem()
var kv []KeyValue
for i := 0; i < dataType.NumField(); i++ {
field := dataType.Field(i)
// 获取字段的值
fieldValue := dataValue.FieldByName(field.Name).Interface()
// 获取 JSON 字段名
jsonTagName := field.Tag.Get("json")
if jsonTagName != "" {
kv = append(kv, KeyValue{Key: jsonTagName, Value: fmt.Sprintf("%v", fieldValue)})
}
}
// 排序
sort.SliceStable(kv, func(i, j int) bool {
return kv[i].Key < kv[j].Key
})
return kv
}

View File

@ -1,6 +1,8 @@
package bo
package qixing
import "github.com/go-playground/validator/v10"
import (
"github.com/go-playground/validator/v10"
)
type QiXingRequestBo struct {
Content string `json:"content" validate:"required"`

View File

@ -0,0 +1,59 @@
package qixing
import (
"fmt"
"time"
"voucher/internal/biz/vo"
)
type CouponState string
const (
CouponStateUnknown CouponState = "COUPON_STATE_UNKNOW" // 未知状态
CouponStateSend CouponState = "COUPON_STATE_SEND" // 可用
CouponStateUsed CouponState = "COUPON_STATE_USED" // 已实扣
CouponStateExpired CouponState = "COUPON_STATE_EXPIRED" // 已过期
)
var CpnStatusMap = map[CouponState]vo.OrderStatus{
CouponStateSend: vo.OrderStatusSuccess,
CouponStateUsed: vo.OrderStatusUse,
CouponStateExpired: vo.OrderStatusExpired,
}
func (o CouponState) GetStatus() (vo.OrderStatus, error) {
if resultStatus, ok := CpnStatusMap[o]; ok {
return resultStatus, nil
}
return 0, fmt.Errorf("CpnStatus[%s]未定义", o)
}
type QxQueryReq struct {
OrderNo string `json:"order_no"`
CouponId string `json:"coupon_id"`
OpenId string `json:"open_id"`
Sign string `json:"sign"`
}
type QxQueryResp struct {
Code any `json:"code"`
Msg string `json:"msg"`
Data *struct {
StockCreatorMchID string `json:"stock_creator_mchid"`
StockID string `json:"stock_id"`
CouponID string `json:"coupon_id"`
CouponName string `json:"coupon_name"`
CouponState CouponState `json:"coupon_state"`
ReceiveTime time.Time `json:"receive_time"`
AvailableBeginTime time.Time `json:"available_begin_time"`
AvailableEndTime time.Time `json:"available_end_time"`
ActivityID string `json:"activity_id"`
MaxUseNumber int `json:"max_use_number"`
AvailableNumber int `json:"available_number"`
UsedNumber int `json:"used_number"`
UseAmountList struct {
UsedAmounts []string `json:"used_amounts"`
} `json:"use_amount_list"`
OpenID string `json:"openid"`
} `json:"data"`
}

View File

@ -3,29 +3,28 @@ package service
import (
"context"
"encoding/json"
"github.com/go-kratos/kratos/v2/errors"
err2 "voucher/api/err"
v1 "voucher/api/v1"
"voucher/internal/biz/bo"
"voucher/internal/biz/businesserr"
"voucher/internal/biz/vo"
)
func (c *CmbService) Order(ctx context.Context, request *v1.CmbRequest) (*v1.CmbReply, error) {
orderNo, err := c.order(ctx, request)
order, err := c.order(ctx, request)
if err != nil {
return c.OrderFail(ctx, err)
return c.OrderFail(ctx, order, err)
}
return c.OrderSuccess(ctx, orderNo)
return c.OrderSuccess(ctx, order.OrderNo)
}
func (c *CmbService) order(ctx context.Context, request *v1.CmbRequest) (string, error) {
func (c *CmbService) order(ctx context.Context, request *v1.CmbRequest) (*bo.OrderBo, error) {
bizContent, err := c.CmbMixRepo.OrderVerify(ctx, request)
if err != nil {
return "", err
return nil, err
}
boReq := &bo.OrderCreateReqBo{
@ -39,12 +38,12 @@ func (c *CmbService) order(ctx context.Context, request *v1.CmbRequest) (string,
NotifyUrl: c.bc.Cmb.NotifyUrl,
}
orderNo, err := c.VoucherBiz.CmbOrder(ctx, boReq)
order, err := c.VoucherBiz.CmbOrder(ctx, boReq)
if err != nil {
return "", err
return nil, err
}
return orderNo, nil
return order, nil
}
func (c *CmbService) OrderSuccess(ctx context.Context, orderNo string) (*v1.CmbReply, error) {
@ -60,19 +59,37 @@ func (c *CmbService) OrderSuccess(ctx context.Context, orderNo string) (*v1.CmbR
return c.GetResponse(ctx, replyBizContent)
}
func (c *CmbService) OrderFail(ctx context.Context, err error) (*v1.CmbReply, error) {
func (c *CmbService) OrderFail(ctx context.Context, order *bo.OrderBo, err error) (*v1.CmbReply, error) {
se := errors.FromError(err)
bizReply := &v1.CmbOrderReply{}
if len(se.Reason) == 0 {
se.Reason = err2.CmbErr_CMB_UNKNOWN.String()
}
if e, ok := err.(*businesserr.BusinessErr); ok {
bizReply := &v1.CmbOrderReply{
RespCode: vo.CmbResponseStatusFail.GetValue(),
RespMsg: se.Message,
CodeNo: "",
ThirdErrCode: se.Reason,
if order.ActivityId != "" {
cmbAPIError := e.HandleMULTIThirdErrCode()
bizReply = &v1.CmbOrderReply{
RespCode: vo.CmbResponseStatusFail.GetValue(),
RespMsg: cmbAPIError.Description,
CodeNo: "",
ThirdErrCode: string(cmbAPIError.ThirdErrCode),
}
} else {
cmbAPIError := e.HandleThirdErrCode()
bizReply = &v1.CmbOrderReply{
RespCode: vo.CmbResponseStatusFail.GetValue(),
RespMsg: cmbAPIError.Description,
CodeNo: "",
ThirdErrCode: string(cmbAPIError.ThirdErrCode),
}
}
} else {
bizReply = &v1.CmbOrderReply{
RespCode: vo.CmbResponseStatusFail.GetValue(),
RespMsg: err.Error(),
CodeNo: "",
ThirdErrCode: string(businesserr.ThirdErrCodeDefault),
}
}
replyBizContent, _ := json.Marshal(bizReply)

View File

@ -12,6 +12,7 @@ import (
"voucher/internal/biz/bo"
"voucher/internal/conf"
"voucher/internal/pkg/helper"
"voucher/internal/pkg/supplier/qixing"
)
type TripartiteService struct {
@ -40,7 +41,7 @@ func (srv *TripartiteService) QiXingNotify(ctx http.Context) error {
return srv.ResponseErr(ctx, fmt.Sprintf("read body error: %v", err))
}
var req *bo.QiXingRequestBo
var req *qixing.QiXingRequestBo
if err = json.Unmarshal(bodyBytes, &req); err != nil {
log.Errorf("qixing notify bodyBytes err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes))
return srv.ResponseErr(ctx, fmt.Sprintf("json unmarshal bodyBytes error: %v", err))
@ -89,13 +90,13 @@ func (srv *TripartiteService) QiXingNotify(ctx http.Context) error {
}
func (this *TripartiteService) ResponseOK(ctx http.Context) error {
return ctx.JSON(http2.StatusOK, &bo.QiXingResponse{
return ctx.JSON(http2.StatusOK, &qixing.QiXingResponse{
Msg: "SUCCESS",
})
}
func (this *TripartiteService) ResponseErr(ctx http.Context, msg string) error {
return ctx.JSON(http2.StatusOK, &bo.QiXingResponse{
return ctx.JSON(http2.StatusOK, &qixing.QiXingResponse{
Msg: msg,
})
}

View File

@ -3,10 +3,13 @@ package test
import (
"encoding/base64"
"encoding/json"
"errors"
"testing"
"time"
"voucher/internal/biz/bo"
"voucher/internal/biz/businesserr"
"voucher/internal/pkg/helper"
"voucher/internal/pkg/supplier/qixing"
"voucher/internal/pkg/wechat/utils"
)
func Test_MarketingSend(t *testing.T) {
@ -25,22 +28,39 @@ func Test_MarketingSend(t *testing.T) {
}
func Test_MarketingQuery(t *testing.T) {
appId := "wx619991cc795028f5"
openId := "oSNb4fixFZ4uRvcP6F25_FySgUE0"
couponId := "147079366189"
tests := []struct {
name string
appId, openId, couponId string
}{
{
name: "查询绑定多笔立减活动的代金券详情", // 查询的商户非创建方商户 查询商户要为创建方商户
appId: "wx619991cc795028f5",
openId: "oSNb4ftgnWC22Z0cWTjsQebdr2Yk",
couponId: "139923450432",
appId: appId,
openId: openId,
couponId: couponId,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := MarketingQuery(tt.appId, tt.openId, tt.couponId)
if err != nil {
t.Errorf("MarketingQuery() error = %v", err)
var e *utils.ApiException
if errors.As(err, &e) {
// 格式:{"code":"INVALID_REQUEST","message":"对应单号已超出重试期;请查单确认后决定是否换单请求"}
var beer *businesserr.BusinessErr
if err = json.Unmarshal(e.Body(), &beer); err != nil {
t.Errorf("微信错误返回body解析报错,body:%s,err:%s", string(e.Body()), err.Error())
}
t.Logf("MarketingQuery error,body:%s,err:%s", string(e.Body()), e.ErrCode)
} else {
t.Errorf("MarketingQuery() error = %v", err)
}
return
}
t.Logf("MarketingQuery() = %v", resp)
@ -78,7 +98,7 @@ func Test_QixingNotifyData(t *testing.T) {
ciphertext := helper.Md5(content + "DrY1zLkOH01q0sN66vrmkdpbWsyb41ho")
req := bo.QiXingRequestBo{
req := qixing.QiXingRequestBo{
Content: content,
Timestamp: time.Now().UnixMilli(),
Ciphertext: ciphertext,

166
test/cmb_test.go Normal file
View File

@ -0,0 +1,166 @@
package test
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"testing"
"time"
v1 "voucher/api/v1"
"voucher/internal/biz/vo"
"voucher/internal/pkg/cmb"
"voucher/internal/pkg/helper"
"voucher/internal/pkg/request"
)
func Test_CMBRequest(t *testing.T) {
bizParma := &v1.CmbNotifyRequest{
Ticket: "190662195271022592015", // 优惠券券码codeNo
Status: "0", // 更新后串码状态0可使用1已使用
TransDate: "", // 验码日期格式yyyy-mm-dd hh:mm:ss.sss
OrgNo: "LANSEXIONGDI", // 固定值
Ext: "", // 扩展字段
Attach: "bFUWzuzvJfBshobjcQPFTvpqcH1AvGqHJtiV44mdsWEQKCPXgydk8ft/b227S3TM", // 扩展字段
TransactionId: "2000000000001422730", // 招行唯一流水号
}
bizParmaJsonBytes, err := json.Marshal(bizParma)
if err != nil {
t.Error(err)
return
}
encryptBody, err := cmb.EncryptBody(&cmb.Encrypts{SoaPubKey: "0416445bc16cbf42e47002ad9fe7c7af67d902b48be1eb69b98f6a006b0918630e1127f5f2fff83b2ecb30fc7fd72c34c33f37c7c355dffde3589f66800f0036ca", JsonParam: string(bizParmaJsonBytes)})
if err != nil {
t.Error(err)
return
}
req := &v1.CmbRequest{
Mid: "d6fdd78b6fd13a808818286b9cad9687",
Aid: "5efaa21263b94f669a1c90ed0279df20",
Date: time.Now().Format("20060102150405"),
Random: string(cmb.RandomBytes(16)),
KeyAlias: "CO_PUB_KEY_SM2",
CmbKeyAlias: "SM2_CMBLIFE",
EncryptBody: encryptBody,
Sign: "",
}
str := fmt.Sprintf("%s?%s", vo.CmbNotifyFuncName, cmb.SortStructStr(req))
sign, err := cmb.SignBody(str, "8d39ff3d2559258c163f4510f082727f51531e1953ab203d5ab1ea4a6d94fd73")
if err != nil {
t.Error(err)
return
}
req.Sign = sign
kvRows := helper.SortStructFieldsByKey(req)
uv := url.Values{}
for _, kv := range kvRows {
uv.Set(kv.Key, fmt.Sprintf("%v", kv.Value))
}
h := http.Header{
"Content-Type": []string{"application/x-www-form-urlencoded"},
}
uri := "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatus.json"
r := uri + "?" + uv.Encode()
respHeader, bodyBytes, err := request.Post(context.Background(), r, nil, request.WithHeaders(h), request.WithTimeout(time.Second*10))
if err != nil {
t.Error(err)
return
}
t.Logf("请求地址:%s", uri)
t.Logf("业务参数:%s", string(bizParmaJsonBytes))
t.Logf("请求响应体:%s", string(bodyBytes))
t.Logf("请求响应头:%+v", respHeader)
}
func Test_CmMultiRequest(t *testing.T) {
bizParma := &v1.CmbMultiNotifyRequest{
TransactionId: "2000000000001636670", // 招行唯一流水号
ActivityId: "11941580000000008",
CouponId: "118770338167",
AcquiredDate: "2025-08-25 14:56:54.123",
Status: "1", // 更新后串码状态0可使用1已使用
CouponStatus: "1", // String|M 整张券总状态0可使用1已使用
TransDate: "2025-08-28 12:09:41.123", // 验码日期格式yyyy-mm-dd hh:mm:ss.sss
TransAmount: "10",
OrderId: "4200002772202508280813272296",
Ticket: "195987259358664294429", // 优惠券券码codeNo
OrgNo: "LANSEXIONGDI", // 固定值
Attach: "bFUWzuzvJfBshobjcQPFTvpqcH1AvGqHJtiV44mdsWEQKCPXgydk8ft/b227S3TM", // 扩展字段
Ext: "", // 扩展字段
}
bizParmaJsonBytes, err := json.Marshal(bizParma)
if err != nil {
t.Error(err)
return
}
encryptBody, err := cmb.EncryptBody(&cmb.Encrypts{SoaPubKey: "0416445bc16cbf42e47002ad9fe7c7af67d902b48be1eb69b98f6a006b0918630e1127f5f2fff83b2ecb30fc7fd72c34c33f37c7c355dffde3589f66800f0036ca", JsonParam: string(bizParmaJsonBytes)})
if err != nil {
t.Error(err)
return
}
req := &v1.CmbRequest{
Mid: "d6fdd78b6fd13a808818286b9cad9687",
Aid: "5efaa21263b94f669a1c90ed0279df20",
Date: time.Now().Format("20060102150405"),
Random: string(cmb.RandomBytes(16)),
KeyAlias: "CO_PUB_KEY_SM2",
CmbKeyAlias: "SM2_CMBLIFE",
EncryptBody: encryptBody,
Sign: "",
}
str := fmt.Sprintf("%s?%s", vo.CmbNotifyFuncNameUpdateCodeStatusForMulti, cmb.SortStructStr(req))
sign, err := cmb.SignBody(str, "8d39ff3d2559258c163f4510f082727f51531e1953ab203d5ab1ea4a6d94fd73")
if err != nil {
t.Error(err)
return
}
req.Sign = sign
kvRows := helper.SortStructFieldsByKey(req)
uv := url.Values{}
for _, kv := range kvRows {
uv.Set(kv.Key, fmt.Sprintf("%v", kv.Value))
}
h := http.Header{
"Content-Type": []string{"application/x-www-form-urlencoded"},
}
uri := "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatusForMulti.json"
r := uri + "?" + uv.Encode()
respHeader, bodyBytes, err := request.Post(context.Background(), r, nil, request.WithHeaders(h), request.WithTimeout(time.Second*10))
if err != nil {
t.Error(err)
return
}
t.Logf("请求地址:%s", uri)
t.Logf("业务参数:%s", string(bizParmaJsonBytes))
t.Logf("请求响应体:%s", string(bodyBytes))
t.Logf("请求响应头:%+v", respHeader)
}

View File

@ -10,6 +10,7 @@ import (
"os"
"path/filepath"
"time"
"voucher/internal/biz/businesserr"
"voucher/internal/biz/do"
"voucher/internal/conf"
"voucher/internal/data"
@ -33,14 +34,13 @@ var bc2 = &conf.Bootstrap{
},
}
func SendCoupon() {
func SendCoupon() error {
ctx := context.Background()
dir, err := os.Getwd()
if err != nil {
fmt.Printf("os.Getwd() error = %v", err)
return
return fmt.Errorf("os.Getwd() error = %v", err)
}
parentDir := filepath.Dir(dir)
@ -52,8 +52,7 @@ func SendCoupon() {
}
client, err := data.GetClient(ctx, server)
if err != nil {
fmt.Println(err)
return
return fmt.Errorf("data.GetClient(ctx, server) error = %v", err)
}
req := cashcoupons.SendCouponRequest{
@ -62,7 +61,7 @@ func SendCoupon() {
Appid: core.String("wxd27e255810842ba8"),
Openid: core.String("o3dEt5b_1lFtKc-aAT3tiYjJIGwk"),
StockId: core.String("21502886"),
StockCreatorMchid: core.String("1652465541"),
StockCreatorMchid: core.String("16524655411111"),
}
fmt.Printf("\nreq:%+v", req)
@ -71,15 +70,26 @@ func SendCoupon() {
resp, result, err := svc.SendCoupon(ctx, req)
if err != nil {
fmt.Println(err)
return
bodyBytes, err := io.ReadAll(result.Response.Body)
if err != nil {
return fmt.Errorf("读取微信错误返回body报错:%s", err.Error())
}
fmt.Printf("\nbodyBytes:%s\n", string(bodyBytes))
var beer *businesserr.BusinessErr
if err = json.Unmarshal(bodyBytes, &beer); err != nil {
return fmt.Errorf("微信错误返回body解析报错,body:%s,err:%s", string(bodyBytes), err.Error())
}
return beer
}
fmt.Printf("\nresp:%+v\n result:%+v", resp, result)
return
return nil
}
func QueryCoupon() {
func QueryCoupon(appId, openId, couponId string) {
ctx := context.Background()
@ -102,10 +112,6 @@ func QueryCoupon() {
return
}
appId := "wx619991cc795028f5"
openId := "oSNb4fnWoktz7YVNIXE5bvoB3-1w"
couponId := "106048490308"
req := cashcoupons.QueryCouponRequest{
CouponId: core.String(couponId),
Appid: core.String(appId),

View File

@ -2,6 +2,7 @@ package test
import (
"testing"
"voucher/internal/biz/businesserr"
)
func Test_SendCoupon(t *testing.T) {
@ -14,22 +15,38 @@ func Test_SendCoupon(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
SendCoupon()
if err := SendCoupon(); err != nil {
if ee, ok := err.(*businesserr.BusinessErr); ok {
t.Errorf("errorcode:%s,errmsg:%s", ee.Code, ee.Message)
}
}
})
}
}
func Test_QueryCoupon(t *testing.T) {
tests := []struct {
name string
name string
appId string
openId string
couponId string
}{
{
name: "查询券订单信息",
name: "查询券订单信息",
appId: "wx619991cc795028f5",
openId: "oSNb4fnRWr7HdgbDOO8TD66LXofE",
couponId: "147089832782",
},
{
name: "查询券订单信息",
appId: "wx619991cc795028f5",
openId: "oSNb4foo87dBNx1D9KTH-bB-G6YA",
couponId: "147094824239",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
QueryCoupon()
QueryCoupon(tt.appId, tt.openId, tt.couponId)
})
}
}
@ -64,3 +81,34 @@ func Test_QueryCallback(t *testing.T) {
})
}
}
func TestGetErrorByDescription(t *testing.T) {
//e := &businesserr.ErrBody{
// Code: "INVALID_REQUEST",
// Message: "活动已结束或未激活",
//}
//t.Log(e.GetWechatError())
//
//err := gorm.ErrRecordNotFound
//
//t.Log(errors.Is(err, gorm.ErrRecordNotFound))
}
func TestErr(t *testing.T) {
//e := &businesserr.ErrBody{
// Code: "INVALID_REQUEST",
// Message: "活动已结束或未激活",
//}
//
//se := errors2.FromError(e.GetWechatError())
//
//t.Log(se.Reason)
//t.Log(se.Message)
//
//e2 := errors.New("活动已结束或未激活")
//se2 := errors2.FromError(e2)
//
//t.Log(se2.Reason)
//t.Log(len(se2.Reason))
//t.Log(se2.Message)
}