feat(payment):重构微信支付页面并优化支付流程

- 重新设计支付页面UI,采用毛玻璃效果和动态背景- 更新支付接口调用方式,从表单提交改为JSON请求
-优化支付签名验证逻辑,统一错误处理方式
- 修改京东支付接口参数和返回结构体
- 移除OpenTelemetry追踪相关代码
- 添加支付倒计时功能和本地存储支持
- 优化支付按钮加载状态和交互反馈
- 调整HTML结构和CSS样式,提升用户体验
This commit is contained in:
danial
2025-10-11 21:56:09 +08:00
parent fde495551d
commit 1a75269a61
6 changed files with 503 additions and 185 deletions

58
CODEBUDDY.md Normal file
View File

@@ -0,0 +1,58 @@
kami_shop
Development commands
- Build (Linux amd64): ./build.sh
- Run (dev): go run main.go
- Tests: go test ./...
- Single test file: go test ./internal/service/scan_shop_test.go
- Verbose tests: go test -v ./...
- Docker build: docker build -t kami_shop . -f deploy/Dockerfile
- Docker Compose up: export VERSION=latest && docker-compose -f deploy/docker-compose.yaml up -d
Project architecture overview
- Runtime: Go 1.24, Beego v2
- Entry: main.go initializes OpenTelemetry/logging and starts Beego
- Config: conf/app.local.conf (dev), conf/app.conf (prod); MySQL, Redis, ports, gateway URLs, secrets
- HTTP routing: internal/routers/router.go maps routes to controllers
- Controllers: internal/controllers/ handle payment endpoints (HTML and JSON flows)
- Services: internal/service/ business logic; payment orchestration and external calls
- Models: internal/models/ data structures and DB ops
- Views/static: views/ templates and static assets for payment pages
- Trace/log: internal/traceRouter provides OTEL setup, context propagation, and zap logger
- Deploy: deploy/Dockerfile, docker-compose*.yaml; .drone.yml for CI image build and remote compose deploy
Key flows
- Web pay (GET/HTML): /pay.html → PayController.Pay validates params, decrypts sign, checks timeout/keys, builds exValue, calls ScanShopController.Shop, redirects to /order-confirm.html or /error.html
- API pay (form): POST /pay → PayWithJson parses params, VerifyPaySign, service.Pay, JSON response
- API pay v2 (JSON): POST /api/pay → PayV2 JSON body, VerifyPaySign, ScanShopController.Shop, returns redirectUrl for client navigation
- JD native: POST /order/pay/original/jd → PayOriginalJD validates OriginalJdReq, VerifyPaySign, service.PayWithJd, returns wxPay
Configuration essentials
- MySQL: dev juhe_pay, prod kami; credentials/hosts in conf/*.conf
- Redis: dev localhost:6379, prod redis:6379
- Gateways: gateway/partial endpoints in conf/*.conf
- Secrets: [secret] key/iv used for encryption/sign; do not commit real secrets
Observability/logging
- traceRouter.InitTracer sets OTLP exporters (traces noop by default), metrics/log exporters, zap logger bridged via otelzap
- Middleware utilities for context propagation and CreateSpan helpers; Logger.WithContext to include trace_id
CI/CD
- .drone.yml builds and pushes image to git.oceanpay.cc and runs remote docker compose with BRANCH/DRONE vars
Local tips
- Windows binary main.exe present; prefer go run main.go during development
- build.sh compiles static Linux binary for container use
Important repository rules from CLAUDE.md
- Use provided dev commands for build/run/test
- Respect architecture boundaries: controllers → services → models; keep validation in controllers, business in services, persistence in models
- Prefer JSON API (/api/pay) for programmatic integrations; HTML endpoints for browser flows

View File

@@ -1,7 +1,6 @@
package controllers
import (
"context"
"encoding/json"
"fmt"
"html/template"
@@ -24,8 +23,6 @@ import (
"github.com/beego/beego/v2/server/web"
"github.com/mitchellh/mapstructure"
"github.com/panjf2000/ants/v2"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)
@@ -133,7 +130,7 @@ func (c *HomeAction) ShowHome() {
}
if r.Code == -1 {
flash := web.NewFlash()
flash.Error(r.Msg)
flash.Error("%s", r.Msg)
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
return
@@ -236,7 +233,7 @@ func (c *HomeAction) ShowHome() {
if res.Code != 0 {
flash := web.NewFlash()
flash.Error(fmt.Sprintf("创建订单失败:%s", res.Msg))
flash.Error("创建订单失败:%s", res.Msg)
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
}
@@ -265,32 +262,6 @@ func (c *HomeAction) ShowHome() {
c.Data["productCode"] = m.ProductCode
c.Data["profitMarginList"] = profitMarginList
if createOrderParams.PaymentName == "index-wxpay.html" {
traceRouter.Span(
c.Ctx.Request.Context(), "拉单", "微信支付",
trace.WithAttributes(attribute.String("orderNo", m.OrderNo)),
)
err = p.Submit(func() {
ctx, span = traceRouter.CreateSpan(
context.WithoutCancel(c.Ctx.Request.Context()), "拉单", "微信支付",
trace.WithAttributes(attribute.String("orderNo", m.OrderNo)),
)
defer span.End()
_, ok, err2 := service.PayWithJd(ctx, c.Ctx.Input.UserAgent(), m.OrderNo, profitMarginList[0].ShowLabel)
if err2 != nil || !ok {
traceRouter.Logger.WithContext(ctx).Error("微信支付拉单错误!", zap.String("orderNo", m.OrderNo),
zap.String("error", err.Error()),
)
return
}
})
if err != nil {
traceRouter.Logger.WithContext(c.Ctx.Request.Context()).Error("微信支付拉单错误!",
zap.String("orderNo", m.OrderNo), zap.String("error", err.Error()),
)
}
}
if slices.Contains([]string{"index-aolai.html", "百礼卡"}, createOrderParams.PaymentName) {
for i := range profitMarginList {
profitMarginList[i].Color = "--bs-orange"

View File

@@ -175,17 +175,25 @@ func (c *PayController) PayWithJson() {
sign := strings.TrimSpace(c.GetString("sign"))
deviceId := strings.TrimSpace(c.GetString("deviceId"))
m, msg, ok := service.VerifyPaySign(ctx, sign)
if !ok || m == nil {
m, err := service.VerifyPaySign(ctx, sign)
if err != nil {
c.Data["json"] = response.CommonResponse{
Code: -1,
Msg: msg,
Msg: err.Error(),
}
_ = c.ServeJSON()
return
}
if m == nil {
c.Data["json"] = response.CommonResponse{
Code: -1,
Msg: "支付秘钥错误",
}
_ = c.ServeJSON()
return
}
ok, _ = service.Pay(ctx, &models.OriginalJdParams{
ok2, _ := service.Pay(ctx, &models.OriginalJdParams{
OrderId: orderNo,
FactMMValue: faceMM,
ProductCode: productCode,
@@ -195,7 +203,7 @@ func (c *PayController) PayWithJson() {
DeviceId: deviceId,
NotifyUrl: m.NotifyUrl,
}, m, c.Ctx.Input.IP())
if ok {
if ok2 {
c.Data["json"] = response.CommonResponse{
Code: 0,
Msg: "成功!",
@@ -223,15 +231,15 @@ func (c *PayController) judgeAmount(amount string) bool {
// PayOriginalJD /* 支付接口 */
func (c *PayController) PayOriginalJD() {
ctx, span := traceRouter.CreateSpan(c.Ctx.Request.Context(), "支付链接", "京东原生支付")
ctx, span := traceRouter.CreateSpan(c.Ctx.Request.Context(), "PayController", "PayController.ayOriginalJD")
defer span.End()
input := models.OriginalJdReq{}
err := c.BindForm(&input)
err := c.BindJSON(&input)
if err != nil {
c.Data["json"] = response.CommonResponse{
Code: -1,
Msg: "解析失败",
Msg: "JSON解析失败",
}
_ = c.ServeJSON()
return
@@ -250,11 +258,11 @@ func (c *PayController) PayOriginalJD() {
}
// 校验sign
m, msg, ok := service.VerifyPaySign(ctx, input.Sign)
if !ok || m == nil {
m, err := service.VerifyPaySign(ctx, input.Sign)
if err != nil || m == nil {
c.Data["json"] = response.CommonResponse{
Code: -1,
Msg: msg,
Msg: err.Error(),
}
_ = c.ServeJSON()
return
@@ -270,8 +278,8 @@ func (c *PayController) PayOriginalJD() {
return
}
wxPay, ok, err := service.PayWithJd(ctx, c.Ctx.Input.UserAgent(), m.OrderNo, faceMMValue)
if err != nil || !ok {
wxPay, err := service.PayWithJd(ctx, m.OrderNo, "cTrip", faceMMValue)
if err != nil {
traceRouter.Logger.WithContext(ctx).Error("微信支付拉单错误!", zap.String("orderNo", m.OrderNo),
zap.String("error", err.Error()),
)
@@ -325,11 +333,11 @@ func (c *PayController) PayV2() {
}
// 解析签名
m, msg, ok := service.VerifyPaySign(ctx, req.Sign)
if !ok || m == nil {
m, err := service.VerifyPaySign(ctx, req.Sign)
if err != nil || m == nil {
c.Data["json"] = response.CommonResponse{
Code: -1,
Msg: msg,
Msg: err.Error(),
}
_ = c.ServeJSON()
return

View File

@@ -13,33 +13,35 @@ import (
"go.uber.org/zap"
)
// 判断sign是否正确
func VerifyPaySign(ctx context.Context, sign string) (*models.OrderParams, string, bool) {
// VerifyPaySign 判断sign是否正确
func VerifyPaySign(ctx context.Context, sign string) (*models.OrderParams, error) {
m := models.OrderParams{}
m.Decrypt(ctx, sign)
if err := m.Decrypt(ctx, sign); err != nil {
return nil, err
}
traceRouter.Logger.WithContext(ctx).Info("订单参数", zap.Any("订单参数", m))
if time.Since(time.Unix(m.GeneratedTime, 0)).Hours() > float64(m.Duration) {
return nil, "订单超时", false
return nil, errors.New("订单超时")
}
if m.PayKey == "" {
return nil, "支付秘钥错误", false
return nil, errors.New("支付秘钥错误")
}
if m.NotifyUrl == "" {
return nil, "通知地址为空", false
return nil, errors.New("通知地址为空")
}
if m.OrderNo == "" {
return nil, "订单号为空", false
return nil, errors.New("订单号为空")
}
return &m, "", true
return &m, nil
}
// 抽离支付接口
// Pay 抽离支付接口
func Pay(ctx context.Context, m *models.OriginalJdParams, order *models.OrderParams, ip string) (bool, error) {
marshal, err := json.Marshal(map[string]string{
"recoveryType": m.RecoveryType,
@@ -75,38 +77,35 @@ func Pay(ctx context.Context, m *models.OriginalJdParams, order *models.OrderPar
}
// PayWithJd 京东原生支付
func PayWithJd(ctx context.Context, userAgent string, orderId string, orderAmount float64) (string, bool, error) {
ctx, span := traceRouter.CreateSpan(ctx, "支付链接", "京东请求")
func PayWithJd(ctx context.Context, orderId, category string, orderAmount float64) (string, error) {
ctx, span := traceRouter.CreateSpan(ctx, "PayWithJd", "PayWithJd")
defer span.End()
orderResp, err := client.Post(ctx, "http://kami_backend:12401/api/cookieInfo/jd/order/placeOrder",
orderResp, err := client.Post(ctx, "http://kami_backend:12401/api/jd-cookie/order/create",
nil, map[string]any{
"merchantOrderId": orderId,
"orderAmount": orderAmount,
"userAgent": userAgent,
"orderId": orderId,
"amount": orderAmount,
"category": category,
},
)
traceRouter.Logger.WithContext(ctx).Info("发送请求", zap.String("请求地址",
"http://kami_backend:12401/api/cookieInfo/jd/order/placeOrder"), zap.Any("请求参数", map[string]any{
"http://kami_backend:12401/api/jd-cookie/order/create"), zap.Any("请求参数", map[string]any{
"merchantOrderId": orderId,
"orderAmount": orderAmount,
"userAgent": userAgent,
}))
if err != nil {
traceRouter.Logger.WithContext(ctx).Error("请求失败", zap.Error(err))
return "", false, err
return "", err
}
traceRouter.Logger.WithContext(ctx).Info("请求结果", zap.String("response", orderResp))
type V1CardRedeemCookieOrder struct {
WxPay string `json:"wxPay" description:"微信支付"`
MerchantOrder string `json:"merchantOrder" description:"银行订单id"`
OrderNo string `json:"orderNo" description:"订单号"`
OrderAmount float64 `json:"orderAmount" description:"订单金额"`
OrderStatus string `json:"orderStatus" description:"订单状态"`
WxPayUrl string `json:"wxPayUrl" dc:"微信支付链接"`
ExpireTime string `json:"expireTime" dc:"链接过期时间"`
JdOrderId string `json:"jdOrderId" dc:"京东订单号"`
}
type Response struct {
@@ -119,15 +118,11 @@ func PayWithJd(ctx context.Context, userAgent string, orderId string, orderAmoun
err = json.Unmarshal([]byte(orderResp), &res)
if err != nil {
traceRouter.Logger.WithContext(ctx).Error("解析失败", zap.Error(err))
return "", false, err
return "", err
}
if res.Data.OrderStatus != "placeSuccess" {
traceRouter.Logger.WithContext(ctx).Error("订单拉单失败", zap.String("订单状态", res.Data.OrderStatus),
zap.String("订单号", res.Data.OrderNo),
traceRouter.Logger.WithContext(ctx).Info("订单拉单失败", zap.Any("订单状态", res.Data),
zap.String("订单", res.Data.JdOrderId),
)
return "", false, errors.New("订单拉单失败")
}
return res.Data.WxPay, true, nil
return res.Data.WxPayUrl, nil
}

BIN
kami_shop Normal file

Binary file not shown.

View File

@@ -1,149 +1,435 @@
<!DOCTYPE html>
<html lang="zh">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>微信支付</title>
<script src="../static/js/tailwindcss.js"></script>
<script src="../static/js/framer-motion.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=SF+Pro+Display:wght@400;500;600;700&display=swap');
:root {
--primary-green: #07C160;
--highlight-green: rgba(7, 193, 96, 0.1);
--primary: #07C160;
--primary-soft: rgba(7, 193, 96, .15);
--apple-white: #ffffff;
--gray: #8e8e93;
--light: #f5f5f7;
--border: #e5e5ea
}
* {
margin: 0;
padding: 0;
box-sizing: border-box
}
html, body {
height: 100%
}
body {
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(180deg, var(--highlight-green) 0%, rgba(255, 255, 255, 1) 100%);
min-height: 100vh;
overflow: hidden;
font-family: 'SF Pro Text', 'SF Pro Display', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--apple-white);
color: #1d1d1f;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden
}
.payment-circle {
background: linear-gradient(135deg, var(--primary-green) 0%, rgba(7, 193, 96, 0.8) 100%);
box-shadow: 0 8px 32px rgba(7, 193, 96, 0.2);
.container {
position: relative;
text-align: center;
padding: 40px 30px;
max-width: 420px;
width: 92%;
z-index: 10
}
.amount-text {
background: linear-gradient(90deg, var(--primary-green) 0%, #05a050 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
.bg-shapes {
position: fixed;
inset: 0;
z-index: 1;
overflow: hidden
}
.bg-shape {
position: absolute;
border-radius: 50%;
filter: blur(70px)
}
.bg-shape:nth-child(1) {
top: -12%;
left: -12%;
width: 520px;
height: 520px;
background: linear-gradient(to right, rgba(7, 193, 96, .25), rgba(7, 193, 96, 0));
animation: float 16s ease-in-out infinite
}
.bg-shape:nth-child(2) {
bottom: -18%;
right: -12%;
width: 620px;
height: 620px;
background: linear-gradient(to left, rgba(7, 193, 96, .18), rgba(7, 193, 96, 0));
animation: float 22s ease-in-out infinite reverse
}
.bg-shape:nth-child(3) {
top: 42%;
left: 62%;
width: 320px;
height: 320px;
background: linear-gradient(to top, rgba(7, 193, 96, .12), rgba(7, 193, 96, 0));
animation: float 19s ease-in-out infinite 2s
}
@keyframes float {
0% {
transform: translate(0, 0) rotate(0)
}
50% {
transform: translate(28px, 18px) rotate(4deg)
}
100% {
transform: translate(0, 0) rotate(0)
}
}
.glass {
background: rgba(255, 255, 255, .75);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, .25);
box-shadow: 0 8px 32px rgba(0, 0, 0, .06);
border-radius: 20px
}
.logo-wrap {
width: 100px;
height: 100px;
margin: 0 auto 26px;
position: relative
}
.logo {
position: relative;
z-index: 2;
width: 100px;
height: 100px;
background: var(--primary);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
box-shadow: 0 10px 25px rgba(7, 193, 96, .35);
animation: scaleIn .5s cubic-bezier(.175, .885, .32, 1.275) forwards
}
.pulse {
position: absolute;
inset: 0;
margin: auto;
width: 150px;
height: 150px;
background: radial-gradient(circle, rgba(7, 193, 96, .8) 0%, rgba(7, 193, 96, 0) 70%);
border-radius: 50%;
z-index: 1;
opacity: 0;
animation: pulse 2s ease-in-out infinite
}
.pulse:nth-child(1) {
animation-delay: .0s
}
.pulse:nth-child(2) {
width: 180px;
height: 180px;
animation-delay: .5s
}
.pulse:nth-child(3) {
width: 210px;
height: 210px;
animation-delay: 1s
}
@keyframes scaleIn {
0% {
transform: scale(0);
opacity: 0
}
100% {
transform: scale(1);
opacity: 1
}
}
@keyframes pulse {
0% {
transform: scale(1);
transform: translate(-0%, -0%) scale(.8);
opacity: 0
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
opacity: .25
}
100% {
transform: scale(1.2);
opacity: 0
}
}
.pulse-animation {
animation: pulse 2s infinite;
h1 {
font-size: 26px;
font-weight: 700;
margin-bottom: 10px;
opacity: 0;
animation: fadeInUp .8s ease .25s forwards
}
p.desc {
font-size: 15px;
color: var(--gray);
margin-bottom: 22px;
opacity: 0;
animation: fadeInUp .8s ease .4s forwards
}
.order-card {
padding: 22px;
margin-bottom: 22px;
text-align: left;
opacity: 0;
animation: fadeInUp .8s ease .55s forwards
}
.row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
font-size: 15px;
gap: 14px;
min-height: 24px
}
.row:last-child {
margin-bottom: 0;
padding-top: 18px;
border-top: 1px solid rgba(0, 0, 0, .06)
}
.label {
color: var(--gray);
flex-shrink: 0;
min-width: 80px;
text-align: left
}
.value {
font-weight: 500;
color: #1d1d1f;
text-align: left;
flex: 1;
word-break: break-word
}
.amount {
font-size: 20px;
color: var(--primary);
font-weight: 700
}
.countdown {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
background: var(--primary-soft);
color: var(--primary)
}
.btn {
width: 100%;
padding: 15px;
border: none;
border-radius: 14px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all .25s ease;
margin-top: 8px;
font-family: 'SF Pro Text', sans-serif;
background: var(--primary);
color: #fff;
box-shadow: 0 4px 15px rgba(7, 193, 96, .35);
opacity: 0;
animation: fadeInUp .8s ease .7s forwards
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 22px rgba(7, 193, 96, .45)
}
.btn:disabled {
background: #c7c7cc;
color: #fff;
cursor: not-allowed;
box-shadow: none;
transform: none
}
.btn.loading {
background: var(--primary);
color: #fff;
cursor: not-allowed;
box-shadow: none;
transform: none;
position: relative;
padding-left: 44px;
}
.btn.loading::before {
content: '';
position: absolute;
left: 15px;
top: 50%;
width: 18px;
height: 18px;
margin-top: -9px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.safe {
margin-top: 14px;
font-size: 12px;
color: #8e8e93
}
@keyframes fadeInUp {
0% {
transform: translateY(18px);
opacity: 0
}
100% {
transform: translateY(0);
opacity: 1
}
}
</style>
</head>
<body>
<div class="bg-shapes">
<div class="bg-shape"></div>
<div class="bg-shape"></div>
<div class="bg-shape"></div>
</div>
<body class="flex flex-col items-center justify-start p-6 relative">
<!-- 顶部Logo -->
<div class="w-full flex justify-center items-center mb-8 motion-safe:animate-fadeIn">
<div class="bg-white rounded-full p-4 shadow-lg">
<svg class="w-12 h-12" viewBox="0 0 24 24" fill="none">
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15l-5-5 1.41-1.41L11 14.17l7.59-7.59L20 8l-9 9z"
fill="var(--primary-green)" />
<div class="container">
<div class="logo-wrap">
<div class="pulse"></div>
<div class="pulse"></div>
<div class="pulse"></div>
<div class="logo">
<svg width="44" height="44" viewBox="0 0 24 24" fill="none">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15l-5-5 1.41-1.41L11 14.17l7.59-7.59L20 8l-9 9z"
fill="#fff"/>
</svg>
</div>
</div>
<!-- 支付金额 -->
<div class="text-center mb-12">
<div class="text-sm text-gray-600 mb-2">支付金额</div>
<div class="flex items-start justify-center">
<span class="text-3xl font-medium mt-2">¥</span>
<span class="amount-text text-8xl font-bold">{{.mmValue}}</span>
<h1>微信支付</h1>
<p class="desc">请在有效期内完成支付,超时订单将自动取消</p>
<div class="order-card glass">
<div class="row">
<span class="label">订单金额</span>
<span class="value amount">¥{{.mmValue}}</span>
</div>
<div class="row">
<span class="label">订单编号</span>
<span class="value">{{.orderNo}}</span>
</div>
<div class="row">
<span class="label">商品名称</span>
<span class="value">微信支付</span>
</div>
<div class="row">
<span class="label">剩余时间</span>
<span class="value"><span id="countdown" class="countdown">--:--:--</span></span>
</div>
</div>
<!-- 支付信息 -->
<div class="w-full max-w-md bg-white rounded-2xl p-6 shadow-lg mb-8">
<div class="flex flex-col space-y-4">
<div class="flex justify-between items-center">
<span class="text-gray-600">订单编号</span>
<span class="font-medium">{{.orderNo}}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600">商品名称</span>
<span class="font-medium">微信支付</span>
</div>
<div class="h-px bg-gray-200 my-2"></div>
<div class="text-center text-gray-600">
<p class="text-lg font-medium text-[var(--primary-green)]">请在24小时内完成支付</p>
<p class="text-sm mt-2">超时订单将自动取消</p>
</div>
</div>
<button id="payBtn" class="btn">确认支付</button>
<div class="safe">本次交易安全可靠</div>
</div>
<!-- 支付按钮 -->
<button
class="payment-circle w-full max-w-md h-14 rounded-full text-white text-xl font-semibold shadow-lg transform transition-transform hover:scale-105 active:scale-95 pulse-animation">
确认支付
</button>
<!-- 底部提示 -->
<div class="mt-8 text-center text-sm text-gray-500">
<p>本次交易安全可靠</p>
</div>
<input type="text" style="display: none" name="mmValue" id="mmValue" value="{{.mmValue}}">
<input type="text" style="display: none" name="productCode" id="productCode" value="{{.productCode}}">
<input type="text" style="display: none" name="notifyUrl" id="notifyUrl" value="{{.notifyUrl}}">
<input type="text" style="display: none" name="orderId" id="orderId" value="{{.orderNo}}">
<input type="text" style="display: none" name="sign" id="sign" value="{{.sign}}">
<input type="text" style="display: none" name="returnUrl" id="returnUrl" value="{{.returnUrl}}">
</body>
<script type="text/javascript" src="../static/js/jquery.min.js"></script>
<script>
document.querySelector('.payment-circle').addEventListener('click', function () {
// 这里添加支付逻辑
this.classList.remove('pulse-animation');
this.textContent = '支付处理中...';
this.disabled = true;
requestOrder();
});
const payBtn = document.getElementById('payBtn');
const countdownEl = document.getElementById('countdown');
const orderId = '{{.orderNo}}';
const EXP_KEY = 'order_expiry_' + orderId;
const now = Date.now();
let expiry = parseInt(localStorage.getItem(EXP_KEY) || '0', 10);
if (!expiry || expiry < now) {
expiry = now + 24 * 60 * 60 * 1000;
localStorage.setItem(EXP_KEY, String(expiry));
}
//编写接口,请求下单
function requestOrder() {
$.ajax({
url: "/order/pay/original/jd",
type: "POST",
data: {
productCode: $("#productCode").val(),
notifyUrl: $("#notifyUrl").val(),
orderId: $("#orderId").val(),
sign: $("#sign").val(),
returnUrl: $("#returnUrl").val(),
mmValue: $("#mmValue").val(),
},
success: function (res) {
if (res.code == 0) {
async function requestOrder() {
try {
payBtn.disabled = true;
payBtn.classList.add('loading');
payBtn.textContent = '支付处理中…';
const payload = {
productCode: '{{.productCode}}',
orderId: '{{.orderNo}}',
sign: '{{.sign}}',
returnUrl: '{{.returnUrl}}',
factMMValue: '{{.mmValue}}'
};
const resp = await fetch('/order/pay/original/jd', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const res = await resp.json();
if (res && res.code === 0 && res.data && res.data.wxPay) {
window.location.href = res.data.wxPay;
} else {
alert(res.msg);
alert(res && res.msg ? res.msg : '下单失败');
payBtn.disabled = false;
payBtn.classList.remove('loading');
payBtn.textContent = '确认支付';
}
} catch (e) {
alert('网络异常,请稍后重试');
payBtn.disabled = false;
payBtn.classList.remove('loading');
payBtn.textContent = '确认支付';
}
}
});
}
</script>
payBtn.addEventListener('click', () => {
if (payBtn.disabled) return;
requestOrder();
});
</script>
</body>
</html>