Compare commits

...

3 Commits

Author SHA1 Message Date
wolter 2a21f854e1 feat: 收银台 2024-12-23 15:42:16 +08:00
wolter 64973f683d feat: 付款链接接口 2024-12-21 17:26:26 +08:00
wolter 0ae425e4bb feat: 下单接口v2 2024-12-21 15:48:48 +08:00
22 changed files with 820 additions and 10 deletions

View File

@ -5,10 +5,12 @@ const (
TOKEN_Admin = "Admin_token_"
ADMIN_V1 = "/pay/admin/api/v1"
FRONT_V1 = "/pay/front/api/v1"
FRONT_V2 = "/pay/front/api/v2"
WXCodeRedirectAddress = FRONT_V1 + "/wx/payurl" // 微信支付授权code后 重定向地址
PayPageAddress = FRONT_V1 + "/payPage" // 收银台页面地址
// 支付渠道枚举,1微信JSAPI2微信H53微信app4微信Native5微信小程序6支付宝网页&移动应用7支付宝小程序8支付宝JSAPI
// 支付渠道枚举,1微信JSAPI2微信H53微信app4微信Native5微信小程序6支付宝网页&移动应用7支付宝小程序8支付宝JSAPI,9支付宝电脑网站支付,10支付宝扫码支付
PAY_CHANNEL_UNKNOWN = 0
PAY_CHANNEL_WECHAT_JSAPI = 1
PAY_CHANNEL_WECHAT_H5 = 2
@ -18,6 +20,7 @@ const (
PAY_CHANNEL_ALIPAY_WEB = 6
PAY_CHANNEL_ALIPAY_MINI = 7
PAY_CHANNEL_ALIPAY_JSAPI = 8
PAY_CHANNEL_ALIPAY_PC = 9
// 统一登陆信息
ADMIN_USER_ID = "User-Id"
@ -75,3 +78,18 @@ var OrderStatusMap = map[int]string{
ORDER_STATUS_FAILED: "执行失败",
ORDER_STATUS_CLOSE: "订单关闭",
}
// 客户端环境
const (
OpenInUnknown int = iota // 未知
OpenInWeChat // 微信客户端
OpenInMobile // 手机客户端
OpenInWindows // PC端
)
// 客户端环境和支付渠道的映射关系
var OpenInPayChannelMap = map[int][]int{
OpenInWeChat: {PAY_CHANNEL_WECHAT_JSAPI},
OpenInMobile: {PAY_CHANNEL_WECHAT_H5, PAY_CHANNEL_ALIPAY_WEB},
OpenInWindows: {PAY_CHANNEL_WECHAT_NATIVE, PAY_CHANNEL_ALIPAY_PC},
}

View File

@ -61,6 +61,7 @@ const (
PayChannelNotFound = 1300
PayChannelNotBuild = 1301
PayChannelExtJsonError = 1302
PayChannelExpired = 1303
//订单
OrdersNotFound = 1401
@ -101,6 +102,8 @@ const (
// 微信授权
WechatAuthFail = 1901
WechatAuthSignFail = 1902
ClientEnvErr = 2000
)
var MsgEN = map[int]string{
@ -187,6 +190,7 @@ var MsgZH = map[int]string{
WechatAuthFail: "微信授权失败",
WechatAuthSignFail: "微信签名失败",
ClientEnvErr: "支付环境错误",
}
var MsgMap map[string]map[int]string = map[string]map[int]string{"en": MsgZH}

View File

@ -0,0 +1,99 @@
package front
import (
"PaymentCenter/app/constants/errorcode"
"PaymentCenter/app/http/controllers"
"PaymentCenter/app/http/entities/front"
"PaymentCenter/app/models/ordersmodel"
"PaymentCenter/app/models/paychannelmodel"
"PaymentCenter/app/services"
"PaymentCenter/app/services/thirdpay"
"fmt"
"github.com/ahmetb/go-linq/v3"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
"xorm.io/builder"
)
// 预支付接口V2, 返回收银台页面
func PayUrlV2(c *gin.Context) {
var res front.ApiResponse
req := controllers.GetRequest(c).(*front.PayReqsV2)
// 更新请求日志需要的参数
c.Set("OutTradeNo", req.OutTradeNo)
appCheckInfo := controllers.GetAppCheckInfo(c).(*services.AppCheck)
result, code := thirdpay.NewPayUrl(*req).
WithApp(appCheckInfo).
WithClientIp(c.ClientIP()).
PayUrlV2Service()
if result.Order != nil {
res.Order = thirdpay.NewOrdersResp(result.Order)
res.Url = result.Url
}
controllers.ApiRes(c, res, code)
return
}
// 收银台页面
func PayPage(c *gin.Context) {
var (
code int
message string
amount string
)
orderId := c.Query("no")
if orderId != "" {
orderInfo := &ordersmodel.Orders{}
orderInfo, code = services.OrderFindOne(&ordersmodel.Orders{}, builder.Eq{"id": orderId})
if code == errorcode.Success {
amount = fmt.Sprintf("%.2f", float64(orderInfo.Amount)/100)
} else {
message = errorcode.GetMsg(code, "")
}
} else {
message = "页面不存在"
}
c.HTML(http.StatusOK, "payPage.html", gin.H{
"code": code,
"message": message,
"id": orderId,
"amount": amount,
})
}
// 收银台获取支付渠道列表
func PayChannelList(c *gin.Context) {
req, _ := controllers.GetRequest(c).(*front.PayChannelListRequest)
req.UserAgent = c.Request.UserAgent()
data, code := services.PayPageChannelList(*req)
result := []front.PayChannelListResponse{}
linq.From(data).SelectT(func(payChannel paychannelmodel.PayChannel) front.PayChannelListResponse {
return front.PayChannelListResponse{
ChannelType: payChannel.ChannelType,
PayName: payChannel.PayName,
PayChannelId: strconv.Itoa(int(payChannel.Id)),
}
}).ToSlice(&result)
controllers.HandCodeRes(c, result, code)
}
// 获取付款链接
func GetPayLink(c *gin.Context) {
req, _ := controllers.GetRequest(c).(*front.GetPayLinkRequest)
result, message, code := thirdpay.GetPayLinkService(*req)
if code == errorcode.Success {
c.HTML(http.StatusOK, "payTemplate.html", gin.H{
"payUrl": result,
})
} else {
c.String(http.StatusOK, errorcode.GetMsg(code, "")+message)
}
}

View File

@ -102,11 +102,11 @@ func AliCallback(c *gin.Context) {
c.String(http.StatusBadRequest, "%s", "fail")
return
}
if payChannelModel.ChannelType != common.PAY_CHANNEL_ALIPAY_WEB {
logger.Error(c, "AliCallback-回调数据解析支付配置错误,查询的数据不是当前渠道")
c.String(http.StatusBadRequest, "%s", "fail")
return
}
//if payChannelModel.ChannelType != common.PAY_CHANNEL_ALIPAY_WEB {
// logger.Error(c, "AliCallback-回调数据解析支付配置错误,查询的数据不是当前渠道")
// c.String(http.StatusBadRequest, "%s", "fail")
// return
//}
var aliConfig paymentService.AliPay
err := json.Unmarshal([]byte(payChannelModel.ExtJson), &aliConfig)

View File

@ -1,5 +1,9 @@
package front
import (
"PaymentCenter/app/models/ordersmodel"
)
type ApiCommonBody struct {
AppId int64 `json:"app_id" validate:"required"`
Timestamp int64 `json:"timestamp" validate:"required"`
@ -56,3 +60,40 @@ type ApiResponse struct {
Order interface{} `json:"order,omitempty"`
Url string `json:"url,omitempty"`
}
type PayChannelListRequest struct {
OrderId string `json:"id" form:"id" validate:"required"`
UserAgent string
}
type PayChannelListResponse struct {
PayName string `json:"pay_name"`
ChannelType int `json:"channel_type"`
PayChannelId string `json:"pay_channel_id"`
}
type PayReqsV2 struct {
AppId int64 `json:"app_id" validate:"required"`
Timestamp int64 `json:"timestamp" validate:"required"`
Amount int `json:"amount" validate:"required" label:"支付金额,单位分"`
OutTradeNo string `json:"out_trade_no" validate:"required" label:"外侧商户订单号"`
ReturnUrl string `json:"return_url" validate:"max=1024" label:"支付成功后跳转的地址"`
Desc string `json:"desc" validate:"max=100" label:"商品描述"`
ExtJson string `json:"ext_json" label:"扩展参数"`
OpenId string `json:"open_id" label:"用户openid"` // 微信公众号支付时需要,如果没有传,可走支付中心的授权获取openid
}
type PayReqsV2Response struct {
RelationOrder *ordersmodel.Orders
Order *ordersmodel.Orders
PayCode int
Url string
ThirdMsg string
}
type GetPayLinkRequest struct {
PayChannelId string `json:"pay_channel_id" form:"pay_channel_id"`
OrderId string `json:"no" form:"no"`
ClientIp string
}

View File

@ -8,15 +8,19 @@ import (
var FrontRequestMap = map[string]func() (validForm interface{}, isSaveLog bool){
common.FRONT_V1 + "/pay/url": func() (interface{}, bool) { return new(front.PayReqs), true },
common.FRONT_V2 + "/pay/url": func() (interface{}, bool) { return new(front.PayReqsV2), true },
common.FRONT_V1 + "/pay/refund": func() (interface{}, bool) { return new(front.RefundReqs), true },
common.FRONT_V1 + "/pay/query": func() (interface{}, bool) { return new(front.QueryReqs), false },
common.FRONT_V1 + "/pay/close": func() (interface{}, bool) { return new(front.CloseReqs), true },
common.FRONT_V1 + "/wx/payurl": func() (interface{}, bool) { return new(front.WxJsApiPayRequest), true },
common.PayPageAddress: func() (interface{}, bool) { return new(entities.IdRequest), true },
}
var FrontRequestMapBeforeDecrypt = map[string]func() interface{}{
common.FRONT_V1 + "/pay/url": func() interface{} { return new(front.RequestBody) },
common.FRONT_V2 + "/pay/url": func() interface{} { return new(front.RequestBody) },
common.FRONT_V1 + "/pay/refund": func() interface{} { return new(front.RequestBody) },
common.FRONT_V1 + "/pay/query": func() interface{} { return new(front.RequestBody) },
common.FRONT_V1 + "/pay/close": func() interface{} { return new(front.CloseReqs) },
@ -25,5 +29,8 @@ var FrontRequestMapBeforeDecrypt = map[string]func() interface{}{
common.FRONT_V1 + "/wx/getWxAuth": func() interface{} { return new(front.GetWxAuthRequest) },
common.FRONT_V1 + "/wx/getCode": func() interface{} { return new(front.GetWxAuthRequest) },
common.FRONT_V1 + "/payPage/list": func() interface{} { return new(front.PayChannelListRequest) },
common.FRONT_V1 + "/payPage/submit": func() interface{} { return new(front.GetPayLinkRequest) },
common.WXCodeRedirectAddress: func() interface{} { return new(front.WxJsApiPayRequest) },
}

View File

@ -79,6 +79,31 @@ func RegisterRoute(router *gin.Engine) {
// 微信jsapi支付链接
router.GET(common.WXCodeRedirectAddress, middlewares.ValidateRequest(), front.WxJsApiPay)
v2 := router.Group(common.FRONT_V2)
{
pay := v2.Group("/pay", middlewares.ValidatePayRequest())
// V2下单,存订单信息,返回收银台地址
pay.POST("/url", front.PayUrlV2)
}
//收银台
{
// 收银台地址
router.GET(common.PayPageAddress, front.PayPage)
payPage := router.Group(common.PayPageAddress, middlewares.ValidateRequest())
// 收银台 支付渠道列表
payPage.POST("/list", front.PayChannelList)
// 获取付款链接
payPage.GET("/submit", front.GetPayLink)
//router.GET(common.PayPageAddress, middlewares.ValidateRequest(), front.PayPage)
//// 收银台 支付渠道列表
//router.POST(common.FRONT_V1+"/payPage/list", middlewares.ValidateRequest(), front.PayChannelList)
//// 获取付款链接
//v1.POST("/payPage/link", middlewares.ValidateRequest(), front.GetPayLink)
}
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}

View File

@ -137,7 +137,7 @@ func OrderFindOne(order *ordersmodel.Orders, conn builder.Cond, col ...string) (
if err == sql.ErrNoRows {
return nil, errorcode.OrdersNotFound
}
return nil, errorcode.SystemError
return nil, handErr(err)
}
return orderInfo, errorcode.Success
}

103
app/services/pay_page.go Normal file
View File

@ -0,0 +1,103 @@
package services
import (
"PaymentCenter/app/constants/common"
"PaymentCenter/app/constants/errorcode"
"PaymentCenter/app/http/entities"
"PaymentCenter/app/http/entities/backend"
"PaymentCenter/app/http/entities/front"
"PaymentCenter/app/models/ordersmodel"
"PaymentCenter/app/models/paychannelmodel"
"PaymentCenter/app/utils"
"strconv"
"time"
"xorm.io/builder"
)
// 收银台支付渠道列表
func PayPageChannelList(reqParam front.PayChannelListRequest) (resultPayChannelList []paychannelmodel.PayChannel, code int) {
orderId, err := strconv.ParseInt(reqParam.OrderId, 10, 64)
if err != nil {
code = errorcode.ParamError
return
}
// 订单检查
orderInfo := &ordersmodel.Orders{Id: orderId}
orderInfo, code = OrderFindOne(orderInfo, builder.Eq{"id": orderId})
if code != errorcode.Success {
return
}
if !(orderInfo.Status == common.ORDER_STATUS_PAYING || orderInfo.Status == common.ORDER_STATUS_WAITPAY) {
code = errorcode.OrderStatusErr
return
}
// 商户拥有的支付渠道检查
req := backend.PayChannelListRequest{
MerchantId: orderInfo.MerchantId,
PageRequest: entities.PageRequest{},
}
payList := make([]paychannelmodel.PayChannel, 0)
payList, _, code = PayChannelList(req)
if code != errorcode.Success {
return
}
if len(payList) == 0 {
code = errorcode.PayChannelNotFound
return
}
merchantPayChannelMap := make(map[int]paychannelmodel.PayChannel, 0)
for _, pay := range payList {
if !pay.ExpireTime.IsZero() && pay.ExpireTime.Unix() < time.Now().Unix() {
continue
}
merchantPayChannelMap[pay.ChannelType] = pay
}
// 客户端环境检查
openType := ClientEnvCheck(reqParam.UserAgent)
if channels, ok := common.OpenInPayChannelMap[openType]; !ok {
code = errorcode.ClientEnvErr
return
} else {
for _, channel := range channels {
if _, ok = merchantPayChannelMap[channel]; ok {
resultPayChannelList = append(resultPayChannelList, merchantPayChannelMap[channel])
}
}
}
return resultPayChannelList, code
}
// 客户端环境检查
func ClientEnvCheck(ua string) int {
if utils.IsWeChatClient(ua) {
return common.OpenInWeChat
} else if utils.IsPC(ua) {
return common.OpenInWindows
} else if utils.IsMobile(ua) {
return common.OpenInMobile
}
return common.OpenInUnknown
}
// 订单是否是支付状态
func OrderStatusCheck(order ordersmodel.Orders) (code int) {
switch order.Status {
case common.ORDER_STATUS_CLOSE:
code = errorcode.OrderClosed
case common.ORDER_STATUS_PAYED:
code = errorcode.OrderPayed
case common.ORDER_STATUS_FAILED:
code = errorcode.OrderFailed
case common.ORDER_STATUS_WAITPAY, common.ORDER_STATUS_PAYING:
code = errorcode.Success
default:
code = errorcode.OrderStatusErr
}
return code
}

View File

@ -4,6 +4,7 @@ import (
"PaymentCenter/app/constants/common"
"PaymentCenter/app/data"
"PaymentCenter/app/models/orderrequestlogmodel"
"encoding/json"
"xorm.io/builder"
)
@ -32,7 +33,11 @@ func RequestLogUpdate(log *orderrequestlogmodel.OrderRequestLog) (logOut *orderr
}
func AddRequestLog(requestDataByte []byte, ip string, url string) (int64, int) {
tempMap := make(map[string]interface{})
_ = json.Unmarshal(requestDataByte, &tempMap)
requestLog, checkCode := RequestLogCreate(&orderrequestlogmodel.OrderRequestLog{
OutTradeNo: tempMap["out_trade_no"].(string),
IpAddress: ip,
MerchantRequest: string(requestDataByte),
URL: url,

View File

@ -128,7 +128,7 @@ func (w *Pay) PayUrl() (url string) {
// 微信公众号支付,返回的是用户授权链接
case common.PAY_CHANNEL_WECHAT_JSAPI:
// 获取授权链接
res.Result, res.Code = w.GetWxAuthUrl(w.PayParam.Channel.AppId, w.Order.Id)
res.Result, res.Code = GetWxAuthUrl(w.PayParam.Channel.AppId, w.Order.Id)
case common.PAY_CHANNEL_ALIPAY_WEB:
// 支付宝h5
res = paymentService.PaymentService(*w.ctx, *thirdPay)
@ -233,7 +233,7 @@ func (w *Pay) CloseOrder() {
}
// appid:微信的公众号appid
func (w *Pay) GetWxAuthUrl(appid string, orderId int64) (targetUrl string, code int) {
func GetWxAuthUrl(appid string, orderId int64) (targetUrl string, code int) {
var (
// 重定向地址
redirectUri = config.GetConf().PayService.Host + common.WXCodeRedirectAddress

View File

@ -11,6 +11,7 @@ import (
var PayWayList = map[int]func(commonPayInfo *paymentService.PayOrderRequest, channel *paychannelmodel.PayChannel) error{
common.PAY_CHANNEL_WECHAT_H5: WechatH5,
common.PAY_CHANNEL_ALIPAY_WEB: AlipayWeb,
common.PAY_CHANNEL_ALIPAY_PC: AlipayWeb,
common.PAY_CHANNEL_WECHAT_JSAPI: WechatJSAPI,
}

View File

@ -0,0 +1,212 @@
package thirdpay
import (
"PaymentCenter/app/constants/common"
"PaymentCenter/app/constants/errorcode"
"PaymentCenter/app/http/entities/backend"
"PaymentCenter/app/http/entities/front"
"PaymentCenter/app/models/orderrequestlogmodel"
"PaymentCenter/app/models/ordersmodel"
"PaymentCenter/app/models/paychannelmodel"
"PaymentCenter/app/services"
"PaymentCenter/app/services/thirdpay/do"
"PaymentCenter/app/third/paymentService"
"PaymentCenter/app/third/paymentService/payCommon"
"PaymentCenter/app/utils"
"PaymentCenter/config"
"context"
"encoding/json"
"fmt"
"strconv"
"time"
"xorm.io/builder"
)
// 收银台获取付款链接
func GetPayLinkService(req front.GetPayLinkRequest) (result, message string, code int) {
var (
payChannel = &paychannelmodel.PayChannel{}
order = &ordersmodel.Orders{}
orderPayRequest front.PayReqsV2
)
// 支付方式校验
payChannel, code = services.PayChannelFindOne(payChannel, builder.Eq{"id": req.PayChannelId})
if code != errorcode.Success {
return
}
if !payChannel.ExpireTime.IsZero() && payChannel.ExpireTime.Unix() < time.Now().Unix() {
code = errorcode.PayChannelExpired
return
}
// 订单校验
order, code = services.OrderFindOne(order, builder.Eq{"id": req.OrderId})
if code != errorcode.Success {
return
}
// 订单状态校验
code = services.OrderStatusCheck(*order)
if code != errorcode.Success {
return
}
// todo 订单的支付方式是否一致
if order.PayChannelId != payChannel.Id {
order.PayChannelId = payChannel.Id
code = services.OrderUpdate(order)
if code != errorcode.Success {
return
}
}
// 获取订单的请求参数
orderRequestLog := orderrequestlogmodel.OrderRequestLog{
OutTradeNo: order.OutTradeNo,
AppId: order.AppId,
}
has, err := services.OrderRequestLogs(&orderRequestLog)
if err != nil {
code = handErr(err)
return
}
if !has {
code = errorcode.OrderPayRequestLogNotExist
return
}
err = json.Unmarshal([]byte(orderRequestLog.MerchantRequest), &orderPayRequest)
if err != nil {
code = handErr(err)
return
}
// 支付方式需要用户授权
if payChannel.ChannelType == common.PAY_CHANNEL_WECHAT_JSAPI && orderPayRequest.OpenId == "" {
result, code = do.GetWxAuthUrl(payChannel.AppId, order.Id)
return
}
// 请求第三方支付获取付款链接
payParam := paymentService.PayOrderRequest{
PayChannelId: payChannel.Id,
OrderId: order.Id,
ChannelType: payChannel.ChannelType,
Description: orderPayRequest.Desc,
Amount: orderPayRequest.Amount,
PayerClientIp: req.ClientIp,
ReturnUrl: orderPayRequest.ReturnUrl,
OpenId: orderPayRequest.OpenId,
}
// 判断是否是支持的支付渠道,找到对应的配置
var payFunc func(commonPayInfo *paymentService.PayOrderRequest, channel *paychannelmodel.PayChannel) error
var ok bool
if payFunc, ok = do.PayWayList[payChannel.ChannelType]; !ok {
code = errorcode.PayChannelNotBuild
return
}
err = payFunc(&payParam, payChannel)
if err != nil {
code = errorcode.PayChannelExtJsonError
return
}
res := paymentService.PaymentService(context.Background(), payParam)
// 下单失败
if res.Code != payCommon.PAY_SUCCESS_CODE {
code = errorcode.PrePayFail
message = res.ErrorMsg
return
}
// 更新订单状态
order.Status = common.ORDER_STATUS_PAYING
code = services.OrderUpdate(order, "status")
if code != errorcode.Success {
utils.Log(nil, "成功下单,更新订单状态失败", order)
}
result = res.Result
code = res.Code
return
}
// 预支付
type payUrl struct {
ClientIp string
param front.PayReqsV2
app *services.AppCheck
result front.PayReqsV2Response
}
func NewPayUrl(param front.PayReqsV2) *payUrl {
return &payUrl{
param: param,
}
}
func (this *payUrl) WithApp(app *services.AppCheck) *payUrl {
this.app = app
return this
}
func (this *payUrl) WithClientIp(ip string) *payUrl {
this.ClientIp = ip
return this
}
func (this *payUrl) saveOrder() {
order := &ordersmodel.Orders{
MerchantId: this.app.App.MerchantId,
//PayChannelId: w.PayParam.Channel.Id,
AppId: this.app.App.Id,
OutTradeNo: this.param.OutTradeNo,
OrderType: common.ORDER_TYPE_PAY,
Amount: this.param.Amount,
ExtJson: this.param.ExtJson,
Desc: this.param.Desc,
Status: common.ORDER_STATUS_WAITPAY,
}
this.result.Order, this.result.PayCode = services.OrderCreate(order)
}
func (this *payUrl) PayUrlV2Service() (result front.PayReqsV2Response, code int) {
var (
channelList []paychannelmodel.PayChannel
order = new(ordersmodel.Orders)
)
// redis 分布式锁 订单号幂等
key := utils.GetRealKey("payUrl:" + this.param.OutTradeNo)
lockValue := fmt.Sprintf("%d", time.Now().UnixNano())
ok, err := utils.AcquireLock(key, lockValue, time.Second*time.Duration(3))
if ok {
defer utils.ReleaseLock(key, lockValue)
} else {
if err != nil {
utils.Log(nil, "", "PayUrlV2Service,获取分布式锁失败", fmt.Sprintf("错误原因:%s", err.Error()))
}
return
}
// 商户是否有配置支付渠道
channelList, _, code = services.PayChannelList(backend.PayChannelListRequest{MerchantId: this.app.App.MerchantId})
if code != errorcode.Success {
return
}
if len(channelList) == 0 {
code = errorcode.PayChannelNotFound
return
}
// 订单是否存在
conn := builder.NewCond()
conn = conn.And(builder.Eq{"out_trade_no": this.param.OutTradeNo})
conn = conn.And(builder.Eq{"app_id": this.param.AppId})
order, code = services.OrderFindOne(order, conn)
if code == errorcode.Success {
// 订单存在
this.result.Order = order
this.result.PayCode = services.OrderStatusCheck(*order)
} else if code == errorcode.OrdersNotFound {
// 订单不存在
this.saveOrder()
} else {
return
}
code = this.result.PayCode
if code == errorcode.Success {
this.result.Url = config.GetConf().PayService.Host + common.PayPageAddress + "?no=" + strconv.FormatInt(this.result.Order.Id, 10)
}
return this.result, code
}

View File

@ -82,6 +82,69 @@ func ALiH5PayInfo(c context.Context, payOrderRequest PayOrderRequest) (string, e
return aliRsp, nil
}
// ALiPCPayInfo 支付宝 支付宝PC网站支付
func ALiPCPayInfo(c context.Context, payOrderRequest PayOrderRequest) (string, error) {
// 初始化支付宝客户端
aliClient, err := AliInitClient(payOrderRequest.Ali)
if err != nil {
return "", err
}
// 初始化 BodyMap
amount := float64(payOrderRequest.Amount) / 100.0
envConfig := config.GetConf()
bm := make(gopay.BodyMap)
bm.Set("out_trade_no", payOrderRequest.OrderId).
Set("total_amount", amount).
Set("subject", payOrderRequest.Description).
Set("qr_pay_mode", 1).
Set("notify_url", fmt.Sprintf(envConfig.PayService.Host+payCommon.ALI_NOTIFY_URL_TEST+"%d", payOrderRequest.PayChannelId))
if payOrderRequest.ReturnUrl != "" {
bm.Set("return_url", payOrderRequest.ReturnUrl)
}
aliRsp, err := aliClient.TradePagePay(c, bm)
if err != nil {
logger.Error(c, "ALiH5PayInfo 发生错误", fmt.Sprintf("错误信息:%s", err.Error()))
if bizErr, ok := alipay.IsBizError(err); ok {
return "", bizErr
}
return "", err
}
return aliRsp, nil
}
// ALiQrCodeTradePrecreate 支付宝 订单码支付
func ALiQrCodeTradePrecreate(c context.Context, payOrderRequest PayOrderRequest) (string, error) {
// 初始化支付宝客户端
aliClient, err := AliInitClient(payOrderRequest.Ali)
if err != nil {
return "", err
}
// 初始化 BodyMap
amount := float64(payOrderRequest.Amount) / 100.0
envConfig := config.GetConf()
bm := make(gopay.BodyMap)
bm.Set("out_trade_no", payOrderRequest.OrderId).
Set("total_amount", amount).
Set("subject", payOrderRequest.Description).
Set("notify_url", fmt.Sprintf(envConfig.PayService.Host+payCommon.ALI_NOTIFY_URL_TEST+"%d", payOrderRequest.PayChannelId))
if payOrderRequest.ReturnUrl != "" {
bm.Set("return_url", payOrderRequest.ReturnUrl)
}
aliRsp, err := aliClient.TradePrecreate(c, bm)
if err != nil {
logger.Error(c, "ALiH5PayInfo 发生错误", fmt.Sprintf("错误信息:%s", err.Error()))
if bizErr, ok := alipay.IsBizError(err); ok {
return "", bizErr
}
return "", err
}
return aliRsp.Response.QrCode, nil
}
// ALiCallBack 支付宝支付回调
func ALiCallBack(notifyReq gopay.BodyMap, aliConfig AliPay) error {
var orderStatus int

View File

@ -4,7 +4,7 @@ import "PaymentCenter/app/constants/common"
const (
// 支付渠道枚举,1微信JSAPI2微信H53微信app4微信Native5微信小程序6支付宝网页&移动应用7支付宝小程序8支付宝JSAPI
// 支付渠道枚举,1微信JSAPI2微信H53微信app4微信Native5微信小程序6支付宝网页&移动应用7支付宝小程序8支付宝JSAPI,9支付宝电脑网页,10支付宝订单码扫码支付
PAY_CHANNEL_UNKNOWN = 0
PAY_CHANNEL_WECHAT_JSAPI = 1
PAY_CHANNEL_WECHAT_H5 = 2
@ -14,6 +14,8 @@ const (
PAY_CHANNEL_ALIPAY_WEB = 6
PAY_CHANNEL_ALIPAY_MINI = 7
PAY_CHANNEL_ALIPAY_JSAPI = 8
PAY_CHANNEL_ALIPAY_PC = 9
PAY_CHANNEL_ALIPAY_QrCode = 10
PAY_SUCCESS_CODE = 200 // 支付响应成功
PAY_ERROR_CODE = 500 // 支付响应失败

View File

@ -63,6 +63,9 @@ func PaymentService(c context.Context, payOrderRequest PayOrderRequest) PayOrder
case payCommon.PAY_CHANNEL_WECHAT_JSAPI:
// 微信JSAPI支付
info, err = WxJsApiPayInfo(c, payOrderRequest)
case payCommon.PAY_CHANNEL_ALIPAY_PC:
// 支付宝PC支付
info, err = ALiPCPayInfo(c, payOrderRequest)
default:
payOrderResponse = PayOrderResponse{

29
app/utils/useragent.go Normal file
View File

@ -0,0 +1,29 @@
// Copyright (c) [2022] [巴拉迪维 BaratSemet]
// [ohUrlShortener] is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
// http://license.coscl.org.cn/MulanPSL2
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
package utils
import (
"strings"
)
func IsWeChatClient(ua string) bool {
// 检查 User-Agent 中是否包含微信客户端的标识
return strings.Contains(ua, "MicroMessenger")
}
func IsMobile(ua string) bool {
// 检查 User-Agent 中是否包含移动设备的标识
return strings.Contains(ua, "Mobile") && !IsWeChatClient(ua)
}
func IsPC(userAgent string) bool {
// 检查 User-Agent 中是否包含 PC 浏览器的标识
t := strings.Contains(userAgent, "Windows NT") || strings.Contains(userAgent, "Macintosh")
return t
}

View File

@ -0,0 +1,8 @@
package utils
import "testing"
func TestIsPC(t *testing.T) {
userAgent := "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
t.Log(IsPC(userAgent))
}

View File

@ -483,3 +483,28 @@ func KeyExistsOrSet(key string, expire int) bool {
}
return false
}
// AcquireLock 尝试获取分布式锁,localValue用于检测是否是同一个客户端持有的锁锁
func AcquireLock(key, lockValue string, expired time.Duration) (bool, error) {
ctx := context.Background()
rd := redis.GetRedis()
result := rd.SetNX(ctx, key, lockValue, expired)
return result.Val(), result.Err()
}
// 释放锁
func ReleaseLock(key string, lockValue string) (bool, error) {
rd := redis.GetRedis()
result := rd.Eval(context.Background(), "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", []string{key}, lockValue)
return result.Val() == int64(1), result.Err()
}
// 判断切片是否包含指定
func Contains[T comparable](s T, slice []T) bool {
for _, v := range slice {
if s == v {
return true
}
}
return false
}

View File

@ -28,6 +28,9 @@ func Bootstrap(conf *config.Config) (err error) {
err = db.Pr.Register(db.SingletonMain, conf.Db)
//cacher := caches.NewLRUCacher2(caches.NewMemoryStore(), time.Hour, 1000)
//db.GetDb().SetDefaultCacher(cacher)
if conf.Debug {
db.GetDb().ShowSQL(true)
}
if err != nil {
return

View File

@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>收银台页面</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
}
p {
color: #666;
}
ul {
list-style: none;
padding: 0;
}
li {
margin: 10px 0;
}
</style>
</head>
{{if eq .code 200 }}
<body>
<!-- 页面内容 -->
{{/*<h1>收银台页面</h1>*/}}
<p>订单号:{{ .id }}</p>
<p>订单金额:{{ .amount }} 元</p>
<ul id="payment-list"></ul>
</body>
<script>
// 从 URL 中提取参数
function getQueryParam(param) {
const queryString = window.location.search.substring(1);
const params = queryString.split('&');
for (let i = 0; i < params.length; i++) {
const [key, value] = params[i].split('=');
if (key === param) {
return decodeURIComponent(value);
}
}
return null;
}
// 获取支付方式列表
function fetchPaymentMethods() {
const id = getQueryParam('no');
console.log('Order no:', id);
if (id) {
fetch(`/pay/front/api/v1/payPage/list?id=${id}`, {
method: 'POST',
})
.then(async response => {
if (response.ok) {
const data = await response.json()
console.log(data)
if (data.code !== 200) {
throw new Error('无效');
} else {
return data;
}
} else {
throw new Error('无效');
}
})
.then(data => {
console.log('Received payment methods:', data);
// 处理返回的数据,例如渲染支付方式列表
renderPaymentMethods(data.data);
})
.catch(error => {
console.error('data', data);
});
} else {
console.error('Order no not found in URL');
}
}
// 渲染支付方式列表
function renderPaymentMethods(paymentMethods) {
const paymentList = document.getElementById('payment-list');
paymentList.innerHTML = '';
paymentMethods.forEach(method => {
const listItem = document.createElement('li');
const radioInput = document.createElement('input');
radioInput.type = 'radio';
radioInput.name = 'paymentMethod';
radioInput.value = method.pay_channel_id;
listItem.appendChild(radioInput);
listItem.appendChild(document.createTextNode(method.pay_name));
paymentList.appendChild(listItem);
});
// 添加提交按钮
const submitButton = document.createElement('button');
submitButton.type = 'button';
submitButton.textContent = '提交';
submitButton.onclick = function () {
const no = getQueryParam('no');
const selectedMethod = document.querySelector('input[name="paymentMethod"]:checked');
if (selectedMethod) {
window.location.href = `/pay/front/api/v1/payPage/submit?pay_channel_id=${selectedMethod.value}&no=${no}`;
} else {
alert('请选择支付方式');
}
};
paymentList.appendChild(submitButton);
}
// 在页面加载时调用 fetchPaymentMethods 函数
window.onload = fetchPaymentMethods;
</script>
{{ else}}
<body>
<p>{{.message}}</p>
</body>
{{end}}
</html>

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
<script>
// 网页默认跳转url
window.location.href = "{{.payUrl}}";
</script>
</html>