From 14f63a203d719a27d910f71cd6306661faa1864d Mon Sep 17 00:00:00 2001 From: RockYang Date: Wed, 20 Mar 2024 16:14:02 +0800 Subject: [PATCH] feat: payment for mobile pages is ready --- api/core/types/config.go | 3 + api/handler/payment_handler.go | 100 +++++++++++++------------- api/main.go | 1 + api/service/payment/payjs_service.go | 39 ++++++++++- web/src/views/mobile/Profile.vue | 101 +++++++++++++++++++-------- 5 files changed, 160 insertions(+), 84 deletions(-) diff --git a/api/core/types/config.go b/api/core/types/config.go index cbf3f9d5..17cde49a 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -81,6 +81,7 @@ type AlipayConfig struct { AlipayPublicKey string // 支付宝公钥文件路径 RootCert string // Root 秘钥路径 NotifyURL string // 异步通知回调 + ReturnURL string // 支付成功返回地址 } type HuPiPayConfig struct { //虎皮椒第四方支付配置 @@ -90,6 +91,7 @@ type HuPiPayConfig struct { //虎皮椒第四方支付配置 AppSecret string // app 密钥 ApiURL string // 支付网关 NotifyURL string // 异步通知回调 + ReturnURL string // 支付成功返回地址 } // JPayConfig PayJs 支付配置 @@ -100,6 +102,7 @@ type JPayConfig struct { PrivateKey string // 私钥 ApiURL string // API 网关 NotifyURL string // 异步回调地址 + ReturnURL string // 支付成功返回地址 } type XXLConfig struct { // XXL 任务调度配置 diff --git a/api/handler/payment_handler.go b/api/handler/payment_handler.go index 278d2177..f17b7411 100644 --- a/api/handler/payment_handler.go +++ b/api/handler/payment_handler.go @@ -279,7 +279,6 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { // Mobile 移动端支付 func (h *PaymentHandler) Mobile(c *gin.Context) { - var data struct { PayWay string `json:"pay_way"` // 支付方式 ProductId uint `json:"product_id"` @@ -309,18 +308,61 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { return } + amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64() var payWay string - var notifyURL string + var notifyURL, returnURL string + var payURL string switch data.PayWay { case "hupi": payWay = PayWayXunHu notifyURL = h.App.Config.HuPiPayConfig.NotifyURL + returnURL = h.App.Config.HuPiPayConfig.ReturnURL + params := payment.HuPiPayReq{ + Version: "1.1", + TradeOrderId: orderNo, + TotalFee: fmt.Sprintf("%f", amount), + Title: product.Name, + NotifyURL: notifyURL, + ReturnURL: returnURL, + CallbackURL: returnURL, + WapName: "极客学长", + } + r, err := h.huPiPayService.Pay(params) + if err != nil { + logger.Error("error with generating Pay URL: ", err.Error()) + resp.ERROR(c, "error with generating Pay URL: "+err.Error()) + return + } + payURL = r.URL case "payjs": payWay = PayWayJs notifyURL = h.App.Config.JPayConfig.NotifyURL - default: + returnURL = h.App.Config.JPayConfig.ReturnURL + params := payment.JPayReq{ + TotalFee: int(math.Ceil(amount * 100)), + OutTradeNo: orderNo, + Subject: product.Name, + NotifyURL: notifyURL, + ReturnURL: returnURL, + } + r := h.js.PayH5(params) + if !r.IsOK() { + resp.ERROR(c, "error with generating Pay URL: "+r.ReturnMsg) + return + } + payURL = r.H5URL + case "alipay": payWay = PayWayAlipay notifyURL = h.App.Config.AlipayConfig.NotifyURL + returnURL = h.App.Config.AlipayConfig.ReturnURL + payURL, err = h.alipayService.PayUrlMobile(orderNo, notifyURL, returnURL, fmt.Sprintf("%.2f", amount), product.Name) + if err != nil { + resp.ERROR(c, "error with generating Pay URL: "+err.Error()) + return + } + default: + resp.ERROR(c, "Unsupported pay way: "+data.PayWay) + return } // 创建订单 remark := types.OrderRemark{ @@ -331,7 +373,6 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { Discount: product.Discount, } - amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64() order := model.Order{ UserId: user.Id, Username: user.Username, @@ -349,54 +390,7 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { return } - // PayJs 单独处理,只能用官方生成的二维码 - if data.PayWay == "payjs" { - params := payment.JPayReq{ - TotalFee: int(math.Ceil(order.Amount * 100)), - OutTradeNo: order.OrderNo, - Subject: product.Name, - } - r := h.js.Pay(params) - if r.IsOK() { - resp.SUCCESS(c, gin.H{"order_no": order.OrderNo, "image": r.Qrcode}) - return - } else { - resp.ERROR(c, "error with generating payment qrcode: "+r.ReturnMsg) - return - } - } - - var logo string - if data.PayWay == "alipay" { - logo = "res/img/alipay.jpg" - } else if data.PayWay == "hupi" { - if h.App.Config.HuPiPayConfig.Name == "wechat" { - logo = "res/img/wechat-pay.jpg" - } else { - logo = "res/img/alipay.jpg" - } - } - - file, err := h.fs.Open(logo) - if err != nil { - resp.ERROR(c, "error with open qrcode log file: "+err.Error()) - return - } - - parse, err := url.Parse(notifyURL) - if err != nil { - resp.ERROR(c, err.Error()) - return - } - - imageURL := fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s", parse.Scheme, parse.Host, orderNo, data.PayWay) - imgData, err := utils.GenQrcode(imageURL, 400, file) - if err != nil { - resp.ERROR(c, err.Error()) - return - } - imgDataBase64 := base64.StdEncoding.EncodeToString(imgData) - resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL}) + resp.SUCCESS(c, payURL) } // 异步通知回调公共逻辑 @@ -444,7 +438,7 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error { power = remark.Power } - } else { // 非 VIP 用户 + } else { // 非 VIP 用户 if remark.Days > 0 { // vip 套餐:days > 0, power == 0 user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix() user.Power += h.App.SysConfig.VipMonthPower diff --git a/api/main.go b/api/main.go index 19d549e4..69089814 100644 --- a/api/main.go +++ b/api/main.go @@ -324,6 +324,7 @@ func main() { group.GET("payWays", h.GetPayWays) group.POST("query", h.OrderQuery) group.POST("qrcode", h.PayQrcode) + group.POST("mobile", h.Mobile) group.POST("alipay/notify", h.AlipayNotify) group.POST("hupipay/notify", h.HuPiPayNotify) group.POST("payjs/notify", h.PayJsNotify) diff --git a/api/service/payment/payjs_service.go b/api/service/payment/payjs_service.go index 98748780..72af9099 100644 --- a/api/service/payment/payjs_service.go +++ b/api/service/payment/payjs_service.go @@ -29,16 +29,18 @@ type JPayReq struct { OutTradeNo string `json:"out_trade_no"` Subject string `json:"body"` NotifyURL string `json:"notify_url"` + ReturnURL string `json:"callback_url"` } type JPayReps struct { - CodeUrl string `json:"code_url"` OutTradeNo string `json:"out_trade_no"` OrderId string `json:"payjs_order_id"` - Qrcode string `json:"qrcode"` ReturnCode int `json:"return_code"` ReturnMsg string `json:"return_msg"` Sign string `json:"Sign"` TotalFee string `json:"total_fee"` + CodeUrl string `json:"code_url,omitempty"` + Qrcode string `json:"qrcode,omitempty"` + H5URL string `json:"h5_url,omitempty"` } func (r JPayReps) IsOK() bool { @@ -78,6 +80,39 @@ func (js *PayJS) Pay(param JPayReq) JPayReps { return data } +func (js *PayJS) PayH5(param JPayReq) JPayReps { + param.NotifyURL = js.config.NotifyURL + var p = url.Values{} + encode := utils.JsonEncode(param) + m := make(map[string]interface{}) + _ = utils.JsonDecode(encode, &m) + for k, v := range m { + p.Add(k, fmt.Sprintf("%v", v)) + } + p.Add("mchid", js.config.AppId) + + p.Add("sign", js.sign(p)) + + cli := http.Client{} + apiURL := fmt.Sprintf("%s/api/h5", js.config.ApiURL) + r, err := cli.PostForm(apiURL, p) + if err != nil { + return JPayReps{ReturnMsg: err.Error()} + } + defer r.Body.Close() + bs, err := io.ReadAll(r.Body) + if err != nil { + return JPayReps{ReturnMsg: err.Error()} + } + + var data JPayReps + err = utils.JsonDecode(string(bs), &data) + if err != nil { + return JPayReps{ReturnMsg: err.Error()} + } + return data +} + func (js *PayJS) sign(params url.Values) string { params.Del(`sign`) var keys = make([]string, 0, 0) diff --git a/web/src/views/mobile/Profile.vue b/web/src/views/mobile/Profile.vue index 3925ba10..6bbf4091 100644 --- a/web/src/views/mobile/Profile.vue +++ b/web/src/views/mobile/Profile.vue @@ -21,23 +21,18 @@ - + - + - - - @@ -50,7 +45,19 @@

{{ item.name }} - 立即购买 +
+ + 支付宝 + + + 微信 + 支付宝 + + + + 微信 + +

@@ -111,7 +118,10 @@ import {onMounted, ref} from "vue"; import {showFailToast, showNotify, showSuccessToast} from "vant"; import {httpGet, httpPost} from "@/utils/http"; import Compressor from 'compressorjs'; +import {dateFormat} from "@/utils/libs"; import {ElMessage} from "element-plus"; +import {checkSession} from "@/action/session"; +import {useRouter} from "vue-router"; const title = ref('用户设置') const form = ref({ @@ -132,29 +142,45 @@ const fileList = ref([ const products = ref([]) const vipMonthCalls = ref(0) const vipMonthImgCalls = ref(0) +const payWays = ref({}) +const router = useRouter() +const loginUser = ref(null) onMounted(() => { - httpGet('/api/user/profile').then(res => { - form.value = res.data - fileList.value[0].url = form.value.avatar - }).catch((e) => { - console.log(e.message) - showFailToast('获取用户信息失败') - }); + checkSession().then(user => { + loginUser.value = user + httpGet('/api/user/profile').then(res => { + form.value = res.data + fileList.value[0].url = form.value.avatar + }).catch((e) => { + console.log(e.message) + showFailToast('获取用户信息失败') + }); - // 获取产品列表 - httpGet("/api/product/list").then((res) => { - products.value = res.data - }).catch(e => { - showFailToast("获取产品套餐失败:" + e.message) + // 获取产品列表 + httpGet("/api/product/list").then((res) => { + products.value = res.data + }).catch(e => { + showFailToast("获取产品套餐失败:" + e.message) + }) + + httpGet("/api/config/get?key=system").then(res => { + vipMonthCalls.value = res.data['vip_month_calls'] + vipMonthImgCalls.value = res.data['vip_month_img_calls'] + }).catch(e => { + showFailToast("获取系统配置失败:" + e.message) + }) + + httpGet("/api/payment/payWays").then(res => { + payWays.value = res.data + }).catch(e => { + ElMessage.error("获取支付方式失败:" + e.message) + }) + + }).catch(() => { + router.push("/login") }) - httpGet("/api/config/get?key=system").then(res => { - vipMonthCalls.value = res.data['vip_month_calls'] - vipMonthImgCalls.value = res.data['vip_month_img_calls'] - }).catch(e => { - showFailToast("获取系统配置失败:" + e.message) - }) }) const afterRead = (file) => { @@ -222,6 +248,18 @@ const updatePass = () => { showPasswordDialog.value = false }) } + +const pay = (payWay, item) => { + httpPost("/api/payment/mobile", { + pay_way: payWay, + product_id: item.id, + user_id: loginUser.value.id + }).then(res => { + location.href = res.data + }).catch(e => { + showFailToast("生成支付订单失败:" + e.message) + }) +}