Compare commits

...

148 Commits

Author SHA1 Message Date
fuzhongyun 3906fb9690 Merge branch 'feature/fzy/multi-notify' into pre 2026-05-29 10:26:19 +08:00
fuzhongyun 5e4b072062 feat(multi-notify): 增加合单支付回调 2026-05-28 16:27:05 +08:00
fuzhongyun b5b07a4d79 Merge pull request 'fix(notify): 临时修复 multi 回调时核销金额为空导致的推送下游问题' (#1) from fix/fzy/notify into pro
Reviewed-on: #1
2026-04-27 18:49:04 +08:00
fuzhongyun 2e3dd99115 fix(notify): 临时修复 multi 回调时核销金额为空导致的推送下游问题 2026-04-27 18:23:09 +08:00
ziming 5e6d810773 改造接口 2026-04-07 13:46:03 +08:00
ziming edae25ff6a 改造接口 2026-04-02 09:05:44 +08:00
ziming 11b42a9b46 改造接口 2026-03-27 18:22:07 +08:00
ziming b0d22b3ad8 改造接口 2026-03-27 18:12:49 +08:00
ziming ee9203f7d6 改造接口 2026-03-27 17:54:10 +08:00
ziming 9f768f037c 改造接口 2026-03-27 17:09:40 +08:00
ziming 2c7d2581f7 改造接口 2026-03-27 17:07:53 +08:00
ziming d24e7671e0 切换主体 2026-03-27 17:07:12 +08:00
ziming be2adefb91 切换主体 2026-03-27 16:47:13 +08:00
ziming 0e1ed3b426 切换主体 2026-03-27 16:16:08 +08:00
ziming 737216ee05 切换主体 2026-03-27 16:14:58 +08:00
ziming 2ceb9ed9d0 切换主体 2026-03-27 16:10:29 +08:00
ziming ae09a7509b Merge branch 'pro' into dev 2026-03-27 16:05:48 +08:00
ziming 5263744d6b 切换主体 2026-03-27 16:05:39 +08:00
ziming 5cf0459b39 切换主体 2026-03-27 14:54:38 +08:00
ziming 1622904903 切换主体 2026-03-27 14:53:48 +08:00
ziming 31373c320f Merge branch 'pro' into dev
# Conflicts:
#	internal/data/wechatrepoimpl/bank_multi_activity.go
#	test/bank_multi_activity_test.go
#	test/coupon.go
2026-03-27 14:52:58 +08:00
ziming 965474a340 切换主体 2026-03-27 14:47:37 +08:00
ziming d308d56e79 切换主体 2026-03-27 11:55:37 +08:00
ziming 7e8ef4cd2b 切换主体 2026-03-27 11:35:59 +08:00
ziming f1e813584f 切换主体 2026-03-27 10:38:07 +08:00
ziming 4be755c113 切换主体 2026-03-26 16:07:47 +08:00
ziming 8f290eb0c2 切换主体 2026-03-26 15:20:25 +08:00
ziming b4d0688bfc 切换主体 2026-03-26 15:17:56 +08:00
ziming 3c4b932f03 切换主体 2026-03-26 15:14:36 +08:00
ziming f34a27fbf2 切换主体 2026-03-26 15:13:02 +08:00
ziming 417e813b84 切换主体 2026-03-26 15:05:18 +08:00
ziming 0d5259c9b0 切换主体 2026-03-26 14:54:54 +08:00
ziming c89a6406b6 切换主体 2026-03-26 14:54:45 +08:00
ziming ce8d6ed988 切换主体 2026-03-26 14:15:12 +08:00
ziming c77e23af8a 切换主体 2026-03-26 14:09:15 +08:00
ziming 2e364f0332 切换主体 2026-03-26 14:03:03 +08:00
ziming 8cf3c66cb3 切换主体 2026-03-26 14:00:55 +08:00
ziming cfe1973bf9 切换主体 2026-03-26 13:57:09 +08:00
ziming 886d140ceb 切换主体 2026-03-26 13:46:36 +08:00
ziming 3f3bcbab8e 切换主体 2026-03-26 13:44:41 +08:00
ziming 2ec6b62912 切换主体 2026-03-26 13:44:05 +08:00
ziming 9c465d1f52 切换主体 2026-03-26 11:33:43 +08:00
ziming a7f41d1c27 切换主体 2026-03-26 11:31:55 +08:00
ziming 6aaf9f3e12 切换主体 2026-03-26 11:30:55 +08:00
ziming 64c7fbecd2 切换主体 2026-03-26 11:28:33 +08:00
ziming 4ec1809383 切换主体 2026-03-26 11:27:34 +08:00
ziming 5db078e26a 切换主体 2026-03-26 11:27:08 +08:00
ziming 7565c48cd9 切换主体 2026-03-26 11:17:56 +08:00
ziming 00e637d383 切换主体 2026-03-26 11:17:06 +08:00
ziming 6e57c2afe5 切换主体 2026-03-26 10:59:24 +08:00
ziming 129b849b02 切换主体 2026-03-26 10:57:57 +08:00
ziming 5051e2e744 切换主体 2026-03-26 10:57:16 +08:00
ziming 6a06e1fed4 切换主体 2026-03-26 10:56:15 +08:00
ziming 2735d8d71d 切换主体 2026-03-26 10:52:22 +08:00
ziming d5460e9415 切换主体 2026-03-26 10:44:29 +08:00
ziming cf9ecc4486 切换主体 2026-03-26 10:14:59 +08:00
ziming 7c851ddb1b 切换主体 2026-03-25 17:29:00 +08:00
ziming 5ba27d7f7b 切换主体 2026-03-25 17:10:27 +08:00
ziming 9c8f64a35a 切换主体 2026-03-25 15:50:54 +08:00
ziming d34403b012 切换主体 2026-03-25 15:39:40 +08:00
ziming ba4c95c7a6 切换主体 2026-03-25 14:22:35 +08:00
ziming 813dfe5ef0 切换主体 2026-03-25 14:13:38 +08:00
ziming 0ddde21287 切换主体 2026-03-25 14:08:29 +08:00
ziming c6028b8c6e 错误映射 2026-03-13 16:13:09 +08:00
ziming 07bacd006b 错误映射 2026-03-13 16:05:06 +08:00
ziming 81c53823af 错误映射 2026-03-13 15:55:41 +08:00
ziming bbfc91da57 多笔立减金,错误映射 2026-03-13 15:37:51 +08:00
ziming 9bd8646e57 query order notify 2026-03-05 18:05:05 +08:00
ziming 10727c1a00 query order notify 2026-03-05 18:04:19 +08:00
ziming 9fc1ab9c82 query order notify 2026-03-05 14:14:58 +08:00
ziming 42b88fd9f9 query order 2026-02-09 14:40:30 +08:00
ziming 2441203833 query order 2026-02-09 14:35:31 +08:00
ziming 8d420e0d00 query order 2026-02-09 14:31:19 +08:00
ziming 7ec609137a query order 2026-02-09 14:24:37 +08:00
ziming ad51cfae46 query order 2026-01-29 16:48:02 +08:00
ziming 6fdd1f3fd6 query order 2026-01-29 16:47:04 +08:00
ziming 5d1ff1bdde query order 2026-01-28 09:36:30 +08:00
ziming 888de99381 多笔立减金 2025-12-22 17:58:03 +08:00
ziming 6fd5dba1eb 多笔立减金 2025-12-22 09:54:00 +08:00
ziming 6ada382188 多笔立减金 2025-12-19 15:30:16 +08:00
ziming ecb9654743 多笔立减金 2025-12-19 15:23:54 +08:00
ziming 6006445958 多笔立减金 2025-12-19 15:01:31 +08:00
ziming 05ba50661c 多笔立减金 2025-12-19 14:38:14 +08:00
ziming b841ad3661 多笔立减金 2025-12-19 14:27:45 +08:00
ziming a7a2fb88ed 多笔立减金 2025-12-19 14:10:09 +08:00
ziming 90a2f6fb32 多笔立减金 2025-12-18 15:15:18 +08:00
ziming 9ed8a296cf 多笔立减金 2025-12-18 15:08:01 +08:00
ziming c490f8335d 多笔立减金 2025-12-18 15:05:57 +08:00
ziming c3d5f18e68 多笔立减金 2025-12-18 14:51:01 +08:00
ziming 06476209b5 多笔立减金 2025-12-17 15:36:39 +08:00
ziming c65c993a25 多笔立减金 2025-12-17 15:21:49 +08:00
ziming 1f81565e15 多笔立减金 2025-12-17 15:04:59 +08:00
ziming 96325fde3b 多笔立减金 2025-12-17 15:02:18 +08:00
ziming 4508dfe00e 多笔立减金 2025-12-17 14:59:09 +08:00
ziming d1fb805dff 多笔立减金 2025-12-17 14:44:18 +08:00
ziming c9c0a6004a 多笔立减金 2025-12-16 18:27:53 +08:00
ziming 76033bc88a 多笔立减金 2025-12-16 14:38:49 +08:00
ziming 24994ec0d7 多笔立减金 2025-12-16 14:38:30 +08:00
ziming f0f4ce1235 多笔立减金 2025-12-15 15:30:33 +08:00
ziming 0b3a16c72a 多笔立减金 2025-12-15 15:20:09 +08:00
ziming 6a5198287e 多笔立减金 2025-12-15 15:15:22 +08:00
ziming 654540ba65 多笔立减金 2025-12-15 15:03:31 +08:00
ziming 408a929b33 多笔立减金 2025-12-15 10:42:42 +08:00
ziming 10eda78e58 多笔立减金 2025-12-15 09:58:12 +08:00
ziming 25424eec72 多笔立减金 2025-12-12 17:39:52 +08:00
ziming 349fe2621c 多笔立减金 2025-12-12 17:18:03 +08:00
ziming 5dfec0d509 多笔立减金 2025-12-12 17:11:23 +08:00
ziming 49e7092839 多笔立减金 2025-12-12 11:44:45 +08:00
ziming 75b5b9af28 多笔立减金 2025-12-12 11:38:21 +08:00
ziming d3584edec5 多笔立减金 2025-12-12 11:30:16 +08:00
ziming 947c4e5e5c 多笔立减金 2025-12-12 11:11:29 +08:00
ziming a1b59c97a9 多笔立减金 2025-12-11 18:30:04 +08:00
ziming 43bf445f8b 多笔立减金 2025-12-11 18:29:23 +08:00
ziming d01900b519 多笔立减金 2025-12-11 18:13:32 +08:00
ziming 413b8100d8 多笔立减金 2025-12-11 17:20:11 +08:00
ziming 1a7df7cbab 多笔立减金 2025-12-11 14:53:41 +08:00
ziming 6b9a5a0666 多笔立减金 2025-12-11 14:40:12 +08:00
ziming dd5380c47d 多笔立减金 2025-12-11 14:34:30 +08:00
ziming c0df2aa399 多笔立减金 2025-12-11 14:32:45 +08:00
ziming b8fc6a7650 多笔立减金 2025-12-11 14:28:34 +08:00
ziming 9493648272 多笔立减金 2025-12-11 14:20:09 +08:00
ziming 2edd4fd8cb 多笔立减金 2025-12-11 11:13:13 +08:00
ziming 11e28adf6d 多笔立减金 2025-12-11 11:13:04 +08:00
ziming 7dccb67de3 voucher 2025-12-10 09:00:36 +08:00
ziming f206764800 voucher 2025-12-05 15:04:27 +08:00
ziming 363254102a voucher 2025-12-05 11:57:26 +08:00
ziming fb8fda6f0e voucher 2025-12-05 09:11:41 +08:00
ziming d64c8ea435 voucher 2025-11-17 09:38:16 +08:00
ziming ef5a80f501 voucher 2025-11-17 09:32:24 +08:00
ziming e61da58512 voucher 2025-11-13 18:03:21 +08:00
ziming b6a618b10c voucher 2025-11-13 17:24:28 +08:00
ziming f348df3229 query1 order 2025-10-20 14:41:17 +08:00
ziming ee73cb8c50 query1 order 2025-10-20 13:38:23 +08:00
ziming 4d6211a30f query1 order 2025-10-20 10:31:22 +08:00
ziming 2238f24820 query1 order 2025-10-17 19:31:24 +08:00
ziming 396e53e9fc query1 order 2025-10-17 19:19:55 +08:00
ziming a01f04a949 order by 2025-10-16 12:01:26 +08:00
ziming 93873c6fd6 order by 2025-10-16 11:37:06 +08:00
ziming 482e8d7f82 order by 2025-10-16 11:35:55 +08:00
ziming 5a3fefdd19 切换商户 2025-10-14 13:43:18 +08:00
李子铭 23bd19bc99 order_notify 2025-09-26 10:02:04 +08:00
李子铭 d6df1e96bb order_notify 2025-09-24 17:05:25 +08:00
李子铭 d2df9235f6 order_notify 2025-09-24 17:01:38 +08:00
李子铭 530652a7d2 order_notify 2025-09-16 10:40:44 +08:00
李子铭 9e984cdb5a order_notify 2025-09-16 10:22:18 +08:00
李子铭 40f51dd4dd order_notify 2025-09-16 10:15:03 +08:00
李子铭 c7a16b3ae9 order_notify 2025-09-16 10:14:44 +08:00
李子铭 4234ddf34d order_notify 2025-09-16 09:56:00 +08:00
86 changed files with 5180 additions and 781 deletions

2
.gitignore vendored
View File

@ -22,6 +22,8 @@ Thumbs.db
*.swp
.vscode/
.idea/
.trae/
bin/
cert/
log

View File

@ -209,7 +209,10 @@ message CmbNotifyRequest {
//
string ext = 13 [json_name = "ext"];
string attach = 14 [json_name = "attach"];
//
string transactionId = 15 [json_name = "transactionId"];
}
message CmbNotifyReply {
// 1000 1001
string respCode = 1 [json_name = "respCode"];
@ -217,6 +220,34 @@ message CmbNotifyReply {
string respMsg = 2 [json_name = "respMsg"];
}
message CmbMultiNotifyRequest {
// cmb业务号
string transactionId = 1 [json_name = "transactionId"];
//
string activityId = 2 [json_name = "activityId"];
// id
string couponId = 3 [json_name = "couponId"];
//
string acquiredDate = 4 [json_name = "acquiredDate"];
// 0使1使
string status = 5 [json_name = "status"];
// String|M 0使1使
string couponStatus = 13 [json_name = "couponStatus"];
// yyyy-MM-dd HH:mm:ss.sss
string transDate = 6 [json_name = "transDate"];
// -
string transAmount = 7 [json_name = "transAmount"];
// ,
string orderId = 8 [json_name = "orderId"];
// codeNo
string ticket = 9 [json_name = "ticket"];
//
string orgNo = 10 [json_name = "orgNo"];
// 500
string attach = 12 [json_name = "attach"];
//
string ext = 11 [json_name = "ext"];
}
message EncryptBodyRequest {
string encryptBody = 1 [json_name = "encryptBody"];

View File

@ -53,6 +53,13 @@ wechat:
mchCertificateSerialNumber: "6006B8208815DB5EAC5BF2E783CB9D34082C3772" #商户证书序列号
wechatPayPublicKeyID: "PUB_KEY_ID_0117109533612025031800326400002563" #微信支付公钥ID
wechatSubject:
- mchID: "1100040695" # 证书所属商户
name: "福建峦峰网络科技有限公司"
mchApiV3Key: "d9af70585b18ae206d981548c766563f"
mchCertificateSerialNumber: "46712853869DB0EDAA9B4DF97DADEECD4CCDC85B" #商户证书序列号
wechatPayPublicKeyID: "PUB_KEY_ID_0111000406952026032500382251001000" #微信支付公钥ID,有它则为新的“公私钥”模式
cmb:
sm2Prk: "8d39ff3d2559258c163f4510f082727f51531e1953ab203d5ab1ea4a6d94fd73"
sm2Puk: "04d827a7dbaaa358ce45b8c7794a7f54819f5c175005a702370e47f135ef6f5f9732758b1474f218419fe9e87f90c28c3b05f08254c651db27df35fae67b77b2e4" # 公钥,给到招行密钥
@ -66,6 +73,7 @@ cmb:
cmbKeyAlias: "SM2_CMBLIFE"
orgNo: "LANSEXIONGDI" # 发码机构号,固定值,掌上生活优惠券系统提供
notifyUrl: "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatus.json" # 招行测试回调地址
multiNotifyUrl: "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatusForMulti.json" # 招行测试多笔立减金回调地址
noticeStartDays: 7
noticeEndDays: 1
@ -89,6 +97,10 @@ cron:
isOpen: true #是否启动 true/false
command: "0 */5 * * * ?" #cron表达式,每5分钟执行一次
tripartite:
qiXing:
appKey: "DrY1zLkOH01q0sN66vrmkdpbWsyb41ho"
rdsMQ:
wechatQuery:
name: "wechatQuery"
@ -114,6 +126,18 @@ rdsMQ:
numWorkers: 1 #协程数量不配置默认为10
waitTime: 1s #处理完成后等待时间
isOpen: false #是否启动消费 true/false
orderNotifyRetry:
name: "orderNotifyRetry"
retryNum: 1 #重试次数
numWorkers: 1 #协程数量不配置默认为10
waitTime: 1s #处理完成后等待时间
isOpen: false #是否启动消费 true/false
usedNotify:
name: "usedNotify"
retryNum: 1 #重试次数
numWorkers: 1 #协程数量不配置默认为10
waitTime: 1s #处理完成后等待时间
isOpen: true #是否启动消费 true/false
aliYunSms:
accessKeyId:

View File

@ -66,6 +66,7 @@ cmb:
cmbKeyAlias: "SM2_CMBLIFE"
orgNo: "LANSEXIONGDI" # 发码机构号,固定值,掌上生活优惠券系统提供
notifyUrl: "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatus.json" # 招行测试回调地址
multiNotifyUrl: "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatus.json" # 招行测试多笔立减金回调地址
noticeStartDays: 7
noticeEndDays: 1
@ -103,6 +104,12 @@ rdsMQ:
numWorkers: 1 #协程数量不配置默认为10
waitTime: 1s #处理完成后等待时间
isOpen: false #是否启动消费 true/false
orderNotifyRetry:
name: "orderNotifyRetry"
retryNum: 1 #重试次数
numWorkers: 1 #协程数量不配置默认为10
waitTime: 1s #处理完成后等待时间
isOpen: false #是否启动消费 true/false
aliYunSms:
accessKeyId: LTAI5tM1X4HuqUwT8D74qXAH
@ -111,6 +118,10 @@ aliYunSms:
signName: 蓝色兄弟
templateWarning: "SMS_489660721"
tripartite:
qiXing:
appKey: "DrY1zLkOH01q0sN66vrmkdpbWsyb41ho"
#配置日志
logs:
business: business.log #业务日志路径:如果不写日志,则不配置或配置为空

View File

@ -6,7 +6,7 @@
# 3. 在指定目录下创建对应表的CRUD操作代码文件定义针对该表的常见增删改查以及列表查询、按字段查询等操作方法。
# 设置数据库连接信息,注意密码部分如果包含特殊字符可能需要进行转义处理,这里示例中未做额外处理,需根据实际情况检查
dsn="root:lansexiongdi6,@tcp(47.97.27.195:3306)/voucher?parseTime=True&loc=Local"
dsn="root:lsxddb123.@tcp(47.108.53.72:3306)/voucher?parseTime=True&loc=Local"
# 获取当前脚本所在的绝对路径,即使脚本被在不同目录下调用也能正确定位相关文件和目录
SHELL_FOLDER=$(cd "$(dirname "\$0")" || exit; pwd)
echo "$SHELL_FOLDER"

View File

@ -0,0 +1,27 @@
package bo
import (
"time"
"voucher/internal/biz/vo"
)
// MultiNotifyDataBo 领域实体Bo结构字段和模型字段保持一致
type MultiNotifyDataBo struct {
ID int64
Source string
IP string
NotifyID string
OrderNo string
OutBizNo string
CouponID string
StockID string
ConsumeAmount int32
ConsumeTime *time.Time
TransactionID string
EventType string
Status vo.WechatVoucherStatus
OriginalData string
NoticeNum int32
CreateTime *time.Time
UpdateTime *time.Time
}

View File

@ -0,0 +1,31 @@
package bo
import (
"time"
"voucher/internal/biz/vo"
)
// MultiNotifyLogBo 领域实体Bo结构字段和模型字段保持一致
type MultiNotifyLogBo struct {
ID int64
MultiNotifyDataID int64
OrderNo string
OutBizNo string
CouponID string
ActivityNo string
StockID string
EventType string
Status vo.WechatVoucherStatus
ConsumeAmount int32
ConsumeTime *time.Time
TransactionID string
RequestURL string
RequestStatus int32
OriginReq string
Request string
Response string
OrderCreateTime *time.Time
CouponCreateTime *time.Time
CreateTime *time.Time
UpdateTime *time.Time
}

View File

@ -0,0 +1,116 @@
package bo
import (
"encoding/json"
"fmt"
"github.com/go-playground/validator/v10"
"time"
"voucher/internal/biz/vo"
)
// ConsumeInformation 定义消费信息结构体
type ConsumeInformation struct {
ConsumeTime time.Time `json:"consume_time" validate:"required"` // 核销时间
ConsumeMchid string `json:"consume_mchid"` // 核销商户号
TransactionID string `json:"transaction_id" validate:"required"` // 微信支付交易单号
ConsumeAmount int `json:"consume_amount"` // 核销金额(单位:分) // 多笔立减金必须 validate:"required"
}
// CombineSubOrder 定义合单子单消费信息结构体
type CombineSubOrder struct {
TransactionID string `json:"transaction_id" validate:"required"` // 合单子单微信支付订单号
ConsumeAmount int `json:"comsume_amount" validate:"required"` // 子单核销金额,微信文档字段名如此定义
ConsumeTime time.Time `json:"consume_time" validate:"required"` // 子单核销时间
}
// CombineOrderInfo 定义合单订单信息结构体
type CombineOrderInfo struct {
CombineConsumeAmount int `json:"combine_consume_amount"` // 合单总核销金额
SubOrders []CombineSubOrder `json:"sub_orders,omitempty"` // 合单子单列表
}
// PlainText 定义明文数据结构体
type PlainText struct {
StockCreatorMchid string `json:"stock_creator_mchid" validate:"required"`
StockID string `json:"stock_id"`
CouponID string `json:"coupon_id"`
CouponName string `json:"coupon_name"`
Description string `json:"description"`
Status vo.WechatVoucherStatus `json:"status"`
CreateTime time.Time `json:"create_time" validate:"required"`
CouponType string `json:"coupon_type"`
NoCash bool `json:"no_cash"`
Singleitem bool `json:"singleitem"`
BusinessType string `json:"business_type"` // 业务类型
IsCombineOrder bool `json:"is_combine_order,omitempty"`
ConsumeInformation *ConsumeInformation `json:"consume_information,omitempty"`
CombineOrderInfo *CombineOrderInfo `json:"combine_order_info,omitempty"`
}
type WechatVoucherNotifyBo struct {
ID string `json:"id" validate:"required"`
CreateTime string `json:"create_time"`
ResourceType string `json:"resource_type"`
EventType string `json:"event_type" validate:"required"`
Summary string `json:"summary"`
OriginalType string `json:"original_type"`
AssociatedData string `json:"associated_data"`
PlainText PlainText `json:"plain_text" validate:"required"`
}
func (d *WechatVoucherNotifyBo) Str() (string, error) {
b, err := json.Marshal(d)
if err != nil {
return "", fmt.Errorf("json marshal original_data error: %v", err)
}
return string(b), nil
}
func (c *WechatVoucherNotifyBo) Validate() error {
if err := validator.New().Struct(c); err != nil {
for _, err = range err.(validator.ValidationErrors) {
return err
}
}
return nil
}
func (c *WechatVoucherNotifyBo) ValidateMultiNotify() error {
if err := c.Validate(); err != nil {
return err
}
if c.PlainText.IsCombineOrder {
if c.PlainText.CombineOrderInfo == nil {
return fmt.Errorf("合单订单信息不能为空")
}
if len(c.PlainText.CombineOrderInfo.SubOrders) == 0 {
return fmt.Errorf("合单子单不能为空")
}
for _, subOrder := range c.PlainText.CombineOrderInfo.SubOrders {
if subOrder.TransactionID == "" {
return fmt.Errorf("合单子单微信支付订单号不能为空")
}
if subOrder.ConsumeAmount <= 0 {
return fmt.Errorf("合单子单核销金额必须大于0")
}
if subOrder.ConsumeTime.IsZero() {
return fmt.Errorf("合单子单核销时间不能为空")
}
}
return nil
}
if c.PlainText.ConsumeInformation == nil {
return fmt.Errorf("消费信息不能为空")
}
if c.PlainText.ConsumeInformation.ConsumeAmount <= 0 {
return fmt.Errorf("消费金额必须大于0")
}
return nil
}

View File

@ -1,36 +0,0 @@
package bo
import "voucher/internal/biz/vo"
// ConsumeInformation 定义消费信息结构体
type ConsumeInformation struct {
ConsumeTime string `json:"consume_time"`
ConsumeMchid string `json:"consume_mchid"`
TransactionID string `json:"transaction_id"`
}
// PlainText 定义明文数据结构体
type PlainText struct {
StockCreatorMchid string `json:"stock_creator_mchid"`
StockID string `json:"stock_id"`
CouponID string `json:"coupon_id"`
CouponName string `json:"coupon_name"`
Description string `json:"description"`
Status vo.WechatVoucherStatus `json:"status"`
CreateTime string `json:"create_time"`
CouponType string `json:"coupon_type"`
NoCash bool `json:"no_cash"`
Singleitem bool `json:"singleitem"`
ConsumeInformation ConsumeInformation `json:"consume_information,omitempty"`
}
type WechatVoucherNotifyBo struct {
ID string `json:"id"`
CreateTime string `json:"create_time"`
ResourceType string `json:"resource_type"`
EventType string `json:"event_type"`
Summary string `json:"summary"`
OriginalType string `json:"original_type"`
AssociatedData string `json:"associated_data"`
PlainText PlainText `json:"plain_text"`
}

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,86 +25,129 @@ 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",
ThirdErrCode: ThirdErrCodeDefault,
},
},
INVALID_REQUEST: {
{
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "HTTP 请求不符合微信支付 APIv3 接口规则",
Hint: "请参阅 接口规则",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "OpenID与AppID不匹配",
Hint: "OpenID与AppID需有对应关系",
ThirdErrCode: ThirdErrCodeAppIDOpenIDMismatch,
},
{
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "非法的商户号",
Hint: "请检查商户号准确性",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: INVALID_REQUEST,
Description: "调用频率过高",
Hint: "请降低API调用频率",
ThirdErrCode: ThirdErrCodeCallHigh,
},
{
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: "参数错误",
Hint: "请根据错误提示正确传入参数",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "AppID必填",
Hint: "请输入AppID",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "OpenID必填",
Hint: "请输入OpenID",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "批次号必填",
Hint: "请输入批次号",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "商户号必填",
Hint: "请输入商户号",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 400,
ErrorCode: PARAM_ERROR,
Description: "非法的批次状态",
Hint: "请检查批次状态,仅支持发放状态为“运营中”的代金券批次",
ThirdErrCode: ThirdErrCodeDefault,
},
},
MCH_NOT_EXISTS: {
@ -114,6 +156,7 @@ var _ = map[ErrCode][]APIError{
ErrorCode: MCH_NOT_EXISTS,
Description: "商户号不合法",
Hint: "请检查商户号准确性",
ThirdErrCode: ThirdErrCodeDefault,
},
},
NOT_ENOUGH: {
@ -122,24 +165,28 @@ var _ = map[ErrCode][]APIError{
ErrorCode: NOT_ENOUGH,
Description: "批次预算不足",
Hint: "批次预算已发放完,请补充批次预算",
ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough,
},
{
StatusCode: 403,
ErrorCode: NOT_ENOUGH,
Description: "发券超过单天限额",
Hint: "已超过该批次设置的单天发放限制额度,无法发放",
ThirdErrCode: ThirdErrCodeDailyLimit,
},
{
StatusCode: 403,
ErrorCode: NOT_ENOUGH,
Description: "账户余额不足,请充值",
Hint: "商户号余额不足,无法继续发券,请充值",
ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough,
},
{
StatusCode: 403,
ErrorCode: NOT_ENOUGH,
Description: "批次预算耗尽",
Hint: "该批次的预算已经耗尽",
ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough,
},
},
REQUEST_BLOCKED: {
@ -148,36 +195,42 @@ var _ = map[ErrCode][]APIError{
ErrorCode: REQUEST_BLOCKED,
Description: "商户无权发券",
Hint: "该批次不支持其他商户发放请参考常见问题Q1",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "批次不支持跨商户发券",
Hint: "该批次不支持其他商户发放请参考常见问题Q1",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "用户被限领拦截",
Hint: "该用户已达到该批次的领取上限请参考常见问题Q6",
ThirdErrCode: ThirdErrCodeUserParticipationExceeded,
},
{
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "不能在API渠道发放",
Hint: "请检查批次信息,仅支持发放微信支付代金券,不支持发放立减与折扣",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "不支持指定面额发券",
Hint: "仅在发券时指定面额及门槛的场景才生效,常规发券场景请勿传入该信息",
ThirdErrCode: ThirdErrCodeDefault,
},
{
StatusCode: 403,
ErrorCode: REQUEST_BLOCKED,
Description: "仅在广告场景下发放批次",
Hint: "该批次已在朋友圈广告发放,不支持在其他渠道发放",
ThirdErrCode: ThirdErrCodeDefault,
},
},
RULE_LIMIT: {
@ -186,12 +239,14 @@ var _ = map[ErrCode][]APIError{
ErrorCode: RULE_LIMIT,
Description: "用户已达最大领券次数",
Hint: "该用户已达到该批次的领取上限请参考常见问题Q6",
ThirdErrCode: ThirdErrCodeUserParticipationExceeded,
},
{
StatusCode: 403,
ErrorCode: RULE_LIMIT,
Description: "被自然人规则拦截",
Hint: "该自然人已达到该批次的领取上限请参考常见问题Q6",
ThirdErrCode: ThirdErrCodeUserAccountAbnormal,
},
},
USER_ACCOUNT_ABNORMAL: {
@ -200,6 +255,7 @@ var _ = map[ErrCode][]APIError{
ErrorCode: USER_ACCOUNT_ABNORMAL,
Description: "用户非法",
Hint: "用户命中微信支付风控模型请参考常见问题Q5",
ThirdErrCode: ThirdErrCodeUserAccountFrozen,
},
},
RESOURCE_NOT_EXISTS: {
@ -208,6 +264,7 @@ var _ = map[ErrCode][]APIError{
ErrorCode: RESOURCE_NOT_EXISTS,
Description: "批次不存在",
Hint: "请检查批次及制券商户号信息",
ThirdErrCode: ThirdErrCodeDefault,
},
},
FREQUENCY_LIMITED: {
@ -216,6 +273,7 @@ var _ = map[ErrCode][]APIError{
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,83 @@
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 (
BatchNotSetStartedError = &BusinessErr{Code: ErrCode("400"), Message: "批次开始时间未设置"}
BatchSetEndedError = &BusinessErr{Code: ErrCode("400"), Message: "批次已结束时间未设置"}
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

@ -69,6 +69,7 @@ func (v *Cmb) bizContent(_ context.Context, order *bo.OrderBo, orderNotify *bo.O
OrgNo: v.bc.Cmb.OrgNo,
Attach: order.Attach,
Ext: "",
TransactionId: order.OutBizNo, // 招行订单号
}
if cmbStatus == vo.CmbStatusUse {

View File

@ -86,10 +86,10 @@ func (this *VoucherBiz) timeSliceQuery(ctx context.Context, startTime, endTime t
log.Warnf("订单定时通知,开始处理,按每两个小时分片处理,范围:%s到%s", startTime.Format(time.DateTime), endTime.Format(time.DateTime))
duration := 2 * time.Hour
duration := 6 * time.Hour
eg := new(errgroup.Group)
eg.SetLimit(10)
eg.SetLimit(8)
for start := startTime; start.Before(endTime); start = start.Add(duration) {

View File

@ -1,5 +1,14 @@
package do
type OrderNotifyRetry struct {
ProductNo string `json:"product_no"`
BatchNo string `json:"batch_no"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
OrderNo string `json:"order_no"`
OutBizNo string `json:"out_biz_no"`
}
type WechatQuery struct {
ProductNo string `json:"product_no"`
BatchNo string `json:"batch_no"`
@ -16,3 +25,23 @@ type RdsWechatQuery struct {
GoNum int `json:"go_num"` // 并发数
TimeSliceHours int64 `json:"time_slice_hours"` // 时间片"小时"
}
type WechatUsedQuery struct {
ProductNo string `json:"product_no"`
BatchNo string `json:"batch_no"`
OrderNo string `json:"order_no"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
}
type RetryQueryNotice struct {
ProductNo string `json:"product_no"`
ReceiveSuccessStartTime string `json:"receive_success_start_time"`
ReceiveSuccessEndTime string `json:"receive_success_end_time"`
OrderNos []string `json:"order_nos"`
BatchNos []string `json:"batch_nos"`
OutBizNos []string `json:"out_biz_nos"`
VoucherNos []string `json:"voucher_nos"`
}

449
internal/biz/multi.go Normal file
View File

@ -0,0 +1,449 @@
package biz
import (
"context"
"encoding/json"
"errors"
"fmt"
v1 "voucher/api/v1"
"voucher/internal/biz/bo"
"voucher/internal/biz/cmb"
"voucher/internal/biz/mixrepos"
"voucher/internal/biz/repo"
"voucher/internal/biz/vo"
"voucher/internal/conf"
"voucher/internal/data"
"voucher/internal/pkg/lock"
"github.com/go-kratos/kratos/v2/log"
"gorm.io/gorm"
)
type MultiBiz struct {
bc *conf.Bootstrap
rdb *data.Rdb
Cmb *cmb.Cmb
ProductRepo repo.ProductRepo
OrderRepo repo.OrderRepo
MultiNotifyDataRepo repo.MultiNotifyDataRepo
MultiNotifyLogRepo repo.MultiNotifyLogRepo
CmbMixRepo mixrepos.CmbMixRepo
}
func NewMultiBiz(
bc *conf.Bootstrap,
rdb *data.Rdb,
cmb *cmb.Cmb,
productRepo repo.ProductRepo,
orderRepo repo.OrderRepo,
multiNotifyDataRepo repo.MultiNotifyDataRepo,
multiNotifyLogRepo repo.MultiNotifyLogRepo,
cmbMixRepo mixrepos.CmbMixRepo,
) *MultiBiz {
return &MultiBiz{
bc: bc,
rdb: rdb,
Cmb: cmb,
ProductRepo: productRepo,
OrderRepo: orderRepo,
MultiNotifyDataRepo: multiNotifyDataRepo,
MultiNotifyLogRepo: multiNotifyLogRepo,
CmbMixRepo: cmbMixRepo,
}
}
func (biz *MultiBiz) Notify(ctx context.Context, ip, source string, req *bo.WechatVoucherNotifyBo) error {
// 商品数据量较少,先查询商品是否存在,过滤多余的通知信息
_, err := biz.ProductRepo.GetByMchStockId(ctx, req.PlainText.StockCreatorMchid, req.PlainText.StockID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return fmt.Errorf("商品查询错误 error: %w", err)
}
cl := vo.MultiNotifyLockKey.BuildCache([]string{
source,
req.PlainText.StockID,
req.PlainText.CouponID,
})
return lock.NewMutex(biz.rdb.Rdb, cl.TTL).Lock(ctx, cl.Key, func(ctx context.Context) error {
if err = req.ValidateMultiNotify(); err != nil {
return fmt.Errorf("multi validate req error: %v", err)
}
order, err := biz.order(ctx, req)
if err != nil {
return err
}
if order.ActivityId == "" {
return fmt.Errorf("批次活动ID为空不是多笔立减金请检查")
}
if err = biz.Run(ctx, ip, source, req, order); err != nil {
return err
}
return nil
})
}
func (biz *MultiBiz) order(ctx context.Context, req *bo.WechatVoucherNotifyBo) (*bo.OrderBo, error) {
order, err := biz.OrderRepo.GetByCouponId(ctx, req.PlainText.StockCreatorMchid, req.PlainText.StockID, req.PlainText.CouponID)
if err != nil {
return nil, fmt.Errorf("订单查询错误 error: %w", err)
}
return order, nil
}
func (biz *MultiBiz) Run(ctx context.Context, ip, source string, req *bo.WechatVoucherNotifyBo, order *bo.OrderBo) error {
if order.ActivityId == "" {
return fmt.Errorf("批次活动ID为空不是多笔立减金请检查")
}
mnd, err := biz.MultiNotifyDataRepo.GetByNotifyID(ctx, source, req.ID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("查询通知数据错误 error: %v", err)
}
if mnd != nil {
if !req.PlainText.IsCombineOrder && mnd.NoticeNum > 0 {
log.Warnf("[%s] multi notify log already exists,req:%+v", source, req)
return nil
}
} else {
mnd, err = biz.mndCreate(ctx, ip, source, req, order)
if err != nil {
return fmt.Errorf("创建通知数据错误 error: %v", err)
}
}
return biz.run(ctx, req, mnd, order)
}
func (biz *MultiBiz) RetryRunByMultiNotifyDataId(ctx context.Context, multiNotifyDataId int64) error {
mnd, err := biz.MultiNotifyDataRepo.GetByID(ctx, multiNotifyDataId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("查询通知数据错误 error: %v", err)
}
order, err := biz.OrderRepo.GetByOrderNo(ctx, mnd.OrderNo)
if err != nil {
return fmt.Errorf("订单查询错误 error: %v", err)
}
var req *bo.WechatVoucherNotifyBo
if err = json.Unmarshal([]byte(mnd.OriginalData), &req); err != nil {
return fmt.Errorf("通知数据 json unmarshal 错误 error: %v", err)
}
return biz.run(ctx, req, mnd, order)
}
func (biz *MultiBiz) run(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) error {
if req.PlainText.IsCombineOrder {
return biz.runCombine(ctx, req, mnd, order)
}
return biz.runSingle(ctx, req, mnd, order)
}
func (biz *MultiBiz) runSingle(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) error {
// 如果核销金额为空,不再推送下游
if mnd.ConsumeAmount == 0 {
log.Warnf("[%s] multi notify log consume amount is 0,req:%+v", mnd.NotifyID, req)
return nil
}
nl, request, err := biz.nlCreate(ctx, req, mnd, order)
if err != nil {
return fmt.Errorf("创建通知日志错误 error: %v", err)
}
if err = biz.Request(ctx, mnd, nl, request); err != nil {
return fmt.Errorf("请求错误 error: %v", err)
}
return biz.updateOrderStatus(ctx, req, order)
}
func (biz *MultiBiz) runCombine(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) error {
for _, subOrder := range req.PlainText.CombineOrderInfo.SubOrders {
exists, err := biz.MultiNotifyLogRepo.ExistsSuccessByDataIDAndTransactionID(ctx, mnd.ID, subOrder.TransactionID)
if err != nil {
return fmt.Errorf("查询合单子单通知记录错误 error: %v", err)
}
if exists {
log.Warnf("[%s] combine sub order already notified,transaction_id:%s", mnd.NotifyID, subOrder.TransactionID)
continue
}
nl, request, err := biz.nlCreateBySubOrder(ctx, req, mnd, order, subOrder)
if err != nil {
return fmt.Errorf("创建合单子单通知日志错误 error: %v", err)
}
if err = biz.Request(ctx, mnd, nl, request); err != nil {
return fmt.Errorf("合单子单请求错误 error: %v", err)
}
}
return biz.updateOrderStatus(ctx, req, order)
}
func (biz *MultiBiz) updateOrderStatus(ctx context.Context, req *bo.WechatVoucherNotifyBo, order *bo.OrderBo) error {
consumeTime := req.PlainText.ConsumeInformation.ConsumeTime
if req.PlainText.Status.IsUsed() {
if order.Status.IsUse() {
if err := biz.OrderRepo.MultiOverUsed(ctx, order.ID, consumeTime, "再次核销完成"); err != nil {
return fmt.Errorf("订单再次核销完成修改发生错误 error: %v", err)
}
} else {
if err := biz.OrderRepo.MultiOverUsed(ctx, order.ID, consumeTime, "核销完成"); err != nil {
return fmt.Errorf("订单核销完成修改发生错误 error: %v", err)
}
}
} else {
if err := biz.OrderRepo.MultiLastUsed(ctx, order.ID, consumeTime); err != nil {
return fmt.Errorf("订单核销修改发生错误 error: %v", err)
}
}
return nil
}
func (biz *MultiBiz) mndCreate(ctx context.Context, ip, source string, req *bo.WechatVoucherNotifyBo, order *bo.OrderBo) (*bo.MultiNotifyDataBo, error) {
originalData, err := req.Str()
if err != nil {
return nil, fmt.Errorf("通知数据 json str 错误 error: %v", err)
}
consumeAmount := int32(req.PlainText.ConsumeInformation.ConsumeAmount)
consumeTime := &req.PlainText.ConsumeInformation.ConsumeTime
transactionID := req.PlainText.ConsumeInformation.TransactionID
if req.PlainText.IsCombineOrder {
consumeAmount = int32(req.PlainText.CombineOrderInfo.CombineConsumeAmount)
}
return biz.MultiNotifyDataRepo.Create(ctx, &bo.MultiNotifyDataBo{
Source: source,
IP: ip,
NotifyID: req.ID,
OrderNo: order.OrderNo,
OutBizNo: order.OutBizNo,
CouponID: req.PlainText.CouponID,
StockID: req.PlainText.StockID,
ConsumeAmount: consumeAmount,
ConsumeTime: consumeTime,
TransactionID: transactionID,
EventType: req.EventType,
Status: req.PlainText.Status,
OriginalData: originalData,
})
}
func (biz *MultiBiz) nlCreateBySubOrder(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo, subOrder bo.CombineSubOrder) (*bo.MultiNotifyLogBo, *v1.CmbRequest, error) {
if biz.bc.Cmb.MultiNotifyUrl == "" {
return nil, nil, fmt.Errorf("CMB多笔立减金通知地址为空")
}
consumeTime := subOrder.ConsumeTime
nl := &bo.MultiNotifyLogBo{
MultiNotifyDataID: mnd.ID,
OrderNo: mnd.OrderNo,
OutBizNo: mnd.OutBizNo,
CouponID: mnd.CouponID,
ActivityNo: order.ProductNo,
StockID: mnd.StockID,
EventType: mnd.EventType,
Status: req.PlainText.Status,
ConsumeAmount: int32(subOrder.ConsumeAmount),
ConsumeTime: &consumeTime,
TransactionID: subOrder.TransactionID,
RequestURL: biz.bc.Cmb.MultiNotifyUrl,
RequestStatus: vo.MultiNotifyLogStatusWait.GetValue(),
OrderCreateTime: order.CreateTime,
CouponCreateTime: &req.PlainText.CreateTime,
}
request, cmbRequestBo, err := biz.GetRequest(ctx, nl, order)
if err != nil {
return nil, nil, err
}
b, _ := json.Marshal(request)
nl.OriginReq = cmbRequestBo.BizContent
nl.Request = string(b)
res, err := biz.MultiNotifyLogRepo.Create(ctx, nl)
if err != nil {
return nil, nil, fmt.Errorf("创建通知日志错误 error: %v", err)
}
res.ConsumeTime = nl.ConsumeTime
return res, request, nil
}
func (biz *MultiBiz) nlCreate(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) (*bo.MultiNotifyLogBo, *v1.CmbRequest, error) {
if biz.bc.Cmb.MultiNotifyUrl == "" {
return nil, nil, fmt.Errorf("CMB多笔立减金通知地址为空")
}
nl := &bo.MultiNotifyLogBo{
MultiNotifyDataID: mnd.ID,
OrderNo: mnd.OrderNo,
OutBizNo: mnd.OutBizNo,
CouponID: mnd.CouponID,
ActivityNo: order.ProductNo,
StockID: mnd.StockID,
EventType: mnd.EventType,
Status: req.PlainText.Status,
ConsumeAmount: mnd.ConsumeAmount,
ConsumeTime: mnd.ConsumeTime,
TransactionID: req.PlainText.ConsumeInformation.TransactionID,
//RequestURL: order.NotifyUrl,
RequestURL: biz.bc.Cmb.MultiNotifyUrl,
RequestStatus: vo.MultiNotifyLogStatusWait.GetValue(),
OrderCreateTime: order.CreateTime,
CouponCreateTime: &req.PlainText.CreateTime,
}
request, cmbRequestBo, err := biz.GetRequest(ctx, nl, order)
if err != nil {
return nil, nil, err
}
b, _ := json.Marshal(request)
nl.OriginReq = cmbRequestBo.BizContent
nl.Request = string(b)
res, err := biz.MultiNotifyLogRepo.Create(ctx, nl)
if err != nil {
return nil, nil, fmt.Errorf("创建通知日志错误 error: %v", err)
}
// 创建出来的核销时间精度有差距,重新赋值返回出去
res.ConsumeTime = mnd.ConsumeTime
return res, request, nil
}
func (biz *MultiBiz) bizContent(nl *bo.MultiNotifyLogBo, order *bo.OrderBo) (string, error) {
req := &v1.CmbMultiNotifyRequest{
TransactionId: nl.OutBizNo, // cmb业务号
ActivityId: nl.ActivityNo, // 批次活动号
CouponId: nl.CouponID, // 微信券券号
AcquiredDate: order.ReceiveSuccessTime.Format("2006-01-02 15:04:05.000"), // 券领取时间
Status: "1", // 券状态 0可使用1已使用
CouponStatus: "0", // 0可使用1已使用
TransDate: nl.ConsumeTime.Format("2006-01-02 15:04:05.000"), // 核销时间验券时间格式yyyy-mm-dd hh:mm:ss.sss
TransAmount: fmt.Sprintf("%d", nl.ConsumeAmount),
OrderId: nl.TransactionID, // 券核销支付单号
Ticket: nl.OrderNo, // 券订单号lsxd订单号
OrgNo: "LANSEXIONGDIMULTI", // cmb固定值
Attach: order.Attach, // cmb拓展参数
Ext: "",
}
if nl.Status.IsUsed() {
req.CouponStatus = "1"
}
bizJsonBytes, err := json.Marshal(req)
if err != nil {
return "", fmt.Errorf("json.Marshal CmbNotifyRequest error: %v", err)
}
return string(bizJsonBytes), nil
}
func (biz *MultiBiz) GetRequest(ctx context.Context, nl *bo.MultiNotifyLogBo, order *bo.OrderBo) (*v1.CmbRequest, *bo.CmbRequestBo, error) {
bizContent, err := biz.bizContent(nl, order)
if err != nil {
return nil, nil, err
}
r := &bo.CmbRequestBo{
FuncName: vo.CmbNotifyFuncNameUpdateCodeStatusForMulti,
BizContent: bizContent,
}
request, err := biz.CmbMixRepo.GetRequest(ctx, r)
if err != nil {
return nil, nil, err
}
return request, r, nil
}
func (biz *MultiBiz) Request(ctx context.Context, mmd *bo.MultiNotifyDataBo, nl *bo.MultiNotifyLogBo, request *v1.CmbRequest) error {
if nl.RequestURL == "" {
if err := biz.notifyFail(ctx, nl, "回调通知招行地址为空,不做通知"); err != nil {
return err
}
// 回调通知地址为空,不返回错误,不做再次通知处理
return nil
}
reply, err := biz.CmbMixRepo.Request(ctx, request, nl.RequestURL)
if err != nil {
if err3 := biz.notifyFail(ctx, nl, err.Error()); err3 != nil {
return err3
}
return err
}
if err = biz.CmbMixRepo.VerifyResponse(ctx, reply); err != nil {
errMsg := fmt.Sprintf("回调通知招行返回验证结果发生错误,resp:%+v error:%s", reply, err.Error())
if err3 := biz.notifyFail(ctx, nl, errMsg); err3 != nil {
return err3
}
return err
}
return biz.notifySuccess(ctx, mmd, nl, reply)
}
func (biz *MultiBiz) notifyFail(ctx context.Context, nl *bo.MultiNotifyLogBo, remark string) error {
if err := biz.MultiNotifyLogRepo.Fail(ctx, nl.ID, remark); err != nil {
return fmt.Errorf("更新通知日志失败状态发生错误 error: %v", err)
}
return nil
}
func (biz *MultiBiz) notifySuccess(ctx context.Context, mmd *bo.MultiNotifyDataBo, nl *bo.MultiNotifyLogBo, reply *v1.CmbReply) error {
response, err := json.Marshal(reply)
if err != nil {
return fmt.Errorf("json.Marshal CmbReply error: %v", err)
}
if err = biz.MultiNotifyLogRepo.Success(ctx, nl.ID, string(response)); err != nil {
return fmt.Errorf("更新通知日志成功状态发生错误 error: %v", err)
}
if err = biz.MultiNotifyDataRepo.AddNoticeNum(ctx, mmd.ID); err != nil {
return fmt.Errorf("更新通知数据通知次数发生错误 error: %v", err)
}
return nil
}

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, *bo.ProductBo, error) {
order, err3 := this.GetByOutBizNo(ctx, req)
if err3 != nil {
return "", err3
return nil, nil, err3
}
if order != nil {
@ -20,24 +22,39 @@ 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 order, nil, err4
}
}
return order.OrderNo, err
return order, nil, nil
}
product, err3 := this.ProductRepo.GetByProductNo(ctx, req.ProductNo)
if err3 != nil {
return "", err3
return nil, product, err3
}
if product.StartTime == nil {
return nil, product, businesserr.BatchNotSetStartedError
}
if product.EndTime == nil {
return nil, product, businesserr.BatchSetEndedError
}
nowTime := time.Now()
if nowTime.Before(*product.StartTime) {
return nil, product, businesserr.BatchNotStartedError
}
if nowTime.After(*product.EndTime) {
return nil, product, businesserr.BatchEndedError
}
order, err3 = this.order(ctx, req, product)
if err3 != nil {
return "", err3
return nil, product, err3
}
return order.OrderNo, nil
return order, product, nil
}
func (this *VoucherBiz) order(ctx context.Context, req *bo.OrderCreateReqBo, product *bo.ProductBo) (*bo.OrderBo, error) {
@ -105,6 +122,10 @@ func (this *VoucherBiz) create(ctx context.Context, req *bo.OrderCreateReqBo, pr
ActivityId: product.ActivityId, // 多笔立减活动
}
if product.ActivityId != "" {
o.NotifyUrl = this.bc.Cmb.MultiNotifyUrl
}
return this.OrderRepo.Create(ctx, o)
}
@ -126,8 +147,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

@ -0,0 +1,66 @@
package biz
import (
"context"
"encoding/json"
"fmt"
"github.com/go-kratos/kratos/v2/transport/http"
"time"
"voucher/internal/biz/do"
)
func (this *VoucherBiz) PushRetryOrderNotice(ctx http.Context, bodyBytes []byte) error {
_, err := this.rdb.Rdb.RPush(ctx, "retryQueryNotice", string(bodyBytes)).Result()
if err != nil {
return fmt.Errorf("添加到队列失败:%v", err)
}
return nil
}
func (this *VoucherBiz) PushOrderNotifyRetry(ctx http.Context, req *do.OrderNotifyRetry) error {
queue := this.bc.RdsMQ.GetOrderNotifyRetry()
if queue == nil {
return fmt.Errorf("队列不存在")
}
msg, err := json.Marshal(req)
if err != nil {
return err
}
strMsg := string(msg)
_, err = this.rdb.Rdb.RPush(ctx, queue.Name, strMsg).Result()
if err != nil {
return fmt.Errorf("添加到队列失败:%v", err)
}
return nil
}
func (this *VoucherBiz) OrderNotifyRetry(ctx context.Context, msg string) error {
var req *do.OrderNotifyRetry
if err := json.Unmarshal([]byte(msg), &req); err != nil {
return err
}
if req.StartTime == "" || req.EndTime == "" {
return fmt.Errorf("start_time or end_time is empty")
}
start, err := time.Parse(time.DateTime, req.StartTime)
if err != nil {
return err
}
end, err := time.Parse(time.DateTime, req.EndTime)
if err != nil {
return err
}
return this.timeSliceQuery(ctx, start, end)
}

View File

@ -5,4 +5,4 @@ import (
)
// ProviderSetBiz is biz providers.
var ProviderSetBiz = wire.NewSet(NewVoucherBiz)
var ProviderSetBiz = wire.NewSet(NewVoucherBiz, NewMultiBiz, NewWechatBiz)

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"strings"
"time"
v1 "voucher/api/v1"
"voucher/internal/biz/bo"
@ -66,7 +67,7 @@ func (this *VoucherBiz) Query(ctx context.Context, order *bo.OrderBo) error {
return nil
}
func (this *VoucherBiz) QueryOrder(ctx context.Context, orderNo string) (string, error) {
func (this *VoucherBiz) QueryOrder(ctx context.Context, orderNo, isNotice string) (string, error) {
order, err3 := this.OrderRepo.GetByOrderNo(ctx, orderNo)
if err3 != nil {
@ -78,5 +79,78 @@ func (this *VoucherBiz) QueryOrder(ctx context.Context, orderNo string) (string,
return "", err
}
return fmt.Sprintf("orderNo:%s,订单状态:%s,微信查询返回状态:%s", orderNo, order.Status.GetText(), status.GetText()), nil
notifyStr := ""
if isNotice == "YES" {
notify, err := this.Cmb.Notify(ctx, order)
if err != nil {
return "", fmt.Errorf("查询通知招行失败:%s,orderNo:%s", err, order.OrderNo)
}
notifyStr = fmt.Sprintf("通知招行成功:notify_id:%d", notify.ID)
}
if order.Status != status {
if err = this.UpdateOrderStatus(ctx, order.ID, status); err != nil {
return "", err
}
}
return this.ToTextDescription(order, status, notifyStr), nil
}
func (this *VoucherBiz) ToTextDescription(bo *bo.OrderBo, orderStatus vo.OrderStatus, notifyStr string) string {
var parts []string
// 拼接每个字段的描述(根据业务重要性调整顺序)
parts = append(parts, fmt.Sprintf("订单ID%d", bo.ID))
parts = append(parts, fmt.Sprintf("订单编号:%s", bo.OrderNo))
parts = append(parts, fmt.Sprintf("外部交易号:%s", bo.OutBizNo))
parts = append(parts, fmt.Sprintf("券ID%s", bo.VoucherNo))
parts = append(parts, fmt.Sprintf("商品编号:%s", bo.ProductNo))
parts = append(parts, fmt.Sprintf("批次号:%s", bo.BatchNo))
parts = append(parts, fmt.Sprintf("活动ID%s", bo.ActivityId))
parts = append(parts, fmt.Sprintf("充值账号:%s", bo.Account))
parts = append(parts, fmt.Sprintf("订单类型:%s", bo.Type.GetText())) // 假设 Type 有 GetText() 方法返回文字描述
parts = append(parts, fmt.Sprintf("账号类型:%s", bo.AccountType.GetText()))
parts = append(parts, fmt.Sprintf("appid%s", bo.AppID))
parts = append(parts, fmt.Sprintf("制券商户:%s", bo.MerchantNo))
parts = append(parts, fmt.Sprintf("回调地址:%s", bo.NotifyUrl))
parts = append(parts, fmt.Sprintf("渠道:%s", bo.Channel.GetText()))
parts = append(parts, fmt.Sprintf("附加信息:%s", bo.Attach))
parts = append(parts, fmt.Sprintf("备注:%s", bo.Remark))
parts = append(parts, fmt.Sprintf("交易ID%s", bo.TransactionId))
parts = append(parts, fmt.Sprintf("订单状态:%s", bo.Status.GetText()))
parts = append(parts, fmt.Sprintf("微信查询返回状态:%s", orderStatus.GetText()))
// 时间字段特殊处理(避免 nil 指针报错)
if bo.ReceiveSuccessTime != nil {
parts = append(parts, fmt.Sprintf("到账时间:%s", bo.ReceiveSuccessTime.Format("2006-01-02 15:04:05")))
} else {
parts = append(parts, "到账时间:未到账")
}
if bo.LastUseTime != nil {
parts = append(parts, fmt.Sprintf("最后使用时间:%s", bo.LastUseTime.Format("2006-01-02 15:04:05")))
} else {
parts = append(parts, "最后使用时间:未使用")
}
if bo.CreateTime != nil {
parts = append(parts, fmt.Sprintf("创建时间:%s", bo.CreateTime.Format("2006-01-02 15:04:05")))
} else {
parts = append(parts, "创建时间:未知")
}
if bo.UpdateTime != nil {
parts = append(parts, fmt.Sprintf("更新时间:%s", bo.UpdateTime.Format("2006-01-02 15:04:05")))
} else {
parts = append(parts, "更新时间:未更新")
}
if notifyStr != "" {
parts = append(parts, fmt.Sprintf("通知结果:%s", notifyStr))
}
// 用换行符拼接所有片段,形成最终描述
return strings.Join(parts, "\n")
}

View File

@ -25,15 +25,27 @@ func (this *VoucherBiz) RegisterTag(ctx context.Context, id int32) error {
return err
}
if this.IsNotifyRegisterTag(stock) {
if err = this.registerNotifyTag(ctx, stock.MchId, stock.BatchNo); err != nil {
return err
}
}
_ = this.ProductRepo.DelCacheByProductNo(ctx, stock.ProductNo)
_, err = this.ProductRepo.GetByProductNo(ctx, stock.ProductNo)
return err
}
func (this *VoucherBiz) IsNotifyRegisterTag(product *bo.ProductBo) bool {
for _, subject := range this.bc.WechatSubject {
if subject.MchID == product.MchId {
return false
}
}
return true
}
func (this *VoucherBiz) registerNotifyTag(ctx context.Context, stockCreatorMchID, stockID string) error {
cl := vo.WechatNotifyRegisterTagCacheLockKey.BuildCache([]string{this.bc.WechatNotifyMQ.Tag, stockCreatorMchID, stockID})

View File

@ -0,0 +1,14 @@
package repo
import (
"context"
"voucher/internal/biz/bo"
)
type MultiNotifyDataRepo interface {
FindNoticeNumZero(ctx context.Context, fun func(ctx context.Context, rows []*bo.MultiNotifyDataBo) error) error
Create(ctx context.Context, req *bo.MultiNotifyDataBo) (*bo.MultiNotifyDataBo, error)
GetByID(ctx context.Context, id int64) (*bo.MultiNotifyDataBo, error)
GetByNotifyID(ctx context.Context, source, notifyId string) (*bo.MultiNotifyDataBo, error)
AddNoticeNum(ctx context.Context, id int64) error
}

View File

@ -0,0 +1,14 @@
package repo
import (
"context"
"voucher/internal/biz/bo"
)
type MultiNotifyLogRepo interface {
Create(ctx context.Context, req *bo.MultiNotifyLogBo) (*bo.MultiNotifyLogBo, error)
GetByID(ctx context.Context, id int64) (*bo.MultiNotifyLogBo, error)
ExistsSuccessByDataIDAndTransactionID(ctx context.Context, multiNotifyDataID int64, transactionID string) (bool, error)
Success(ctx context.Context, id int64, response string) error
Fail(ctx context.Context, id int64, remark string) error
}

View File

@ -2,28 +2,32 @@ package repo
import (
"context"
"time"
"voucher/internal/biz/bo"
"voucher/internal/biz/do"
"voucher/internal/biz/vo"
)
type OrderRepo interface {
FinUsedInBatches(ctx context.Context, req *do.WechatUsedQuery, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
SpecifyFindInBatches(ctx context.Context, w *bo.FindInBatchesBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
FinSucByStockIdInBatches(ctx context.Context, req *do.WechatQuery, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
FinFailByStockIdInBatches(ctx context.Context, batchNo string, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
FindIngInBatches(ctx context.Context, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
//FinFailByStockIdInBatches(ctx context.Context, batchNo string, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
//FindIngInBatches(ctx context.Context, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
FindInBatches(ctx context.Context, w *bo.FindInBatchesUseBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
FindRetryQuery(ctx context.Context, req *do.RetryQueryNotice, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
GetByOutBizNo(ctx context.Context, t vo.OrderType, outBizNo string) (*bo.OrderBo, error)
GetByOrderNo(ctx context.Context, orderNo string) (*bo.OrderBo, error)
GetByCouponId(ctx context.Context, merchantNo, batchNo, voucherNo string) (*bo.OrderBo, error)
GetByTransactionId(ctx context.Context, stockCreatorMchId, stockID, transactionId string) (*bo.OrderBo, error)
Create(ctx context.Context, req *bo.OrderBo) (*bo.OrderBo, error)
GetByID(ctx context.Context, id uint64) (*bo.OrderBo, error)
Ing(ctx context.Context, id uint64) error
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
Expired(ctx context.Context, id uint64) error
}

View File

@ -1,10 +0,0 @@
package repo
import (
"context"
"voucher/internal/biz/bo"
)
type OrderBakRepo interface {
SpecifyFindInBatches(ctx context.Context, w *bo.FindInBatchesBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
}

View File

@ -0,0 +1,17 @@
package repo
import (
"context"
"voucher/internal/biz/bo"
"voucher/internal/biz/do"
)
type OrderBakRepo interface {
SpecifyFindInBatches(ctx context.Context, w *bo.FindInBatchesBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
FindRetryQuery(ctx context.Context, req *do.RetryQueryNotice, fun func(ctx context.Context, rows []*bo.OrderBo) error) error
GetByID(ctx context.Context, id uint64) (*bo.OrderBo, error)
Used(ctx context.Context, id uint64) error
Available(ctx context.Context, id uint64) error
Expired(ctx context.Context, id uint64) error
}

View File

@ -10,6 +10,8 @@ type ProductRepo interface {
GetById(ctx context.Context, id int32) (*bo.ProductBo, error)
FindWarningBudget(ctx context.Context, fun func(ctx context.Context, rows []*bo.ProductBo) error) error
GetByBatchNo(ctx context.Context, batchNo string) (*bo.ProductBo, error)
GetByMchStockId(ctx context.Context, mchId, stockId string) (*bo.ProductBo, error)
GetByProductNo(ctx context.Context, productNo string) (*bo.ProductBo, error)
DelCacheByProductNo(ctx context.Context, productNo string) error
UpdateByWxResp(ctx context.Context, id int32, req *do.WxResp) error
}

View File

@ -21,6 +21,7 @@ type Query struct {
productRepo repo.ProductRepo
orderRepo repo.OrderRepo
orderBakRepo repo.OrderBakRepo
wechatCpnRepo wechatrepo.WechatCpnRepo
mqSendMixRepo mixrepos.MQSendMixRepo
@ -32,6 +33,7 @@ func NewQuery(
cmb *cmb.Cmb,
productRepo repo.ProductRepo,
orderRepo repo.OrderRepo,
orderBakRepo repo.OrderBakRepo,
wechatCpnRepo wechatrepo.WechatCpnRepo,
mqSendMixRepo mixrepos.MQSendMixRepo) *Query {
return &Query{
@ -41,6 +43,7 @@ func NewQuery(
cmb: cmb,
productRepo: productRepo,
orderRepo: orderRepo,
orderBakRepo: orderBakRepo,
wechatCpnRepo: wechatCpnRepo,
mqSendMixRepo: mqSendMixRepo}
}

View File

@ -7,6 +7,11 @@ import (
func (v *Query) wechatQuery(ctx context.Context, order *bo.OrderBo, useNum *int) error {
if order.Status.IsExpired() {
_, err := v.cmb.Notify(ctx, order)
return err
}
status, err := v.wechatCpnRepo.Query(ctx, order)
if err != nil {
return err
@ -27,7 +32,8 @@ func (v *Query) wechatQuery(ctx context.Context, order *bo.OrderBo, useNum *int)
func (v *Query) queryUsed(ctx context.Context, order *bo.OrderBo) error {
if order.Status.IsUse() {
return v.notify(ctx, order)
_, err := v.cmb.Notify(ctx, order)
return err
}
if err := v.orderRepo.Used(ctx, order.ID); err != nil {
@ -40,7 +46,8 @@ func (v *Query) queryUsed(ctx context.Context, order *bo.OrderBo) error {
func (v *Query) queryExpired(ctx context.Context, order *bo.OrderBo) error {
if order.Status.IsExpired() {
return nil
_, err := v.cmb.Notify(ctx, order)
return err
}
if err := v.orderRepo.Expired(ctx, order.ID); err != nil {
@ -50,6 +57,20 @@ func (v *Query) queryExpired(ctx context.Context, order *bo.OrderBo) error {
return v.notify(ctx, order)
}
func (v *Query) querySuccess(ctx context.Context, order *bo.OrderBo) error {
if order.Status.IsSuccess() {
_, err := v.cmb.Notify(ctx, order)
return err
}
if err := v.orderRepo.Available(ctx, order.ID); err != nil {
return err
}
return v.notify(ctx, order)
}
func (v *Query) notify(ctx context.Context, order *bo.OrderBo) error {
order, err := v.orderRepo.GetByID(ctx, order.ID)

View File

@ -0,0 +1,62 @@
package timeslicequery
import (
"context"
"voucher/internal/biz/bo"
)
func (v *Query) queryUsedBak(ctx context.Context, order *bo.OrderBo) error {
if order.Status.IsUse() {
_, err := v.cmb.Notify(ctx, order)
return err
}
if err := v.orderBakRepo.Used(ctx, order.ID); err != nil {
return err
}
return v.notify(ctx, order)
}
func (v *Query) queryExpiredBak(ctx context.Context, order *bo.OrderBo) error {
if order.Status.IsExpired() {
_, err := v.cmb.Notify(ctx, order)
return err
}
if err := v.orderBakRepo.Expired(ctx, order.ID); err != nil {
return err
}
return v.notify(ctx, order)
}
func (v *Query) querySuccessBak(ctx context.Context, order *bo.OrderBo) error {
if order.Status.IsSuccess() {
_, err := v.cmb.Notify(ctx, order)
return err
}
if err := v.orderBakRepo.Available(ctx, order.ID); err != nil {
return err
}
return v.notify(ctx, order)
}
func (v *Query) notifyBak(ctx context.Context, order *bo.OrderBo) error {
order, err := v.orderBakRepo.GetByID(ctx, order.ID)
if err != nil {
return err
}
if _, err = v.cmb.Notify(ctx, order); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,246 @@
package timeslicequery
import (
"context"
"encoding/json"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"github.com/hashicorp/go-multierror"
"golang.org/x/sync/errgroup"
"runtime"
"sync"
"time"
"voucher/internal/biz/bo"
"voucher/internal/biz/do"
)
func (v *Query) RetryQueryNotice(ctx context.Context, msg string) error {
var req *do.RetryQueryNotice
if err := json.Unmarshal([]byte(msg), &req); err != nil {
return err
}
err := v.RetryQueryNoticeOrder(ctx, req)
if err != nil {
return err
}
return v.RetryQueryNoticeOrderBak(ctx, req)
}
func (v *Query) RetryQueryNoticeOrder(ctx context.Context, req *do.RetryQueryNotice) error {
start := time.Now()
num := 0
errNum := 0
sucNum := 0
var mu sync.Mutex
errs := make([]error, 0)
eg := new(errgroup.Group)
eg.SetLimit(5)
err := v.orderRepo.FindRetryQuery(ctx, req, func(ctx context.Context, rows []*bo.OrderBo) error {
eg.Go(func() error {
defer func() {
if err := recover(); err != nil {
// 获取调用栈信息
_, file, line, _ := runtime.Caller(1) // 1 表示获取当前调用者的调用信息
mu.Lock()
errs = append(errs, fmt.Errorf("panic: %v,file:%s, line:%d", err, file, line))
mu.Unlock()
}
}()
for _, order := range rows {
if err := v.retryQueryNoticeOrder(ctx, order); err != nil {
logFields := map[string]string{
"order_no": order.OrderNo,
"coupon_id": order.VoucherNo,
"open_id": order.Account,
"stock_id": order.BatchNo,
"err": err.Error(),
}
log.Errorf("微信券查询order,错误:%+v", logFields)
errNum++
if errNum > 20 {
return fmt.Errorf("微信券查询order,已经连续发生20次错误%+v", logFields)
}
} else {
sucNum++
}
}
return nil
})
return nil
})
// 等待所有任务完成
if err := eg.Wait(); err != nil {
return fmt.Errorf("微信券查询order任务执行失败: %v", err)
}
logFields := map[string]any{
"num": num,
"sucNum": sucNum,
"errNum": errNum,
"elapsed": time.Now().Sub(start).String(),
}
log.Warnf("微信券查询order,处理完毕:%+v", logFields)
// 收集错误
var result error
for _, err2 := range errs {
result = multierror.Append(result, err2)
}
return err
}
func (v *Query) retryQueryNoticeOrder(ctx context.Context, order *bo.OrderBo) error {
if order.Status.IsExpired() {
_, err := v.cmb.Notify(ctx, order)
return err
}
status, err := v.wechatCpnRepo.Query(ctx, order)
if err != nil {
return err
}
if status.IsUse() {
return v.queryUsed(ctx, order)
} else if status.IsSuccess() {
return v.querySuccess(ctx, order)
} else if status.IsExpired() {
return v.queryExpired(ctx, order)
} else {
log.Warnf("微信券查询order,未知状态orderNo:%s,status:%d", order.OrderNo, status)
}
return nil
}
func (v *Query) RetryQueryNoticeOrderBak(ctx context.Context, req *do.RetryQueryNotice) error {
start := time.Now()
num := 0
errNum := 0
sucNum := 0
var mu sync.Mutex
errs := make([]error, 0)
eg := new(errgroup.Group)
eg.SetLimit(5)
err := v.orderBakRepo.FindRetryQuery(ctx, req, func(ctx context.Context, rows []*bo.OrderBo) error {
eg.Go(func() error {
defer func() {
if err := recover(); err != nil {
// 获取调用栈信息
_, file, line, _ := runtime.Caller(1) // 1 表示获取当前调用者的调用信息
mu.Lock()
errs = append(errs, fmt.Errorf("panic: %v,file:%s, line:%d", err, file, line))
mu.Unlock()
}
}()
for _, order := range rows {
if err := v.retryQueryNoticeOrderBal(ctx, order); err != nil {
logFields := map[string]string{
"order_no": order.OrderNo,
"coupon_id": order.VoucherNo,
"open_id": order.Account,
"stock_id": order.BatchNo,
"err": err.Error(),
}
log.Errorf("微信券查询orderBak,错误:%+v", logFields)
errNum++
if errNum > 20 {
return fmt.Errorf("微信券查询orderBak,已经连续发生20次错误%+v", logFields)
}
} else {
sucNum++
}
}
return nil
})
return nil
})
if err != nil {
return err
}
// 等待所有任务完成
if err2 := eg.Wait(); err2 != nil {
return fmt.Errorf("微信券查询orderBak任务执行失败: %v", err2)
}
logFields := map[string]any{
"num": num,
"sucNum": sucNum,
"errNum": errNum,
"elapsed": time.Now().Sub(start).String(),
}
log.Warnf("微信券查询orderBak,处理完毕:%+v", logFields)
// 收集错误
var result error
for _, err2 := range errs {
result = multierror.Append(result, err2)
}
return result
}
func (v *Query) retryQueryNoticeOrderBal(ctx context.Context, order *bo.OrderBo) error {
if order.Status.IsExpired() {
_, err := v.cmb.Notify(ctx, order)
return err
}
status, err := v.wechatCpnRepo.Query(ctx, order)
if err != nil {
return err
}
if status.IsUse() {
return v.queryUsedBak(ctx, order)
} else if status.IsSuccess() {
return v.querySuccessBak(ctx, order)
} else if status.IsExpired() {
return v.queryExpiredBak(ctx, order)
} else {
log.Warnf("微信券查询orderBak,未知状态orderNo:%s,status:%d", order.OrderNo, status)
}
return nil
}

104
internal/biz/used_notify.go Normal file
View File

@ -0,0 +1,104 @@
package biz
import (
"context"
"encoding/json"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/transport/http"
"golang.org/x/sync/errgroup"
"runtime"
"voucher/internal/biz/bo"
"voucher/internal/biz/do"
)
func (this *VoucherBiz) UsedNotifyPush(ctx http.Context, req *do.WechatUsedQuery) error {
queue := this.bc.RdsMQ.GetUsedNotify()
if queue == nil {
return fmt.Errorf("队列不存在")
}
msg, err := json.Marshal(req)
if err != nil {
return err
}
strMsg := string(msg)
_, err = this.rdb.Rdb.RPush(ctx, queue.Name, strMsg).Result()
if err != nil {
return fmt.Errorf("添加到队列失败:%v", err)
}
return nil
}
func (this *VoucherBiz) UsedNotify(ctx context.Context, msg string) error {
log.Warnf("核销重试通知处理,开始:%s", msg)
var req *do.WechatUsedQuery
if err := json.Unmarshal([]byte(msg), &req); err != nil {
return err
}
errNum := 0
eg := new(errgroup.Group)
eg.SetLimit(3)
err := this.OrderRepo.FinUsedInBatches(ctx, req, func(ctx context.Context, rows []*bo.OrderBo) error {
for _, order := range rows {
eg.Go(func() error {
if err := this.usedNotify(ctx, order); err != nil {
errNum++
if errNum > 50 {
return fmt.Errorf("核销重试通知处理,通知失败次数超过50次,请检查:%v", err)
}
log.Warnf("核销重试通知处理,通知失败:%v", err)
}
return nil
})
}
return nil
})
if err != nil {
return err
}
return eg.Wait() // 仅返回第一个错误
}
func (this *VoucherBiz) usedNotify(ctx context.Context, order *bo.OrderBo) error {
defer func() {
if err := recover(); err != nil {
_, file, line, _ := runtime.Caller(1) // 1 表示获取当前调用者的调用信息
log.Errorf("核销重试通知处理,发生错误:req:%s,err:%v,file:%s,line:%d", order.OrderNo, err, file, line)
}
}()
event, err := order.Status.GetOrderNotifyEvent()
if err != nil {
return err
}
notify := &bo.OrderNotifyBo{
OrderNo: order.OrderNo,
NotifyUrl: order.NotifyUrl,
Channel: order.Channel,
Event: event,
Type: order.Type,
}
return this.request(ctx, order, notify)
}

View File

@ -29,6 +29,10 @@ const (
ProductQueryLockKey CacheKey = "product_query_lock"
)
const (
MultiNotifyLockKey CacheKey = "multi_notify_lock_key"
)
var (
WarningBudgetCron CacheKey = "warning_budget_cron"
WarningBudgetSendIncr CacheKey = "warning_budget_incr"
@ -51,6 +55,8 @@ var CacheKeyMap = map[CacheKey]time.Duration{
WarningBudgetSendIncr: 3 * time.Hour,
WarningBudgetCron: 5 * time.Minute,
MultiNotifyLockKey: 30 * time.Second,
}
type Cache struct {

View File

@ -6,6 +6,7 @@ type CmbFuncName string
const (
// CmbNotifyFuncName . 券状态回调通知方法
CmbNotifyFuncName CmbFuncName = "updateCodeStatus.json"
CmbNotifyFuncNameUpdateCodeStatusForMulti CmbFuncName = "updateCodeStatusForMulti.json"
)
func (s CmbFuncName) GetValue() string {

View File

@ -0,0 +1,38 @@
package vo
type MultiNotifyLogStatus int32
const (
MultiNotifyLogStatusWait MultiNotifyLogStatus = iota + 1
MultiNotifyLogStatusSuccess
MultiNotifyLogStatusFail
)
var MultiNotifyLogStatusMap = map[MultiNotifyLogStatus]string{
MultiNotifyLogStatusWait: "待请求",
MultiNotifyLogStatusSuccess: "请求成功",
MultiNotifyLogStatusFail: "请求失败",
}
func (s MultiNotifyLogStatus) GetText() string {
if t, ok := MultiNotifyLogStatusMap[s]; ok {
return t
}
return "未知请求状态"
}
func (s MultiNotifyLogStatus) GetValue() int32 {
return int32(s)
}
func (s MultiNotifyLogStatus) IsWait() bool {
return s == MultiNotifyLogStatusWait
}
func (s MultiNotifyLogStatus) IsSuccess() bool {
return s == MultiNotifyLogStatusSuccess
}
func (s MultiNotifyLogStatus) IsFail() bool {
return s == MultiNotifyLogStatusFail
}

View File

@ -26,6 +26,7 @@ type VoucherBiz struct {
DingMixRepo mixrepos.DingMixRepo
CmbMixRepo mixrepos.CmbMixRepo
SmsMixRepo mixrepos.SmsMixRepo
MultiBiz *MultiBiz
mu sync.RWMutex
queryMap map[string]bool
@ -47,6 +48,7 @@ func NewVoucherBiz(
DingMixRepo mixrepos.DingMixRepo,
CmbMixRepo mixrepos.CmbMixRepo,
SmsMixRepo mixrepos.SmsMixRepo,
MultiBiz *MultiBiz,
) *VoucherBiz {
return &VoucherBiz{
bc: bc,
@ -64,6 +66,7 @@ func NewVoucherBiz(
DingMixRepo: DingMixRepo,
CmbMixRepo: CmbMixRepo,
SmsMixRepo: SmsMixRepo,
MultiBiz: MultiBiz,
queryMap: make(map[string]bool),
}

36
internal/biz/wechat.go Normal file
View File

@ -0,0 +1,36 @@
package biz
import (
"context"
"net/http"
"voucher/internal/biz/bo"
"voucher/internal/biz/wechatrepo"
)
type WechatBiz struct {
BankMultiActivityRepo wechatrepo.BankMultiActivityRepo
}
func NewWechatBiz(bankMultiActivityRepo wechatrepo.BankMultiActivityRepo) *WechatBiz {
return &WechatBiz{BankMultiActivityRepo: bankMultiActivityRepo}
}
func (biz *WechatBiz) CallBack(ctx context.Context, mchId string, headers *http.Header, respBody []byte) (*bo.WechatVoucherNotifyBo, error) {
response, err := biz.BankMultiActivityRepo.Notify(ctx, mchId, headers, respBody)
if err != nil {
return nil, err
}
return response, nil
}
func (biz *WechatBiz) DecodeBody(ctx context.Context, mchId string, respBody []byte) (*bo.WechatVoucherNotifyBo, error) {
response, err := biz.BankMultiActivityRepo.DecodeBody(ctx, mchId, respBody)
if err != nil {
return nil, err
}
return response, nil
}

View File

@ -2,18 +2,29 @@ package biz
import (
"context"
"errors"
"fmt"
"gorm.io/gorm"
errPb "voucher/api/err"
"voucher/internal/biz/bo"
"voucher/internal/biz/vo"
"voucher/internal/pkg/lock"
)
func (this *VoucherBiz) WechatNotifyConsumer(ctx context.Context, tag string, req *bo.WechatVoucherNotifyBo) error {
func (this *VoucherBiz) WechatNotifyConsumer(ctx context.Context, ip string, req *bo.WechatVoucherNotifyBo) error {
c := vo.WechatNotifyConsumeLockKey.BuildCache([]string{tag, req.PlainText.StockID, req.PlainText.CouponID})
if req.PlainText.StockCreatorMchid == "" || req.PlainText.StockID == "" || req.PlainText.CouponID == "" {
return fmt.Errorf("回调必要信息不能为空")
}
// 商品数据量较少,先查询商品是否存在,过滤多余的通知信息
_, err := this.ProductRepo.GetByMchStockId(ctx, req.PlainText.StockCreatorMchid, req.PlainText.StockID)
if err != nil {
//if errors.Is(err, gorm.ErrRecordNotFound) {
// return nil
//}
return fmt.Errorf("商品查询错误 error: %w", err)
}
c := vo.WechatNotifyConsumeLockKey.BuildCache([]string{req.PlainText.StockCreatorMchid, req.PlainText.StockID, req.PlainText.CouponID})
return lock.NewMutex(this.rdb.Rdb, c.TTL).Lock(ctx, c.Key, func(ctx context.Context) error {
@ -22,6 +33,13 @@ func (this *VoucherBiz) WechatNotifyConsumer(ctx context.Context, tag string, re
return err
}
if order.ActivityId != "" {
if err = req.ValidateMultiNotify(); err != nil {
return fmt.Errorf("multi validate req error: %v", err)
}
return this.MultiBiz.Run(ctx, ip, req.PlainText.StockCreatorMchid, req, order)
}
if req.PlainText.Status.IsSended() {
return this.available(ctx, order)
@ -33,7 +51,6 @@ func (this *VoucherBiz) WechatNotifyConsumer(ctx context.Context, tag string, re
} else if req.PlainText.Status.IsExpired() {
return this.expired(ctx, order)
}
return fmt.Errorf("未知通知类型:%s", req.PlainText.Status.GetText())
@ -43,31 +60,8 @@ func (this *VoucherBiz) WechatNotifyConsumer(ctx context.Context, tag string, re
func (this *VoucherBiz) getOrder(ctx context.Context, req *bo.WechatVoucherNotifyBo) (*bo.OrderBo, error) {
order, err := this.OrderRepo.GetByCouponId(ctx, req.PlainText.StockCreatorMchid, req.PlainText.StockID, req.PlainText.CouponID)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
order, err = this.OrderRepo.GetByTransactionId(ctx, req.PlainText.StockCreatorMchid, req.PlainText.StockID, req.PlainText.ConsumeInformation.TransactionID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("微信回调消费,订单不存在,StockCreatorMchid:%s,StockID:%s,CouponID:%s,CreateTime:%s",
req.PlainText.StockCreatorMchid,
req.PlainText.StockID,
req.PlainText.CouponID,
req.PlainText.CreateTime,
)
}
return nil, err
}
return order, nil
return nil, fmt.Errorf("订单查询错误 error: %w", err)
}
return order, nil
@ -80,10 +74,17 @@ 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 {
err := this.OrderRepo.NotifyUsed(
ctx,
order.ID,
req.PlainText.ConsumeInformation.TransactionID,
req.PlainText.ConsumeInformation.ConsumeTime,
)
if err != nil {
return err
}
order.LastUseTime = &req.PlainText.ConsumeInformation.ConsumeTime
return this.notify(ctx, order)
}
@ -117,20 +118,21 @@ func (this *VoucherBiz) expired(ctx context.Context, order *bo.OrderBo) error {
func (this *VoucherBiz) notify(ctx context.Context, order *bo.OrderBo) error {
if order.ActivityId == "" {
if order.ActivityId != "" {
return nil // 多笔立减活动,不做通知(?不知道有没有核销通知,多笔核销情况未知)
}
return this.cmbNotify(ctx, order.ID)
return this.cmbNotify(ctx, order)
}
func (this *VoucherBiz) cmbNotify(ctx context.Context, orderId uint64) error {
func (this *VoucherBiz) cmbNotify(ctx context.Context, orderReq *bo.OrderBo) error {
order, err := this.OrderRepo.GetByID(ctx, orderId)
order, err := this.OrderRepo.GetByID(ctx, orderReq.ID)
if err != nil {
return err
}
order.LastUseTime = orderReq.LastUseTime
if orderNotify, err2 := this.Cmb.Notify(ctx, order); err != nil {
if !errPb.IsNeedRetryNotify(err2) {

View File

@ -1,9 +1,13 @@
package wechatrepo
import (
"context"
"net/http"
"voucher/internal/biz/bo"
)
type BankMultiActivityRepo interface {
Order(order *bo.OrderBo) (couponId string, err error)
Notify(ctx context.Context, mchId string, headers *http.Header, respBody []byte) (response *bo.WechatVoucherNotifyBo, err error)
DecodeBody(ctx context.Context, mchId string, respBody []byte) (*bo.WechatVoucherNotifyBo, error)
}

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,8 @@ message Bootstrap {
Cron cron = 9;
RdsMQ rdsMQ = 10;
AliYunSms aliYunSms = 11;
Tripartite tripartite = 12;
repeated WechatSubject wechatSubject = 13;
}
message Server {
@ -88,6 +90,7 @@ message Cmb {
string cmbKeyAlias = 8;
string orgNo = 9;
string notifyUrl = 10;
string multiNotifyUrl = 13;
int64 noticeStartDays = 11;
int64 noticeEndDays = 12;
}
@ -134,6 +137,8 @@ message RdsMQ {
Queue wechatTimeSliceQuery = 2;
Queue wechatRetry = 3;
Queue retryNotify = 4;
Queue orderNotifyRetry = 5;
Queue usedNotify = 6;
}
message AliYunSms {
@ -144,7 +149,22 @@ message AliYunSms {
string templateWarning = 5;
}
message Tripartite {
message QiXing {
string appKey = 1;
}
QiXing qiXing = 1;
}
message Logs {
string business = 1;
string access = 2;
}
message WechatSubject {
string mchID = 1;
string name = 4;
string mchCertificateSerialNumber = 2;
string wechatPayPublicKeyID = 3;
string mchApiV3Key = 6;
}

View File

@ -36,8 +36,8 @@ func db(data *conf.Data_Database) *gorm.DB {
panic("failed to gormDB " + err.Error())
}
sqlDB.SetMaxIdleConns(100)
sqlDB.SetMaxOpenConns(1000)
sqlDB.SetMaxIdleConns(50)
sqlDB.SetMaxOpenConns(200)
return gormDB
}

View File

@ -289,19 +289,19 @@ func (s *CmbMixRepoImpl) Request(ctx context.Context, req *v1.CmbRequest, uri st
_, bodyBytes, err := request.Post(ctx, r, nil, request.WithHeaders(h), request.WithTimeout(time.Second*20))
if err != nil {
log.Errorf("请求掌上生活报错,url:%s,err:%v", r, err)
return nil, err
//log.Errorf("请求掌上生活报错,url:%s,err:%v", r, err)
return nil, fmt.Errorf("CMB请求失败:%v", err)
}
var response *v1.CmbReply
if err = json.Unmarshal(bodyBytes, &response); err != nil {
log.Errorf("请求掌上生活返回数据解析报错:%s,url:%s,bodyBytes:%s", err.Error(), r, string(bodyBytes))
return nil, err
return nil, fmt.Errorf("CMB数据解析错误:%s", err.Error())
}
if response.RespCode != vo.CmbResponseStatusSuccess.GetValue() {
log.Errorf("请求掌上生活返回报错:msg:%s,url:%s,bodyBytes:%s", response.RespMsg, r, string(bodyBytes))
return nil, fmt.Errorf(response.RespMsg)
//log.Errorf("请求掌上生活返回报错:msg:%s,url:%s,bodyBytes:%s", response.RespMsg, r, string(bodyBytes))
return nil, fmt.Errorf("CMB请求返回错误:%s", response.RespMsg)
}
return response, nil

View File

@ -0,0 +1,37 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameMultiNotifyDatum = "multi_notify_data"
// MultiNotifyDatum mapped from table <multi_notify_data>
type MultiNotifyDatum struct {
ID int64 `gorm:"column:id;primaryKey" json:"id"`
IP string `gorm:"column:ip;not null;comment:ip" json:"ip"` // ip
Source string `gorm:"column:source;not null;comment:来源" json:"source"` // 来源
NotifyID string `gorm:"column:notify_id;not null;comment:回调通知id" json:"notify_id"` // 回调通知id
OrderNo string `gorm:"column:order_no;not null;comment:订单号" json:"order_no"` // 订单号
OutBizNo string `gorm:"column:out_biz_no;not null;comment:外部业务号" json:"out_biz_no"` // 外部业务号
CouponID string `gorm:"column:coupon_id;not null;comment:券id" json:"coupon_id"` // 券id
StockID string `gorm:"column:stock_id;not null;comment:微信批次号" json:"stock_id"` // 微信批次号
ConsumeAmount int32 `gorm:"column:consume_amount;not null;comment:核销金额" json:"consume_amount"` // 核销金额
ConsumeTime *time.Time `gorm:"column:consume_time;not null;comment:核销时间" json:"consume_time"` // 核销时间
TransactionID string `gorm:"column:transaction_id;not null;comment:微信支付系统生成的订单号" json:"transaction_id"` // 微信支付系统生成的订单号
EventType string `gorm:"column:event_type;not null;comment:通知的类型" json:"event_type"` // 通知的类型
Status string `gorm:"column:status;not null;comment:券状态" json:"status"` // 券状态
OriginalData string `gorm:"column:original_data;not null;comment:微信回调通知原始数据" json:"original_data"` // 微信回调通知原始数据
NoticeNum int32 `gorm:"column:notice_num;not null;comment:通知下游次数" json:"notice_num"` // 通知下游次数
CreateTime *time.Time `gorm:"column:create_time;not null;comment:创建时间" json:"create_time"` // 创建时间
UpdateTime *time.Time `gorm:"column:update_time;comment:修改时间" json:"update_time"` // 修改时间
}
// TableName MultiNotifyDatum's table name
func (*MultiNotifyDatum) TableName() string {
return TableNameMultiNotifyDatum
}

View File

@ -0,0 +1,41 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameMultiNotifyLog = "multi_notify_log"
// MultiNotifyLog mapped from table <multi_notify_log>
type MultiNotifyLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
MultiNotifyDataID int64 `gorm:"column:multi_notify_data_id;not null" json:"multi_notify_data_id"`
OrderNo string `gorm:"column:order_no;not null;comment:订单号" json:"order_no"` // 订单号
OutBizNo string `gorm:"column:out_biz_no;not null;comment:外部请求号" json:"out_biz_no"` // 外部请求号
CouponID string `gorm:"column:coupon_id;not null;comment:微信券id" json:"coupon_id"` // 微信券id
ActivityNo string `gorm:"column:activity_no;not null;comment:活动编号CMB开头" json:"activity_no"` // 活动编号CMB开头
StockID string `gorm:"column:stock_id;not null;comment:微信批次号" json:"stock_id"` // 微信批次号
EventType string `gorm:"column:event_type;not null;comment:通知类型 COUPON.USE" json:"event_type"` // 通知类型 COUPON.USE
Status string `gorm:"column:status;not null;comment:券状态" json:"status"` // 券状态
ConsumeAmount int32 `gorm:"column:consume_amount;not null;comment:核销金额" json:"consume_amount"` // 核销金额
ConsumeTime *time.Time `gorm:"column:consume_time;not null;comment:核销时间" json:"consume_time"` // 核销时间
TransactionID string `gorm:"column:transaction_id;not null;comment:微信支付系统生成的订单号" json:"transaction_id"` // 微信支付系统生成的订单号
RequestURL string `gorm:"column:request_url;not null;comment:请求地址" json:"request_url"` // 请求地址
RequestStatus int32 `gorm:"column:request_status;not null;comment:请求状态" json:"request_status"` // 请求状态
OriginReq string `gorm:"column:origin_req;not null;comment:请求数据" json:"origin_req"` // 请求数据
Request string `gorm:"column:request;not null;comment:请求数据" json:"request"` // 请求数据
Response string `gorm:"column:response;not null;comment:响应结果" json:"response"` // 响应结果
OrderCreateTime *time.Time `gorm:"column:order_create_time;not null;comment:券收单时间-蓝色兄弟" json:"order_create_time"` // 券收单时间-蓝色兄弟
CouponCreateTime *time.Time `gorm:"column:coupon_create_time;not null;comment:券创建时间-微信侧" json:"coupon_create_time"` // 券创建时间-微信侧
CreateTime *time.Time `gorm:"column:create_time;not null;comment:创建时间" json:"create_time"` // 创建时间
UpdateTime *time.Time `gorm:"column:update_time;comment:修改时间" json:"update_time"` // 修改时间
}
// TableName MultiNotifyLog's table name
func (*MultiNotifyLog) TableName() string {
return TableNameMultiNotifyLog
}

View File

@ -0,0 +1,130 @@
package repoimpl
import (
"context"
"gorm.io/gorm"
"time"
"voucher/internal/biz/bo"
"voucher/internal/biz/repo"
"voucher/internal/data"
"voucher/internal/data/model"
)
// MultiNotifyDataRepoImpl .
type MultiNotifyDataRepoImpl struct {
Base[model.MultiNotifyDatum, bo.MultiNotifyDataBo]
db *data.Db
}
// NewMultiNotifyDataRepoImpl .
func NewMultiNotifyDataRepoImpl(db *data.Db) repo.MultiNotifyDataRepo {
return &MultiNotifyDataRepoImpl{db: db}
}
func (p *MultiNotifyDataRepoImpl) DB(ctx context.Context) *gorm.DB {
return p.db.DB(ctx).WithContext(ctx).Model(model.MultiNotifyDatum{})
}
func (p *MultiNotifyDataRepoImpl) FindNoticeNumZero(ctx context.Context, fun func(ctx context.Context, rows []*bo.MultiNotifyDataBo) error) error {
tx := p.DB(ctx).Where("notice_num = 0")
tx.Order("id asc") // 显式清除排序,移除默认的 ORDER BY
tx.Limit(200)
var results = make([]*model.MultiNotifyDatum, 0)
result := tx.FindInBatches(&results, 50, func(tx *gorm.DB, batch int) error {
return fun(ctx, p.ToBos(results))
})
if result.Error != nil {
return result.Error
}
return nil
}
func (p *MultiNotifyDataRepoImpl) Create(ctx context.Context, req *bo.MultiNotifyDataBo) (*bo.MultiNotifyDataBo, error) {
now := time.Now()
info := &model.MultiNotifyDatum{
Source: req.Source,
IP: req.IP,
NotifyID: req.NotifyID,
OrderNo: req.OrderNo,
OutBizNo: req.OutBizNo,
CouponID: req.CouponID,
StockID: req.StockID,
ConsumeAmount: req.ConsumeAmount,
ConsumeTime: req.ConsumeTime,
TransactionID: req.TransactionID,
EventType: req.EventType,
Status: req.Status.GetValue(),
OriginalData: req.OriginalData,
NoticeNum: 0,
CreateTime: &now,
}
if err := p.DB(ctx).Create(info).Error; err != nil {
return nil, err
}
return p.ToBo(info), nil
}
func (p *MultiNotifyDataRepoImpl) GetByID(ctx context.Context, id int64) (*bo.MultiNotifyDataBo, error) {
var item model.MultiNotifyDatum
tx := p.DB(ctx).Where(model.MultiNotifyDatum{ID: id}).First(&item)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, gorm.ErrRecordNotFound
}
return p.ToBo(&item), nil
}
func (p *MultiNotifyDataRepoImpl) GetByNotifyID(ctx context.Context, source, notifyId string) (*bo.MultiNotifyDataBo, error) {
var item model.MultiNotifyDatum
tx := p.DB(ctx).Where(model.MultiNotifyDatum{Source: source, NotifyID: notifyId}).First(&item)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, gorm.ErrRecordNotFound
}
return p.ToBo(&item), nil
}
func (p *MultiNotifyDataRepoImpl) AddNoticeNum(ctx context.Context, id int64) error {
now := time.Now()
u := map[string]interface{}{
"notice_num": gorm.Expr("notice_num + ?", 1),
"update_time": &now,
}
tx := p.DB(ctx).Where("id = ?", id).Updates(u)
if tx.Error != nil {
return tx.Error
}
if tx.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}

View File

@ -0,0 +1,149 @@
package repoimpl
import (
"context"
"errors"
"fmt"
"time"
"unicode/utf8"
err2 "voucher/api/err"
"voucher/internal/biz/bo"
"voucher/internal/biz/repo"
"voucher/internal/biz/vo"
"voucher/internal/data"
"voucher/internal/data/model"
"gorm.io/gorm"
)
// MultiNotifyLogRepoImpl .
type MultiNotifyLogRepoImpl struct {
Base[model.MultiNotifyLog, bo.MultiNotifyLogBo]
db *data.Db
}
// NewMultiNotifyLogRepoImpl .
func NewMultiNotifyLogRepoImpl(db *data.Db) repo.MultiNotifyLogRepo {
return &MultiNotifyLogRepoImpl{db: db}
}
func (p *MultiNotifyLogRepoImpl) DB(ctx context.Context) *gorm.DB {
return p.db.DB(ctx).WithContext(ctx).Model(model.MultiNotifyLog{})
}
func (p *MultiNotifyLogRepoImpl) Create(ctx context.Context, req *bo.MultiNotifyLogBo) (*bo.MultiNotifyLogBo, error) {
now := time.Now()
info := &model.MultiNotifyLog{
MultiNotifyDataID: req.MultiNotifyDataID,
OrderNo: req.OrderNo,
OutBizNo: req.OutBizNo,
CouponID: req.CouponID,
ActivityNo: req.ActivityNo,
StockID: req.StockID,
EventType: req.EventType,
Status: req.Status.GetValue(),
ConsumeAmount: req.ConsumeAmount,
ConsumeTime: req.ConsumeTime,
TransactionID: req.TransactionID,
OriginReq: req.OriginReq,
Request: req.Request,
RequestURL: req.RequestURL,
RequestStatus: vo.MultiNotifyLogStatusWait.GetValue(),
OrderCreateTime: req.OrderCreateTime,
CouponCreateTime: req.CouponCreateTime,
CreateTime: &now,
}
if err := p.DB(ctx).Create(info).Error; err != nil {
return nil, err
}
return p.ToBo(info), nil
}
func (p *MultiNotifyLogRepoImpl) GetByID(ctx context.Context, id int64) (*bo.MultiNotifyLogBo, error) {
var item model.MultiNotifyLog
tx := p.DB(ctx).Where(model.MultiNotifyLog{ID: id}).First(&item)
if tx.Error != nil {
return nil, fmt.Errorf("b fail %w", tx.Error)
}
if tx.RowsAffected == 0 {
return nil, err2.ErrorDbNotFound("数据不存在")
}
return p.ToBo(&item), nil
}
func (p *MultiNotifyLogRepoImpl) ExistsSuccessByDataIDAndTransactionID(ctx context.Context, multiNotifyDataID int64, transactionID string) (bool, error) {
var item model.MultiNotifyLog
err := p.DB(ctx).
Select("id").
Where("multi_notify_data_id = ? AND transaction_id = ? AND request_status = ?", multiNotifyDataID, transactionID, vo.MultiNotifyLogStatusSuccess.GetValue()).
Limit(1).
Take(&item).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, err
}
return true, nil
}
func (p *MultiNotifyLogRepoImpl) Success(ctx context.Context, id int64, response string) error {
now := time.Now()
res := p.DB(ctx).
Where(model.MultiNotifyLog{
ID: id,
RequestStatus: vo.MultiNotifyLogStatusWait.GetValue(),
}).
Updates(model.MultiNotifyLog{
RequestStatus: vo.MultiNotifyLogStatusSuccess.GetValue(),
Response: response,
UpdateTime: &now,
})
if res.Error != nil {
return res.Error
}
return nil
}
func (p *MultiNotifyLogRepoImpl) Fail(ctx context.Context, id int64, remark string) error {
if utf8.RuneCountInString(remark) > 255 {
runes := []rune(remark)
if len(runes) > 255 {
remark = string(runes[:255])
}
}
now := time.Now()
res := p.DB(ctx).
Where(model.MultiNotifyLog{
ID: id,
RequestStatus: vo.MultiNotifyLogStatusWait.GetValue(),
}).
Updates(model.MultiNotifyLog{
RequestStatus: vo.MultiNotifyLogStatusFail.GetValue(),
Response: remark,
UpdateTime: &now,
})
if res.Error != nil {
return res.Error
}
return nil
}

View File

@ -55,6 +55,8 @@ func (p *OrderRepoImpl) SpecifyFindInBatches(ctx context.Context, req *bo.FindIn
tx = tx.Where("voucher_no IN (?)", req.VoucherNos)
}
tx.Order("receive_success_time asc") // 显式清除排序,移除默认的 ORDER BY
var results = make([]*model.Order, 0)
result := tx.FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
@ -72,8 +74,11 @@ func (p *OrderRepoImpl) SpecifyFindInBatches(ctx context.Context, req *bo.FindIn
func (p *OrderRepoImpl) FinSucByStockIdInBatches(ctx context.Context, req *do.WechatQuery, fun func(ctx context.Context, rows []*bo.OrderBo) error) error {
tx := p.DB(ctx).
Where("`status` = ?", vo.OrderStatusSuccess.GetValue()).
Where("activity_id = ''")
Where("`status` in (?)", []uint8{
vo.OrderStatusSuccess.GetValue(),
vo.OrderStatusUse.GetValue(),
vo.OrderStatusExpired.GetValue(),
}).Where("activity_id = ''")
if req.ProductNo != "" {
tx = tx.Where("product_no = ?", req.ProductNo)
@ -93,7 +98,9 @@ func (p *OrderRepoImpl) FinSucByStockIdInBatches(ctx context.Context, req *do.We
var results = make([]*model.Order, 0)
result := tx.FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
tx.Order("receive_success_time asc") // 显式清除排序,移除默认的 ORDER BY
result := tx.FindInBatches(&results, 1000, func(tx *gorm.DB, batch int) error {
return fun(ctx, p.ToBos(results))
})
@ -105,14 +112,32 @@ func (p *OrderRepoImpl) FinSucByStockIdInBatches(ctx context.Context, req *do.We
return nil
}
func (p *OrderRepoImpl) FinFailByStockIdInBatches(ctx context.Context, batchNo string, fun func(ctx context.Context, rows []*bo.OrderBo) error) error {
func (p *OrderRepoImpl) FinUsedInBatches(ctx context.Context, req *do.WechatUsedQuery, fun func(ctx context.Context, rows []*bo.OrderBo) error) error {
var results = make([]*model.Order, 0)
result := p.DB(ctx).
Where("batch_no = ?", batchNo).
Where("`status` = ?", vo.OrderStatusFail.GetValue()).
FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
tx := p.DB(ctx).
Where("`status` = ?", vo.OrderStatusUse.GetValue()).
Where("activity_id = ''")
if req.StartTime != "" {
tx = tx.Where("last_use_time > ?", req.StartTime)
}
if req.EndTime != "" {
tx = tx.Where("last_use_time <= ?", req.EndTime)
}
if req.ProductNo != "" {
tx = tx.Where("product_no = ?", req.ProductNo)
}
if req.BatchNo != "" {
tx = tx.Where("batch_no = ?", req.ProductNo)
}
if req.OrderNo != "" {
tx = tx.Where("order_no = ?", req.OrderNo)
}
// 显式清除排序,移除默认的 ORDER BY
result := tx.Order("last_use_time asc").FindInBatches(&results, 500, func(tx *gorm.DB, batch int) error {
return fun(ctx, p.ToBos(results))
})
@ -123,23 +148,43 @@ func (p *OrderRepoImpl) FinFailByStockIdInBatches(ctx context.Context, batchNo s
return nil
}
func (p *OrderRepoImpl) FindIngInBatches(ctx context.Context, fun func(ctx context.Context, rows []*bo.OrderBo) error) error {
//func (p *OrderRepoImpl) FinFailByStockIdInBatches(ctx context.Context, batchNo string, fun func(ctx context.Context, rows []*bo.OrderBo) error) error {
//
// var results = make([]*model.Order, 0)
//
// result := p.DB(ctx).
// Where("batch_no = ?", batchNo).
// Where("`status` = ?", vo.OrderStatusFail.GetValue()).
// Order("receive_success_time asc"). // 显式清除排序,移除默认的 ORDER BY
// FindInBatches(&results, 200, func(tx *gorm.DB, batch int) error {
// return fun(ctx, p.ToBos(results))
// })
//
// if result.Error != nil {
// return result.Error
// }
//
// return nil
//}
var results = make([]*model.Order, 0)
result := p.DB(ctx).
Where("`status` = ?", vo.OrderStatusIng.GetValue()).
Limit(20).
FindInBatches(&results, 10, func(tx *gorm.DB, batch int) error {
return fun(ctx, p.ToBos(results))
})
if result.Error != nil {
return result.Error
}
return nil
}
//func (p *OrderRepoImpl) FindIngInBatches(ctx context.Context, fun func(ctx context.Context, rows []*bo.OrderBo) error) error {
//
// var results = make([]*model.Order, 0)
//
// result := p.DB(ctx).
// Where("`status` = ?", vo.OrderStatusIng.GetValue()).
// Limit(20).
// Order("receive_success_time asc"). // 显式清除排序,移除默认的 ORDER BY
// FindInBatches(&results, 10, func(tx *gorm.DB, batch int) error {
// return fun(ctx, p.ToBos(results))
// })
//
// if result.Error != nil {
// return result.Error
// }
//
// return nil
//}
func (p *OrderRepoImpl) FindInBatches(ctx context.Context, req *bo.FindInBatchesUseBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error {
@ -149,7 +194,8 @@ func (p *OrderRepoImpl) FindInBatches(ctx context.Context, req *bo.FindInBatches
Where("activity_id = ''").
Where("`status` IN (?)", []uint8{vo.OrderStatusSuccess.GetValue(), vo.OrderStatusUse.GetValue()}).
Where("receive_success_time BETWEEN ? AND ?", req.StartTime, req.EndTime).
FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
Order("receive_success_time asc"). // 显式清除排序,移除默认的 ORDER BY
FindInBatches(&results, 1000, func(tx *gorm.DB, batch int) error {
// tx.RowsAffected 提供当前批处理中记录的计数the count of records in the current batch
// 'batch' 变量表示当前批号the current batch number
// 返回 error 将阻止更多的批处理
@ -163,6 +209,61 @@ func (p *OrderRepoImpl) FindInBatches(ctx context.Context, req *bo.FindInBatches
return nil
}
func (p *OrderRepoImpl) FindRetryQuery(ctx context.Context, req *do.RetryQueryNotice, fun func(ctx context.Context, rows []*bo.OrderBo) error) error {
statusArr := []uint8{
vo.OrderStatusSuccess.GetValue(),
vo.OrderStatusUse.GetValue(),
vo.OrderStatusExpired.GetValue(),
}
tx := p.DB(ctx).
Where("`status` in (?)", statusArr).
Where("activity_id = ''")
if req.ProductNo != "" {
tx = tx.Where("product_no = ?", req.ProductNo)
}
if req.ReceiveSuccessStartTime != "" {
tx = tx.Where("receive_success_time > ?", req.ReceiveSuccessStartTime)
}
if req.ReceiveSuccessEndTime != "" {
tx = tx.Where("receive_success_time <= ?", req.ReceiveSuccessEndTime)
}
if req.ProductNo != "" {
tx = tx.Where("product_no = ?", req.ProductNo)
}
if req.OrderNos != nil {
tx = tx.Where("order_no IN (?)", req.OrderNos)
}
if req.OutBizNos != nil {
tx = tx.Where("out_biz_no IN (?)", req.OutBizNos)
}
if req.VoucherNos != nil {
tx = tx.Where("voucher_no IN (?)", req.VoucherNos)
}
var results = make([]*model.Order, 0)
tx.Order("receive_success_time asc") // 显式清除排序,移除默认的 ORDER BY
result := tx.FindInBatches(&results, 1000, func(tx *gorm.DB, batch int) error {
return fun(ctx, p.ToBos(results))
})
if result.Error != nil {
return result.Error
}
return nil
}
func (p *OrderRepoImpl) Create(ctx context.Context, req *bo.OrderBo) (*bo.OrderBo, error) {
now := time.Now()
@ -274,22 +375,6 @@ func (p *OrderRepoImpl) GetByCouponId(ctx context.Context, merchantNo, batchNo,
return p.ToBo(info), nil
}
func (this *OrderRepoImpl) GetByTransactionId(ctx context.Context, stockCreatorMchId, stockID, transactionId string) (*bo.OrderBo, error) {
row := &model.Order{}
tx := this.DB(ctx).Where(model.Order{MerchantNo: stockCreatorMchId, BatchNo: stockID, TransactionId: transactionId}).First(&row)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, gorm.ErrRecordNotFound
}
return this.ToBo(row), nil
}
func (p *OrderRepoImpl) Ing(ctx context.Context, id uint64) error {
now := time.Now()
@ -410,7 +495,48 @@ func (p *OrderRepoImpl) Used(ctx context.Context, id uint64) error {
return nil
}
func (p *OrderRepoImpl) NotifyUsed(ctx context.Context, id uint64, transactionId string) error {
func (p *OrderRepoImpl) MultiLastUsed(ctx context.Context, id uint64, lastUseTime time.Time) error {
now := time.Now()
tx := p.DB(ctx).
Where(model.Order{
ID: id,
}).
Updates(model.Order{
Remark: "核销",
LastUseTime: &lastUseTime,
UpdateTime: &now,
})
if tx.Error != nil {
return fmt.Errorf("update db fail %w", tx.Error)
}
return nil
}
func (p *OrderRepoImpl) MultiOverUsed(ctx context.Context, id uint64, lastUseTime time.Time, remark string) error {
now := time.Now()
tx := p.DB(ctx).
Where(model.Order{
ID: id,
}).
Updates(model.Order{
Status: vo.OrderStatusUse.GetValue(),
Remark: remark,
LastUseTime: &lastUseTime,
UpdateTime: &now,
})
if tx.Error != nil {
return fmt.Errorf("update db fail %w", tx.Error)
}
return nil
}
func (p *OrderRepoImpl) NotifyUsed(ctx context.Context, id uint64, transactionId string, lastUseTime time.Time) error {
now := time.Now()
tx := p.DB(ctx).
@ -421,7 +547,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

@ -2,9 +2,15 @@ package repoimpl
import (
"context"
"errors"
"fmt"
"gorm.io/gorm"
"time"
err2 "voucher/api/err"
"voucher/internal/biz/bo"
"voucher/internal/biz/do"
"voucher/internal/biz/repo"
"voucher/internal/biz/vo"
"voucher/internal/data"
"voucher/internal/data/model"
)
@ -61,3 +67,142 @@ func (p *OrderBakRepoImpl) SpecifyFindInBatches(ctx context.Context, req *bo.Fin
return nil
}
func (p *OrderBakRepoImpl) FindRetryQuery(ctx context.Context, req *do.RetryQueryNotice, fun func(ctx context.Context, rows []*bo.OrderBo) error) error {
statusArr := []uint8{
vo.OrderStatusSuccess.GetValue(),
vo.OrderStatusUse.GetValue(),
vo.OrderStatusExpired.GetValue(),
}
tx := p.DB(ctx).
Where("`status` in (?)", statusArr).
Where("activity_id = ''")
if req.ProductNo != "" {
tx = tx.Where("product_no = ?", req.ProductNo)
}
if req.ReceiveSuccessStartTime != "" {
tx = tx.Where("receive_success_time > ?", req.ReceiveSuccessStartTime)
}
if req.ReceiveSuccessEndTime != "" {
tx = tx.Where("receive_success_time <= ?", req.ReceiveSuccessEndTime)
}
if req.ProductNo != "" {
tx = tx.Where("product_no = ?", req.ProductNo)
}
if req.OrderNos != nil {
tx = tx.Where("order_no IN (?)", req.OrderNos)
}
if req.OutBizNos != nil {
tx = tx.Where("out_biz_no IN (?)", req.OutBizNos)
}
if req.VoucherNos != nil {
tx = tx.Where("voucher_no IN (?)", req.VoucherNos)
}
var results = make([]*model.OrderBak, 0)
tx.Order("receive_success_time asc") // 显式清除排序,移除默认的 ORDER BY
result := tx.FindInBatches(&results, 1000, func(tx *gorm.DB, batch int) error {
return fun(ctx, p.ToBos(results))
})
if result.Error != nil {
return result.Error
}
return nil
}
func (p *OrderBakRepoImpl) GetByID(ctx context.Context, id uint64) (*bo.OrderBo, error) {
info := &model.OrderBak{}
tx := p.DB(ctx).Where(model.OrderBak{ID: id}).First(&info)
if tx.Error != nil {
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
return nil, err2.ErrorDbNotFound("订单数据不存在")
}
return nil, fmt.Errorf("order db fail %w", tx.Error)
}
if tx.RowsAffected == 0 {
return nil, err2.ErrorDbNotFound("订单数据不存在")
}
return p.ToBo(info), nil
}
func (p *OrderBakRepoImpl) Used(ctx context.Context, id uint64) error {
now := time.Now()
tx := p.DB(ctx).
Where(model.OrderBak{
ID: id,
}).
Updates(model.OrderBak{
Status: vo.OrderStatusUse.GetValue(),
Remark: "核销",
LastUseTime: &now,
UpdateTime: &now,
})
if tx.Error != nil {
return fmt.Errorf("update db fail %w", tx.Error)
}
return nil
}
func (p *OrderBakRepoImpl) Expired(ctx context.Context, id uint64) error {
now := time.Now()
tx := p.DB(ctx).
Where(model.OrderBak{
ID: id,
}).
Updates(model.OrderBak{
Status: vo.OrderStatusExpired.GetValue(),
Remark: "过期",
UpdateTime: &now,
})
if tx.Error != nil {
return fmt.Errorf("update db fail %w", tx.Error)
}
return nil
}
func (p *OrderBakRepoImpl) Available(ctx context.Context, id uint64) error {
now := time.Now()
tx := p.DB(ctx).
Where(model.OrderBak{
ID: id,
Status: vo.OrderStatusUse.GetValue(),
}).
Updates(model.OrderBak{
Status: vo.OrderStatusSuccess.GetValue(),
Remark: "重置为成功,领取成功时间重置",
ReceiveSuccessTime: &now, // 领取成功时间重置
UpdateTime: &now,
})
if tx.Error != nil {
return fmt.Errorf("update db fail %w", tx.Error)
}
return nil
}

View File

@ -104,19 +104,42 @@ func (r *ProductRepoImpl) GetByBatchNo(ctx context.Context, batchNo string) (*bo
tx := db.Where(model.Product{BatchNo: batchNo}).First(&item)
if tx.Error != nil {
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
return nil, err2.ErrorDbNotFound("商品数据不存在")
}
return nil, fmt.Errorf("product db fail %w", tx.Error)
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, err2.ErrorDbNotFound("商品数据不存在")
return nil, gorm.ErrRecordNotFound
}
return r.ToBo(item), nil
}
func (r *ProductRepoImpl) GetByMchStockId(ctx context.Context, mchId, stockId string) (*bo.ProductBo, error) {
var item *model.Product
tx := r.db.DB(ctx).Where(model.Product{MchId: mchId, BatchNo: stockId}).First(&item)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, gorm.ErrRecordNotFound
}
return r.ToBo(item), nil
}
func (r *ProductRepoImpl) DelCacheByProductNo(ctx context.Context, productNo string) error {
c := vo.ProductQueryKey.BuildCache([]string{productNo})
_, _ = r.rdb.Rdb.Del(ctx, c.Key).Result()
return nil
}
func (r *ProductRepoImpl) GetByProductNo(ctx context.Context, productNo string) (*bo.ProductBo, error) {
c := vo.ProductQueryKey.BuildCache([]string{productNo})

View File

@ -11,4 +11,6 @@ var ProviderRepoImplSet = wire.NewSet(
NewOrderNotifyRepoImpl,
NewWechatNotifyRegisterTagRepoImpl,
NewOrderBakRepoImpl,
NewMultiNotifyDataRepoImpl,
NewMultiNotifyLogRepoImpl,
)

View File

@ -1,11 +1,16 @@
package wechatrepoimpl
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"net/http"
err2 "voucher/api/err"
"voucher/internal/biz/bo"
"voucher/internal/biz/businesserr"
"voucher/internal/biz/wechatrepo"
"voucher/internal/conf"
"voucher/internal/data"
@ -43,11 +48,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
@ -55,3 +69,66 @@ func (w *BankMultiActivityImpl) Order(order *bo.OrderBo) (couponId string, err e
return *resp.CouponId, nil
}
func (w *BankMultiActivityImpl) Notify(ctx context.Context, mchId string, headers *http.Header, respBody []byte) (*bo.WechatVoucherNotifyBo, error) {
t, err := w.wx.Get(mchId)
if err != nil {
return nil, err
}
body, decodeBodyStr, err := t.Notify(ctx, headers, respBody)
if err != nil {
return nil, err
}
var plainText bo.PlainText
if err = json.Unmarshal([]byte(decodeBodyStr), &plainText); err != nil {
return nil, fmt.Errorf("plainText json.Unmarshal error: %v", err)
}
return &bo.WechatVoucherNotifyBo{
ID: body.Id,
CreateTime: body.CreateTime,
ResourceType: body.ResourceType,
EventType: body.EventType,
Summary: body.Summary,
OriginalType: body.Resource.OriginalType,
AssociatedData: body.Resource.AssociatedData,
PlainText: plainText,
}, nil
}
func (w *BankMultiActivityImpl) DecodeBody(ctx context.Context, mchId string, respBody []byte) (*bo.WechatVoucherNotifyBo, error) {
t, err := w.wx.Get(mchId)
if err != nil {
return nil, err
}
var body utils.WxNotifyBody
if err = json.Unmarshal(respBody, &body); err != nil {
return nil, err
}
decryptedText, err := t.DecodeBody(&body)
if err != nil {
return nil, err
}
var plainText bo.PlainText
if err = json.Unmarshal([]byte(decryptedText), &plainText); err != nil {
return nil, fmt.Errorf("plainText json.Unmarshal error: %v", err)
}
return &bo.WechatVoucherNotifyBo{
ID: body.Id,
CreateTime: body.CreateTime,
ResourceType: body.ResourceType,
EventType: body.EventType,
Summary: body.Summary,
OriginalType: body.Resource.OriginalType,
AssociatedData: body.Resource.AssociatedData,
PlainText: plainText,
}, nil
}

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)
@ -122,6 +200,12 @@ func (c *CpnRepoImpl) Query(ctx context.Context, orderWechat *bo.OrderBo) (vo.Or
return 0, err
}
cpnStatus := CpnStatus(*resp.Status)
if cpnStatus.IsRevoked() {
}
return CpnStatus(*resp.Status).GetStatus()
}

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

@ -13,12 +13,16 @@ const (
CpnStatusAvailable = "SENDED"
CpnStatusUsed = "USED"
CpnStatusExpired = "EXPIRED"
CpnStatusRevoked = "REVOKED"
CpnStatusRecover = "RECOVER"
)
var CpnStatusTextMap = map[CpnStatus]string{
CpnStatusAvailable: "可用",
CpnStatusUsed: "已实扣",
CpnStatusExpired: "已过期",
CpnStatusRevoked: "已失效",
CpnStatusRecover: "已回收",
}
var CpnStatusMap = map[CpnStatus]vo.OrderStatus{
@ -34,9 +38,13 @@ func (o CpnStatus) GetText() string {
return "未知"
}
func (o CpnStatus) IsRevoked() bool {
return o == CpnStatusRevoked
}
func (o CpnStatus) GetStatus() (vo.OrderStatus, error) {
if resultStatus, ok := CpnStatusMap[o]; ok {
return resultStatus, nil
}
return 0, fmt.Errorf("CpnStatus[%s]未定义", o)
return 0, fmt.Errorf("CpnStatus[%s-%s]未定义", o, o.GetText())
}

View File

@ -24,6 +24,14 @@ func NewWx(c *conf.Bootstrap) (*Wx, error) {
clients[c.Wechat.MchID] = client
for _, v := range c.WechatSubject {
cli, e := buildWechat(v)
if e != nil {
return nil, e
}
clients[v.MchID] = cli
}
return &Wx{Clients: clients}, nil
}
@ -44,6 +52,33 @@ func (this *Wx) Get(mchId string) (*marketing.Marketing, error) {
return nil, fmt.Errorf("微信调用client不存在[%s]", mchId)
}
func buildWechat(wx *conf.WechatSubject) (*marketing.Marketing, error) {
dir, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("商户ID[%s]商户名称[%s]获取目的地址有误[%v]", wx.MchID, wx.Name, err)
}
filePath := fmt.Sprintf("%s/cert/wechat/%s", dir, wx.MchID)
if !helper.FileExists(filePath) {
panic(fmt.Sprintf("商户ID[%s]商户名称[%s]微信密钥证书信息不存在,请联系技术人员处理", wx.MchID, wx.Name))
}
cc, err := utils2.CreateMchConfig(
wx.MchID, // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
wx.MchCertificateSerialNumber, // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
fmt.Sprintf("%s/%s", filePath, "wechat_private_key.pem"), // 商户API证书私钥文件路径本地文件路径
wx.WechatPayPublicKeyID, // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径
wx.MchApiV3Key,
)
if err != nil {
return nil, err
}
return &marketing.Marketing{MchConfig: cc}, nil
}
func buildWx(wx *conf.Wechat) (*marketing.Marketing, error) {
dir, err := os.Getwd()
@ -62,6 +97,7 @@ func buildWx(wx *conf.Wechat) (*marketing.Marketing, error) {
fmt.Sprintf("%s/%s", filePath, "wechat_private_key.pem"), // 商户API证书私钥文件路径本地文件路径
wx.WechatPayPublicKeyID, // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径
"",
)
if err != nil {
return nil, err

View File

@ -186,14 +186,11 @@ func TestVerifyBody(t *testing.T) {
}
func TestDecryptBody(t *testing.T) {
//priKey := "f6a8d2f412e289686aba6a0f33cad1a64367d0ba012046ee0fbbefd3ffd675bd"
priKey := "8d39ff3d2559258c163f4510f082727f51531e1953ab203d5ab1ea4a6d94fd73"
priKey := "f6a8d2f412e289686aba6a0f33cad1a64367d0ba012046ee0fbbefd3ffd675bd" // 测试
//priKey := "8d39ff3d2559258c163f4510f082727f51531e1953ab203d5ab1ea4a6d94fd73"
//content := "BAdcIauIjNx3LsrplpJiZoljE4hCiGHra6ulhgG1qL0tKcAeenX+Z9VaHfXLSdkji1fYBpdZiiI35R0vFtnXPXJCJdHsGbfbae+PzNznYQS3KM8/90Y/FIWzSoszfUiF6fAuv8I6v9kQuqHUTidHeHyICDoyvJ0nhbNyUyg85bAKd6TmkVX1MgXLQ81m|5KfR/5UkpVBEQv1dx+iJbojOykNRuDV8Gsy3QOIlRI+cZvafRRPUUG6eeixnPMumhOvyZwsSG/OBeg0U/lSlAepg12tXWcQ601wjgyLaKN1iMvb1DCtfnJFAm8EWAc2SLH3NQuyhxGe/jgCXvj0wGphh4vBUzm8la8i8Aij0BI5lfgU5OzglkKDln6zHN3vBHDqOurEh18eU6z1bfvNnDpzdwEcygcEIH/6lGiqVnGH+C2+QpcKeCnj5qKGFiuSC"
content := "BH666zulhEdX6Axd3+LUaPo0a6WU1ze2qKFrtu+pxq4EZAy1TPY1NQ0+53+WpscLsoksfjlpCOPVpSLQDKe1IHARFTgJSNgsOfvdBGIEoyDEDdMOc6JzONUuijKsfUTnQI+UsecoJOmRo4xtSloR0sql9FPqhxTn9quMrsJQVL/DNnol+vs8r6RsyLPL|ArPZsEB4BoGL5UX0BTb9vdyOuSt+cMf7msRcEIUUX6O/oOoCdQmxYIVsaLCgq9IkK6mP4O1oAiu6R8gfdsyHgIQSDtKiv7DRAwrI/b4UD4ka+BVVhJXkX0uYlqmA8yCrcmdM2zwp7c/v0qeEr0LnJhrm2tPwAEhLdWEDJskNsH/dQsVkWxf77Vj1OtLQjl+6Ir0RYzdEXlBxNGBv6Y0x2khQcbcTbVfcMFJ3zoGcCEU75UrDLS/ph73t/BM2dICakqL+ymlKiqMm/+K77B4pdYbJ2SpiUdydv4wUzNLPsx2mhuh8KhuyEo1DI8Fzjqd3TiuXiIZISyxaXJ3aV9NdxdYVkZ4j5iLpYdL7GqAxBtdLLK/J6KtqzW+3rwM3L7alxbXHVdcgoxVZanunisaNWPsn4V5BUoL8halT3/smwZM="
//content := "BDUuuPClIlUKDJdpHtNBIU6u5JTetrVG53AfKDSrhah9Q0QZWAj3K5pZF8G/HFtzj/KvbrHfP/gnHri0L7L91HaKYYc1vqy/q8Z69v7MEiIWL2LeLOsc2b0cHmnt7Qey5aZzYVbZJJNhus2gvhahGwOPpPL50JFC8IqAlU4+E/kUBgx+RgzAIkLLs8Se|k1WOt2Eb+xkxSobw/1DaLblguznhnsk1ga87sfqrrPATRuTyszzdVrRtCUuUHRKJ+vQwlZAR1ypkBC1vPMdt8zccI/CeNof+4Ap23enbQwTJQ4KRvij4kbJd6ycY97B+vHnI+oSnwfmjK0EWNUeRrCr2Uau7yxmzlFwJHprbgPZpHVuUjAzMIXNixOfxSTdc9dL+j3/tR7+yookvd9W3hNm96XZFPTheI0FqUKkbKWUUcVs4lC1/yEgjwXFcD4NM"
//content := "BDqANzEC9vq82FTu9wR89Ou/8o2J2vD7yeoExb1Uxl2PSA9EsvI7YQNGSkSdxoloqRBn4vIrA8Uh+FmWtJ9wbFEEtbC+Epj8qHWN8S+lH9JEnvJhPhEtMgmHeuGDtvzHMACvc7KoBmJ/6XZIoEyLeRgkJnCTVVxnD78KMJ4eXzj7C2ErkISJ5r3qcL9t|cPw/uy87PW74aeU1RwGwUsYXZlXYkG0m+BoO/3qT4rLSIWsHvBN76yQJlTk89aa9VALdtwcU7H0q7ivCLZ9uflB6YFkg5QRQoj1b2AFFU4TPkpgAdCBAKs5+6z9cwDn+QQyuamKAnp1wJfMj7Ksa3HxLwCxTh/w2dF6LuruLFWZstMK1g9ID7tVTPqc05VNmktmNhc97BYuIH66xa0ZFgr+0i/3hFMx0hNTOau5UWFM6v4Lb1iM/v9ggM5ZvAheUspfgvRUhpD2TZvgyneWxqg=="
//content := "BOk+pytaf5faPeISKp/rzspV2ngCr/RuHe3lTTGv+0csaKh3y/x/e6njEbpYpQXCcYTI4eN9pjKMyU9ByA1hsfmiCSU4ziUCu5+ltxDFt/FHuhspBwIYRkVCQM9WefydXgGEuXBd0S+yB6BhBMqJgSUuyjxyF5AecdFoB9AoRyslbqSflqn2cctw5pIx|yvTLMRUfjO+9XV4ilxNBMD9jLwA1PYdthta5uhOOSzaJVOdNDhC+aAltyrLrZkBIUOrPZX7U3RtJVLnsk0y+t8/di+XxriZtnZZ0vTJC0q5tAONN9IE7s9clGGclX5dXYGV4jhP70tFzSeTlWsviLAs9sCvsuIloKEGLJdDvJ53vxiYskX3+pOF0uvOkAlJb70pfOHXsIzSAEhFC88O6o9AR4BRa18ex3V+CqN0CkFUqVazvxoyPFlhzxRXZyBkt"
content := "BOk+pytaf5faPeISKp/rzspV2ngCr/RuHe3lTTGv+0csaKh3y/x/e6njEbpYpQXCcYTI4eN9pjKMyU9ByA1hsfmiCSU4ziUCu5+ltxDFt/FHuhspBwIYRkVCQM9WefydXgGEuXBd0S+yB6BhBMqJgSUuyjxyF5AecdFoB9AoRyslbqSflqn2cctw5pIx|yvTLMRUfjO+9XV4ilxNBMD9jLwA1PYdthta5uhOOSzaJVOdNDhC+aAltyrLrZkBIUOrPZX7U3RtJVLnsk0y+t8/di+XxriZtnZZ0vTJC0q5tAONN9IE7s9clGGclX5dXYGV4jhP70tFzSeTlWsviLAs9sCvsuIloKEGLJdDvJ53vxiYskX3+pOF0uvOkAlJb70pfOHXsIzSAEhFC88O6o9AR4BRa18ex3V+CqN0CkFUqVazvxoyPFlhzxRXZyBkt"
rs, err := DecryptBody(&Decrypts{content, priKey})
if err != nil {

65
internal/pkg/helper/ip.go Normal file
View File

@ -0,0 +1,65 @@
package helper
import (
"net"
"strings"
"github.com/go-kratos/kratos/v2/transport/http"
)
// GetClientIP 获取客户端真实 IP
func GetClientIP(ctx http.Context) string {
// 检查 X-Forwarded-For 头(多个代理时格式为 "client, proxy1, proxy2"
if xff := ctx.Header().Get("X-Forwarded-For"); xff != "" {
ips := strings.Split(xff, ",")
for _, ip := range ips {
ip = strings.TrimSpace(ip)
if ip != "" {
// 验证是否为合法 IP
if isValidIP(ip) {
return ip
}
}
}
}
// 检查 X-Real-IP 头
if realIP := ctx.Header().Get("X-Real-IP"); realIP != "" {
if isValidIP(realIP) {
return realIP
}
}
// 检查 X-Forwarded
if forwarded := ctx.Header().Get("X-Forwarded"); forwarded != "" {
// 格式可能为 "for=client-ip;host=example.com;proto=https"
parts := strings.Split(forwarded, ";")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "for=") {
ip := strings.TrimPrefix(part, "for=")
ip = strings.Trim(ip, `"`) // 可能被引号包围
if isValidIP(ip) {
return ip
}
}
}
}
// 直接从 RemoteAddr 获取
remoteAddr := ctx.Request().RemoteAddr
if ip, _, err := net.SplitHostPort(remoteAddr); err == nil {
if isValidIP(ip) {
return ip
}
}
return ""
}
// 验证是否为合法 IP
func isValidIP(ip string) bool {
parsedIP := net.ParseIP(ip)
return parsedIP != nil
}

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,7 @@
package helper
import (
"encoding/base64"
"fmt"
"testing"
"time"
@ -43,6 +44,63 @@ func queryUsed(useNum *int) {
}
func TestMd5(t *testing.T) {
s := Md5(`{"product_no":"","start_time":"2025-04-20 09:00:00","end_time":"2025-05-01 00:00:00"}`)
jsonStr := `{"content":"1","timestamp":1765447477945,"ciphertext":"77CAC2FCFDEBAC6665025A1B81E3BBF9"}`
ciphertext := Md5(jsonStr)
t.Log(ciphertext)
}
func Test_DecodeString(t *testing.T) {
wxNotifyData, _ := base64.StdEncoding.DecodeString("ewogICJpZCI6ICI0YWIyNjk5ZC1lOTFkLTU0NjAtOTgxMC0yNWZkNmQ0YzY5YTUiLAogICJjcmVhdGVfdGltZSI6ICIyMDI1LTEyLTA4VDE3OjU0OjI0KzA4OjAwIiwKICAicmVzb3VyY2VfdHlwZSI6ICJlbmNyeXB0LXJlc291cmNlIiwKICAiZXZlbnRfdHlwZSI6ICJDT1VQT04uVVNFIiwKICAic3VtbWFyeSI6ICLku6Pph5HliLjmoLjplIDpgJrnn6UiLAogICJvcmlnaW5hbF90eXBlIjogImNvdXBvbiIsCiAgImFzc29jaWF0ZWRfZGF0YSI6ICJjb3Vwb24iLAogICJwbGFpbl90ZXh0IjogewogICAgInN0b2NrX2NyZWF0b3JfbWNoaWQiOiAiMTY1MjQ2NTU0MSIsCiAgICAic3RvY2tfaWQiOiAiMjEzODY0ODQiLAogICAgImNvdXBvbl9pZCI6ICIxNDIzODgzNTQ5OTQiLAogICAgImNvdXBvbl9uYW1lIjogIumTtuihjOWNoeWkmueslOeri+WHjyIsCiAgICAiZGVzY3JpcHRpb24iOiAiIiwKICAgICJzdGF0dXMiOiAiU0VOREVEIiwKICAgICJjcmVhdGVfdGltZSI6ICIyMDI1LTEyLTA4VDE3OjUwOjQ4KzA4OjAwIiwKICAgICJjb3Vwb25fdHlwZSI6ICJOT1JNQUwiLAogICAgIm5vX2Nhc2giOiBmYWxzZSwKICAgICJzaW5nbGVpdGVtIjogZmFsc2UsCiAgICAiYnVzaW5lc3NfdHlwZSI6ICIiLAogICAgImNvbnN1bWVfaW5mb3JtYXRpb24iOiB7CiAgICAgICJjb25zdW1lX3RpbWUiOiAiMjAyNS0xMi0wOFQxNzo1NDoyNCswODowMCIsCiAgICAgICJjb25zdW1lX21jaGlkIjogIjEyNzQ5Mzg2MDEiLAogICAgICAidHJhbnNhY3Rpb25faWQiOiAiNDIwMDAwMjk5NjIwMjUxMjA4MzA2MzA1MTgzNCIsCiAgICAgICJjb25zdW1lX2Ftb3VudCI6IDE2CiAgICB9CiAgfQp9")
t.Log(string(wxNotifyData))
}
func TestLength(t *testing.T) {
jsonStr := `{
"id": "4ab2699d-e91d-5460-9810-25fd6d4c69a5",
"create_time": "2025-12-08T17:54:24+08:00",
"resource_type": "encrypt-resource",
"event_type": "COUPON.USE",
"summary": "代金券核销通知",
"original_type": "coupon",
"associated_data": "coupon",
"plain_text": {
"stock_creator_mchid": "1652465541",
"stock_id": "21386484",
"coupon_id": "142388354994",
"coupon_name": "银行卡多笔立减",
"description": "",
"status": "SENDED",
"create_time": "2025-12-08T17:50:48+08:00",
"coupon_type": "NORMAL",
"no_cash": false,
"singleitem": false,
"business_type": "",
"consume_information": {
"consume_time": "2025-12-08T17:54:24+08:00",
"consume_mchid": "1274938601",
"transaction_id": "4200002996202512083063051834",
"consume_amount": 16
}
}
}`
s := len(jsonStr)
t.Log(s)
}
func Test_Time(t *testing.T) {
strTime := "2026-03-27 10:33:20"
tt, _ := time.Parse("2006-01-02 15:04:05", strTime)
t.Log(tt.Format("2006-01-02 15:04:05.000"))
//for i := 0; i < 20; i++ {
// formatTime := time.Now().Format("2006-01-02 15:04:05.999")
// t.Log(formatTime)
// formatTime2 := time.Now().Format("2006-01-02 15:04:05.000")
// t.Log(formatTime2)
// time.Sleep(1 * time.Second)
//}
}

View File

@ -2,6 +2,8 @@ package request
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"testing"
@ -24,8 +26,8 @@ func Test_Get(t *testing.T) {
return
}
t.Logf("响应体:", string(respBody))
t.Logf("响应头:", respHeader)
t.Logf("响应体:%s", string(respBody))
t.Logf("响应头:%+v", respHeader)
}
func Test_RequestHeaders(t *testing.T) {
@ -43,11 +45,12 @@ func Test_RequestHeaders(t *testing.T) {
return
}
t.Logf("响应体:", string(respBody))
t.Logf("响应头:", respHeader)
t.Logf("响应体:%s", string(respBody))
t.Logf("响应头:%+v", respHeader)
}
func Test_RequestStatusCode(t *testing.T) {
uri := "http://example.com/api/update"
body := []byte("update data")
@ -61,6 +64,43 @@ func Test_RequestStatusCode(t *testing.T) {
return
}
t.Logf("响应体:", string(respBody))
t.Logf("响应头:", respHeader)
t.Logf("响应体:%s", string(respBody))
t.Logf("响应头:%+v", respHeader)
}
func Test_WxNotifyRequest(t *testing.T) {
uri := "https://gateway.dev.cdlsxd.cn/voucher/v1/notify/1100040695"
//uri := "https://voucher.86698.cn/voucher/v1/notify/1100040695"
headerBytes := []byte(`{"Accept":["*/*"],"Cache-Control":["no-cache"],"Connection":["close"],"Content-Length":["1113"],"Content-Type":["application/json"],"Pragma":["no-cache"],"User-Agent":["Mozilla/4.0"],"Wechatpay-Nonce":["TD238pwsm3x0rYo57A8Z27Qa3AFDmToI"],"Wechatpay-Serial":["PUB_KEY_ID_0111000406952026032500382251001000"],"Wechatpay-Signature":["a64Z4Lngoz61OhhpG82g/rv8tYV0KmnE7g5KA4hvj7bY6XKSdQaifajXnLQHxA70owmd1kGA6wY5ZxDCvxnNHRjiNP4f68Ii7swL0TCfcU3aLpkLlmPM4wtzSh4YK1cIg4z/mqpxLDkxUky2vp4BvmGx6oKjK5Yv/emFerS2rWWdM2tTdi5Rv1rAGz/osqmrW8d+ZsLlGKHZ6wh7GUmNqt/qF+/kx+GB1jpzCgn8aUQqNvfnbtFcs9SKMQqm+DmBUO85IHL2WCQsuQ6cWpTrRCg5TGj0M0b8TEVHLbX0eBxtUyyGtdwuQbVbEzqCbwLs234YLG1Koq8lxZx6Nib1OQ=="],"Wechatpay-Signature-Type":["WECHATPAY2-SHA256-RSA2048"],"Wechatpay-Timestamp":["1774864935"],"X-Forwarded-For":["121.51.58.174, 172.29.42.89"]}`)
bodyBytes := []byte(`{"id":"ab4dbb6c-d9e9-5d2f-9f22-9697dfe2a58e","create_time":"2026-03-30T18:02:15+08:00","resource_type":"encrypt-resource","event_type":"COUPON.USE","summary":"代金券核销通知","resource":{"original_type":"coupon","algorithm":"AEAD_AES_256_GCM","ciphertext":"lsBBeh/DxRiecPLgtFyZov6tsCA38vWQ4QZxZouyp48mr3/kP3jSu/l2Sl1Rr59a4F0iIFSxuc7p6+/ovX11Ol7i+gbJmNlc+F3Yfo/bb8zyt/ubys4Ep0Or3YEvZr+h4G4k8rPn0iTYlzuY2hBcfLAazt/qTSz87aNmpBz3XN1CJL+5C0LffWdd5j1zjxfY2AYiUwtSuzo2cRYrxberxyeL9SeHJnqc+8o0njB6fyOJ4q5D9SyPqSNe228tW2xw0/d9QNWtzIlwQhyCwuhdXh0GBSWO7jdEmD3tQCIOk3OgoooywBZFtfwUvMiy2/tohEKd9JIQi3OVV2ctUx+cBdJ7RIH2r8PFZO55PqIND9itDCraSbfHQpqlYfVLZMossa92U54khv9FOzy/RQJPut/YWFkC4PsXgtfPpXQI0l6M+if1AVX+h4XHKk+wCd3fQ37tAwpxNF9kCuLgCIzgmnWjxj8296z6lQkKFomDn6xLdv6ECtnNYGhItQqU6v122/klkeysURSn1qFSbkujFvByCEKSmasTHe1yLorvfAYl+YJVnvLKsBxjjlCiEdCjspTFlDG3m+N/DxsYpYegYxvzfrM2MhgjJwPBbuxxgz4KMtYyiOrHvMDg8qCA41fRkxO4JkSA0JP22Q7v6zXOcHgf/w5aNo6xyICUrg2AueY0UV2uDA4yF6hYTho+CfGLjRfdFCuDReBPlA2TcJKSEtAWJW25nFrqKY/C87xAiD99JPf+tg7h2HcDCzasnV8h/AGA3PSQf2ZX4TsuyRdyPQ+uQWSW","associated_data":"coupon","nonce":"c6H0W8tKIiJP"}}`)
var headerMap http.Header
if err := json.Unmarshal(headerBytes, &headerMap); err != nil {
t.Error(fmt.Sprintf("解析 headers 失败: %v", err))
return
}
hc := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 1, // 最大空闲连接数
MaxIdleConnsPerHost: 1, // 每个主机的最大空闲连接数
IdleConnTimeout: 10 * time.Second, // 空闲连接超时时间
},
}
isSuccess := func(code int) bool {
return code == http.StatusOK || code == http.StatusCreated
}
respHeader, respBody, err := Post(context.Background(), uri, bodyBytes, WithHttpClient(hc), WithStatusCodeFunc(isSuccess), WithHeaders(headerMap))
if err != nil {
t.Error(err)
return
}
t.Logf("响应体:%s", string(respBody))
t.Logf("响应头:%v", respHeader)
}

View File

@ -0,0 +1,27 @@
package qixing
import (
"github.com/go-playground/validator/v10"
)
type QiXingRequestBo struct {
Content string `json:"content" validate:"required"`
Timestamp int64 `json:"timestamp" validate:"required"`
Ciphertext string `json:"ciphertext" validate:"required"`
}
// QiXingResponse 响应结构体 {"msg":"SUCCESS"} / {"msg":"操作成功"}
type QiXingResponse struct {
Msg string `json:"msg"`
}
func (c *QiXingRequestBo) Validate() error {
if err := validator.New().Struct(c); err != nil {
for _, err = range err.(validator.ValidationErrors) {
return err
}
}
return nil
}

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

@ -1,7 +1,9 @@
package marketing
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strings"
"voucher/internal/pkg/wechat/srv"
@ -39,13 +41,13 @@ func (srv *Marketing) Send(openId string, req *SendReq) (response *SendResp, err
}
// Query @link https://pay.weixin.qq.com/doc/v3/merchant/4014569864
func (srv *Marketing) Query(appid, openId, couponId string) (response *SendResp, err error) {
func (srv *Marketing) Query(appId, openId, couponId string) (response *SendResp, err error) {
path := strings.Replace(queryPath, "{openid}", url.PathEscape(openId), -1)
path = strings.Replace(path, "{coupon_id}", url.PathEscape(couponId), -1)
var uv = url.Values{}
uv.Set("appid", appid)
uv.Set("appid", appId)
path += "?" + uv.Encode()
respBody, err := srv.Request2(utils.Host, utils.MethodGET, path, nil)
@ -59,3 +61,18 @@ func (srv *Marketing) Query(appid, openId, couponId string) (response *SendResp,
return response, nil
}
func (srv *Marketing) Notify(_ context.Context, headers *http.Header, respBody []byte) (body *utils.WxNotifyBody, response string, err error) {
wxNotifyBody, err := srv.GetNotifyBody(headers, respBody)
if err != nil {
return nil, "", err
}
bizStr, err := srv.DecodeBody(wxNotifyBody)
if err != nil {
return nil, "", err
}
return wxNotifyBody, bizStr, nil
}

View File

@ -0,0 +1,87 @@
package utils
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
"fmt"
)
const (
keyLengthByte = 32 // AES-256 密钥长度(字节)
authTagLengthByte = 16 // GCM 认证标签长度(字节)
)
// AesUtil 用于微信支付 AES-256-GCM 解密的工具类
type AesUtil struct {
aesKey []byte // 32字节的AES密钥
}
// NewAesUtil 创建AesUtil实例验证密钥长度
func NewAesUtil(aesKey string) (*AesUtil, error) {
if len(aesKey) != keyLengthByte {
return nil, errors.New("无效的ApiV3Key长度应为32个字节")
}
return &AesUtil{aesKey: []byte(aesKey)}, nil
}
// DecryptToString 解密 AEAD_AES_256_GCM 加密的数据
// associatedData: 附加认证数据
// nonceStr: 随机数12字节
// ciphertext: 加密后的密文Base64编码
func (a *AesUtil) DecryptToString(associatedData, nonceStr, ciphertext string) (string, error) {
// 1. Base64解码密文
cipherBytes, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", fmt.Errorf("密文Base64解码失败: %w", err)
}
// 2. 验证密文长度(需包含认证标签)
if len(cipherBytes) <= authTagLengthByte {
return "", errors.New("密文长度不足,无法解析认证标签")
}
// 3. 分离密文和认证标签GCM模式中认证标签通常附加在密文末尾
ctext := cipherBytes[:len(cipherBytes)-authTagLengthByte]
authTag := cipherBytes[len(cipherBytes)-authTagLengthByte:]
// 4. 初始化AES-GCM加密器
block, err := aes.NewCipher(a.aesKey)
if err != nil {
return "", fmt.Errorf("创建AES加密器失败: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", fmt.Errorf("创建GCM模式失败: %w", err)
}
// 5. 构建附加数据GCM的AAD
additionalData := []byte(associatedData)
// 6. 解密GCM模式会自动验证认证标签
plaintext, err := gcm.Open(nil, []byte(nonceStr), append(ctext, authTag...), additionalData)
if err != nil {
return "", fmt.Errorf("解密失败(可能是密钥错误或数据被篡改): %w", err)
}
return string(plaintext), nil
}
type Resource struct {
OriginalType string `json:"original_type"`
Algorithm string `json:"algorithm"`
Ciphertext string `json:"ciphertext"` // 如支付通知中的 encrypted_data
AssociatedData string `json:"associated_data"` // 微信支付回调的附加数据 通常为空字符串或回调相关信息
Nonce string `json:"nonce"` // 12字节字符串
}
type WxNotifyBody struct {
Id string `json:"id"`
CreateTime string `json:"create_time"`
ResourceType string `json:"resource_type"`
EventType string `json:"event_type"`
Summary string `json:"summary"`
Resource Resource `json:"resource"`
}

View File

@ -0,0 +1,22 @@
package utils
import (
"testing"
)
func TestAes(t *testing.T) {
aesUtil, err := NewAesUtil("d9af70585b18ae206d981548c766563f")
if err != nil {
t.Errorf("NewAesUtil() error = %v", err)
return
}
gotResponse, err := aesUtil.DecryptToString("coupon", "EGvd12Nf2Z1X", "+sUnGECvoHvsFE0ZnTT1ij/b8TKUxYS9jRh3gUSZbsO0WXEgXnwOvfOQjugnqx58lmYafa4BEoJ08c8La0HJpzZUthd6OEkcYlELcQOfqr72sikfFa5izbsNHh+hqd0fmUmpioyCre/BcrDneFqrOShrcLqUdb7FugI362Q1wgCgVJ1DqbGFvQuLHSHZ+UG0HY7MF7lt5r1w72tklHuvbxdfJZ2Vaj5muRUUpZIsZDZ76pqDlsTepxt5upWeVOBMLHjXCMEIJzcV0jF5tFgn7GTea/DORmRLEpY03uMiAyTYYaVbww1S0b96VuoTjVAAf0+oeiLtn+ZoWO1ZL5+Rbz1GTxivluoD+WVn/V5WUYNYnftiQ0LZcigxXIp+DYJOnuoLMDdWQCih4bElJGZnFuJIWpkS73Dn3HfbAx8lg0vq882PY/tk0bOXH6yrWGT9DO6ZHnPNABy4pb6xLRJtvmflSDhYc5Fa7qn0ySRAIK8e4C+z7aeKhzhWpNe2kGyjKKQqN92j9kMjsbfDqOSPRVB3FIxOvbzrJyR3erkN2WYRzTCZ6U7Qbgg/+tdWPdYNhAMrVbdmp2J1/daetCmmrd2HIIB6HqhsHYQMu0/NxQVPLGXuHzHY9fuNiYczST6KobLOFQ0fEO2dx6AFNImwHxHbdSHpchUqcE6sdjfoHNgSW7Jv4Fclm8GDqQmu4l6/ZhJrpn+I4ePijo7U/Ewh4KrCBHiw0JEttcZuBDqMM1Q9rmI4trcfekT2wy7HGgK9qetuOHMC5ENfO/U1gpa4wIU=")
if err != nil {
t.Errorf("DecryptToString() error = %v", err)
return
}
t.Log(gotResponse)
}

View File

@ -19,7 +19,6 @@ import (
"net/url"
"os"
"sort"
"strconv"
"strings"
"time"
)
@ -42,6 +41,8 @@ type MchConfig struct {
wechatPayPublicKeyFilePath string
privateKey *rsa.PrivateKey
wechatPayPublicKey *rsa.PublicKey
aesKey string
}
// MchId 商户号
@ -76,6 +77,7 @@ func CreateMchConfig(
privateKeyFilePath string,
wechatPayPublicKeyId string,
wechatPayPublicKeyFilePath string,
aesKey string,
) (*MchConfig, error) {
mchConfig := &MchConfig{
mchId: mchId,
@ -83,6 +85,7 @@ func CreateMchConfig(
privateKeyFilePath: privateKeyFilePath,
wechatPayPublicKeyId: wechatPayPublicKeyId,
wechatPayPublicKeyFilePath: wechatPayPublicKeyFilePath,
aesKey: aesKey,
}
privateKey, err := LoadPrivateKeyWithPath(mchConfig.privateKeyFilePath)
if err != nil {
@ -293,13 +296,13 @@ func ValidateResponse(
nonce := headers.Get(WechatPayNonce)
// 拒绝过期请求
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid timestamp: %v", err)
}
if time.Now().Sub(time.Unix(timestamp, 0)) > 5*time.Minute {
return errors.New("invalid timestamp")
}
//timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
//if err != nil {
// return fmt.Errorf("invalid timestamp: %v", err)
//}
//if time.Now().Sub(time.Unix(timestamp, 0)) > 5*time.Minute {
// return errors.New("invalid timestamp")
//}
if serialNo != wechatpayPublicKeyId {
return fmt.Errorf(
@ -572,6 +575,49 @@ func (srv *MchConfig) Verify(request *http.Request) (string, error) {
return EncryptOAEPWithPublicKey(string(respBody), srv.wechatPayPublicKey)
}
func (srv *MchConfig) GetNotifyBody(headers *http.Header, respBody []byte) (*WxNotifyBody, error) {
if respBody == nil {
return nil, fmt.Errorf("request HttpBody is nil")
}
err := ValidateResponse(
srv.WechatPayPublicKeyId(),
srv.WechatPayPublicKey(),
headers,
respBody,
)
if err != nil {
return nil, err
}
var wxNotifyBody WxNotifyBody
if err = json.Unmarshal(respBody, &wxNotifyBody); err != nil {
return nil, err
}
return &wxNotifyBody, nil
}
func (srv *MchConfig) DecodeBody(wxNotifyBody *WxNotifyBody) (string, error) {
aesUtil, err := NewAesUtil(srv.aesKey)
if err != nil {
return "", err
}
decryptedText, err := aesUtil.DecryptToString(
wxNotifyBody.Resource.AssociatedData,
wxNotifyBody.Resource.Nonce,
wxNotifyBody.Resource.Ciphertext,
)
if err != nil {
return "", err
}
return decryptedText, nil
}
// BuildSortedQueryString 函数接受一个 map返回按照字段名排序后的 URL 键值对格式字符串
func BuildSortedQueryString(params map[string]any) string {
// 创建一个字符串切片,用于保存所有的键名

View File

@ -0,0 +1,68 @@
package utils
import (
"crypto/rsa"
"reflect"
"testing"
)
func TestMchConfig_Request(t *testing.T) {
type fields struct {
mchId string
certificateSerialNo string
privateKeyFilePath string
wechatPayPublicKeyId string
wechatPayPublicKeyFilePath string
privateKey *rsa.PrivateKey
wechatPayPublicKey *rsa.PublicKey
}
type args struct {
host string
method string
path string
reqBody []byte
}
tests := []struct {
name string
fields fields
args args
wantResponse []byte
wantErr bool
}{
{
name: "test",
fields: fields{
mchId: "1652322442",
certificateSerialNo: "certificateSerialNo",
privateKeyFilePath: "privateKeyFilePath",
wechatPayPublicKeyId: "wechatPayPublicKeyId",
wechatPayPublicKeyFilePath: "wechatPayPublicKeyFilePath",
},
args: args{
host: "https://api.mch.weixin.qq.com",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srv := &MchConfig{
mchId: tt.fields.mchId,
certificateSerialNo: tt.fields.certificateSerialNo,
privateKeyFilePath: tt.fields.privateKeyFilePath,
wechatPayPublicKeyId: tt.fields.wechatPayPublicKeyId,
wechatPayPublicKeyFilePath: tt.fields.wechatPayPublicKeyFilePath,
privateKey: tt.fields.privateKey,
wechatPayPublicKey: tt.fields.wechatPayPublicKey,
}
gotResponse, err := srv.Request(tt.args.host, tt.args.method, tt.args.path, tt.args.reqBody)
if (err != nil) != tt.wantErr {
t.Errorf("Request() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotResponse, tt.wantResponse) {
t.Errorf("Request() gotResponse = %v, want %v", gotResponse, tt.wantResponse)
}
})
}
}

View File

@ -11,7 +11,6 @@ import (
"github.com/go-kratos/kratos/v2/middleware/recovery"
"github.com/go-kratos/kratos/v2/middleware/validate"
"github.com/go-kratos/kratos/v2/transport/http"
"github.com/go-kratos/kratos/v2/transport/http/pprof"
"github.com/gorilla/handlers"
http2 "net/http"
"time"
@ -27,25 +26,52 @@ func NewHTTPServer(
log *log.Helper,
accessLogger *log2.AccessLogger,
cmb *service.CmbService,
tripartiteService *service.TripartiteService,
notifyService *service.NotifyService,
) *http.Server {
//构建 server
srv := buildHTTPServer(c, accessLogger, log)
srv.Handle("/voucher/debug/pprof/", pprof.NewHandler())
//srv.Handle("/voucher/debug/pprof/", pprof.NewHandler())
srv.Route("/voucher/").GET("ping", func(ctx http.Context) error {
return ctx.String(http2.StatusOK, "pong")
})
// 启星(启星-蓝色兄弟立减金代配) /voucher/qixing/v1/notify
srv.Route("/voucher/").POST("qixing/v1/notify", tripartiteService.QiXingNotify)
// 通知url必须为公网可访问的URL必须为HTTPS不能携带参数
// https://域名.cn/voucher/v1/notify/123456 123456为微信主体商户号 /voucher/v1/notify/1100040695
// https://gateway.dev.cdlsxd.cn/voucher/v1/notify/123456 123456为微信主体商户号 测试环境
// https://voucher.86698.cn/voucher/v1/notify/123456 123456为微信主体商户号 正式环境
srv.Route("/voucher/").POST("/v1/notify/{mch_id}", notifyService.Notify)
srv.Route("/voucher/").POST("/v1/notifyMock", notifyService.NotifyMock)
// ---脚本--
// 订单通知重试 -- 不健全
srv.Route("/voucher/").POST("orderNotifyRetry", cmb.OrderNotifyRetry)
// 重试订单通知,查询微信状态再通知下游招行
srv.Route("/voucher/").POST("retryOrderNotice", cmb.RetryOrderNotice)
// 重试通知
srv.Route("/voucher/").POST("notifyRetry/{id}", cmb.NotifyRetry)
srv.Route("/voucher/").POST("queryOrder/{order_no}", cmb.QueryOrder)
// 查询订单状态及微信状态
srv.Route("/voucher/").POST("queryOrder/{order_no}/{phone}/{is_notice}", cmb.QueryOrder)
srv.Route("/voucher/").GET("query-order/{order_no}/{phone}/{is_notice}", cmb.QueryOrder)
// 查询商品
srv.Route("/voucher/").POST("queryStock/{product_no}", cmb.QueryStock)
// 注册商品tag到刚哥那边
srv.Route("/voucher/").POST("registerTag/{id}", cmb.RegisterTag)
// 成功订单查询最终状态处理
srv.Route("/voucher/").POST("pushWechatQuery", cmb.PushWechatQuery)
// 订单查询最终状态处理
srv.Route("/voucher/").POST("timeSliceQueryPush", cmb.TimeSliceQueryPush)
srv.Route("/voucher/").POST("pushWechatRetry/{batch_no}", cmb.PushWechatRetry)
// 商品预警通知
srv.Route("/voucher/").POST("warningBudget/{id}", cmb.WarningBudget)
// 指定重复通知对应单子数据
srv.Route("/voucher/").POST("specifyNotification", cmb.SpecifyNotification)
// 订单使用通知下游
srv.Route("/voucher/").POST("UsedNotifyPush", cmb.UsedNotifyPush)
v1.RegisterCmbHTTPServer(srv, cmb)

View File

@ -26,6 +26,10 @@ func NewRdbConsumer(
) *RdbConsumer {
manager := rdsmq.NewConsumerManager()
if cfTr := voucherService.GetRetryQueryNoticeConfig(); cfTr != nil {
manager.Add(cfTr)
}
if cf := voucherService.GetWechatQueryConfig(); cf != nil {
manager.Add(cf)
}
@ -34,14 +38,22 @@ func NewRdbConsumer(
manager.Add(cf1)
}
if cf2 := voucherService.GetWechatRetryConfig(); cf2 != nil {
manager.Add(cf2)
}
//if cf2 := voucherService.GetWechatRetryConfig(); cf2 != nil {
// manager.Add(cf2)
//}
if cf3 := voucherService.GetRetryNotifyConfig(); cf3 != nil {
manager.Add(cf3)
}
if cf4 := voucherService.GetOrderNotifyRetryConfig(); cf4 != nil {
manager.Add(cf4)
}
if cf5 := voucherService.GetUsedNotifyConfig(); cf5 != nil {
manager.Add(cf5)
}
return &RdbConsumer{hLog: hLog, conf: conf, manager: manager}
}

View File

@ -150,10 +150,11 @@ func (w *WechatNotifyConsumer) processMessage(msg mq_http_sdk.ConsumeMessageEntr
}
}()
//log.Warnf("微信回调消费接收消息成功 messageId:%s, messageTag:%s, message:%s", msg.MessageId, msg.MessageTag, msg.MessageBody)
ctx := context.Background()
if err := w.voucherService.WechatUseNotifyConsumer(ctx, msg.MessageTag, msg.MessageBody); err != nil {
//log.Errorf("微信回调消费接收消息成功,处理失败 messageId:%s, messageTag:%s, message:%s, err:%+v", msg.MessageId, msg.MessageTag, msg.MessageBody, err)
log.Errorf("微信回调消费接收消息成功,处理失败 messageId:%s, err:%+v", msg.MessageId, err)
log.Errorf("微信回调消费接收消息成功,处理失败 messageId:%s, messageTag:%s, message:%s, err:%+v", msg.MessageId, msg.MessageTag, msg.MessageBody, err)
}
}

133
internal/service/notify.go Normal file
View File

@ -0,0 +1,133 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/transport/http"
"gorm.io/gorm"
"io"
http2 "net/http"
"voucher/internal/biz"
"voucher/internal/biz/bo"
"voucher/internal/pkg/helper"
)
type NotifyService struct {
WechatBiz *biz.WechatBiz
VoucherBiz *biz.VoucherBiz
}
func NewNotifyService(wechatBiz *biz.WechatBiz, VoucherBiz *biz.VoucherBiz) *NotifyService {
return &NotifyService{WechatBiz: wechatBiz, VoucherBiz: VoucherBiz}
}
// Notify https://pay.weixin.qq.com/doc/v3/merchant/4012285250
func (srv *NotifyService) Notify(ctx http.Context) error {
mchId := ctx.Vars().Get("mch_id")
if mchId == "" {
log.Errorf("微信回调通知mchId参数不能为空")
return ctx.JSON(http2.StatusNetworkAuthenticationRequired, map[string]string{
"code": "FAIL",
"message": "微信回调通知mchId参数不能为空",
})
}
headers := ctx.Request().Header
if headers == nil {
log.Errorf("微信回调通知[%s]headers参数不能为空", mchId)
return ctx.JSON(http2.StatusNetworkAuthenticationRequired, map[string]string{
"code": "FAIL",
"message": "微信回调通知headers参数不能为空",
})
}
bodyBytes, err := io.ReadAll(ctx.Request().Body)
if err != nil {
log.Errorf("微信回调通知[%s],读取响应体失败: %v", mchId, err)
return fmt.Errorf("读取响应体失败: %w", err)
}
if len(bodyBytes) == 0 {
log.Errorf("微信回调通知[%s],响应体不能为空", mchId)
return ctx.JSON(http2.StatusNetworkAuthenticationRequired, map[string]string{
"code": "FAIL",
"message": "微信回调通知,响应体不能为空",
})
}
bizData, err := srv.WechatBiz.CallBack(ctx, mchId, &headers, bodyBytes)
if err != nil {
headerJson, _ := json.Marshal(headers)
log.Errorf("微信回调通知[%s],callBack处理失败:%s\nheaders:%s\nbody:%s", mchId, err.Error(), headerJson, string(bodyBytes))
return ctx.JSON(http2.StatusNetworkAuthenticationRequired, map[string]string{
"code": "FAIL",
"message": err.Error(),
})
}
ip := helper.GetClientIP(ctx)
if err = srv.VoucherBiz.WechatNotifyConsumer(ctx, ip, bizData); err != nil {
headerJson, _ := json.Marshal(headers)
log.Errorf("微信回调通知[%s],consumer处理失败:%s\nheaders:%s\nbody:%s\n解析数据:%+v", mchId, err.Error(), headerJson, string(bodyBytes), bizData)
if errors.Is(err, gorm.ErrRecordNotFound) {
return ctx.JSON(http2.StatusOK, nil)
}
return ctx.JSON(http2.StatusBadRequest, map[string]string{
"code": "FAIL",
"message": err.Error(),
})
}
return ctx.JSON(http2.StatusOK, nil)
}
// NotifyMock 模拟微信回调通知,用于测试
func (srv *NotifyService) NotifyMock(ctx http.Context) error {
bodyBytes, err := io.ReadAll(ctx.Request().Body)
if err != nil {
log.Errorf("微信回调通知,读取响应体失败: %v", err)
return fmt.Errorf("读取响应体失败: %w", err)
}
if len(bodyBytes) == 0 {
log.Errorf("微信回调通知,响应体不能为空")
return ctx.JSON(http2.StatusOK, map[string]string{
"code": "FAIL",
"message": "微信回调通知,响应体不能为空",
})
}
var bizData *bo.WechatVoucherNotifyBo
err = json.Unmarshal(bodyBytes, &bizData)
if err != nil {
log.Errorf("微信回调通知,解析响应体失败: %v", err)
return ctx.JSON(http2.StatusOK, map[string]string{
"code": "FAIL",
"message": "微信回调通知,解析响应体失败",
})
}
ip := helper.GetClientIP(ctx)
if err = srv.VoucherBiz.WechatNotifyConsumer(ctx, ip, bizData); err != nil {
log.Errorf("微信回调通知consumer处理失败:%s\nbody:%s\n解析数据:%+v", err.Error(), string(bodyBytes), bizData)
if errors.Is(err, gorm.ErrRecordNotFound) {
return ctx.JSON(http2.StatusOK, nil)
}
return ctx.JSON(http2.StatusOK, map[string]string{
"code": "FAIL",
"message": err.Error(),
})
}
return ctx.JSON(http2.StatusOK, nil)
}

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, product, err := c.order(ctx, request)
if err != nil {
return c.OrderFail(ctx, err)
return c.OrderFail(ctx, order, product, 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, *bo.ProductBo, error) {
bizContent, err := c.CmbMixRepo.OrderVerify(ctx, request)
if err != nil {
return "", err
return nil, 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, product, err := c.VoucherBiz.CmbOrder(ctx, boReq)
if err != nil {
return "", err
return nil, nil, err
}
return orderNo, nil
return order, product, nil
}
func (c *CmbService) OrderSuccess(ctx context.Context, orderNo string) (*v1.CmbReply, error) {
@ -60,19 +59,45 @@ 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, product *bo.ProductBo, 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 {
isMultiErr := false
if product != nil && product.ActivityId != "" {
isMultiErr = true
} else if order != nil && order.ActivityId != "" {
isMultiErr = true
}
bizReply := &v1.CmbOrderReply{
if isMultiErr {
cmbAPIError := e.HandleMULTIThirdErrCode()
bizReply = &v1.CmbOrderReply{
RespCode: vo.CmbResponseStatusFail.GetValue(),
RespMsg: se.Message,
RespMsg: cmbAPIError.Description,
CodeNo: "",
ThirdErrCode: se.Reason,
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

@ -0,0 +1,45 @@
package service
import (
"context"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"voucher/internal/pkg/rdsmq"
)
func (s *VoucherService) GetOrderNotifyRetryConfig() *rdsmq.ConsumeConfig {
queue := s.bc.RdsMQ.GetOrderNotifyRetry()
if queue == nil {
return nil
}
if !queue.GetIsOpen() {
log.Warn(fmt.Sprintf("[%s]RdsMQ is not open", queue.Name))
return nil
}
return &rdsmq.ConsumeConfig{
Rdb: s.rdb.Rdb,
QueueName: queue.Name,
NumWorkers: queue.NumWorkers,
WaitTime: queue.GetWaitTime().AsDuration(),
RetryNum: queue.RetryNum,
Fn: s.HandleOrderNotifyRetry,
Logger: s.logHelper,
}
}
func (s *VoucherService) HandleOrderNotifyRetry(ctx context.Context, msg string) error {
if msg == "" {
s.logHelper.Errorf("RdsMQ keySend error: msg is empty")
return nil
}
if err := s.VoucherBiz.OrderNotifyRetry(ctx, msg); err != nil {
s.logHelper.Error(err)
}
return nil
}

View File

@ -8,4 +8,6 @@ import (
var ProviderSetService = wire.NewSet(
NewVoucherService,
NewCmbService,
NewTripartiteService,
NewNotifyService,
)

102
internal/service/qixing.go Normal file
View File

@ -0,0 +1,102 @@
package service
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/transport/http"
"io"
http2 "net/http"
"voucher/internal/biz"
"voucher/internal/biz/bo"
"voucher/internal/conf"
"voucher/internal/pkg/helper"
"voucher/internal/pkg/supplier/qixing"
)
type TripartiteService struct {
bc *conf.Bootstrap
multiBiz *biz.MultiBiz
}
func NewTripartiteService(bc *conf.Bootstrap, multiBiz *biz.MultiBiz) *TripartiteService {
return &TripartiteService{bc: bc, multiBiz: multiBiz}
}
// QiXingNotify
// 重试规则
// 1.重试间隔每次推送失败后间隔5分钟重新推送
// 2.最大重试次数累计推送5 次(首次 + 4 次重试;
// 3.停止条件:返回成功响应 或 达到 4 次重试上限,停止推送。
func (srv *TripartiteService) QiXingNotify(ctx http.Context) error {
ip := helper.GetClientIP(ctx)
if ip == "" {
return fmt.Errorf("获取请求 IP 失败")
}
bodyBytes, err := io.ReadAll(ctx.Request().Body)
if err != nil {
return srv.ResponseErr(ctx, fmt.Sprintf("read body error: %v", err))
}
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))
}
if err = req.Validate(); err != nil {
log.Errorf("qixing notify req validate err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes))
return srv.ResponseErr(ctx, fmt.Sprintf("validate req error: %v", err))
}
// 加密校验串,(生成规则MD5(content+公钥))
if srv.bc.Tripartite.QiXing.AppKey == "" {
return srv.ResponseErr(ctx, "qixing appKey is empty")
}
sign := helper.Md5(req.Content + srv.bc.Tripartite.QiXing.AppKey)
if sign != req.Ciphertext {
log.Errorf("qixing notify sign err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes))
return srv.ResponseErr(ctx, "verify sign error")
}
wxNotifyDataStr, err := base64.StdEncoding.DecodeString(req.Content)
if err != nil {
log.Errorf("qixing notify wxNotifyDataStr err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes))
return srv.ResponseErr(ctx, fmt.Sprintf("base64 decode req content error: %v", err))
}
var wxNotifyData *bo.WechatVoucherNotifyBo
if err = json.Unmarshal(wxNotifyDataStr, &wxNotifyData); err != nil {
log.Errorf("qixing notify wxNotifyData err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes))
return srv.ResponseErr(ctx, fmt.Sprintf("json unmarshal wxNotifyData error: %v", err))
}
if err = wxNotifyData.Validate(); err != nil {
log.Errorf("qixing notify wxNotifyData validate err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes))
return srv.ResponseErr(ctx, fmt.Sprintf("validate wxNotifyData error: %v", err))
}
err = srv.multiBiz.Notify(ctx, ip, "qixing_"+wxNotifyData.PlainText.StockCreatorMchid, wxNotifyData)
if err != nil {
log.Errorf("qixing notify run err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes))
return srv.ResponseErr(ctx, err.Error())
}
return srv.ResponseOK(ctx)
}
func (this *TripartiteService) ResponseOK(ctx http.Context) error {
return ctx.JSON(http2.StatusOK, &qixing.QiXingResponse{
Msg: "SUCCESS",
})
}
func (this *TripartiteService) ResponseErr(ctx http.Context, msg string) error {
return ctx.JSON(http2.StatusOK, &qixing.QiXingResponse{
Msg: msg,
})
}

View File

@ -11,6 +11,48 @@ import (
"voucher/internal/biz/do"
)
func (this *CmbService) RetryOrderNotice(ctx http.Context) error {
bodyBytes, err := io.ReadAll(ctx.Request().Body)
if err != nil {
return err
}
if bodyBytes == nil {
return fmt.Errorf("bodyBytes is empty")
}
var req *do.RetryQueryNotice
if err = json.Unmarshal(bodyBytes, &req); err != nil {
return err
}
return this.VoucherBiz.PushRetryOrderNotice(ctx, bodyBytes)
}
func (this *CmbService) OrderNotifyRetry(ctx http.Context) error {
bodyBytes, err := io.ReadAll(ctx.Request().Body)
if err != nil {
return err
}
var req *do.OrderNotifyRetry
if err = json.Unmarshal(bodyBytes, &req); err != nil {
return err
}
if req == nil {
return fmt.Errorf("req is empty")
}
if req.StartTime == "" || req.EndTime == "" {
return fmt.Errorf("start_time or end_time is empty")
}
return this.VoucherBiz.PushOrderNotifyRetry(ctx, req)
}
func (this *CmbService) NotifyRetry(ctx http.Context) error {
id := ctx.Vars().Get("id")
if id == "" {
@ -25,21 +67,40 @@ func (this *CmbService) NotifyRetry(ctx http.Context) error {
return this.VoucherBiz.PushNotifyRetryDelayMQ(ctx, 1, orderNotifyId)
}
var ds = map[string]bool{
"13474987525": true,
"15221117226": true,
"18666173766": true,
}
func (this *CmbService) QueryOrder(ctx http.Context) error {
phone := ctx.Vars().Get("phone")
if phone == "" {
return fmt.Errorf("phone is empty")
}
_, ok := ds[phone]
if !ok {
return fmt.Errorf("无权限~")
}
orderNo := ctx.Vars().Get("order_no")
if orderNo == "" {
return fmt.Errorf("orderNo is empty")
}
str, err := this.VoucherBiz.QueryOrder(ctx, orderNo)
isNotice := ctx.Vars().Get("is_notice")
str, err := this.VoucherBiz.QueryOrder(ctx, orderNo, isNotice)
if err != nil {
return err
}
return ctx.JSON(http2.StatusOK, map[string]interface{}{
"data": str,
})
ctx.Response().Header().Add("Content-Type", "text/plain; charset=utf-8")
ctx.Response().WriteHeader(http2.StatusOK)
_, err = ctx.Response().Write([]byte(str))
return err
}
func (this *CmbService) QueryStock(ctx http.Context) error {
@ -183,3 +244,31 @@ func (this *CmbService) SpecifyNotification(ctx http.Context) error {
return ctx.String(http2.StatusOK, string(bodyBytes))
}
func (this *CmbService) UsedNotifyPush(ctx http.Context) error {
bodyBytes, err := io.ReadAll(ctx.Request().Body)
if err != nil {
return err
}
var req *do.WechatUsedQuery
if err = json.Unmarshal(bodyBytes, &req); err != nil {
return err
}
if req == nil {
return fmt.Errorf("req is empty")
}
if req.StartTime == "" && req.EndTime == "" {
return fmt.Errorf("start time or end time is empty")
}
err = this.VoucherBiz.UsedNotifyPush(ctx, req)
if err != nil {
return err
}
return ctx.String(http2.StatusOK, string(bodyBytes))
}

View File

@ -0,0 +1,45 @@
package service
import (
"context"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"voucher/internal/pkg/rdsmq"
)
func (s *VoucherService) GetUsedNotifyConfig() *rdsmq.ConsumeConfig {
queue := s.bc.RdsMQ.GetUsedNotify()
if queue == nil {
return nil
}
if !queue.GetIsOpen() {
log.Warn(fmt.Sprintf("[%s]RdsMQ is not open", queue.Name))
return nil
}
return &rdsmq.ConsumeConfig{
Rdb: s.rdb.Rdb,
QueueName: queue.Name,
NumWorkers: queue.NumWorkers,
WaitTime: queue.GetWaitTime().AsDuration(),
RetryNum: queue.RetryNum,
Fn: s.HandleUsedNotify,
Logger: s.logHelper,
}
}
func (s *VoucherService) HandleUsedNotify(ctx context.Context, msg string) error {
if msg == "" {
s.logHelper.Errorf("RdsMQ used notify error: msg is empty")
return nil
}
if err := s.VoucherBiz.UsedNotify(ctx, msg); err != nil {
s.logHelper.Error(err)
}
return nil
}

View File

@ -133,5 +133,5 @@ func (v *VoucherService) WechatUseNotifyConsumer(ctx context.Context, tag, msg s
return err
}
return v.VoucherBiz.WechatNotifyConsumer(ctx, tag, req)
return v.VoucherBiz.WechatNotifyConsumer(ctx, "127.0.0.1", req)
}

View File

@ -80,3 +80,30 @@ func (s *VoucherService) WechatTimeSliceQueryHandle(ctx context.Context, msg str
return nil
}
func (s *VoucherService) GetRetryQueryNoticeConfig() *rdsmq.ConsumeConfig {
return &rdsmq.ConsumeConfig{
Rdb: s.rdb.Rdb,
QueueName: "retryQueryNotice",
NumWorkers: 1,
WaitTime: 30,
RetryNum: 1,
Fn: s.RetryQueryNotice,
Logger: s.logHelper,
}
}
func (s *VoucherService) RetryQueryNotice(ctx context.Context, msg string) error {
if msg == "" {
s.logHelper.Errorf("wechat TimeSlice query error: batchNo is empty")
return nil
}
if err := s.timeSliceQuery.RetryQueryNotice(ctx, msg); err != nil {
s.logHelper.Errorf("retry query notice msg:%s error: %v", msg, err)
}
return nil
}

View File

@ -36,6 +36,7 @@ func marketing() *marketing2.Marketing {
fmt.Sprintf("%s/%s", filePath, "wechat_private_key.pem"), // 商户API证书私钥文件路径本地文件路径
wechatPayPublicKeyId, // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径
"",
)
if err != nil {
panic(err)
@ -56,13 +57,9 @@ func marketingFJxw() *marketing2.Marketing {
//openssl x509 -in xxx.pem -noout -serial
mchId := "1652465541"
wechatPayPublicKeyId := "PUB_KEY_ID_0117109533612025031800326400002563"
wechatPayPublicKeyId := " PUB_KEY_ID_0116524655412025070900181741001803"
certificateSerialNo := "1E3F2CE013203BA9C3DEFC5782FCD3329C3DAC1C"
//mchId := "1652465541"
//wechatPayPublicKeyId := ""
//certificateSerialNo := "1E3F2CE013203BA9C3DEFC5782FCD3329C3DAC1C"
filePath := fmt.Sprintf("%s/cert/wechat/%s", parentDir, mchId)
c, err := utils.CreateMchConfig(
@ -71,6 +68,38 @@ func marketingFJxw() *marketing2.Marketing {
fmt.Sprintf("%s/%s", filePath, "wechat_private_key.pem"), // 商户API证书私钥文件路径本地文件路径
wechatPayPublicKeyId, // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径
"",
)
if err != nil {
panic(err)
}
return &marketing2.Marketing{MchConfig: c}
}
func marketingFJLF() *marketing2.Marketing {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
parentDir := filepath.Dir(dir)
mchId := "1100040695"
wechatPayPublicKeyId := "PUB_KEY_ID_0111000406952026032500382251001000"
certificateSerialNo := "46712853869DB0EDAA9B4DF97DADEECD4CCDC85B"
filePath := fmt.Sprintf("%s/cert/wechat/%s", parentDir, mchId)
fmt.Printf("filePath: %s\n", filePath)
c, err := utils.CreateMchConfig(
mchId, // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
certificateSerialNo, // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
fmt.Sprintf("%s/%s", filePath, "wechat_private_key.pem"), // 商户API证书私钥文件路径本地文件路径
wechatPayPublicKeyId, // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径
"d9af70585b18ae206d981548c766563f",
)
if err != nil {
panic(err)
@ -81,27 +110,13 @@ func marketingFJxw() *marketing2.Marketing {
func MarketingSend() {
//openId := "oSNb4ftgnWC22Z0cWTjsQebdr2Yk"
//appId := "wx619991cc795028f5"
openId := "oSNb4ftgnWC22Z0cWTjsQebdr2Yk"
appId := "wx619991cc795028f5"
//195516196845312409613
openId := "ocuH-0Nymo4sJLRNabIBbg9H2XCo"
appId := "wx5d3e839568f24b2b"
//respBody={"coupon_id":"116076813524"}
//openId := "ocZ-njugTd_fgCJMHTG8PukPAVm4"
//appId := "wxd9137161bc8f9ca9"
//respBody={"coupon_id":"117888124542"}
//195516196845312409613
//1958100775326560252
//1958100775326560251
//19581007753265602565
//19581007753265602564
request := &marketing2.SendReq{
ActivityId: utils.String("11941580000000005"),
StockId: utils.String("20847510"),
OutRequestNo: utils.String("195516196845312409613"),
ActivityId: utils.String("11941580000000012"),
StockId: utils.String("20964154"),
OutRequestNo: utils.String("196605139814767820814"),
Appid: utils.String(appId),
StockCreatorMchId: utils.String("1652465541"),
}
@ -128,17 +143,7 @@ func MarketingSend() {
fmt.Printf("请求成功: %s\n", *response.CouponId)
}
func MarketingQuery() {
appId := "wx619991cc795028f5"
openId := "oSNb4ftgnWC22Z0cWTjsQebdr2Yk"
couponId := "113831004454"
response, err := marketing().Query(appId, openId, couponId)
if err != nil {
fmt.Print(err)
return
}
fmt.Printf("请求成功: %+v\n", response)
func MarketingQuery(appId, openId, couponId string) (response *marketing2.SendResp, err error) {
return marketingFJxw().Query(appId, openId, couponId)
//return marketingFJLF().Query(appId, openId, couponId)
}

View File

@ -1,8 +1,22 @@
package test
import "testing"
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"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 TestMarketingSend(t *testing.T) {
func Test_MarketingSend(t *testing.T) {
tests := []struct {
name string
}{
@ -16,3 +30,125 @@ func TestMarketingSend(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: 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 {
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)
})
}
// 合单订单就是没有核销回调通知的
// {"code":"APPID_MCHID_NOT_MATCH","message":"商户号与AppID不匹配"}
// APPID_MCHID_NOT_MATCH 商户号与AppID不匹配 调用接口的商户号需与接口传入的AppID有绑定关系
//openid不是自己的appid下的喔这也能查询到吗”
//不行的需要是在自己appid下的才能查到
}
func Test_QixingNotifyDataDecodeString(t *testing.T) {
content := `eyJzdW1tYXJ5Ijoi5Luj6YeR5Yi45qC46ZSA6YCa55+lIiwiYXNzb2NpYXRlZF9kYXRhIjoiY291cG9uIiwiZXZlbnRfdHlwZSI6IkNPVVBPTi5VU0UiLCJjcmVhdGVfdGltZSI6IjIwMjUtMTItMjRUMTk6NDg6NDYrMDg6MDAiLCJwbGFpbl90ZXh0Ijp7InN0b2NrX2NyZWF0b3JfbWNoaWQiOiIxNzE1MzQ5NTc4Iiwic3RvY2tfaWQiOiIyMTQzODg1MSIsImNvdXBvbl9pZCI6IjE0NDQ2MTIyOTg5MCIsInNpbmdsZWl0ZW1fZGlzY291bnRfb2ZmIjpudWxsLCJkaXNjb3VudF90byI6bnVsbCwiY291cG9uX25hbWUiOiLpk7booYzljaHlpJrnrJTnq4vlh48iLCJzdGF0dXMiOiJVU0VEIiwiZGVzY3JpcHRpb24iOiIiLCJjcmVhdGVfdGltZSI6IjIwMjUtMTItMThUMTU6NDQ6NTIrMDg6MDAiLCJjb3Vwb25fdHlwZSI6Ik5PUk1BTCIsIm5vX2Nhc2giOmZhbHNlLCJhdmFpbGFibGVfYmVnaW5fdGltZSI6IjIwMjUtMTItMThUMDA6MDA6MDArMDg6MDAiLCJhdmFpbGFibGVfZW5kX3RpbWUiOiIyMDI2LTAxLTE2VDIzOjU5OjU5KzA4OjAwIiwic2luZ2xlaXRlbSI6ZmFsc2UsIm5vcm1hbF9jb3Vwb25faW5mb3JtYXRpb24iOnsiY291cG9uX2Ftb3VudCI6MzAwLCJ0cmFuc2FjdGlvbl9taW5pbXVtIjoxODAwfSwiY29uc3VtZV9pbmZvcm1hdGlvbiI6eyJjb25zdW1lX3RpbWUiOiIyMDI1LTEyLTI0VDE5OjQ4OjQ2KzA4OjAwIiwiY29uc3VtZV9tY2hpZCI6IjEyNzQ5Mzg2MDEiLCJ0cmFuc2FjdGlvbl9pZCI6IjQyMDAwMDI5MTcyMDI1MTIyNDgzODQ2MTg0MzgiLCJjb25zdW1lX2Ftb3VudCI6MzAwLCJnb29kc19kZXRhaWwiOm51bGx9fSwicmVzb3VyY2VfdHlwZSI6ImVuY3J5cHQtcmVzb3VyY2UiLCJvcmlnaW5hbF90eXBlIjoiY291cG9uIiwiaWQiOiI4NzkxZTMyZS0zYjFiLTViNDktYTNiNi1mYzY0YTc5NTVlZjEifQ==`
b, err := base64.StdEncoding.DecodeString(content)
if err != nil {
t.Errorf("base64.StdEncoding.DecodeString() error = %v", err)
return
}
t.Log(string(b))
}
func Test_QixingNotifyData(t *testing.T) {
//wxBody := `{"id":"4ab2699d-e91d-5460-9810-25fd6d4c69a5","create_time":"2025-12-08T17:54:24+08:00","resource_type":"encrypt-resource","event_type":"COUPON.USE","summary":"代金券核销通知","original_type":"coupon","associated_data":"coupon","plain_text":{"stock_creator_mchid":"1652465541","stock_id":"21386484","coupon_id":"142388354994","coupon_name":"银行卡多笔立减","description":"","status":"SENDED","create_time":"2025-12-08T17:50:48+08:00","coupon_type":"NORMAL","no_cash":false,"singleitem":false,"business_type":"","consume_information":{"consume_time":"2025-12-08T17:54:24+08:00","consume_mchid":"1274938601","transaction_id":"4200002996202512083063051834","consume_amount":16}}}`
wxBody := `{"id":"4ab2699d-e91d-5460-9810-25fd6d4c69a6","create_time":"2025-12-08T17:54:24+08:00","resource_type":"encrypt-resource","event_type":"COUPON.USE","summary":"代金券核销通知","original_type":"coupon","associated_data":"coupon","plain_text":{"stock_creator_mchid":"1652465541","stock_id":"21386484","coupon_id":"118758145502","coupon_name":"银行卡多笔立减","description":"","status":"USED","create_time":"2025-12-08T17:50:48+08:00","coupon_type":"NORMAL","no_cash":false,"singleitem":false,"business_type":"","consume_information":{"consume_time":"2025-12-08T17:54:24+08:00","consume_mchid":"1274938601","transaction_id":"4200002996202512083063051834","consume_amount":16}}}`
t.Log(len(wxBody))
content := base64.StdEncoding.EncodeToString([]byte(wxBody))
ciphertext := helper.Md5(content + "DrY1zLkOH01q0sN66vrmkdpbWsyb41ho")
req := qixing.QiXingRequestBo{
Content: content,
Timestamp: time.Now().UnixMilli(),
Ciphertext: ciphertext,
}
b, _ := json.Marshal(req)
t.Log(string(b))
// {"content":"base64(微信通知json对象数据)","timestamp":1765447477945,"ciphertext":"md5(base64(微信通知json对象数据)+key)"}
//t.Logf(`{"content":"%s","timestamp":122345677890,"ciphertext":"%s"}`, content, ciphertext)
}
func Test_MarketingNotify(t *testing.T) {
headerBytes := []byte(`{"Accept":["*/*"],"Cache-Control":["no-cache"],"Connection":["close"],"Content-Length":["1137"],"Content-Type":["application/json"],"Pragma":["no-cache"],"User-Agent":["Mozilla/4.0"],"Wechatpay-Nonce":["kHzkvdHwssdU0CRpFfgCpzdxtdzQGsIS"],"Wechatpay-Serial":["PUB_KEY_ID_0111000406952026032500382251001000"],"Wechatpay-Signature":["jt/2zYvqTlvOHAb9Lb1bfbLUDnqy59dc1JF87AiHVtagZAxzWNP5Jgsrr/jv9C3UVv+MHvbTxuaDQjJAfXx4CT7ihYUNEF6rQL/ilToSMuZpw23/pPjyAzXvBWBsj3AY3rxfa4OkaviRnG6vRA5HKnaHHG5wmDdrcwOoKBiLJ6cax8OYu9GV8Opr0uSzWj7ZxPoSxXy65MxEaampVXJcLnCm1iVp2mHZH6jafBxyjhDGIZ6uOJD0LdCUCJMfbDKvlthO7CrfLRsdosVrVmnL3lJU2ti5rjngmzAxHFi+J4JUsbTvkWnBEXZXaXZ4vNi4gkGnOGPJHs4ch+s0MV/ZFg=="],"Wechatpay-Signature-Type":["WECHATPAY2-SHA256-RSA2048"],"Wechatpay-Timestamp":["1774605950"],"X-Forwarded-For":["121.51.58.172, 172.17.0.1"]}`)
bodyBytes := []byte(`{"id":"4a5e22d9-9018-5bfc-94ba-3c9957a62355","create_time":"2026-03-27T18:05:50+08:00","resource_type":"encrypt-resource","event_type":"COUPON.USE","summary":"代金券核销通知","resource":{"original_type":"coupon","algorithm":"AEAD_AES_256_GCM","ciphertext":"aTS2kVk7l/lzfIEUMUVfk5+6ouRSakspFTP/nR4sDPp0nvrj2VtEQBXkYqTgDDxs35V9CrJI90X31Mho2+adZm1ScPPH0HL6iUaUcypzgvHAhSaJBhugz8slqoQ3zaynBKa/HpyU6Jnfd3OTbhcrS2i27cBdWawd2UQ5HBgAemV8/k/gf+LPC04fJNFfrrttTxqtBDpXr3H/ob5pV825C57SJJhpXxgZtnX/e9avpWZlPIXhOSA3FSwYZtoW1BYoXrsLID6fGcP5IJuc8EE40Z7si95tQbQcM1eqH19OZbRAmXitV+sY1pOlo0Zehnc5vyH5RunVYa2lwdsirSGU1EI0PlDYwHoNxUEtSpKZ4MP8IReNdkjKlwpBQXtgZMGqMNdDU+/Db/ZUr/R8xaF7RoHfiiTHEHQaEK9xbCZji+F8YDiU0K8I8getKsyHiZwHZrU67p/6ql6zVzcWBjaDyL71tcl1k1ppkyOFdg9g0WqcJD56xa9qMhXaOyQoIU4hJsMMTvBWxyRelE4+o1Z79nUVUMswvg/hPZ4QEeIF8C+ezzk+/PhggYhZE4g4WnzqKRh/WASXRja0UsucBVIs9hViV+aDuQVEjxmy29S3UTrwL0Kr0+5hUz7q87gJUaAvehF6pIPtvliJvR0tnpdCrLHuIsc/LiSxoX2WSkGGZet58swdJ7wHYzdfNFKbhRS2IBj4JzI6wc+zRkGmLCijTRFSadZGnNZf5SPez1iEqr0fecCDjN6NUMv/YETfjeEqUxRvNVjMK9vCzEm+bdVJNNL+3i5RiJA9Bng1JtmTMsHatgn8dg==","associated_data":"coupon","nonce":"L3TIxaSQ12pD"}}`)
httpHeaders := make(http.Header)
if err := json.Unmarshal(headerBytes, &httpHeaders); err != nil {
fmt.Printf("headers Unmarshal err: %+v\n", err)
return
}
body, bizContent, err := marketingFJLF().Notify(context.Background(), &httpHeaders, bodyBytes)
if err != nil {
t.Errorf("notify err: %+v\n", err)
return
}
var plainText bo.PlainText
if err = json.Unmarshal([]byte(bizContent), &plainText); err != nil {
t.Errorf("Unmarshal err: %+v\n", err)
return
}
notifyBo := bo.WechatVoucherNotifyBo{
ID: body.Id,
CreateTime: body.CreateTime,
ResourceType: body.ResourceType,
EventType: body.EventType,
Summary: body.Summary,
OriginalType: body.Resource.OriginalType,
AssociatedData: body.Resource.AssociatedData,
PlainText: plainText,
}
fmt.Printf("bizContent: %s\n", bizContent)
fmt.Printf("notifyBo: %+v\n", notifyBo)
}

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,7 +10,9 @@ import (
"os"
"path/filepath"
"time"
"voucher/internal/biz/do"
v1 "voucher/api/v1"
"voucher/internal/biz/businesserr"
"voucher/internal/biz/vo"
"voucher/internal/conf"
"voucher/internal/data"
)
@ -24,14 +26,34 @@ var bc = &conf.Bootstrap{
},
}
func SendCoupon() {
// bcFJXW Callback{NotifyUrl:https://nsall.86698.cn/wechatPay/coupon_notify/fjxingwang, Mchid:1652465541}
var bcFJXW = &conf.Bootstrap{
Wechat: &conf.Wechat{
MchID: "1652465541", // notifyUrl https://nsall.86698.cn/wechatPay/coupon_notify/fjxingwang
MchCertificateSerialNumber: "1E3F2CE013203BA9C3DEFC5782FCD3329C3DAC1C",
WechatPayPublicKeyID: "PUB_KEY_ID_0116524655412025070900181741001803",
Name: "福建兴旺",
},
}
// bcFJLF Callback{NotifyUrl:https://gateway.dev.cdlsxd.cn/voucher/v1/notify/1100040695, Mchid:1100040695}
// /voucher/v1/notify/1100040695
var bcFJLF = &conf.Bootstrap{
Wechat: &conf.Wechat{
MchID: "1100040695",
MchCertificateSerialNumber: "46712853869DB0EDAA9B4DF97DADEECD4CCDC85B",
WechatPayPublicKeyID: "PUB_KEY_ID_0111000406952026032500382251001000",
Name: "福建峦峰",
},
}
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)
@ -43,17 +65,16 @@ 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{
OutRequestNo: core.String("LQ1948534036766040066"),
OutRequestNo: core.String("LQ2011991700944699394"), // {CouponId:129623470711}
// 微信为发券方商户分配的公众账号ID接口传入的所有appid应该为公众号的appid在mp.weixin.qq.com申请的不能为APP的appid在open.weixin.qq.com申请的
Appid: core.String("wxd27e255810842ba8"),
Openid: core.String("o3dEt5cA8jt3Kz5wNzAO6-3YQHsE"),
StockId: core.String("20391098"),
StockCreatorMchid: core.String("1652465541"),
Openid: core.String("o3dEt5b_1lFtKc-aAT3tiYjJIGwk"),
StockId: core.String("21502886"),
StockCreatorMchid: core.String("16524655411111"),
}
fmt.Printf("\nreq:%+v", req)
@ -62,15 +83,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()
@ -93,32 +125,6 @@ func QueryCoupon() {
return
}
//req := cashcoupons.QueryCouponRequest{
// //CouponId: core.String("101270193400"),
// CouponId: core.String("101270193400"),
// Appid: core.String("wx619991cc795028f5"),
// Openid: core.String("oSNb4fmScVLmXILaolXVdBpJmYbQ"),
//}
//req := cashcoupons.QueryCouponRequest{
// CouponId: core.String("101274529925"),
// Appid: core.String("wx619991cc795028f5"),
// Openid: core.String("oSNb4fpnLNdkFPk-O43o6f2hxxRo"),
//}
//appId := "wx619991cc795028f5"
//openId := "oSNb4ftgnWC22Z0cWTjsQebdr2Yk"
//couponId := "113831004454"
//req := cashcoupons.QueryCouponRequest{
// CouponId: core.String(couponId),
// Appid: core.String(appId),
// Openid: core.String(openId),
//}
appId := "wx5d3e839568f24b2b"
openId := "ocuH-0Nymo4sJLRNabIBbg9H2XCo"
couponId := "116076813524"
req := cashcoupons.QueryCouponRequest{
CouponId: core.String(couponId),
Appid: core.String(appId),
@ -170,8 +176,8 @@ func QueryProduct() {
}
req := cashcoupons.QueryStockRequest{
StockId: core.String("20847510"),
StockCreatorMchid: core.String("1652465541"),
StockId: core.String("21928191"),
StockCreatorMchid: core.String("1100040695"),
}
svc := cashcoupons.StockApiService{Client: client}
@ -181,8 +187,8 @@ func QueryProduct() {
return
}
j, _ := json.Marshal(resp)
fmt.Printf("\nresp:%s\n", string(j))
originData, _ := json.Marshal(resp)
fmt.Printf("\nOriginData:%s\n", string(originData))
availableStock := *resp.StockUseRule.MaxCoupons - *resp.DistributedCoupons
couponAmount := *resp.StockUseRule.FixedNormalCoupon.CouponAmount / 100
@ -196,47 +202,54 @@ func QueryProduct() {
fmt.Printf("\n剩余库存:%d", availableStock)
fmt.Printf("\n剩余预算:%d", availableStock*couponAmount)
fmt.Printf("\nWxResp:%+v", WxResp(resp))
WxResp(resp)
return
}
func WxResp(wxResp *cashcoupons.Stock) (reps *do.WxResp) {
func WxResp(wxResp *cashcoupons.Stock) {
availableStock := *wxResp.StockUseRule.MaxCoupons - *wxResp.DistributedCoupons
couponAmount := *wxResp.StockUseRule.FixedNormalCoupon.CouponAmount / 100
remainingBudget := availableStock * couponAmount
stockUsageRate := float64(*wxResp.DistributedCoupons) / float64(*wxResp.StockUseRule.MaxCoupons) * 100
req := &do.WxResp{
Amount: couponAmount,
AllBudget: *wxResp.StockUseRule.MaxAmount / 100,
AllStock: *wxResp.StockUseRule.MaxCoupons,
UsedStock: *wxResp.DistributedCoupons,
UsedBudget: *wxResp.DistributedCoupons * couponAmount,
AvailableStock: availableStock,
AvailableBudget: remainingBudget,
StockUsageRate: stockUsageRate,
xx := &v1.CmbQueryProductReply{
RespCode: vo.CmbResponseStatusSuccess.GetValue(),
RespMsg: "成功",
ActivityName: *wxResp.StockName,
ActivityId: "CMB" + *wxResp.StockId,
Amount: "",
MinAmount: "",
AvailableType: "",
AvailableDays: "", // 动态有效期天数
StartTime: "",
EndTime: "",
AvailableStock: "",
Detail: *wxResp.Description,
}
inputFormat := time.RFC3339
if wxResp.AvailableBeginTime != nil {
availableBeginTime, _ := time.Parse(inputFormat, *wxResp.AvailableBeginTime)
req.StartTime = &availableBeginTime
xx.StartTime = availableBeginTime.Format("2006-01-02 15:04:05.000")
xx.SaleStartTime = xx.StartTime
}
if wxResp.AvailableEndTime != nil {
availableEndTime, _ := time.Parse(inputFormat, *wxResp.AvailableEndTime)
req.EndTime = &availableEndTime
xx.EndTime = availableEndTime.Format("2006-01-02 15:04:05.000")
xx.SaleEndTime = xx.EndTime
}
return req
xx.Amount = fmt.Sprintf("%d", *wxResp.StockUseRule.FixedNormalCoupon.CouponAmount)
xx.MinAmount = fmt.Sprintf("%d", *wxResp.StockUseRule.FixedNormalCoupon.TransactionMinimum)
availableStock := *wxResp.StockUseRule.MaxCoupons - *wxResp.DistributedCoupons
xx.AvailableStock = fmt.Sprintf("%d", availableStock)
cmbData, _ := json.Marshal(xx)
fmt.Printf("\nCMB[%s]", string(cmbData))
}
func QueryCallback() {
func QueryCallback(bc *conf.Bootstrap) {
ctx := context.Background()
@ -262,7 +275,47 @@ func QueryCallback() {
svc := cashcoupons.CallBackUrlApiService{Client: client}
response, _, err := svc.QueryCallback(ctx, cashcoupons.QueryCallbackRequest{
Mchid: core.String("1652465541"),
Mchid: core.String(bc.Wechat.MchID),
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("\nresp:%+v\n", response)
return
}
func SetCallback(bc *conf.Bootstrap) {
ctx := context.Background()
dir, err := os.Getwd()
if err != nil {
fmt.Printf("os.Getwd() error = %v", err)
return
}
parentDir := filepath.Dir(dir)
server := data.Server{
MchID: bc.Wechat.MchID,
MchCertificateSerialNumber: bc.Wechat.MchCertificateSerialNumber,
WechatPayPublicKeyID: bc.Wechat.WechatPayPublicKeyID,
Dir: parentDir,
}
client, err := data.GetClient(ctx, server)
if err != nil {
fmt.Println(err)
return
}
svc := cashcoupons.CallBackUrlApiService{Client: client}
response, _, err := svc.SetCallback(ctx, cashcoupons.SetCallbackRequest{
Mchid: core.String(bc.Wechat.MchID),
//NotifyUrl: core.String("https://gateway.dev.cdlsxd.cn/voucher/v1/notify/" + bc.Wechat.MchID),
NotifyUrl: core.String("https://voucher.86698.cn/voucher/v1/notify/" + bc.Wechat.MchID),
Switch: core.Bool(true),
})
if err != nil {
fmt.Println(err)

View File

@ -2,6 +2,7 @@ package test
import (
"testing"
"voucher/internal/biz/businesserr"
)
func Test_SendCoupon(t *testing.T) {
@ -14,7 +15,11 @@ 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)
}
}
})
}
}
@ -22,14 +27,26 @@ func Test_SendCoupon(t *testing.T) {
func Test_QueryCoupon(t *testing.T) {
tests := []struct {
name string
appId string
openId string
couponId string
}{
{
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)
})
}
}
@ -60,7 +77,23 @@ func Test_QueryCallback(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
QueryCallback()
QueryCallback(bcFJLF)
})
}
}
func Test_SetCallback(t *testing.T) {
tests := []struct {
name string
}{
{
name: "设置券核销通知地址",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
SetCallback(bcFJLF)
})
}
}