Compare commits
148 Commits
ed44b84d31
...
3906fb9690
| Author | SHA1 | Date |
|---|---|---|
|
|
3906fb9690 | |
|
|
5e4b072062 | |
|
|
b5b07a4d79 | |
|
|
2e3dd99115 | |
|
|
5e6d810773 | |
|
|
edae25ff6a | |
|
|
11b42a9b46 | |
|
|
b0d22b3ad8 | |
|
|
ee9203f7d6 | |
|
|
9f768f037c | |
|
|
2c7d2581f7 | |
|
|
d24e7671e0 | |
|
|
be2adefb91 | |
|
|
0e1ed3b426 | |
|
|
737216ee05 | |
|
|
2ceb9ed9d0 | |
|
|
ae09a7509b | |
|
|
5263744d6b | |
|
|
5cf0459b39 | |
|
|
1622904903 | |
|
|
31373c320f | |
|
|
965474a340 | |
|
|
d308d56e79 | |
|
|
7e8ef4cd2b | |
|
|
f1e813584f | |
|
|
4be755c113 | |
|
|
8f290eb0c2 | |
|
|
b4d0688bfc | |
|
|
3c4b932f03 | |
|
|
f34a27fbf2 | |
|
|
417e813b84 | |
|
|
0d5259c9b0 | |
|
|
c89a6406b6 | |
|
|
ce8d6ed988 | |
|
|
c77e23af8a | |
|
|
2e364f0332 | |
|
|
8cf3c66cb3 | |
|
|
cfe1973bf9 | |
|
|
886d140ceb | |
|
|
3f3bcbab8e | |
|
|
2ec6b62912 | |
|
|
9c465d1f52 | |
|
|
a7f41d1c27 | |
|
|
6aaf9f3e12 | |
|
|
64c7fbecd2 | |
|
|
4ec1809383 | |
|
|
5db078e26a | |
|
|
7565c48cd9 | |
|
|
00e637d383 | |
|
|
6e57c2afe5 | |
|
|
129b849b02 | |
|
|
5051e2e744 | |
|
|
6a06e1fed4 | |
|
|
2735d8d71d | |
|
|
d5460e9415 | |
|
|
cf9ecc4486 | |
|
|
7c851ddb1b | |
|
|
5ba27d7f7b | |
|
|
9c8f64a35a | |
|
|
d34403b012 | |
|
|
ba4c95c7a6 | |
|
|
813dfe5ef0 | |
|
|
0ddde21287 | |
|
|
c6028b8c6e | |
|
|
07bacd006b | |
|
|
81c53823af | |
|
|
bbfc91da57 | |
|
|
9bd8646e57 | |
|
|
10727c1a00 | |
|
|
9fc1ab9c82 | |
|
|
42b88fd9f9 | |
|
|
2441203833 | |
|
|
8d420e0d00 | |
|
|
7ec609137a | |
|
|
ad51cfae46 | |
|
|
6fdd1f3fd6 | |
|
|
5d1ff1bdde | |
|
|
888de99381 | |
|
|
6fd5dba1eb | |
|
|
6ada382188 | |
|
|
ecb9654743 | |
|
|
6006445958 | |
|
|
05ba50661c | |
|
|
b841ad3661 | |
|
|
a7a2fb88ed | |
|
|
90a2f6fb32 | |
|
|
9ed8a296cf | |
|
|
c490f8335d | |
|
|
c3d5f18e68 | |
|
|
06476209b5 | |
|
|
c65c993a25 | |
|
|
1f81565e15 | |
|
|
96325fde3b | |
|
|
4508dfe00e | |
|
|
d1fb805dff | |
|
|
c9c0a6004a | |
|
|
76033bc88a | |
|
|
24994ec0d7 | |
|
|
f0f4ce1235 | |
|
|
0b3a16c72a | |
|
|
6a5198287e | |
|
|
654540ba65 | |
|
|
408a929b33 | |
|
|
10eda78e58 | |
|
|
25424eec72 | |
|
|
349fe2621c | |
|
|
5dfec0d509 | |
|
|
49e7092839 | |
|
|
75b5b9af28 | |
|
|
d3584edec5 | |
|
|
947c4e5e5c | |
|
|
a1b59c97a9 | |
|
|
43bf445f8b | |
|
|
d01900b519 | |
|
|
413b8100d8 | |
|
|
1a7df7cbab | |
|
|
6b9a5a0666 | |
|
|
dd5380c47d | |
|
|
c0df2aa399 | |
|
|
b8fc6a7650 | |
|
|
9493648272 | |
|
|
2edd4fd8cb | |
|
|
11e28adf6d | |
|
|
7dccb67de3 | |
|
|
f206764800 | |
|
|
363254102a | |
|
|
fb8fda6f0e | |
|
|
d64c8ea435 | |
|
|
ef5a80f501 | |
|
|
e61da58512 | |
|
|
b6a618b10c | |
|
|
f348df3229 | |
|
|
ee73cb8c50 | |
|
|
4d6211a30f | |
|
|
2238f24820 | |
|
|
396e53e9fc | |
|
|
a01f04a949 | |
|
|
93873c6fd6 | |
|
|
482e8d7f82 | |
|
|
5a3fefdd19 | |
|
|
23bd19bc99 | |
|
|
d6df1e96bb | |
|
|
d2df9235f6 | |
|
|
530652a7d2 | |
|
|
9e984cdb5a | |
|
|
40f51dd4dd | |
|
|
c7a16b3ae9 | |
|
|
4234ddf34d |
|
|
@ -22,6 +22,8 @@ Thumbs.db
|
|||
*.swp
|
||||
.vscode/
|
||||
.idea/
|
||||
.trae/
|
||||
|
||||
bin/
|
||||
cert/
|
||||
log
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 #业务日志路径:如果不写日志,则不配置或配置为空
|
||||
|
|
|
|||
2
gorm.sh
2
gorm.sh
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,19 +1,18 @@
|
|||
package wechatrepoimpl
|
||||
package businesserr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/errors"
|
||||
"strings"
|
||||
err2 "voucher/api/err"
|
||||
)
|
||||
|
||||
// ErrCode 定义错误码类型
|
||||
// https://pay.weixin.qq.com/doc/v3/merchant/4012463767
|
||||
type ErrCode string
|
||||
|
||||
// 定义错误码常量
|
||||
// https://pay.weixin.qq.com/doc/v3/merchant/4012463767
|
||||
const (
|
||||
SYSTEM_ERROR ErrCode = "SYSTEM_ERROR"
|
||||
SIGN_ERROR ErrCode = "SIGN_ERROR"
|
||||
|
||||
APPID_MCHID_NOT_MATCH ErrCode = "APPID_MCHID_NOT_MATCH"
|
||||
INVALID_REQUEST ErrCode = "INVALID_REQUEST"
|
||||
PARAM_ERROR ErrCode = "PARAM_ERROR"
|
||||
|
|
@ -26,196 +25,255 @@ const (
|
|||
FREQUENCY_LIMITED ErrCode = "FREQUENCY_LIMITED"
|
||||
)
|
||||
|
||||
// APIError 定义 API 错误结构体
|
||||
type APIError struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
ErrorCode ErrCode `json:"error_code"`
|
||||
Description string `json:"description"`
|
||||
Hint string `json:"hint"`
|
||||
}
|
||||
const (
|
||||
BATCH_NOT_STARTED ErrCode = "BATCH_NOT_STARTED"
|
||||
BATCH_ENDED ErrCode = "BATCH_ENDED"
|
||||
)
|
||||
|
||||
// CmbAPIErrorMap 定义错误映射,方便根据错误码获取错误信息
|
||||
var CmbAPIErrorMap = map[ErrCode][]CmbAPIError{
|
||||
|
||||
SYSTEM_ERROR: {
|
||||
{
|
||||
StatusCode: 500,
|
||||
ErrorCode: SYSTEM_ERROR,
|
||||
Description: "系统异常,请稍后重试",
|
||||
Hint: "请稍后重试",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
},
|
||||
SIGN_ERROR: {
|
||||
{
|
||||
StatusCode: 401,
|
||||
ErrorCode: SIGN_ERROR,
|
||||
Description: "验证不通过",
|
||||
Hint: "请参阅 签名常见问题",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
},
|
||||
|
||||
// 定义错误映射,方便根据错误码获取错误信息
|
||||
var _ = map[ErrCode][]APIError{
|
||||
APPID_MCHID_NOT_MATCH: {
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: APPID_MCHID_NOT_MATCH,
|
||||
Description: "商户号与AppID不匹配",
|
||||
Hint: "调用接口的商户号需与接口传入的AppID有绑定关系,请参考常见问题Q4",
|
||||
StatusCode: 400,
|
||||
ErrorCode: APPID_MCHID_NOT_MATCH,
|
||||
Description: "商户号与AppID不匹配",
|
||||
Hint: "调用接口的商户号需与接口传入的AppID有绑定关系,请参考常见问题Q4",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
},
|
||||
INVALID_REQUEST: {
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: INVALID_REQUEST,
|
||||
Description: "OpenID与AppID不匹配",
|
||||
Hint: "OpenID与AppID需有对应关系",
|
||||
StatusCode: 400,
|
||||
ErrorCode: INVALID_REQUEST,
|
||||
Description: "HTTP 请求不符合微信支付 APIv3 接口规则",
|
||||
Hint: "请参阅 接口规则",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: INVALID_REQUEST,
|
||||
Description: "非法的商户号",
|
||||
Hint: "请检查商户号准确性",
|
||||
StatusCode: 400,
|
||||
ErrorCode: INVALID_REQUEST,
|
||||
Description: "OpenID与AppID不匹配",
|
||||
Hint: "OpenID与AppID需有对应关系",
|
||||
ThirdErrCode: ThirdErrCodeAppIDOpenIDMismatch,
|
||||
},
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: INVALID_REQUEST,
|
||||
Description: "调用频率过高",
|
||||
Hint: "请降低API调用频率",
|
||||
StatusCode: 400,
|
||||
ErrorCode: INVALID_REQUEST,
|
||||
Description: "非法的商户号",
|
||||
Hint: "请检查商户号准确性",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: INVALID_REQUEST,
|
||||
Description: "活动已结束或未激活",
|
||||
Hint: "请检查批次状态",
|
||||
StatusCode: 400,
|
||||
ErrorCode: INVALID_REQUEST,
|
||||
Description: "调用频率过高",
|
||||
Hint: "请降低API调用频率",
|
||||
ThirdErrCode: ThirdErrCodeCallHigh,
|
||||
},
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: INVALID_REQUEST,
|
||||
Description: "批次信息获取失败,请确认参数是否有误",
|
||||
Hint: "请检查创建商户号与批次号的对应关系",
|
||||
StatusCode: 400,
|
||||
ErrorCode: INVALID_REQUEST,
|
||||
Description: "活动已结束或未激活",
|
||||
Hint: "请检查批次状态",
|
||||
ThirdErrCode: ThirdErrCodeBatchEnded, //前置判断时间处理一下
|
||||
},
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: INVALID_REQUEST,
|
||||
Description: "批次信息获取失败,请确认参数是否有误",
|
||||
Hint: "请检查创建商户号与批次号的对应关系",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
},
|
||||
PARAM_ERROR: {
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: PARAM_ERROR,
|
||||
Description: "AppID必填",
|
||||
Hint: "请输入AppID",
|
||||
StatusCode: 400,
|
||||
ErrorCode: PARAM_ERROR,
|
||||
Description: "参数错误",
|
||||
Hint: "请根据错误提示正确传入参数",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: PARAM_ERROR,
|
||||
Description: "AppID必填",
|
||||
Hint: "请输入AppID",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: PARAM_ERROR,
|
||||
Description: "OpenID必填",
|
||||
Hint: "请输入OpenID",
|
||||
StatusCode: 400,
|
||||
ErrorCode: PARAM_ERROR,
|
||||
Description: "OpenID必填",
|
||||
Hint: "请输入OpenID",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: PARAM_ERROR,
|
||||
Description: "批次号必填",
|
||||
Hint: "请输入批次号",
|
||||
StatusCode: 400,
|
||||
ErrorCode: PARAM_ERROR,
|
||||
Description: "批次号必填",
|
||||
Hint: "请输入批次号",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: PARAM_ERROR,
|
||||
Description: "商户号必填",
|
||||
Hint: "请输入商户号",
|
||||
StatusCode: 400,
|
||||
ErrorCode: PARAM_ERROR,
|
||||
Description: "商户号必填",
|
||||
Hint: "请输入商户号",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
{
|
||||
StatusCode: 400,
|
||||
ErrorCode: PARAM_ERROR,
|
||||
Description: "非法的批次状态",
|
||||
Hint: "请检查批次状态,仅支持发放状态为“运营中”的代金券批次",
|
||||
StatusCode: 400,
|
||||
ErrorCode: PARAM_ERROR,
|
||||
Description: "非法的批次状态",
|
||||
Hint: "请检查批次状态,仅支持发放状态为“运营中”的代金券批次",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
},
|
||||
MCH_NOT_EXISTS: {
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: MCH_NOT_EXISTS,
|
||||
Description: "商户号不合法",
|
||||
Hint: "请检查商户号准确性",
|
||||
StatusCode: 403,
|
||||
ErrorCode: MCH_NOT_EXISTS,
|
||||
Description: "商户号不合法",
|
||||
Hint: "请检查商户号准确性",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
},
|
||||
NOT_ENOUGH: {
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: NOT_ENOUGH,
|
||||
Description: "批次预算不足",
|
||||
Hint: "批次预算已发放完,请补充批次预算",
|
||||
StatusCode: 403,
|
||||
ErrorCode: NOT_ENOUGH,
|
||||
Description: "批次预算不足",
|
||||
Hint: "批次预算已发放完,请补充批次预算",
|
||||
ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough,
|
||||
},
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: NOT_ENOUGH,
|
||||
Description: "发券超过单天限额",
|
||||
Hint: "已超过该批次设置的单天发放限制额度,无法发放",
|
||||
StatusCode: 403,
|
||||
ErrorCode: NOT_ENOUGH,
|
||||
Description: "发券超过单天限额",
|
||||
Hint: "已超过该批次设置的单天发放限制额度,无法发放",
|
||||
ThirdErrCode: ThirdErrCodeDailyLimit,
|
||||
},
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: NOT_ENOUGH,
|
||||
Description: "账户余额不足,请充值",
|
||||
Hint: "商户号余额不足,无法继续发券,请充值",
|
||||
StatusCode: 403,
|
||||
ErrorCode: NOT_ENOUGH,
|
||||
Description: "账户余额不足,请充值",
|
||||
Hint: "商户号余额不足,无法继续发券,请充值",
|
||||
ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough,
|
||||
},
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: NOT_ENOUGH,
|
||||
Description: "批次预算耗尽",
|
||||
Hint: "该批次的预算已经耗尽",
|
||||
StatusCode: 403,
|
||||
ErrorCode: NOT_ENOUGH,
|
||||
Description: "批次预算耗尽",
|
||||
Hint: "该批次的预算已经耗尽",
|
||||
ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough,
|
||||
},
|
||||
},
|
||||
REQUEST_BLOCKED: {
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "商户无权发券",
|
||||
Hint: "该批次不支持其他商户发放,请参考常见问题Q1",
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "商户无权发券",
|
||||
Hint: "该批次不支持其他商户发放,请参考常见问题Q1",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "批次不支持跨商户发券",
|
||||
Hint: "该批次不支持其他商户发放,请参考常见问题Q1",
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "批次不支持跨商户发券",
|
||||
Hint: "该批次不支持其他商户发放,请参考常见问题Q1",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "用户被限领拦截",
|
||||
Hint: "该用户已达到该批次的领取上限,请参考常见问题Q6",
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "用户被限领拦截",
|
||||
Hint: "该用户已达到该批次的领取上限,请参考常见问题Q6",
|
||||
ThirdErrCode: ThirdErrCodeUserParticipationExceeded,
|
||||
},
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "不能在API渠道发放",
|
||||
Hint: "请检查批次信息,仅支持发放微信支付代金券,不支持发放立减与折扣",
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "不能在API渠道发放",
|
||||
Hint: "请检查批次信息,仅支持发放微信支付代金券,不支持发放立减与折扣",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "不支持指定面额发券",
|
||||
Hint: "仅在发券时指定面额及门槛的场景才生效,常规发券场景请勿传入该信息",
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "不支持指定面额发券",
|
||||
Hint: "仅在发券时指定面额及门槛的场景才生效,常规发券场景请勿传入该信息",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "仅在广告场景下发放批次",
|
||||
Hint: "该批次已在朋友圈广告发放,不支持在其他渠道发放",
|
||||
StatusCode: 403,
|
||||
ErrorCode: REQUEST_BLOCKED,
|
||||
Description: "仅在广告场景下发放批次",
|
||||
Hint: "该批次已在朋友圈广告发放,不支持在其他渠道发放",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
},
|
||||
RULE_LIMIT: {
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: RULE_LIMIT,
|
||||
Description: "用户已达最大领券次数",
|
||||
Hint: "该用户已达到该批次的领取上限,请参考常见问题Q6",
|
||||
StatusCode: 403,
|
||||
ErrorCode: RULE_LIMIT,
|
||||
Description: "用户已达最大领券次数",
|
||||
Hint: "该用户已达到该批次的领取上限,请参考常见问题Q6",
|
||||
ThirdErrCode: ThirdErrCodeUserParticipationExceeded,
|
||||
},
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: RULE_LIMIT,
|
||||
Description: "被自然人规则拦截",
|
||||
Hint: "该自然人已达到该批次的领取上限,请参考常见问题Q6",
|
||||
StatusCode: 403,
|
||||
ErrorCode: RULE_LIMIT,
|
||||
Description: "被自然人规则拦截",
|
||||
Hint: "该自然人已达到该批次的领取上限,请参考常见问题Q6",
|
||||
ThirdErrCode: ThirdErrCodeUserAccountAbnormal,
|
||||
},
|
||||
},
|
||||
USER_ACCOUNT_ABNORMAL: {
|
||||
{
|
||||
StatusCode: 403,
|
||||
ErrorCode: USER_ACCOUNT_ABNORMAL,
|
||||
Description: "用户非法",
|
||||
Hint: "用户命中微信支付风控模型,请参考常见问题Q5",
|
||||
StatusCode: 403,
|
||||
ErrorCode: USER_ACCOUNT_ABNORMAL,
|
||||
Description: "用户非法",
|
||||
Hint: "用户命中微信支付风控模型,请参考常见问题Q5",
|
||||
ThirdErrCode: ThirdErrCodeUserAccountFrozen,
|
||||
},
|
||||
},
|
||||
RESOURCE_NOT_EXISTS: {
|
||||
{
|
||||
StatusCode: 404,
|
||||
ErrorCode: RESOURCE_NOT_EXISTS,
|
||||
Description: "批次不存在",
|
||||
Hint: "请检查批次及制券商户号信息",
|
||||
StatusCode: 404,
|
||||
ErrorCode: RESOURCE_NOT_EXISTS,
|
||||
Description: "批次不存在",
|
||||
Hint: "请检查批次及制券商户号信息",
|
||||
ThirdErrCode: ThirdErrCodeDefault,
|
||||
},
|
||||
},
|
||||
FREQUENCY_LIMITED: {
|
||||
{
|
||||
StatusCode: 429,
|
||||
ErrorCode: FREQUENCY_LIMITED,
|
||||
Description: "当前请求人数过多,请稍后重试",
|
||||
Hint: "请降低API调用频率",
|
||||
StatusCode: 429,
|
||||
ErrorCode: FREQUENCY_LIMITED,
|
||||
Description: "当前请求人数过多,请稍后重试",
|
||||
Hint: "请降低API调用频率",
|
||||
ThirdErrCode: ThirdErrCodeCallHigh,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -284,13 +342,13 @@ var WechatError = map[string]*errors.Error{
|
|||
ErrorWechatAccountFail: err2.ErrorWechatAccountFail(ErrorWechatAccountFail),
|
||||
}
|
||||
|
||||
type ErrBody struct {
|
||||
type WechatErrBody struct {
|
||||
Code ErrCode `json:"Code"`
|
||||
Message string `json:"Message"`
|
||||
}
|
||||
|
||||
// GetWechatError 根据错误描述获取具体的错误处理
|
||||
func (s ErrBody) GetWechatError() *errors.Error {
|
||||
func (s WechatErrBody) GetWechatError() *errors.Error {
|
||||
lowerDesc := strings.ToLower(s.Message)
|
||||
for desc, err := range WechatError {
|
||||
if strings.ToLower(desc) == lowerDesc {
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -63,12 +63,13 @@ func (v *Cmb) bizContent(_ context.Context, order *bo.OrderBo, orderNotify *bo.O
|
|||
}
|
||||
|
||||
req := &v1.CmbNotifyRequest{
|
||||
Ticket: orderNotify.OrderNo,
|
||||
Status: cmbStatus.GetValue(),
|
||||
TransDate: "", // 格式yyyy-mm-dd hh:mm:ss.sss
|
||||
OrgNo: v.bc.Cmb.OrgNo,
|
||||
Attach: order.Attach,
|
||||
Ext: "",
|
||||
Ticket: orderNotify.OrderNo,
|
||||
Status: cmbStatus.GetValue(),
|
||||
TransDate: "", // 格式yyyy-mm-dd hh:mm:ss.sss
|
||||
OrgNo: v.bc.Cmb.OrgNo,
|
||||
Attach: order.Attach,
|
||||
Ext: "",
|
||||
TransactionId: order.OutBizNo, // 招行订单号
|
||||
}
|
||||
|
||||
if cmbStatus == vo.CmbStatusUse {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -5,4 +5,4 @@ import (
|
|||
)
|
||||
|
||||
// ProviderSetBiz is biz providers.
|
||||
var ProviderSetBiz = wire.NewSet(NewVoucherBiz)
|
||||
var ProviderSetBiz = wire.NewSet(NewVoucherBiz, NewMultiBiz, NewWechatBiz)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,15 +25,27 @@ func (this *VoucherBiz) RegisterTag(ctx context.Context, id int32) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err = this.registerNotifyTag(ctx, stock.MchId, stock.BatchNo); err != nil {
|
||||
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})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ type Query struct {
|
|||
rdb *data.Rdb
|
||||
cmb *cmb.Cmb
|
||||
|
||||
productRepo repo.ProductRepo
|
||||
orderRepo repo.OrderRepo
|
||||
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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ type CmbFuncName string
|
|||
|
||||
const (
|
||||
// CmbNotifyFuncName . 券状态回调通知方法
|
||||
CmbNotifyFuncName CmbFuncName = "updateCodeStatus.json"
|
||||
CmbNotifyFuncName CmbFuncName = "updateCodeStatus.json"
|
||||
CmbNotifyFuncNameUpdateCodeStatusForMulti CmbFuncName = "updateCodeStatusForMulti.json"
|
||||
)
|
||||
|
||||
func (s CmbFuncName) GetValue() string {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,16 +112,34 @@ 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 {
|
||||
return fun(ctx, p.ToBos(results))
|
||||
})
|
||||
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))
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -11,4 +11,6 @@ var ProviderRepoImplSet = wire.NewSet(
|
|||
NewOrderNotifyRepoImpl,
|
||||
NewWechatNotifyRegisterTagRepoImpl,
|
||||
NewOrderBakRepoImpl,
|
||||
NewMultiNotifyDataRepoImpl,
|
||||
NewMultiNotifyLogRepoImpl,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
//}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
// 创建一个字符串切片,用于保存所有的键名
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
bizReply := &v1.CmbOrderReply{
|
||||
RespCode: vo.CmbResponseStatusFail.GetValue(),
|
||||
RespMsg: se.Message,
|
||||
CodeNo: "",
|
||||
ThirdErrCode: se.Reason,
|
||||
isMultiErr := false
|
||||
|
||||
if product != nil && product.ActivityId != "" {
|
||||
isMultiErr = true
|
||||
} else if order != nil && order.ActivityId != "" {
|
||||
isMultiErr = true
|
||||
}
|
||||
|
||||
if isMultiErr {
|
||||
cmbAPIError := e.HandleMULTIThirdErrCode()
|
||||
bizReply = &v1.CmbOrderReply{
|
||||
RespCode: vo.CmbResponseStatusFail.GetValue(),
|
||||
RespMsg: cmbAPIError.Description,
|
||||
CodeNo: "",
|
||||
ThirdErrCode: string(cmbAPIError.ThirdErrCode),
|
||||
}
|
||||
} else {
|
||||
cmbAPIError := e.HandleThirdErrCode()
|
||||
bizReply = &v1.CmbOrderReply{
|
||||
RespCode: vo.CmbResponseStatusFail.GetValue(),
|
||||
RespMsg: cmbAPIError.Description,
|
||||
CodeNo: "",
|
||||
ThirdErrCode: string(cmbAPIError.ThirdErrCode),
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
bizReply = &v1.CmbOrderReply{
|
||||
RespCode: vo.CmbResponseStatusFail.GetValue(),
|
||||
RespMsg: err.Error(),
|
||||
CodeNo: "",
|
||||
ThirdErrCode: string(businesserr.ThirdErrCodeDefault),
|
||||
}
|
||||
}
|
||||
|
||||
replyBizContent, _ := json.Marshal(bizReply)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -8,4 +8,6 @@ import (
|
|||
var ProviderSetService = wire.NewSet(
|
||||
NewVoucherService,
|
||||
NewCmbService,
|
||||
NewTripartiteService,
|
||||
NewNotifyService,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
187
test/coupon.go
187
test/coupon.go
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package test
|
|||
|
||||
import (
|
||||
"testing"
|
||||
"voucher/internal/biz/businesserr"
|
||||
)
|
||||
|
||||
func Test_SendCoupon(t *testing.T) {
|
||||
|
|
@ -14,22 +15,38 @@ func Test_SendCoupon(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
SendCoupon()
|
||||
if err := SendCoupon(); err != nil {
|
||||
if ee, ok := err.(*businesserr.BusinessErr); ok {
|
||||
t.Errorf("errorcode:%s,errmsg:%s", ee.Code, ee.Message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_QueryCoupon(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
name string
|
||||
appId string
|
||||
openId string
|
||||
couponId string
|
||||
}{
|
||||
{
|
||||
name: "查询券订单信息",
|
||||
name: "查询券订单信息",
|
||||
appId: "wx619991cc795028f5",
|
||||
openId: "oSNb4fnRWr7HdgbDOO8TD66LXofE",
|
||||
couponId: "147089832782",
|
||||
},
|
||||
{
|
||||
name: "查询券订单信息",
|
||||
appId: "wx619991cc795028f5",
|
||||
openId: "oSNb4foo87dBNx1D9KTH-bB-G6YA",
|
||||
couponId: "147094824239",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
QueryCoupon()
|
||||
QueryCoupon(tt.appId, tt.openId, tt.couponId)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue