diff --git a/api/v1/cmb_cpn.proto b/api/v1/cmb_cpn.proto index 444411f..5ca44e6 100644 --- a/api/v1/cmb_cpn.proto +++ b/api/v1/cmb_cpn.proto @@ -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 { diff --git a/internal/biz/businesserr/cmb.go b/internal/biz/businesserr/cmb.go new file mode 100644 index 0000000..f84460d --- /dev/null +++ b/internal/biz/businesserr/cmb.go @@ -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, + }, + }, +} diff --git a/internal/data/wechatrepoimpl/cpn_code.go b/internal/biz/businesserr/cpn.go similarity index 50% rename from internal/data/wechatrepoimpl/cpn_code.go rename to internal/biz/businesserr/cpn.go index 4dfe297..06eeb41 100644 --- a/internal/data/wechatrepoimpl/cpn_code.go +++ b/internal/biz/businesserr/cpn.go @@ -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 { diff --git a/internal/biz/businesserr/cpn_multi.go b/internal/biz/businesserr/cpn_multi.go new file mode 100644 index 0000000..182f5ac --- /dev/null +++ b/internal/biz/businesserr/cpn_multi.go @@ -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, + }, + }, +} diff --git a/internal/biz/businesserr/err.go b/internal/biz/businesserr/err.go new file mode 100644 index 0000000..3bd9943 --- /dev/null +++ b/internal/biz/businesserr/err.go @@ -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, + } +} diff --git a/internal/biz/cmb/notify.go b/internal/biz/cmb/notify.go index 4c7d80e..1a0e54f 100644 --- a/internal/biz/cmb/notify.go +++ b/internal/biz/cmb/notify.go @@ -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 { diff --git a/internal/biz/order.go b/internal/biz/order.go index c00dda2..c6dede3 100644 --- a/internal/biz/order.go +++ b/internal/biz/order.go @@ -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 } diff --git a/internal/biz/repo/order.go b/internal/biz/repo/order.go index 633ff9a..2da57b0 100644 --- a/internal/biz/repo/order.go +++ b/internal/biz/repo/order.go @@ -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 diff --git a/internal/biz/wechat_notify.go b/internal/biz/wechat_notify.go index be88e76..877975c 100644 --- a/internal/biz/wechat_notify.go +++ b/internal/biz/wechat_notify.go @@ -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 } diff --git a/internal/data/repoimpl/order.go b/internal/data/repoimpl/order.go index 8fa2c54..03a25c7 100644 --- a/internal/data/repoimpl/order.go +++ b/internal/data/repoimpl/order.go @@ -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, }) diff --git a/internal/data/wechatrepoimpl/bank_multi_activity.go b/internal/data/wechatrepoimpl/bank_multi_activity.go index 2e0fc0c..0e877d5 100644 --- a/internal/data/wechatrepoimpl/bank_multi_activity.go +++ b/internal/data/wechatrepoimpl/bank_multi_activity.go @@ -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 diff --git a/internal/data/wechatrepoimpl/cpn.go b/internal/data/wechatrepoimpl/cpn.go index 5fd9078..367162c 100644 --- a/internal/data/wechatrepoimpl/cpn.go +++ b/internal/data/wechatrepoimpl/cpn.go @@ -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) diff --git a/internal/data/wechatrepoimpl/cpn_code_test.go b/internal/data/wechatrepoimpl/cpn_code_test.go index ef2e8b0..00762b5 100644 --- a/internal/data/wechatrepoimpl/cpn_code_test.go +++ b/internal/data/wechatrepoimpl/cpn_code_test.go @@ -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) -} diff --git a/internal/pkg/helper/sort_struct.go b/internal/pkg/helper/sort_struct.go new file mode 100644 index 0000000..7661391 --- /dev/null +++ b/internal/pkg/helper/sort_struct.go @@ -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 +} diff --git a/internal/biz/bo/qixing.go b/internal/pkg/supplier/qixing/qixing.go similarity index 88% rename from internal/biz/bo/qixing.go rename to internal/pkg/supplier/qixing/qixing.go index 6eaf2da..38d0489 100644 --- a/internal/biz/bo/qixing.go +++ b/internal/pkg/supplier/qixing/qixing.go @@ -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"` diff --git a/internal/pkg/supplier/qixing/query.go b/internal/pkg/supplier/qixing/query.go new file mode 100644 index 0000000..6c6424f --- /dev/null +++ b/internal/pkg/supplier/qixing/query.go @@ -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"` +} diff --git a/internal/service/order.go b/internal/service/order.go index 398076b..148df0f 100644 --- a/internal/service/order.go +++ b/internal/service/order.go @@ -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) diff --git a/internal/service/qixing.go b/internal/service/qixing.go index 134c9c5..ba36742 100644 --- a/internal/service/qixing.go +++ b/internal/service/qixing.go @@ -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, }) } diff --git a/test/bank_multi_activity_test.go b/test/bank_multi_activity_test.go index 057fcca..d8bcbc6 100644 --- a/test/bank_multi_activity_test.go +++ b/test/bank_multi_activity_test.go @@ -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, diff --git a/test/cmb_test.go b/test/cmb_test.go new file mode 100644 index 0000000..0181fdc --- /dev/null +++ b/test/cmb_test.go @@ -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) +} diff --git a/test/coupon.go b/test/coupon.go index f63f57e..2cc88a0 100644 --- a/test/coupon.go +++ b/test/coupon.go @@ -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), diff --git a/test/coupon_test.go b/test/coupon_test.go index 61daa6f..2dfe81f 100644 --- a/test/coupon_test.go +++ b/test/coupon_test.go @@ -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) +}