From 0ed61a0f2cfe0cedc5e47c7df947f216d02d94a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=AD=90=E9=93=AD?= Date: Fri, 14 Mar 2025 20:18:10 +0800 Subject: [PATCH] notice --- api/err/wechat.proto | 27 +- api/v1/cmb_cpn.proto | 2 +- internal/biz/cmb.go | 3 +- internal/data/wechat.go | 4 +- internal/data/wechatrepoimpl/cpn.go | 29 +- internal/data/wechatrepoimpl/cpn_code.go | 299 ++++++++++++++++++ internal/data/wechatrepoimpl/cpn_code_test.go | 13 + internal/data/wechatrepoimpl/wechat.go | 7 - 8 files changed, 359 insertions(+), 25 deletions(-) create mode 100644 internal/data/wechatrepoimpl/cpn_code.go create mode 100644 internal/data/wechatrepoimpl/cpn_code_test.go delete mode 100644 internal/data/wechatrepoimpl/wechat.go diff --git a/api/err/wechat.proto b/api/err/wechat.proto index 0f88649..442c1dd 100644 --- a/api/err/wechat.proto +++ b/api/err/wechat.proto @@ -6,6 +6,29 @@ option go_package = "voucher/api/err"; enum WechatErr{ option (errors.default_code) = 1; - // 微信失败 - WECHAT_FAIL = 0 [(errors.code) = 500]; + WechatFAIL = 0 [(errors.code) = 500]; + + WechatUserIllegal = 1 [(errors.code) = 500]; + WechatMismatch = 2 [(errors.code) = 500]; + WechatInvalidMerchantID = 4 [(errors.code) = 500]; + WechatHighFrequency = 5 [(errors.code) = 500]; + WechatActivityInactive = 6 [(errors.code) = 500]; + WechatBatchInfoError = 7 [(errors.code) = 500]; + WechatParamRequired = 8 [(errors.code) = 500]; + WechatInvalidBatchStatus = 10 [(errors.code) = 500]; + WechatMchNotExists = 11 [(errors.code) = 500]; + WechatBatchBudgetInsufficient = 9 [(errors.code) = 500]; + WechatDailyLimitExceeded = 12 [(errors.code) = 500]; + WechatAccountBalanceInsufficient = 13 [(errors.code) = 500]; + WechatBatchBudgetDepleted = 14 [(errors.code) = 500]; + WechatMerchantNoPermission = 15 [(errors.code) = 500]; + WechatCrossMerchantNotSupported = 16 [(errors.code) = 500]; + WechatUserReceiveLimit = 17 [(errors.code) = 500]; + WechatAPIChannelNotSupported = 18 [(errors.code) = 500]; + WechatSpecifiedDenominationNotSupported = 19 [(errors.code) = 500]; + WechatOnlyAdvertisingBatch = 20 [(errors.code) = 500]; + WechatUserMaxCoupons = 21 [(errors.code) = 500]; + WechatNaturalPersonRuleBlocked = 22 [(errors.code) = 500]; + WechatResourceNotExists = 23 [(errors.code) = 500]; + WechatFrequencyLimited = 24 [(errors.code) = 500]; } diff --git a/api/v1/cmb_cpn.proto b/api/v1/cmb_cpn.proto index f4ceb61..39e9daf 100644 --- a/api/v1/cmb_cpn.proto +++ b/api/v1/cmb_cpn.proto @@ -61,11 +61,11 @@ message CmbOrderRequest { string attach = 15 [json_name = "attach"]; } message CmbOrderReply { + // 业务参数 // 接口调用返回码,1000 成功,1001 失败 string respCode = 1 [json_name = "respCode"]; // 返回话术,失败信息落此字段 string respMsg = 2 [json_name = "respMsg"]; - // 业务参数 // 权益标识,优惠券券码,最大长度为50位 string codeNo = 9 [json_name = "codeNo"]; // 错误码 diff --git a/internal/biz/cmb.go b/internal/biz/cmb.go index c7ff300..a8bcc75 100644 --- a/internal/biz/cmb.go +++ b/internal/biz/cmb.go @@ -6,6 +6,7 @@ import ( "fmt" "gorm.io/gorm" "time" + err2 "voucher/api/err" v1 "voucher/api/v1" "voucher/internal/biz/bo" "voucher/internal/biz/vo" @@ -42,7 +43,7 @@ func (v *VoucherBiz) CmbOrder(ctx context.Context, req *bo.OrderCreateReqBo) (vo } if !product.Channel.IsWeChat() { - return fmt.Errorf("只支持微信") + return err2.ErrorCmbProductNotSupported("只支持微信") } order, err = v.order(ctx, req, product) diff --git a/internal/data/wechat.go b/internal/data/wechat.go index 166799b..86330fb 100644 --- a/internal/data/wechat.go +++ b/internal/data/wechat.go @@ -32,7 +32,7 @@ func (c *Server) Validate() error { return nil } -func newClient(ctx context.Context, c Server) (*core.Client, error) { +func NewClient(ctx context.Context, c Server) (*core.Client, error) { dir, err := os.Getwd() if err != nil { @@ -101,7 +101,7 @@ func GetClient(ctx context.Context, c Server) (*core.Client, error) { instance.once.Do(func() { - i, err2 := newClient(ctx, c) + i, err2 := NewClient(ctx, c) if err2 != nil { err = err2 return diff --git a/internal/data/wechatrepoimpl/cpn.go b/internal/data/wechatrepoimpl/cpn.go index 0b2d992..b3e97f3 100644 --- a/internal/data/wechatrepoimpl/cpn.go +++ b/internal/data/wechatrepoimpl/cpn.go @@ -64,11 +64,12 @@ func (c *CpnRepoImpl) Order(ctx context.Context, order *bo.OrderBo) (string, err log.Errorf("请求微信返回错误:%s", string(bodyBytes)) - if err = json.Unmarshal(bodyBytes, &ErrBody); err != nil { + var errBody ErrBody + if err = json.Unmarshal(bodyBytes, &errBody); err != nil { return "", err } - return "", fmt.Errorf(ErrBody.Message) + return "", errBody.GetWechatError() } if result.Response.StatusCode != CodeSuccess { @@ -103,16 +104,17 @@ func (c *CpnRepoImpl) Query(ctx context.Context, orderWechat *bo.OrderBo) (vo.Or log.Errorf("请求微信返回错误:%s", string(bodyBytes)) - if err = json.Unmarshal(bodyBytes, &ErrBody); err != nil { + var errBody ErrBody + if err = json.Unmarshal(bodyBytes, &errBody); err != nil { return 0, err } - return 0, fmt.Errorf("微信返回错误:%s", ErrBody.Message) + return 0, errBody.GetWechatError() } if result.Response.StatusCode != CodeSuccess { - return 0, fmt.Errorf("微信返回错误 Response StatusCode[%s]", result.Response.StatusCode) + return 0, fmt.Errorf("微信返回错误 Response StatusCode[%d]", result.Response.StatusCode) } return CpnStatus(*resp.Status).GetStatus() @@ -138,15 +140,16 @@ func (c *CpnRepoImpl) QueryProduct(ctx context.Context, stockCreatorMchId, stock return nil, err } - if err = json.Unmarshal(bodyBytes, &ErrBody); err != nil { + var errBody ErrBody + if err = json.Unmarshal(bodyBytes, &errBody); err != nil { return nil, err } - return nil, fmt.Errorf("微信返回错误:%s", ErrBody.Message) + return nil, errBody.GetWechatError() } if result.Response.StatusCode != CodeSuccess { - return nil, fmt.Errorf("查询活动微信返回错误 Response StatusCode[%s]", result.Response.StatusCode) + return nil, fmt.Errorf("查询活动微信返回错误 Response StatusCode[%d]", result.Response.StatusCode) } return response, nil @@ -183,11 +186,12 @@ func (c *CpnRepoImpl) QueryCallback(ctx context.Context, mchId, mchCertNo string return nil, err } - if err = json.Unmarshal(bodyBytes, &ErrBody); err != nil { + var errBody ErrBody + if err = json.Unmarshal(bodyBytes, &errBody); err != nil { return nil, err } - return nil, fmt.Errorf("微信返回错误:%s", ErrBody.Message) + return nil, errBody.GetWechatError() } if result.Response.StatusCode != CodeSuccess { @@ -230,11 +234,12 @@ func (c *CpnRepoImpl) SetCallback(ctx context.Context, mchId, mchCertNo, url str return nil, err } - if err = json.Unmarshal(bodyBytes, &ErrBody); err != nil { + var errBody ErrBody + if err = json.Unmarshal(bodyBytes, &errBody); err != nil { return nil, err } - return nil, fmt.Errorf("微信返回错误:%s", ErrBody.Message) + return nil, fmt.Errorf("微信返回错误:%s", errBody.Message) } if result.Response.StatusCode != CodeSuccess { diff --git a/internal/data/wechatrepoimpl/cpn_code.go b/internal/data/wechatrepoimpl/cpn_code.go new file mode 100644 index 0000000..38ac951 --- /dev/null +++ b/internal/data/wechatrepoimpl/cpn_code.go @@ -0,0 +1,299 @@ +package wechatrepoimpl + +import ( + "fmt" + "strings" + + "github.com/go-kratos/kratos/v2/errors" + err2 "voucher/api/err" +) + +// ErrCode 定义错误码类型 +// https://pay.weixin.qq.com/doc/v3/merchant/4012463767 +type ErrCode string + +// 定义错误码常量 +const ( + APPID_MCHID_NOT_MATCH ErrCode = "APPID_MCHID_NOT_MATCH" + INVALID_REQUEST ErrCode = "INVALID_REQUEST" + PARAM_ERROR ErrCode = "PARAM_ERROR" + MCH_NOT_EXISTS ErrCode = "MCH_NOT_EXISTS" + NOT_ENOUGH ErrCode = "NOT_ENOUGH" + REQUEST_BLOCKED ErrCode = "REQUEST_BLOCKED" + RULE_LIMIT ErrCode = "RULE_LIMIT" + USER_ACCOUNT_ABNORMAL ErrCode = "USER_ACCOUNT_ABNORMAL" + RESOURCE_NOT_EXISTS ErrCode = "RESOURCE_NOT_EXISTS" + 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"` +} + +// 定义错误映射,方便根据错误码获取错误信息 +var _ = map[ErrCode][]APIError{ + APPID_MCHID_NOT_MATCH: { + { + StatusCode: 400, + ErrorCode: APPID_MCHID_NOT_MATCH, + Description: "商户号与AppID不匹配", + Hint: "调用接口的商户号需与接口传入的AppID有绑定关系,请参考常见问题Q4", + }, + }, + INVALID_REQUEST: { + { + StatusCode: 400, + ErrorCode: INVALID_REQUEST, + Description: "OpenID与AppID不匹配", + Hint: "OpenID与AppID需有对应关系", + }, + { + StatusCode: 400, + ErrorCode: INVALID_REQUEST, + Description: "非法的商户号", + Hint: "请检查商户号准确性", + }, + { + StatusCode: 400, + ErrorCode: INVALID_REQUEST, + Description: "调用频率过高", + Hint: "请降低API调用频率", + }, + { + StatusCode: 400, + ErrorCode: INVALID_REQUEST, + Description: "活动已结束或未激活", + Hint: "请检查批次状态", + }, + { + StatusCode: 400, + ErrorCode: INVALID_REQUEST, + Description: "批次信息获取失败,请确认参数是否有误", + Hint: "请检查创建商户号与批次号的对应关系", + }, + }, + PARAM_ERROR: { + { + StatusCode: 400, + ErrorCode: PARAM_ERROR, + Description: "AppID必填", + Hint: "请输入AppID", + }, + { + StatusCode: 400, + ErrorCode: PARAM_ERROR, + Description: "OpenID必填", + Hint: "请输入OpenID", + }, + { + StatusCode: 400, + ErrorCode: PARAM_ERROR, + Description: "批次号必填", + Hint: "请输入批次号", + }, + { + StatusCode: 400, + ErrorCode: PARAM_ERROR, + Description: "商户号必填", + Hint: "请输入商户号", + }, + { + StatusCode: 400, + ErrorCode: PARAM_ERROR, + Description: "非法的批次状态", + Hint: "请检查批次状态,仅支持发放状态为“运营中”的代金券批次", + }, + }, + MCH_NOT_EXISTS: { + { + StatusCode: 403, + ErrorCode: MCH_NOT_EXISTS, + Description: "商户号不合法", + Hint: "请检查商户号准确性", + }, + }, + NOT_ENOUGH: { + { + StatusCode: 403, + ErrorCode: NOT_ENOUGH, + Description: "批次预算不足", + Hint: "批次预算已发放完,请补充批次预算", + }, + { + StatusCode: 403, + ErrorCode: NOT_ENOUGH, + Description: "发券超过单天限额", + Hint: "已超过该批次设置的单天发放限制额度,无法发放", + }, + { + StatusCode: 403, + ErrorCode: NOT_ENOUGH, + Description: "账户余额不足,请充值", + Hint: "商户号余额不足,无法继续发券,请充值", + }, + { + StatusCode: 403, + ErrorCode: NOT_ENOUGH, + Description: "批次预算耗尽", + Hint: "该批次的预算已经耗尽", + }, + }, + REQUEST_BLOCKED: { + { + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "商户无权发券", + Hint: "该批次不支持其他商户发放,请参考常见问题Q1", + }, + { + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "批次不支持跨商户发券", + Hint: "该批次不支持其他商户发放,请参考常见问题Q1", + }, + { + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "用户被限领拦截", + Hint: "该用户已达到该批次的领取上限,请参考常见问题Q6", + }, + { + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "不能在API渠道发放", + Hint: "请检查批次信息,仅支持发放微信支付代金券,不支持发放立减与折扣", + }, + { + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "不支持指定面额发券", + Hint: "仅在发券时指定面额及门槛的场景才生效,常规发券场景请勿传入该信息", + }, + { + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "仅在广告场景下发放批次", + Hint: "该批次已在朋友圈广告发放,不支持在其他渠道发放", + }, + }, + RULE_LIMIT: { + { + StatusCode: 403, + ErrorCode: RULE_LIMIT, + Description: "用户已达最大领券次数", + Hint: "该用户已达到该批次的领取上限,请参考常见问题Q6", + }, + { + StatusCode: 403, + ErrorCode: RULE_LIMIT, + Description: "被自然人规则拦截", + Hint: "该自然人已达到该批次的领取上限,请参考常见问题Q6", + }, + }, + USER_ACCOUNT_ABNORMAL: { + { + StatusCode: 403, + ErrorCode: USER_ACCOUNT_ABNORMAL, + Description: "用户非法", + Hint: "用户命中微信支付风控模型,请参考常见问题Q5", + }, + }, + RESOURCE_NOT_EXISTS: { + { + StatusCode: 404, + ErrorCode: RESOURCE_NOT_EXISTS, + Description: "批次不存在", + Hint: "请检查批次及制券商户号信息", + }, + }, + FREQUENCY_LIMITED: { + { + StatusCode: 429, + ErrorCode: FREQUENCY_LIMITED, + Description: "当前请求人数过多,请稍后重试", + Hint: "请降低API调用频率", + }, + }, +} + +// 使用常量定义错误描述,减少硬编码 +const ( + ErrorUserIllegal = "用户非法" + ErrorAppIDMchIDMismatch = "商户号与AppID不匹配" + ErrorOpenIDAppIDMismatch = "OpenID与AppID不匹配" + ErrorInvalidMerchantID = "非法的商户号" + ErrorHighFrequency = "调用频率过高" + ErrorActivityInactive = "活动已结束或未激活" + ErrorBatchInfoError = "批次信息获取失败,请确认参数是否有误" + ErrorAppIDRequired = "AppID必填" + ErrorOpenIDRequired = "OpenID必填" + ErrorBatchIDRequired = "批次号必填" + ErrorMerchantIDRequired = "商户号必填" + ErrorInvalidBatchStatus = "非法的批次状态" + ErrorMchNotExists = "商户号不合法" + ErrorBatchBudgetInsufficient = "批次预算不足" + ErrorDailyLimitExceeded = "发券超过单天限额" + ErrorAccountBalanceInsufficient = "账户余额不足,请充值" + ErrorBatchBudgetDepleted = "批次预算耗尽" + ErrorMerchantNoPermission = "商户无权发券" + ErrorCrossMerchantNotSupported = "批次不支持跨商户发券" + ErrorUserReceiveLimit = "用户被限领拦截" + ErrorAPIChannelNotSupported = "不能在API渠道发放" + ErrorSpecifiedDenominationNotSupported = "不支持指定面额发券" + ErrorOnlyAdvertisingBatch = "仅在广告场景下发放批次" + ErrorUserMaxCoupons = "用户已达最大领券次数" + ErrorNaturalPersonRuleBlocked = "被自然人规则拦截" + ErrorResourceNotExists = "批次不存在" + ErrorFrequencyLimited = "当前请求人数过多,请稍后重试" +) + +// WechatError 映射错误描述到具体的错误处理 +var WechatError = map[string]*errors.Error{ + ErrorUserIllegal: err2.ErrorWechatUserIllegal(ErrorUserIllegal), + ErrorAppIDMchIDMismatch: err2.ErrorWechatMismatch(ErrorAppIDMchIDMismatch), + ErrorOpenIDAppIDMismatch: err2.ErrorWechatMismatch(ErrorOpenIDAppIDMismatch), + ErrorInvalidMerchantID: err2.ErrorWechatInvalidMerchantID(ErrorInvalidMerchantID), + ErrorHighFrequency: err2.ErrorWechatHighFrequency(ErrorHighFrequency), + ErrorActivityInactive: err2.ErrorWechatActivityInactive(ErrorActivityInactive), + ErrorBatchInfoError: err2.ErrorWechatBatchInfoError(ErrorBatchInfoError), + ErrorAppIDRequired: err2.ErrorWechatParamRequired(ErrorAppIDRequired), + ErrorOpenIDRequired: err2.ErrorWechatParamRequired(ErrorOpenIDRequired), + ErrorBatchIDRequired: err2.ErrorWechatParamRequired(ErrorBatchIDRequired), + ErrorMerchantIDRequired: err2.ErrorWechatParamRequired(ErrorMerchantIDRequired), + ErrorInvalidBatchStatus: err2.ErrorWechatInvalidBatchStatus(ErrorInvalidBatchStatus), + ErrorMchNotExists: err2.ErrorWechatMchNotExists(ErrorMchNotExists), + ErrorBatchBudgetInsufficient: err2.ErrorWechatBatchBudgetInsufficient(ErrorBatchBudgetInsufficient), + ErrorDailyLimitExceeded: err2.ErrorWechatDailyLimitExceeded(ErrorDailyLimitExceeded), + ErrorAccountBalanceInsufficient: err2.ErrorWechatAccountBalanceInsufficient(ErrorAccountBalanceInsufficient), + ErrorBatchBudgetDepleted: err2.ErrorWechatBatchBudgetDepleted(ErrorBatchBudgetDepleted), + ErrorMerchantNoPermission: err2.ErrorWechatMerchantNoPermission(ErrorMerchantNoPermission), + ErrorCrossMerchantNotSupported: err2.ErrorWechatCrossMerchantNotSupported(ErrorCrossMerchantNotSupported), + ErrorUserReceiveLimit: err2.ErrorWechatUserReceiveLimit(ErrorUserReceiveLimit), + ErrorAPIChannelNotSupported: err2.ErrorWechatAPIChannelNotSupported(ErrorAPIChannelNotSupported), + ErrorSpecifiedDenominationNotSupported: err2.ErrorWechatSpecifiedDenominationNotSupported(ErrorSpecifiedDenominationNotSupported), + ErrorOnlyAdvertisingBatch: err2.ErrorWechatOnlyAdvertisingBatch(ErrorOnlyAdvertisingBatch), + ErrorUserMaxCoupons: err2.ErrorWechatUserMaxCoupons(ErrorUserMaxCoupons), + ErrorNaturalPersonRuleBlocked: err2.ErrorWechatNaturalPersonRuleBlocked(ErrorNaturalPersonRuleBlocked), + ErrorResourceNotExists: err2.ErrorWechatResourceNotExists(ErrorResourceNotExists), + ErrorFrequencyLimited: err2.ErrorWechatFrequencyLimited(ErrorFrequencyLimited), +} + +type ErrBody struct { + Code ErrCode `json:"Code"` + Message string `json:"Message"` +} + +// GetWechatError 根据错误描述获取具体的错误处理 +func (s ErrBody) GetWechatError() *errors.Error { + lowerDesc := strings.ToLower(s.Message) + for desc, err := range WechatError { + if strings.ToLower(desc) == lowerDesc { + return err + } + } + return err2.ErrorWechatFAIL(fmt.Sprintf("微信返回错误:%s", s.Message)) +} diff --git a/internal/data/wechatrepoimpl/cpn_code_test.go b/internal/data/wechatrepoimpl/cpn_code_test.go new file mode 100644 index 0000000..3fd77dc --- /dev/null +++ b/internal/data/wechatrepoimpl/cpn_code_test.go @@ -0,0 +1,13 @@ +package wechatrepoimpl + +import ( + "testing" +) + +func TestGetErrorByDescription(t *testing.T) { + e := &ErrBody{ + Code: "INVALID_REQUEST", + Message: "活动已结束或未激活", + } + t.Log(e.GetWechatError()) +} diff --git a/internal/data/wechatrepoimpl/wechat.go b/internal/data/wechatrepoimpl/wechat.go deleted file mode 100644 index 24cdc03..0000000 --- a/internal/data/wechatrepoimpl/wechat.go +++ /dev/null @@ -1,7 +0,0 @@ -package wechatrepoimpl - -var ErrBody struct { - StatusCode string `json:"StatusCode"` - Code string `json:"Code"` - Message string `json:"Message"` -}