Files
kami_shop/internal/controllers/page_controller.go
danial 1a75269a61 feat(payment):重构微信支付页面并优化支付流程
- 重新设计支付页面UI,采用毛玻璃效果和动态背景- 更新支付接口调用方式,从表单提交改为JSON请求
-优化支付签名验证逻辑,统一错误处理方式
- 修改京东支付接口参数和返回结构体
- 移除OpenTelemetry追踪相关代码
- 添加支付倒计时功能和本地存储支持
- 优化支付按钮加载状态和交互反馈
- 调整HTML结构和CSS样式,提升用户体验
2025-10-11 21:56:09 +08:00

398 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package controllers
import (
"encoding/json"
"fmt"
"html/template"
"net/url"
"os"
"shop/internal/config"
"shop/internal/models"
"shop/internal/models/merchant_deploy"
"shop/internal/schema/request"
"shop/internal/schema/response"
"shop/internal/service"
"shop/internal/traceRouter"
"shop/internal/utils"
"shop/internal/utils/client"
"slices"
"sort"
"strings"
"time"
"github.com/beego/beego/v2/server/web"
"github.com/mitchellh/mapstructure"
"github.com/panjf2000/ants/v2"
"go.uber.org/zap"
)
var p, _ = ants.NewPool(10000)
type HomeAction struct {
web.Controller
}
func (c *HomeAction) OrderConfirm() {
c.Data["amount"] = c.GetString("amount")
c.Data["orderId"] = c.GetString("orderId")
c.Data["returnUrl"] = c.GetString("returnUrl")
c.Data["payKey"] = c.GetString("payKey")
c.TplName = "order_confirm.html"
}
// ShowHome /*加载首页及数据*/
func (c *HomeAction) ShowHome() {
ctx, span := traceRouter.CreateSpan(c.Ctx.Request.Context(), "span", "首页")
defer span.End()
siteName, _ := web.AppConfig.String("siteName")
returnUrl := c.GetString("returnUrl")
productName := "支付功能(非商品消费)"
sign := c.GetString("sign")
if sign == "" {
flash := web.NewFlash()
flash.Error("交易签名不能为空")
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
return
}
m := models.OrderParams{}
err := m.Decrypt(ctx, sign)
if err != nil {
traceRouter.Logger.WithContext(ctx).Error("交易签名解密错误!", zap.String("sign", sign), zap.String("error", err.Error()))
flash := web.NewFlash()
flash.Error("交易签名错误,请检查签名是否正确!")
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
return
}
if m.PayKey == "" {
flash := web.NewFlash()
traceRouter.Logger.WithContext(ctx).Error("不存在paykey", zap.String("sign", sign))
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
return
}
merchantInfo := service.GetMerchantByPasskey(ctx, m.PayKey)
if merchantInfo.Id == 0 {
flash := web.NewFlash()
flash.Error("商户不存在,请联系客服")
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
return
}
c.Data["sign"] = sign
queryData := request.QueryOrder{
Appkey: m.PayKey,
OrderNo: m.OrderNo,
Timestamp: time.Now().Unix(),
}
queryData.Sign = utils.GetMD5SignMF(queryData.ToStrMap(), merchantInfo.MerchantSecret)
//判断订单状态
if resp, err2 := client.PostWithFormData(ctx, config.GetMerchantOrder(), map[string]string{}, queryData.ToMap()); err2 == nil {
respData := response.MerchantQueryResp{}
_ = json.Unmarshal([]byte(resp), &respData)
switch respData.Data.Status {
case "success":
str := "/ok.html" + "?orderId=" + respData.Data.OrderNo + "&amount=" + respData.Data.Amount +
"&returnUrl=" + returnUrl
c.Ctx.Output.Header("Location", str)
c.Redirect("/ok.html", 302)
return
case "fail":
str := "/error.html" + "?orderId=" + respData.Data.OrderNo + "&amount=" + respData.Data.Amount +
"&returnUrl=" + returnUrl + "&errorMsg=" + respData.Data.CardReturnData
c.Ctx.Output.Header("Location", str)
c.Redirect(str, 302)
case "wait":
str := "/order-confirm.html" + "?orderId=" + respData.Data.OrderNo + "&returnUrl=" + returnUrl +
"&payKey=" + m.PayKey + "&amount=" + respData.Data.Amount
c.Ctx.Output.Header("Location", str)
c.Redirect(str, 302)
}
}
resp, err := client.Get(ctx, config.GetMMValue(), map[string]string{
"productCode": m.ProductCode,
"showMMValue": fmt.Sprintf("%f", m.ShowMMValue),
"payKey": m.PayKey,
})
r, err := client.Unmarshal(resp)
if err != nil {
flash := web.NewFlash()
traceRouter.Logger.WithContext(ctx).Error("获取面额失败!", zap.Any("data", r.Data))
flash.Error("获取面额失败!")
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
}
if r.Code == -1 {
flash := web.NewFlash()
flash.Error("%s", r.Msg)
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
return
}
if r.Data == nil {
flash := web.NewFlash()
traceRouter.Logger.WithContext(ctx).Error("获取面额失败!", zap.Any("data", r.Data))
flash.Error("获取价格失败")
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
return
}
//解析页面详情数据
var profitMarginList []merchant_deploy.ProfitMargin
err = utils.MapToStruct(r.Data.([]interface{}), &profitMarginList)
if err != nil || len(profitMarginList) == 0 {
flash := web.NewFlash()
traceRouter.Logger.WithContext(ctx).Error("获取真实价格失败!", zap.Any("data", r.Data))
flash.Error("获取真实价格失败")
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
return
}
sort.SliceStable(profitMarginList, func(i, j int) bool {
return profitMarginList[i].Sort < profitMarginList[j].Sort
})
// 处理linkID
for i, margin := range profitMarginList {
if strings.Contains(string(margin.LinkID), "?") && !margin.IsLinkSingle {
// 分离出?后面的内容
linkIDList := strings.Split(string(margin.LinkID), "?")
profitMarginList[i].LinkID = template.URL(linkIDList[0] + "?" + url.QueryEscape(strings.Join(linkIDList[1:], "?")))
}
profitMarginList[i].Color = "--bs-info"
if strings.Contains(margin.PlatformLabel, "淘宝") || strings.Contains(margin.PlatformLabel, "点击跳转购买") {
profitMarginList[i].Color = "--bs-orange"
if !margin.IsLinkSingle {
profitMarginList[i].LinkID = template.URL("tbopen://" + fmt.Sprintf("m.taobao.com/tbopen/index.html?h5Url=https%%3A%%2F%%2Fh5.m.taobao.com%%2Fawp%%2Fcore%%2Fdetail.htm%%3Fid%%3D%s", margin.LinkID))
}
}
if strings.Contains(margin.PlatformLabel, "拼多多") {
profitMarginList[i].Color = "--bs-red"
if !margin.IsLinkSingle {
profitMarginList[i].LinkID = template.URL(fmt.Sprintf("pinduoduo://com.xunmeng.pinduoduo/https_detail.html?goods_id=%s", margin.LinkID))
}
}
if strings.Contains(margin.PlatformLabel, "京东") {
profitMarginList[i].Color = "--bs-red"
if !margin.IsLinkSingle {
profitMarginList[i].LinkID = template.URL("openapp.jdmobile://" + fmt.Sprintf("virtual?params={\"des\":\"m\",\"url\":\"https://item.m.jd.com/product/%v.html?ad_od=share&utm_source=androidapp&utm_medium=appshare&utm_campaign=t_335139774&utm_term=CopyURL\",\"category\":\"jump\"}", margin.LinkID))
}
}
if strings.Contains(margin.PlatformLabel, "抖音") {
profitMarginList[i].Color = "--bs-black"
if !margin.IsLinkSingle {
profitMarginList[i].LinkID = template.URL("snssdk1128://" + fmt.Sprintf("ec_goods_detail?promotion_id=%s", margin.LinkID))
}
}
}
requestOrder := request.CreatedOrder{
PayKey: m.PayKey,
OrderNo: m.OrderNo,
OrderPrice: profitMarginList[0].FactLabel,
OrderPeriod: 24,
NotifyUrl: m.NotifyUrl,
Sign: "",
ProductCode: m.ProductCode,
Timestamp: time.Now().Unix(),
}
requestOrder.Sign = utils.GetMD5SignMF(requestOrder.ToStrMap(), merchantInfo.MerchantSecret)
//创建订单
response, err := client.Post(ctx, service.CreateOrderHost, map[string]string{}, requestOrder.ToMap())
if err != nil {
flash := web.NewFlash()
flash.Error("订单创建失败")
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
return
}
res, err := client.Unmarshal(response)
if err != nil {
flash := web.NewFlash()
flash.Error("订单创建失败")
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
return
}
if res.Code != 0 {
flash := web.NewFlash()
flash.Error("创建订单失败:%s", res.Msg)
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
}
//接口类型转map类型
createOrderParams := models.CreateOrderResponse{}
err = mapstructure.Decode(res.Data, &createOrderParams)
traceRouter.Logger.WithContext(ctx).Info("创建订单返回参数", zap.Any("data", createOrderParams))
if err != nil {
flash := web.NewFlash()
flash.Error("订单创建失败")
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
return
}
c.Data["dynamicUrl"] = config.GetPortalUrl()
c.Data["siteName"] = siteName
c.Data["pname"] = productName
c.Data["orderNo"] = m.OrderNo
c.Data["returnUrl"] = returnUrl
c.Data["mmValue"] = profitMarginList[0].FactLabel
c.Data["showMMValue"] = profitMarginList[0].ShowLabel
c.Data["notifyUrl"] = m.NotifyUrl
c.Data["productCode"] = m.ProductCode
c.Data["profitMarginList"] = profitMarginList
if slices.Contains([]string{"index-aolai.html", "百礼卡"}, createOrderParams.PaymentName) {
for i := range profitMarginList {
profitMarginList[i].Color = "--bs-orange"
}
}
//获取views目录下所有文件
views, _ := os.ReadDir("views")
for _, view := range views {
if view.IsDir() {
continue
}
if view.Name() == createOrderParams.PaymentName {
c.TplName = createOrderParams.PaymentName
return
}
}
c.Data["cardName"] = createOrderParams.PaymentName
c.TplName = "index-template.html"
}
// Notify /*通知*/
func (c *HomeAction) Notify() {
c.Ctx.WriteString("success")
}
// ShowOK /*加载首页及数据*/
func (c *HomeAction) ShowOK() {
//取值
c.Data["orderId"] = c.GetString("orderId")
c.Data["amount"] = c.GetString("amount")
c.Data["returnUrl"], _ = web.AppConfig.String("returnUrl")
c.TplName = "ok.html"
}
func (c *HomeAction) ErrorPage() {
c.Data["orderId"] = c.GetString("orderId")
c.Data["amount"] = c.GetString("amount")
c.Data["errorReason"] = c.GetString("errorMsg")
c.Data["returnUrl"], _ = web.AppConfig.String("returnUrl")
c.TplName = "error.html"
}
// QueryOrderStatus 查询订单状态
func (c *HomeAction) QueryOrderStatus() {
ctx, span := traceRouter.CreateSpan(c.Ctx.Request.Context(), "span", "查询订单状态")
defer span.End()
orderNo := c.GetString("orderNo")
payKey := c.GetString("payKey")
returnUrl := c.GetString("returnUrl")
if orderNo == "" || payKey == "" {
c.Data["json"] = map[string]interface{}{
"code": 400,
"msg": "订单编号或支付密钥不能为空",
}
c.ServeJSON()
return
}
merchantInfo := service.GetMerchantByPasskey(ctx, payKey)
if merchantInfo.Id == 0 {
c.Data["json"] = map[string]interface{}{
"code": 400,
"msg": "商户不存在",
}
c.ServeJSON()
return
}
queryData := request.QueryOrder{
Appkey: payKey,
OrderNo: orderNo,
Timestamp: time.Now().Unix(),
}
queryData.Sign = utils.GetMD5SignMF(queryData.ToStrMap(), merchantInfo.MerchantSecret)
// 判断订单状态
if resp, err2 := client.PostWithFormData(ctx, config.GetMerchantOrder(), map[string]string{}, queryData.ToMap()); err2 == nil {
respData := response.MerchantQueryResp{}
_ = json.Unmarshal([]byte(resp), &respData)
switch respData.Data.Status {
case "success":
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "支付成功",
"data": map[string]interface{}{
"status": "success",
"orderNo": respData.Data.OrderNo,
"amount": respData.Data.Amount,
"returnUrl": returnUrl,
},
}
case "fail":
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "支付失败",
"data": map[string]interface{}{
"status": "fail",
"orderNo": respData.Data.OrderNo,
"amount": respData.Data.Amount,
"errorReason": respData.Data.CardReturnData,
"returnUrl": returnUrl,
},
}
case "wait":
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "等待支付",
"data": map[string]interface{}{
"status": "wait",
},
}
default:
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "处理中",
"data": map[string]interface{}{
"status": "processing",
},
}
}
} else {
c.Data["json"] = map[string]interface{}{
"code": 500,
"msg": "查询订单状态失败",
}
}
c.ServeJSON()
}