You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
452 lines
14 KiB
452 lines
14 KiB
package pay_service
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
core2 "github.com/chanxuehong/wechat/mch/core"
|
|
"github.com/chanxuehong/wechat/mch/pay"
|
|
"github.com/pkg/errors"
|
|
"go.uber.org/zap"
|
|
"hudongzhuanjia/logger"
|
|
"hudongzhuanjia/models"
|
|
"hudongzhuanjia/utils"
|
|
"io"
|
|
"math"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
func init() {
|
|
go loopUnifiedOrder()
|
|
}
|
|
|
|
var orderDelayQueue = make(chan *orderDelayQueueParam, math.MaxInt8)
|
|
|
|
type orderDelayQueueParam struct {
|
|
First bool `json:"first"`
|
|
Expires int64 `json:"expires"`
|
|
Delay int `json:"delay"`
|
|
OutTradeNo string `json:"out_trade_no"`
|
|
Body string `json:"body"`
|
|
Amount int `json:"amount"` // 金额
|
|
Status int `json:"status"` // 0 订单 3 退款
|
|
OpenId string `json:"open_id"` // 被操作人
|
|
}
|
|
|
|
func PutOrderDelayQueue(body, outTradeNo, openId string, amount, status int, expires int64, delay int) {
|
|
if expires == 0 {
|
|
expires = time.Now().Add(12 * time.Hour).Unix() // 2 个小时
|
|
}
|
|
|
|
if delay == 0 {
|
|
delay = 100
|
|
}
|
|
|
|
orderDelayQueue <- &orderDelayQueueParam{
|
|
First: true,
|
|
Expires: expires,
|
|
Delay: delay,
|
|
OutTradeNo: outTradeNo,
|
|
Body: body,
|
|
Amount: amount,
|
|
Status: status,
|
|
OpenId: openId,
|
|
}
|
|
}
|
|
|
|
func loopUnifiedOrder() {
|
|
orders, err := models.GetUserOrdersByStatus(core2.FormatTime(time.Now()), 0, 3)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
for _, order := range orders {
|
|
expire, err := core2.ParseTime(order.TimeExpire)
|
|
if err != nil {
|
|
expire = time.Now().Add(12 * time.Hour)
|
|
}
|
|
PutOrderDelayQueue(order.Body, order.OutTradeNo, order.OpenId, int(order.TotalFee), order.Status, expire.Unix(), 0)
|
|
}
|
|
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
logger.Error("订单轮询查询", zap.Any("panic 恢复错误", err))
|
|
}
|
|
// 重启
|
|
time.Sleep(5 * time.Second)
|
|
loopUnifiedOrder()
|
|
}()
|
|
for {
|
|
select {
|
|
case param, ok := <-orderDelayQueue:
|
|
if !ok {
|
|
panic("通道异常关闭")
|
|
}
|
|
if param.Expires <= time.Now().Unix() {
|
|
//if param.Status == 0 {
|
|
//order := new(models.UserOrder)
|
|
//_, err = order.UpdateStatusByOutTradeNo(param.OutTradeNo, 7)
|
|
//Close(param.OutTradeNo) // 超时关闭订单
|
|
//}
|
|
continue // 超时
|
|
}
|
|
|
|
// 首次进入不延迟
|
|
if !param.First {
|
|
time.Sleep(time.Duration(param.Delay) * time.Millisecond)
|
|
}
|
|
param.First = false
|
|
|
|
if param.Status == 0 {
|
|
res, err := OrderQuery(param.OutTradeNo)
|
|
|
|
// 出现错误
|
|
if err != nil {
|
|
logger.Error("查询订单出现错误", zap.String("错误原因", err.Error()),
|
|
zap.String("交易订单号", param.OutTradeNo))
|
|
orderDelayQueue <- param // 重新进入队列
|
|
continue
|
|
}
|
|
|
|
if res.Query.TradeState == CODE_TRADE_SUCCESS {
|
|
go Handle(res.Order)
|
|
} else if res.Query.TradeState == CODE_TRADE_REFUND {
|
|
param.Status = 3
|
|
orderDelayQueue <- param
|
|
continue
|
|
} else if res.Query.TradeState == CODE_TRADE_NOTPAY || res.Query.TradeState == CODE_TRADE_USERPAYING {
|
|
orderDelayQueue <- param
|
|
continue
|
|
}
|
|
} else if param.Status == 3 {
|
|
_, err = QueryRefund(param.OutTradeNo)
|
|
if err != nil {
|
|
logger.Error("退款订单查询错误", zap.String("错误原因", err.Error()),
|
|
zap.String("交易订单号", param.OutTradeNo))
|
|
continue
|
|
}
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const CallbackOrderUrl = "https://api.ouxuanhudong.com/PcClient/common/WeChatOauthCtl/callbackOrder"
|
|
|
|
func UnifiedOrder(body, openid string, fee, goodType, userId, activityId int64) (map[string]interface{}, error) {
|
|
client, err := Client()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
now := time.Now()
|
|
timeStart := core2.FormatTime(now)
|
|
timeExpire := core2.FormatTime(now.Add(12 * time.Hour))
|
|
outTradeNo := utils.RandomStr(32)
|
|
nonceStr := utils.RandomStr(32)
|
|
resp, err := pay.UnifiedOrder2(client, &pay.UnifiedOrderRequest{
|
|
Body: body,
|
|
OutTradeNo: outTradeNo,
|
|
TotalFee: fee,
|
|
NotifyURL: CallbackOrderUrl,
|
|
TradeType: "JSAPI",
|
|
DeviceInfo: "WEB",
|
|
NonceStr: nonceStr,
|
|
SignType: core2.SignType_MD5,
|
|
TimeStart: now,
|
|
OpenId: openid,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 记录这次订单
|
|
userOrder := new(models.UserOrder)
|
|
userOrder.DeviceInfo = "WEB"
|
|
userOrder.Body = body
|
|
userOrder.UserId = userId
|
|
userOrder.ActivityId = activityId
|
|
userOrder.FeeType = "CNY"
|
|
userOrder.GoodType = goodType
|
|
userOrder.OpenId = openid
|
|
userOrder.OutTradeNo = outTradeNo
|
|
userOrder.TimeStart = timeStart
|
|
userOrder.TimeExpire = timeExpire
|
|
userOrder.TotalFee = fee
|
|
userOrder.TradeType = "JSAPI"
|
|
userOrder.PrepayId = resp.PrepayId
|
|
userOrder.Status = 0
|
|
userOrder.IsDelete = false
|
|
userOrder.CreatedAt = time.Now()
|
|
userOrder.UpdatedAt = time.Now()
|
|
if _, err = userOrder.AddUserOrder(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
|
//获取H5支付需要的paySign
|
|
pac := "prepay_id=" + resp.PrepayId
|
|
paySign := core2.JsapiSign(client.AppId(), timestamp, nonceStr, pac, core2.SignType_MD5, ApiKey)
|
|
|
|
go PutOrderDelayQueue(userOrder.Body, userOrder.OutTradeNo, userOrder.OpenId, int(userOrder.TotalFee), userOrder.Status, 0, 0)
|
|
return map[string]interface{}{
|
|
"appid": Appid,
|
|
"timestamp": timestamp,
|
|
"nonce_str": nonceStr,
|
|
"package": pac,
|
|
"sign_type": core2.SignType_MD5,
|
|
"pay_sign": paySign,
|
|
"out_trade_no": outTradeNo,
|
|
"user_order_id": userOrder.Id,
|
|
}, nil
|
|
}
|
|
|
|
func ReOrder(outTradeNo string) (map[string]interface{}, error) {
|
|
userOrder := new(models.UserOrder)
|
|
exist, err := userOrder.GetByOutTradeNo(outTradeNo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exist {
|
|
return nil, errors.New("订单不存在")
|
|
}
|
|
|
|
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
|
nonceStr := utils.RandomStr(32)
|
|
//获取H5支付需要的paySign
|
|
pac := "prepay_id=" + userOrder.PrepayId
|
|
paySign := core2.JsapiSign(Appid, timestamp, nonceStr, pac, core2.SignType_MD5, ApiKey)
|
|
go PutOrderDelayQueue(userOrder.Body, userOrder.OutTradeNo, userOrder.OpenId, int(userOrder.TotalFee), userOrder.Status, 0, 0)
|
|
return map[string]interface{}{
|
|
"appid": Appid,
|
|
"timestamp": timestamp,
|
|
"nonce_str": nonceStr,
|
|
"package": pac,
|
|
"sign_type": core2.SignType_MD5,
|
|
"pay_sign": paySign,
|
|
"out_trade_no": outTradeNo,
|
|
"user_order_id": userOrder.Id,
|
|
}, nil
|
|
|
|
}
|
|
|
|
// Notify
|
|
type NotifyRequest struct {
|
|
ReturnCode string `xml:"return_code,omitempty" json:"return_code,omitempty"`
|
|
ReturnMsg string `xml:"return_msg,omitempty" json:"return_msg,omitempty"`
|
|
ResultCode string `xml:"result_code,omitempty" json:"result_code,omitempty"`
|
|
ErrCode string `xml:"err_code,omitempty" json:"err_code,omitempty"`
|
|
ErrCodeDes string `xml:"err_code_des,omitempty" json:"err_code_des,omitempty"`
|
|
Appid string `xml:"appid,omitempty" json:"appid,omitempty"`
|
|
MchId string `xml:"mch_id,omitempty" json:"mch_id,omitempty"`
|
|
DeviceInfo string `xml:"device_info,omitempty" json:"device_info,omitempty"`
|
|
NonceStr string `xml:"nonce_str,omitempty" json:"nonce_str,omitempty"`
|
|
Sign string `xml:"sign,omitempty" json:"sign,omitempty"`
|
|
SignType string `xml:"sign_type,omitempty" json:"sign_type,omitempty"`
|
|
Openid string `xml:"openid,omitempty" json:"openid,omitempty"`
|
|
IsSubscribe string `xml:"is_subscribe,omitempty" json:"is_subscribe,omitempty"`
|
|
TradeType string `xml:"trade_type,omitempty" json:"trade_type,omitempty"`
|
|
BankType string `xml:"bank_type,omitempty" json:"bank_type,omitempty"`
|
|
TotalFee int `xml:"total_fee,omitempty" json:"total_fee,omitempty"`
|
|
SettlementTotalFee int `xml:"settlement_total_fee,omitempty" json:"settlement_total_fee,omitempty"`
|
|
FeeType string `xml:"fee_type,omitempty" json:"fee_type,omitempty"`
|
|
CashFee int `xml:"cash_fee,omitempty" json:"cash_fee,omitempty"`
|
|
CashFeeType string `xml:"cash_fee_type,omitempty" json:"cash_fee_type,omitempty"`
|
|
CouponFee int `xml:"coupon_fee,omitempty" json:"coupon_fee,omitempty"`
|
|
CouponCount int `xml:"coupon_count,omitempty" json:"coupon_count,omitempty"`
|
|
CouponType0 string `xml:"coupon_type_0,omitempty" json:"coupon_type_0,omitempty"`
|
|
CouponType1 string `xml:"coupon_type_1,omitempty" json:"coupon_type_1,omitempty"`
|
|
CouponId0 string `xml:"coupon_id_0,omitempty" json:"coupon_id_0,omitempty"`
|
|
CouponId1 string `xml:"coupon_id_1,omitempty" json:"coupon_id_1,omitempty"`
|
|
CouponFee0 int `xml:"coupon_fee_0,omitempty" json:"coupon_fee_0,omitempty"`
|
|
CouponFee1 int `xml:"coupon_fee_1,omitempty" json:"coupon_fee_1,omitempty"`
|
|
TransactionId string `xml:"transaction_id,omitempty" json:"transaction_id,omitempty"`
|
|
OutTradeNo string `xml:"out_trade_no,omitempty" json:"out_trade_no,omitempty"`
|
|
Attach string `xml:"attach,omitempty" json:"attach,omitempty"`
|
|
TimeEnd string `xml:"time_end,omitempty" json:"time_end,omitempty"`
|
|
}
|
|
|
|
func NotifyOrder(w io.Reader) (*models.UserOrder, error) {
|
|
res := new(NotifyRequest)
|
|
if err := xml.NewDecoder(w).Decode(res); err != nil {
|
|
return nil, fmt.Errorf("xml.NewDecoder.Decode:%w", err)
|
|
}
|
|
|
|
if res.ReturnCode != CODE_SUCCESS {
|
|
return nil, fmt.Errorf("network error, retrun_code: %v and return_msg: %v", res.ReturnCode, res.ReturnMsg)
|
|
}
|
|
|
|
if res.ResultCode == CODE_SUCCESS && res.TradeType == CODE_TRADE_SUCCESS {
|
|
userOrder := new(models.UserOrder)
|
|
userOrder.TimeEnd = res.TimeEnd
|
|
userOrder.TransactionId = res.TransactionId
|
|
userOrder.Status = 1
|
|
if _, err := userOrder.UpdateStatusByOutTradeNo(res.OutTradeNo, 1); err != nil {
|
|
return nil, err
|
|
}
|
|
return userOrder, nil
|
|
} else {
|
|
return nil, fmt.Errorf("trade error, err_code: %v and err_code_des: %v", res.ErrCode, res.ErrCodeDes)
|
|
}
|
|
}
|
|
|
|
type OrderQueryResult struct {
|
|
Order *models.UserOrder
|
|
Query *pay.OrderQueryResponse
|
|
}
|
|
|
|
func OrderQuery(outTradeNo string) (*OrderQueryResult, error) {
|
|
client, err := Client()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// 请求订单查询,成功后得到结果
|
|
userOrder := new(models.UserOrder)
|
|
exist, err := userOrder.GetByOutTradeNo(outTradeNo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exist {
|
|
return nil, errors.New("订单不存在")
|
|
}
|
|
|
|
res, err := pay.OrderQuery2(client, &pay.OrderQueryRequest{
|
|
OutTradeNo: outTradeNo,
|
|
NonceStr: utils.RandomStr(32),
|
|
SignType: core2.SignType_MD5,
|
|
})
|
|
|
|
if err != nil {
|
|
userOrder.ErrMsg = err.Error()
|
|
userOrder.UpdateErrByOutTradeNo(outTradeNo)
|
|
return nil, err
|
|
}
|
|
|
|
userOrder.TransactionId = res.TransactionId
|
|
userOrder.TimeEnd = core2.FormatTime(res.TimeEnd)
|
|
switch res.TradeState {
|
|
case CODE_TRADE_SUCCESS:
|
|
userOrder.Status = 1
|
|
case CODE_TRADE_REVOKED:
|
|
userOrder.Status = 2
|
|
case CODE_TRADE_REFUND:
|
|
userOrder.Status = 3
|
|
case CODE_TRADE_PAYERROR:
|
|
userOrder.Status = 5
|
|
case CODE_TRADE_CLOSED:
|
|
userOrder.Status = 6
|
|
case CODE_TRADE_NOTPAY: // 超过期限,就关闭
|
|
userOrder.Status = 0
|
|
}
|
|
|
|
if _, err = userOrder.UpdateStatusByOutTradeNo(outTradeNo, userOrder.Status); err != nil {
|
|
return nil, err
|
|
}
|
|
return &OrderQueryResult{
|
|
Order: userOrder,
|
|
Query: res,
|
|
}, nil
|
|
}
|
|
|
|
func Close(outTradeNo string) error {
|
|
client, err := Client()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = pay.CloseOrder2(client, &pay.CloseOrderRequest{
|
|
OutTradeNo: outTradeNo,
|
|
NonceStr: utils.RandomStr(32),
|
|
SignType: core2.SignType_MD5,
|
|
})
|
|
// 请求关闭订单,成功后得到结果
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
//const CALLBACK_REFUND_URL = "https://api.ouxuanhudong.com/PcClient/common/WeChatOauthCtl/callbackRefund"
|
|
|
|
func Refund(reason, outTradeNo string) (*pay.RefundResponse, error) {
|
|
userOrder := new(models.UserOrder)
|
|
exist, err := userOrder.GetByOutTradeNo(outTradeNo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exist {
|
|
return nil, errors.New("订单不存在")
|
|
}
|
|
client, err := Client()
|
|
|
|
outRefundNo := utils.RandomStr(64)
|
|
nonceStr := utils.RandomStr(32)
|
|
res, err := pay.Refund2(client, &pay.RefundRequest{
|
|
TransactionId: "",
|
|
OutTradeNo: outTradeNo,
|
|
OutRefundNo: outRefundNo,
|
|
TotalFee: userOrder.TotalFee,
|
|
RefundFee: userOrder.TotalFee,
|
|
NonceStr: nonceStr,
|
|
SignType: core2.SignType_MD5,
|
|
RefundFeeType: "CNY",
|
|
RefundDesc: reason,
|
|
})
|
|
//
|
|
if err != nil {
|
|
userOrder.ErrMsg = err.Error()
|
|
userOrder.UpdateErrByOutTradeNo(outTradeNo)
|
|
return nil, err
|
|
}
|
|
userOrder.Status = 3
|
|
_, err = userOrder.UpdateStatusByOutTradeNo(outTradeNo, userOrder.Status)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
PutOrderDelayQueue(userOrder.Body, userOrder.OutTradeNo, userOrder.OpenId, int(userOrder.TotalFee), userOrder.Status, 0, 0) // 退款查询
|
|
return res, nil
|
|
}
|
|
|
|
type QueryRefundResult struct {
|
|
Order *models.UserOrder
|
|
Query *pay.RefundQueryResponse
|
|
}
|
|
|
|
func QueryRefund(outTradeNo string) (*QueryRefundResult, error) {
|
|
userOrder := new(models.UserOrder)
|
|
exist, err := userOrder.GetByOutTradeNo(outTradeNo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exist {
|
|
return nil, errors.New("不存在改笔退款")
|
|
}
|
|
|
|
client, err := Client()
|
|
res, err := pay.RefundQuery2(client, &pay.RefundQueryRequest{
|
|
OutTradeNo: outTradeNo,
|
|
NonceStr: utils.RandomStr(32),
|
|
SignType: core2.SignType_MD5,
|
|
})
|
|
//请求申请退款
|
|
if err != nil {
|
|
userOrder.ErrMsg = err.Error()
|
|
userOrder.UpdateErrByOutTradeNo(outTradeNo)
|
|
return nil, err
|
|
}
|
|
|
|
userOrder.RefundAccount = res.RefundList[0].RefundAccount
|
|
userOrder.RefundRecvAccount = res.RefundList[0].RefundRecvAccout
|
|
userOrder.SuccessTime = res.RefundList[0].RefundSuccessTime
|
|
userOrder.Status = 4
|
|
_, err = userOrder.UpdateRefundByOutTradeNo(outTradeNo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &QueryRefundResult{
|
|
Order: userOrder,
|
|
Query: res,
|
|
}, nil
|
|
}
|