feat(payment):重构微信支付页面并优化支付流程
- 重新设计支付页面UI,采用毛玻璃效果和动态背景- 更新支付接口调用方式,从表单提交改为JSON请求 -优化支付签名验证逻辑,统一错误处理方式 - 修改京东支付接口参数和返回结构体 - 移除OpenTelemetry追踪相关代码 - 添加支付倒计时功能和本地存储支持 - 优化支付按钮加载状态和交互反馈 - 调整HTML结构和CSS样式,提升用户体验
This commit is contained in:
58
CODEBUDDY.md
Normal file
58
CODEBUDDY.md
Normal 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user