feat(views): 添加订单确认页面并更新图标引用

- 添加新的订单确认页面模板 (order_confirm.html)
- 启用 remixicon 图标库的样式引用
- 将成功图标从 ri-check-line 更新为 ri-check-double-fill- 添加 CLAUDE.md 开发指南文件
- 添加 Claude Code 本地设置文件
This commit is contained in:
danial
2025-09-23 19:59:18 +08:00
parent cc48364eb3
commit b5a0e97793
18 changed files with 4261 additions and 284 deletions

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(cat:*)"
],
"deny": [],
"ask": []
}
}

117
CLAUDE.md Normal file
View File

@@ -0,0 +1,117 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
### Build and Run
```bash
# Build for production (Linux AMD64)
./build.sh
# Development mode
go run main.go
# Docker build
docker build -t kami_shop . -f deploy/Dockerfile
```
### Testing
```bash
# Run all tests
go test ./...
# Run specific test file
go test ./internal/service/scan_shop_test.go
# Run tests with verbose output
go test -v ./...
```
### Docker Deployment
```bash
# Build and deploy with Docker Compose
export VERSION=latest
docker-compose -f deploy/docker-compose.yaml up -d
```
## Project Architecture
This is a **payment processing platform** built with Go and Beego framework, supporting multiple payment methods including WeChat Pay, Alipay, JD.com, Taobao, Pinduoduo, Apple Pay, Walmart, and various gift cards.
### Key Components
**Application Structure**:
- `main.go` - Application entry point with OpenTelemetry initialization
- `internal/controllers/` - HTTP handlers for payment processing
- `internal/service/` - Business logic layer
- `internal/models/` - Data models and database operations
- `internal/routers/` - Route definitions
- `views/` - HTML templates for payment pages (30+ payment method templates)
- `conf/` - Configuration files (app.conf for production, app.local.conf for development)
**External Dependencies**:
- **Database**: MySQL (development: `juhe_pay`, production: `kami`)
- **Cache**: Redis for session management and caching
- **Gateway Service**: `http://localhost:12309` (development) / `http://127.0.0.1:12309` (production)
- **Partial Service**: `http://localhost:12310` (development) / `http://127.0.0.1:12310` (production)
### Configuration
**Development** (`conf/app.local.conf`):
- Port: 12305, Host: localhost
- Database: MySQL on localhost:3306
- Redis: localhost:6379
- Run mode: dev
**Production** (`conf/app.conf`):
- Port: 12305, Host: 0.0.0.0
- Database: MySQL on 127.0.0.1:3306
- Redis: redis:6379 (Docker service)
- Run mode: prod
### Technology Stack
- **Framework**: Beego v2.3.7
- **Language**: Go 1.23.0
- **Observability**: Complete OpenTelemetry stack (tracing, metrics, logging)
- **Logging**: Zap structured logging
- **Concurrency**: Goroutine pooling with ants/v2
- **Security**: AES encryption, Edwards25519 cryptographic operations
### Development Patterns
**Code Organization**:
- Controllers handle HTTP requests and validation
- Service layer implements business logic
- Models manage data persistence
- Utils provide common functionality
- Schema defines request/response structures
**Testing Strategy**:
- Unit tests focused on service layer
- Test files follow `*_test.go` naming convention
- Use standard Go testing package
### Payment Processing Flow
1. Order creation through controller endpoints
2. Service layer processes payment logic
3. Integration with external payment gateways
4. Notification handling for payment callbacks
5. Template-based payment page generation
6. Profit margin calculation and reporting
### Deployment
**Docker Environment**:
- Multi-stage build using golang:1.23 builder
- Alpine Linux runtime image
- Volume mounts for configuration and logs
- Network: 1panel-network
- Health checks and automatic restarts
**CI/CD**:
- Drone CI pipeline with SSH deployment
- Docker image registry push
- Production server deployment via Docker Compose

View File

@@ -1,4 +1,4 @@
FROM golang:1.23 AS builder
FROM golang:1.24 AS builder
WORKDIR /build

63
go.mod
View File

@@ -1,41 +1,42 @@
module shop
go 1.23.0
go 1.24.0
toolchain go1.23.6
toolchain go1.24.6
require github.com/beego/beego/v2 v2.3.7
require github.com/beego/beego/v2 v2.3.8
require (
github.com/go-sql-driver/mysql v1.9.2
github.com/duke-git/lancet/v2 v2.3.7
github.com/go-sql-driver/mysql v1.9.3
github.com/mitchellh/mapstructure v1.5.0
github.com/panjf2000/ants/v2 v2.11.3
github.com/widuu/gojson v0.0.0-20170212122013-7da9d2cd949b
go.opentelemetry.io/contrib/bridges/otelzap v0.10.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0
go.opentelemetry.io/otel/log v0.11.0
go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/sdk/log v0.11.0
go.opentelemetry.io/otel/trace v1.35.0
go.opentelemetry.io/contrib/bridges/otelzap v0.13.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0
go.opentelemetry.io/otel/log v0.14.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/sdk/log v0.14.0
go.opentelemetry.io/otel/trace v1.38.0
go.uber.org/zap v1.27.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -46,19 +47,19 @@ require (
github.com/prometheus/procfs v0.16.1 // indirect
github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

138
go.sum
View File

@@ -1,34 +1,36 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/beego/beego/v2 v2.3.7 h1:z4btKtjU/rfp5BiYHkGD2QPjK9i1E9GH+I7vfhn6Agk=
github.com/beego/beego/v2 v2.3.7/go.mod h1:5cqHsOHJIxkq44tBpRvtDe59GuVRVv/9/tyVDxd5ce4=
github.com/beego/beego/v2 v2.3.8 h1:wplhB1pF4TxR+2SS4PUej8eDoH4xGfxuHfS7wAk9VBc=
github.com/beego/beego/v2 v2.3.8/go.mod h1:8vl9+RrXqvodrl9C8yivX1e6le6deCK6RWeq8R7gTTg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/duke-git/lancet/v2 v2.3.7 h1:nnNBA9KyoqwbPm4nFmEFVIbXeAmpqf6IDCH45+HHHNs=
github.com/duke-git/lancet/v2 v2.3.7/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -41,8 +43,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -59,70 +61,78 @@ github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 h1:v9ezJDHA1XGxViAUSIoO/Id7Fl63u6d0YmsAm+/p2hs=
github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02/go.mod h1:RF16/A3L0xSa0oSERcnhd8Pu3IXSDZSK2gmGIMsttFE=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/widuu/gojson v0.0.0-20170212122013-7da9d2cd949b h1:ieRJ8K7QAPWWltEOv7rzMruuPd7gbeAqTaBFhUECIy0=
github.com/widuu/gojson v0.0.0-20170212122013-7da9d2cd949b/go.mod h1:9W1pyetRkwXqjR9tjOSrSuhGHBK0EqXoQSwWbhBHHwA=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU=
go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 h1:C/Wi2F8wEmbxJ9Kuzw/nhP+Z9XaHYMkyDmXy6yR2cjw=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0/go.mod h1:0Lr9vmGKzadCTgsiBydxr6GEZ8SsZ7Ks53LzjWG5Ar4=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y=
go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c=
go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 h1:aBKdhLVieqvwWe9A79UHI/0vgp2t/s2euY8X59pGRlw=
go.opentelemetry.io/contrib/bridges/otelzap v0.13.0/go.mod h1:SYqtxLQE7iINgh6WFuVi2AI70148B8EI35DSk0Wr8m4=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
go.opentelemetry.io/otel/log/logtest v0.14.0 h1:BGTqNeluJDK2uIHAY8lRqxjVAYfqgcaTbVk1n3MWe5A=
go.opentelemetry.io/otel/log/logtest v0.14.0/go.mod h1:IuguGt8XVP4XA4d2oEEDMVDBBCesMg8/tSGWDjuKfoA=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20221208152030-732eee02a75a h1:4iLhBPcpqFmylhnkbY3W0ONLUYYkDAW9xMFLfxgsvCw=
golang.org/x/exp v0.0.0-20221208152030-732eee02a75a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9 h1:jm6v6kMRpTYKxBRrDkYAitNJegUeO1Mf3Kt80obv0gg=
google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9/go.mod h1:LmwNphe5Afor5V3R5BppOULHOnt2mCIf+NxMd4XiygE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -19,6 +19,10 @@ func GetMMValue() (gUrl string) {
return fmt.Sprintf("%s/gateway/getAllowedMM", GetGateway())
}
func GetMerchantOrder() (gUrl string) {
return fmt.Sprintf("%s/gateway/merchant/query", GetGateway())
}
func GetPortalUrl() (url string) {
url = env.Get("portalAddr", url)
if url != "" {

View File

@@ -2,6 +2,7 @@ package controllers
import (
"context"
"encoding/json"
"fmt"
"html/template"
"net/url"
@@ -10,6 +11,7 @@ import (
"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"
@@ -33,6 +35,14 @@ 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", "首页")
@@ -69,21 +79,50 @@ func (c *HomeAction) ShowHome() {
return
}
c.Data["sign"] = sign
resp, err := client.Get(ctx, config.GetMMValue(), map[string]string{
"productCode": m.ProductCode,
"showMMValue": fmt.Sprintf("%f", m.ShowMMValue),
"payKey": m.PayKey,
})
if err != nil {
merchantInfo := service.GetMerchantByPasskey(ctx, m.PayKey)
if merchantInfo.Id == 0 {
flash := web.NewFlash()
traceRouter.Logger.WithContext(ctx).Error("签名错误,请检查签名是否正确!", zap.String("sign", err.Error()))
flash.Error("获取面额失败!")
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()
@@ -175,15 +214,6 @@ func (c *HomeAction) ShowHome() {
Timestamp: time.Now().Unix(),
}
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
}
requestOrder.Sign = utils.GetMD5SignMF(requestOrder.ToStrMap(), merchantInfo.MerchantSecret)
//创建订单
response, err := client.Post(ctx, service.CreateOrderHost, map[string]string{}, requestOrder.ToMap())
@@ -290,18 +320,107 @@ func (c *HomeAction) Notify() {
// ShowOK /*加载首页及数据*/
func (c *HomeAction) ShowOK() {
//取值
orderNo := c.GetString("orderNo")
faceMM := c.GetString("faceMM")
c.Data["orderNo"] = orderNo
c.Data["faceMM"] = faceMM
c.Data["orderId"] = c.GetString("orderId")
c.Data["amount"] = c.GetString("faceMM")
c.Data["returnUrl"], _ = web.AppConfig.String("returnUrl")
c.TplName = "ok.html"
}
func (c *HomeAction) ErrorPage() {
flash := web.ReadFromRequest(&c.Controller)
e := flash.Data["error"]
c.Data["error"] = e
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()
}

View File

@@ -25,7 +25,6 @@ func (c *PayController) Pay() {
defer span.End()
flash := web.NewFlash()
// ctx := context.Background()
returnUrl := strings.TrimSpace(c.GetString("returnUrl"))
orderNo := strings.TrimSpace(c.GetString("orderId"))
productCode := strings.TrimSpace(c.GetString("productCode"))
@@ -38,8 +37,10 @@ func (c *PayController) Pay() {
if orderNo == "" {
flash.Error("订单号为空")
flash.Store(&c.Controller)
c.Ctx.Output.Header("Location", "/error.html")
c.Redirect("/error.html", 302)
str := "/error.html" + "?orderId=" + orderNo + "&amount=" + "0" +
"&returnUrl=" + returnUrl + "&errorMsg=" + "订单号为空!"
c.Ctx.Output.Header("Location", str)
c.Redirect(str, 302)
return
}
@@ -47,42 +48,48 @@ func (c *PayController) Pay() {
if !c.judgeAmount(faceMM) {
flash.Error("金额有误")
flash.Store(&c.Controller)
c.Ctx.Output.Header("Location", "/error.html")
c.Redirect("/error.html", 302)
str := "/error.html" + "?orderId=" + orderNo + "&amount=" + faceMM +
"&returnUrl=" + returnUrl + "&errorMsg=" + "金额有误!"
c.Ctx.Output.Header("Location", str)
c.Redirect(str, 302)
return
}
m := models.OrderParams{}
m.Decrypt(ctx, sign)
if time.Since(time.Unix(m.GeneratedTime, 0)).Hours() > float64(m.Duration) {
flash.Error("订单超时!")
flash.Store(&c.Controller)
c.Ctx.Output.Header("Location", "/error.html")
c.Redirect("/error.html", 302)
str := "/error.html" + "?orderId=" + orderNo + "&amount=" + faceMM +
"&returnUrl=" + returnUrl + "&errorMsg=" + "订单超时!"
c.Ctx.Output.Header("Location", str)
c.Redirect(str, 302)
return
}
if m.PayKey == "" {
flash.Error("支付秘钥错误!")
flash.Store(&c.Controller)
c.Ctx.Output.Header("Location", "/error.html")
c.Redirect("/error.html", 302)
str := "/error.html" + "?orderId=" + orderNo + "&amount=" + faceMM +
"&returnUrl=" + returnUrl + "&errorMsg=" + "支付秘钥错误!"
c.Ctx.Output.Header("Location", str)
c.Redirect(str, 302)
return
}
if m.NotifyUrl == "" {
flash.Error("通知地址为空!")
flash.Store(&c.Controller)
c.Ctx.Output.Header("Location", "/error.html")
c.Redirect("/error.html", 302)
str := "/error.html" + "?orderId=" + orderNo + "&amount=" + faceMM +
"&returnUrl=" + returnUrl + "&errorMsg=" + "通知地址为空!"
c.Ctx.Output.Header("Location", str)
c.Redirect(str, 302)
return
}
if m.OrderNo == "" {
flash.Error("订单号为空!")
flash.Store(&c.Controller)
c.Ctx.Output.Header("Location", "/error.html")
c.Redirect("/error.html", 302)
str := "/error.html" + "?orderId=" + orderNo + "&amount=" + faceMM +
"&returnUrl=" + returnUrl + "&errorMsg=" + "订单号为空!"
c.Ctx.Output.Header("Location", str)
c.Redirect(str, 302)
return
}
pp := map[string]string{
@@ -92,10 +99,11 @@ func (c *PayController) Pay() {
}
marshal, err := json.Marshal(&pp)
if err != nil {
flash.Error("卡密数据解析错误!")
flash.Store(&c.Controller)
c.Ctx.Output.Header("Location", "/error.html")
c.Redirect("/error.html", 302)
str := "/error.html" + "?orderId=" + orderNo + "&amount=" + faceMM +
"&returnUrl=" + returnUrl + "&errorMsg=" + "卡密数据解析错误!"
c.Ctx.Output.Header("Location", str)
c.Redirect(str, 302)
return
}
@@ -113,10 +121,7 @@ func (c *PayController) Pay() {
res := scanShop.Shop(ctx, m.PayKey)
if res.Code == 200 {
str := "/ok.html" + "?orderNo=" + orderNo + "&faceMM=" + faceMM
if returnUrl != "" {
str += "&returnUrl=" + returnUrl
}
str := "/ok.html" + "?orderId=" + orderNo + "&amount=" + faceMM + "&returnUrl=" + returnUrl
c.Ctx.Output.Header("Location", str)
c.Redirect(str, 302)
return
@@ -124,15 +129,10 @@ func (c *PayController) Pay() {
flash.Error(res.Msg)
flash.Store(&c.Controller)
str := "/error.html"
if returnUrl != "" {
str += "?returnUrl=" + returnUrl
}
str := "/error.html" + "?orderId=" + orderNo + "&amount=" + faceMM +
"&returnUrl=" + returnUrl + "&errorMsg=" + "不存在这样的支付类型!"
c.Ctx.Output.Header("Location", str)
c.Redirect(str, 302)
flash.Error("不存在这样的支付类型")
flash.Store(&c.Controller)
c.Redirect("/error.html", 302)
}
// PayWithJson 后端接口支付

View File

@@ -21,7 +21,9 @@ func init() {
web.Router("/pay.html", &controllers.PayController{}, "*:Pay")
web.Router("/pay", &controllers.PayController{}, "*:PayWithJson")
web.Router("/error.html", &controllers.HomeAction{}, "*:ErrorPage")
web.Router("/order-confirm.html", &controllers.HomeAction{}, "*:OrderConfirm")
web.Router("/ok.html", &controllers.HomeAction{}, "*:ShowOK")
web.Router("/shop/notify", &controllers.HomeAction{}, "*:Notify")
web.Router("/order/pay/original/jd", &controllers.PayController{}, "*:PayOriginalJD")
web.Router("/api/query-order-status", &controllers.HomeAction{}, "*:QueryOrderStatus")
}

View File

@@ -0,0 +1,34 @@
package request
import "strconv"
// "appKey": m.PayKey,
// "orderNo": m.OrderNo,
// "timestamp": time.Now().Unix(),
type QueryOrder struct {
Appkey string `json:"appkey" valid:"Required;"`
OrderNo string `json:"orderNo" valid:"Required;"`
Timestamp int64 `json:"timestamp" valid:"Required;"`
Sign string `json:"sign" valid:"Required;"`
}
func (q *QueryOrder) ToMap() map[string]interface{} {
data := map[string]interface{}{
"appKey": q.Appkey,
"orderNo": q.OrderNo,
"timestamp": q.Timestamp,
"sign": q.Sign,
}
return data
}
func (q *QueryOrder) ToStrMap() map[string]string {
data := map[string]string{
"appKey": q.Appkey,
"orderNo": q.OrderNo,
"timestamp": strconv.FormatInt(q.Timestamp, 10),
"sign": q.Sign,
}
return data
}

View File

@@ -0,0 +1,16 @@
package response
// MerchantQueryResp 是一个泛型响应结构体,可以用于任何数据类型
type MerchantQueryResp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
OrderNo string `json:"orderNo"`
CardNo string `json:"cardNo"`
CardPwd string `json:"cardPwd"`
Status string `json:"status"`
FaceVal float64 `json:"faceVal"`
Amount string `json:"amount"`
CardReturnData string `json:"cardReturnData"`
} `json:"data"`
}

View File

@@ -54,7 +54,6 @@ func (c *ScanShopController) Shop(ctx context.Context, payKey string) *ResponseJ
merchantInfo := GetMerchantByPasskey(ctx, payKey)
c.Params["sign"] = utils.GetMD5SignMF(c.Params, merchantInfo.MerchantSecret)
traceRouter.Logger.WithContext(ctx).Info(fmt.Sprintf("requestUrl %s", reqUrl), zap.Any("params", c.Params))
req := httplib.NewBeegoRequestWithCtx(ctx, reqUrl, "POST").
SetTimeout(30*time.Second, 30*time.Second).
SetTransport(otelhttp.NewTransport(http.DefaultTransport))
@@ -64,6 +63,7 @@ func (c *ScanShopController) Shop(ctx context.Context, payKey string) *ResponseJ
}
response, err := req.String()
traceRouter.Logger.WithContext(ctx).Info("请求参数", zap.String("请求地址", reqUrl), zap.Any("请求参数", c.Params))
if err != nil {
traceRouter.Logger.WithContext(ctx).Error(fmt.Sprintf("err %s", err.Error()))
responseJSON.Code = -1

View File

@@ -9,6 +9,7 @@ import (
"shop/internal/traceRouter"
"time"
"github.com/duke-git/lancet/v2/convertor"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.uber.org/zap"
@@ -32,8 +33,7 @@ func Get(ctx context.Context, baseUrl string, params map[string]string) (result
return "", err
}
realUrl.RawQuery = urlParams.Encode()
traceRouter.Logger.WithContext(ctx).Info("请求地址", zap.String("url", realUrl.String()))
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), "GET", realUrl.String(), nil)
req, err := http.NewRequestWithContext(context.Background(), "GET", realUrl.String(), nil)
if err != nil {
return "", err
}
@@ -55,7 +55,7 @@ func Get(ctx context.Context, baseUrl string, params map[string]string) (result
}
func Post(ctx context.Context, baseUrl string, params map[string]string, data map[string]interface{}) (result string, err error) {
ctx, span := traceRouter.CreateSpan(context.WithoutCancel(ctx), "通讯", "Post")
ctx, span := traceRouter.CreateSpan(context.Background(), "通讯", "Post")
defer span.End()
urlParams := url.Values{}
for k, v := range params {
@@ -74,6 +74,29 @@ func Post(ctx context.Context, baseUrl string, params map[string]string, data ma
return response, err
}
func PostWithFormData(ctx context.Context, baseUrl string, params map[string]string, data map[string]interface{}) (result string, err error) {
ctx, span := traceRouter.CreateSpan(ctx, "通讯", "Post")
defer span.End()
urlParams := url.Values{}
for k, v := range params {
urlParams.Add(k, v)
}
realUrl, err := url.Parse(baseUrl)
if err != nil {
return "", err
}
realUrl.RawQuery = urlParams.Encode()
req := httplib.NewBeegoRequestWithCtx(ctx, realUrl.String(), "POST").
SetTransport(otelhttp.NewTransport(http.DefaultTransport)).
SetTimeout(30*time.Second, 30*time.Second)
for s, i := range data {
req.Param(s, convertor.ToString(i))
}
response, err := req.String()
traceRouter.Logger.WithContext(ctx).Info("请求地址", zap.String("url", realUrl.String()), zap.String("response", response))
return response, err
}
func Unmarshal(s string) (Response, error) {
var r Response
err := json.Unmarshal([]byte(s), &r)

2938
static/css/remixicon.css Normal file

File diff suppressed because it is too large Load Diff

0
static/fonts/.gitkeep Normal file
View File

View File

@@ -6,189 +6,399 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>支付失败</title>
<!-- 引入字体 -->
<link rel="stylesheet" href="../static/css/index-anxin.css">
<!-- 引入图标库 -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/remixicon/4.6.0/remixicon.min.css">
<!-- 引入动画库 -->
<link rel="stylesheet" href="../static/css/animate.min.css">
<!-- 引入图标库 -->
<link rel="stylesheet" href="../static/css/index-anxin.css">
<style>
/* 全局样式和重置 */
:root {
--error-red: #ff3b30;
--error-light-red: rgba(255, 59, 48, 0.15);
--apple-white: #ffffff;
--apple-gray: #8e8e93;
--apple-light-gray: #f5f5f7;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body,
html {
margin: 0;
padding: 0;
font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #ffffff;
/* 主背景改为白色 */
color: #333333;
/* 主要文字颜色改为深色 */
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;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
/* 防止滚动 */
}
/* 容器样式 */
.container {
position: relative;
text-align: center;
padding: 40px 30px;
/* 增加上下内边距 */
max-width: 400px;
/* 限制最大宽度 */
width: 90%;
background: linear-gradient(135deg, #FF8E8E 0%, #FF6B6B 100%);
/* 容器红色渐变背景 */
color: #ffffff;
/* 容器内文字颜色改为白色 */
border-radius: 20px;
/* 圆角调整 */
box-shadow: 0 10px 25px rgba(255, 107, 107, 0.3);
/* 调整阴影以适应白色背景 */
/* backdrop-filter: blur(10px); */
/* 移除毛玻璃效果 */
/* border: 1px solid rgba(255, 255, 255, 0.2); */
/* 移除边框 */
animation: fadeIn 1s ease-out;
z-index: 10;
}
/* 错误图标样式 */
/* 背景效果 */
.bg-shapes {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
overflow: hidden;
}
.bg-shape {
position: absolute;
border-radius: 50%;
filter: blur(70px);
}
.bg-shape:nth-child(1) {
top: -10%;
left: -10%;
width: 500px;
height: 500px;
background: linear-gradient(to right, rgba(255, 59, 48, 0.2), rgba(255, 59, 48, 0));
animation: float 15s ease-in-out infinite;
}
.bg-shape:nth-child(2) {
bottom: -15%;
right: -10%;
width: 600px;
height: 600px;
background: linear-gradient(to left, rgba(255, 59, 48, 0.15), rgba(255, 59, 48, 0));
animation: float 20s ease-in-out infinite reverse;
}
.bg-shape:nth-child(3) {
top: 40%;
left: 60%;
width: 300px;
height: 300px;
background: linear-gradient(to top, rgba(255, 59, 48, 0.1), rgba(255, 59, 48, 0));
animation: float 18s ease-in-out infinite 2s;
}
@keyframes float {
0% {
transform: translate(0, 0) rotate(0deg);
}
50% {
transform: translate(30px, 20px) rotate(5deg);
}
100% {
transform: translate(0, 0) rotate(0deg);
}
}
/* 玻璃态效果 */
.glass {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
border-radius: 20px;
}
/* 错误图标容器 */
.error-icon-container {
position: relative;
width: 100px;
height: 100px;
margin: 0 auto 30px;
}
/* 错误图标 */
.error-icon {
font-size: 70px;
/* 图标大小调整 */
color: #ffffff;
/* 图标颜色改为白色,因为背景是红色 */
margin-bottom: 20px;
/* text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); */
/* 移除阴影 */
animation: bounceIn 1s ease;
position: relative;
z-index: 2;
width: 100px;
height: 100px;
background: var(--error-red);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 50px;
box-shadow: 0 10px 25px rgba(255, 59, 48, 0.3);
animation: scaleIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
/* 镭射效果 */
.laser-effect {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 150px;
height: 150px;
background: radial-gradient(circle, rgba(255, 59, 48, 0.8) 0%, rgba(255, 59, 48, 0) 70%);
border-radius: 50%;
z-index: 1;
opacity: 0;
animation: pulse 2s ease-in-out infinite;
}
.laser-effect:nth-child(2) {
width: 180px;
height: 180px;
animation-delay: 0.5s;
}
.laser-effect:nth-child(3) {
width: 210px;
height: 210px;
animation-delay: 1s;
}
/* 标题样式 */
h1 {
font-size: 26px;
/* 字体稍大 */
font-size: 28px;
font-weight: 700;
margin-bottom: 15px;
color: #ffffff;
/* 标题颜色改为白色 */
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
color: #1d1d1f;
font-family: 'SF Pro Display', sans-serif;
animation: fadeInUp 0.8s ease forwards;
animation-delay: 0.3s;
opacity: 0;
}
/* 描述文本样式 */
p {
font-size: 16px;
font-size: 17px;
font-weight: 400;
line-height: 1.6;
margin-bottom: 35px;
/* 增加底部间距 */
color: #ffffff;
/* 描述文本颜色改为白色 */
opacity: 0.95;
/* 略微提高不透明度 */
line-height: 1.5;
margin-bottom: 30px;
color: var(--apple-gray);
animation: fadeInUp 0.8s ease forwards;
animation-delay: 0.5s;
opacity: 0;
}
/* 错误信息卡片 */
.error-card {
padding: 25px;
margin-bottom: 25px;
text-align: left;
animation: fadeInUp 0.8s ease forwards;
animation-delay: 0.7s;
opacity: 0;
}
.error-detail {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
font-size: 15px;
}
.error-detail:last-child {
margin-bottom: 0;
padding-top: 15px;
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.detail-label {
color: var(--apple-gray);
}
.detail-value {
font-weight: 500;
color: #1d1d1f;
}
.error-reason {
color: var(--error-red);
font-weight: 600;
}
/* 按钮样式 */
.btn-container {
animation: fadeInUp 0.8s ease forwards;
animation-delay: 0.9s;
opacity: 0;
}
.btn {
display: inline-block;
padding: 12px 35px;
/* 增加左右内边距 */
background: #ffffff;
/* 按钮背景改为白色 */
color: #FF6B6B;
/* 按钮文字颜色改为红色 */
font-size: 17px;
/* 字体稍大 */
font-weight: 500;
text-decoration: none;
border-radius: 12px;
/* 圆角调整 */
width: 100%;
padding: 15px;
border: none;
/* 移除边框 */
box-shadow: 0 5px 15px rgba(255, 107, 107, 0.3);
/* 调整阴影 */
transition: all 0.3s ease;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 15px;
font-family: 'SF Pro Text', sans-serif;
}
/* 按钮悬停/点击效果 */
.btn:hover {
background: #fff5f5;
/* 悬停时淡红色背景 */
box-shadow: 0 7px 18px rgba(255, 107, 107, 0.4);
/* 悬停时阴影变化 */
.btn-primary {
background: var(--error-red);
color: white;
box-shadow: 0 4px 15px rgba(255, 59, 48, 0.3);
}
.btn-primary:hover {
background: #d70015;
transform: translateY(-2px);
/* 轻微上移 */
box-shadow: 0 6px 20px rgba(255, 59, 48, 0.4);
}
.btn:active {
background: #ffeaea;
/* 点击时更深的淡红色 */
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
/* 点击时模拟按下效果 */
transform: translateY(1px);
/* 轻微下移 */
.btn-secondary {
background: transparent;
color: var(--error-red);
border: 2px solid var(--error-red);
}
/* 定义动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
.btn-secondary:hover {
background: var(--error-light-red);
}
@keyframes bounceIn {
/* 动画定义 */
@keyframes scaleIn {
0% {
transform: scale(0);
opacity: 0;
transform: scale(0.3) translateY(-50px);
}
50% {
opacity: 1;
transform: scale(1.1) translateY(10px);
}
70% {
transform: scale(0.9) translateY(-5px);
}
100% {
transform: scale(1) translateY(0);
transform: scale(1);
opacity: 1;
}
}
@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
}
50% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.3;
}
100% {
transform: translate(-50%, -50%) scale(1.2);
opacity: 0;
}
}
@keyframes fadeInUp {
0% {
transform: translateY(20px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
/* 订单编号样式 */
.order-id {
font-size: 14px;
color: var(--apple-gray);
margin-bottom: 20px;
word-break: break-all;
animation: fadeInUp 0.8s ease forwards;
animation-delay: 0.7s;
opacity: 0;
}
/* 安全提示 */
.security-note {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
font-size: 14px;
color: var(--apple-gray);
animation: fadeInUp 0.8s ease forwards;
animation-delay: 1.1s;
opacity: 0;
}
.security-note i {
margin-right: 6px;
font-size: 16px;
color: var(--error-red);
}
</style>
</head>
<body>
<div class="container animate__animated animate__fadeIn">
<!-- 错误图标 -->
<div class="error-icon animate__animated animate__bounceIn">
<i class="ri-close-circle-fill"></i>
</div>
<!-- 错误标题 -->
<h1>支付失败</h1>
<!-- 错误描述 -->
<p>抱歉,您的支付未能成功处理。<br>{{.error}}<br>请检查您的支付信息或稍后再试。</p>
<!-- 操作按钮 -->
<!-- <a href="#" class="btn animate__animated animate__pulse animate__infinite animate__slower">重试</a> -->
<!-- 可以添加返回按钮 -->
<!-- <a href="#" class="btn" style="margin-left: 15px;">返回首页</a> -->
<!-- 背景效果 -->
<div class="bg-shapes">
<div class="bg-shape"></div>
<div class="bg-shape"></div>
<div class="bg-shape"></div>
</div>
<script>
// 可选的 JavaScript 交互
// 例如,按钮点击事件处理
document.querySelector('.btn').addEventListener('click', function (e) {
e.preventDefault(); // 阻止默认链接行为
// 添加重试逻辑或页面跳转
alert('正在尝试重新支付...');
// window.location.href = 'retry-payment-url'; // 跳转到重试链接
});
</script>
</body>
<div class="container">
<!-- 错误图标 -->
<div class="error-icon-container">
<div class="laser-effect"></div>
<div class="laser-effect"></div>
<div class="laser-effect"></div>
<div class="error-icon">
<i class="ri-close-circle-fill"></i>
</div>
</div>
<!-- 错误标题 -->
<h1>支付失败</h1>
<!-- 错误描述 -->
<p>抱歉,您的支付未能成功处理。</p>
<!-- 订单编号 -->
<div class="order-id">
订单编号:{{.orderId}}
</div>
<!-- 错误信息卡片 -->
<div class="error-card glass">
<div class="error-detail">
<span class="detail-label">订单金额</span>
<span class="detail-value" style="font-size: 18px; color: var(--error-red);">¥{{.amount}}</span>
</div>
<div class="error-detail">
<span class="detail-label">失败原因</span>
<span class="detail-value error-reason">{{.errorReason}}</span>
</div>
</div>
<!-- 安全提示 -->
<div class="security-note">
<i class="ri-shield-warning-line"></i>
<span>您的支付信息安全</span>
</div>
</div>
</body>
</html>

View File

@@ -10,7 +10,7 @@
href="https://fonts.googleapis.com/css2?family=SF+Pro+Display:wght@400;500;600;700&family=SF+Pro+Text:wght@400;500;600&display=swap"
rel="stylesheet">
<!-- 引入图标库 -->
<!-- <link rel="stylesheet" href="../static/css/remixicon.min.css"> -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/remixicon/4.6.0/remixicon.min.css">
<!-- 引入动画库 -->
<link rel="stylesheet" href="../static/css/animate.min.css">
<!-- 引入图标库 -->
@@ -293,9 +293,10 @@
.order-id {
font-size: 14px;
color: var(--apple-gray);
margin-top: 30px;
margin-bottom: 20px;
word-break: break-all;
animation: fadeInUp 0.8s ease forwards;
animation-delay: 0.9s;
animation-delay: 0.7s;
opacity: 0;
}
@@ -335,7 +336,7 @@
<div class="laser-effect"></div>
<div class="laser-effect"></div>
<div class="success-icon">
<i class="ri-check-line"></i>
<i class="ri-check-fill"></i>
</div>
</div>
@@ -345,17 +346,22 @@
<!-- 成功描述 -->
<p>您的卡密已提交完成,请耐心等待处理结果。感谢您的配合</p>
<!-- 订单编号 -->
<div class="order-id">
订单编号:{{.orderId}}
</div>
<!-- 交易信息卡片 -->
<div class="transaction-card glass">
<div class="transaction-detail">
<span class="detail-label">交易状态</span>
<span class="detail-value" style="color: var(--success-green); display: flex; align-items: center;">
<span class="completed-icon"><i class="ri-check-line"></i></span>卡密已提交
<span class="completed-icon"><i class="ri-check-fill"></i></span>卡密已提交
</span>
</div>
<div class="transaction-detail">
<span class="detail-label">交易金额</span>
<span class="detail-value" style="font-size: 18px; color: #1d1d1f;">¥{{.faceMM}}</span>
<span class="detail-value" style="font-size: 18px; color: #1d1d1f;">¥{{.amount}}</span>
</div>
</div>

488
views/order_confirm.html Normal file
View File

@@ -0,0 +1,488 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>订单确认</title>
<!-- 引入字体 -->
<!-- 引入图标库 -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/remixicon/4.6.0/remixicon.min.css">
<!-- 引入动画库 -->
<link rel="stylesheet" href="../static/css/animate.min.css">
<!-- 引入图标库 -->
<link rel="stylesheet" href="../static/css/index-anxin.css">
<style>
/* 全局样式和重置 */
:root {
--primary-blue: #007aff;
--primary-light-blue: rgba(0, 122, 255, 0.15);
--apple-white: #ffffff;
--apple-gray: #8e8e93;
--apple-light-gray: #f5f5f7;
--border-color: #e5e5ea;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body,
html {
margin: 0;
padding: 0;
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;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
/* 容器样式 */
.container {
position: relative;
text-align: center;
padding: 40px 30px;
max-width: 400px;
width: 90%;
z-index: 10;
}
/* 背景效果 */
.bg-shapes {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
overflow: hidden;
}
.bg-shape {
position: absolute;
border-radius: 50%;
filter: blur(70px);
}
.bg-shape:nth-child(1) {
top: -10%;
left: -10%;
width: 500px;
height: 500px;
background: linear-gradient(to right, rgba(0, 122, 255, 0.2), rgba(0, 122, 255, 0));
animation: float 15s ease-in-out infinite;
}
.bg-shape:nth-child(2) {
bottom: -15%;
right: -10%;
width: 600px;
height: 600px;
background: linear-gradient(to left, rgba(0, 122, 255, 0.15), rgba(0, 122, 255, 0));
animation: float 20s ease-in-out infinite reverse;
}
.bg-shape:nth-child(3) {
top: 40%;
left: 60%;
width: 300px;
height: 300px;
background: linear-gradient(to top, rgba(0, 122, 255, 0.1), rgba(0, 122, 255, 0));
animation: float 18s ease-in-out infinite 2s;
}
@keyframes float {
0% {
transform: translate(0, 0) rotate(0deg);
}
50% {
transform: translate(30px, 20px) rotate(5deg);
}
100% {
transform: translate(0, 0) rotate(0deg);
}
}
/* 玻璃态效果 */
.glass {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
border-radius: 20px;
}
/* 确认图标容器 */
.confirm-icon-container {
position: relative;
width: 100px;
height: 100px;
margin: 0 auto 30px;
}
/* 确认图标 */
.confirm-icon {
position: relative;
z-index: 2;
width: 100px;
height: 100px;
background: var(--primary-blue);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 40px;
box-shadow: 0 10px 25px rgba(0, 122, 255, 0.3);
animation: scaleIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
/* 镭射效果 */
.laser-effect {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 150px;
height: 150px;
background: radial-gradient(circle, rgba(0, 122, 255, 0.8) 0%, rgba(0, 122, 255, 0) 70%);
border-radius: 50%;
z-index: 1;
opacity: 0;
animation: pulse 2s ease-in-out infinite;
}
.laser-effect:nth-child(2) {
width: 180px;
height: 180px;
animation-delay: 0.5s;
}
.laser-effect:nth-child(3) {
width: 210px;
height: 210px;
animation-delay: 1s;
}
/* 标题样式 */
h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 15px;
color: #1d1d1f;
font-family: 'SF Pro Display', sans-serif;
animation: fadeInUp 0.8s ease forwards;
animation-delay: 0.3s;
opacity: 0;
}
/* 描述文本样式 */
p {
font-size: 17px;
font-weight: 400;
line-height: 1.5;
margin-bottom: 30px;
color: var(--apple-gray);
animation: fadeInUp 0.8s ease forwards;
animation-delay: 0.5s;
opacity: 0;
}
/* 订单信息卡片 */
.order-card {
padding: 25px;
margin-bottom: 25px;
text-align: left;
animation: fadeInUp 0.8s ease forwards;
animation-delay: 0.7s;
opacity: 0;
}
.order-detail {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
font-size: 15px;
}
.order-detail:last-child {
margin-bottom: 0;
padding-top: 15px;
border-top: 1px solid rgba(0, 0, 0, 0.05);
font-weight: 600;
}
.detail-label {
color: var(--apple-gray);
}
.detail-value {
font-weight: 500;
color: #1d1d1f;
}
/* 按钮样式 */
.btn-container {
animation: fadeInUp 0.8s ease forwards;
animation-delay: 0.9s;
opacity: 0;
}
.btn {
width: 100%;
padding: 15px;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 15px;
font-family: 'SF Pro Text', sans-serif;
}
.btn-primary {
background: var(--primary-blue);
color: white;
box-shadow: 0 4px 15px rgba(0, 122, 255, 0.3);
}
.btn-primary:hover {
background: #0051d5;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 122, 255, 0.4);
}
.btn-secondary {
background: transparent;
color: var(--primary-blue);
border: 2px solid var(--primary-blue);
}
.btn-secondary:hover {
background: var(--primary-light-blue);
}
/* 动画定义 */
@keyframes scaleIn {
0% {
transform: scale(0);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
}
50% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.3;
}
100% {
transform: translate(-50%, -50%) scale(1.2);
opacity: 0;
}
}
@keyframes fadeInUp {
0% {
transform: translateY(20px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
/* 订单编号样式 */
.order-id {
font-size: 14px;
color: var(--apple-gray);
margin-bottom: 20px;
word-break: break-all;
animation: fadeInUp 0.8s ease forwards;
animation-delay: 0.7s;
opacity: 0;
}
/* 状态标签 */
.status-badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
/* 旋转动画 */
.animate-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.status-pending {
background: rgba(255, 149, 0, 0.15);
color: #ff9500;
}
.status-completed {
background: rgba(52, 199, 89, 0.15);
color: #34c759;
}
.status-processing {
background: rgba(0, 122, 255, 0.15);
color: #007aff;
}
</style>
</head>
<body>
<!-- 背景效果 -->
<div class="bg-shapes">
<div class="bg-shape"></div>
<div class="bg-shape"></div>
<div class="bg-shape"></div>
</div>
<div class="container">
<!-- 确认图标 -->
<div class="confirm-icon-container">
<div class="laser-effect"></div>
<div class="laser-effect"></div>
<div class="laser-effect"></div>
<div class="confirm-icon">
<i class="ri-loader-4-line animate-spin"></i>
</div>
</div>
<!-- 确认标题 -->
<h1>订单确认</h1>
<!-- 确认描述 -->
<p>请稍候,我们正在确认您的订单。完成后我们将通知您。</p>
<!-- 订单编号 -->
<div class="order-id">
订单编号:{{.orderId}}
</div>
<!-- 订单信息卡片 -->
<div class="order-card glass">
<div class="order-detail">
<span class="detail-label">订单金额</span>
<span class="detail-value" style="font-size: 18px; color: var(--primary-blue);">¥{{.amount}}</span>
</div>
<div class="order-detail">
<span class="detail-label">订单状态</span>
<span class="detail-value">
<span class="status-badge status-processing">处理中</span>
</span>
</div>
</div>
</div>
<script>
// 订单状态查询参数
const orderParams = {
orderNo: '{{.orderId}}',
payKey: '{{.payKey}}',
returnUrl: '{{.returnUrl}}'
};
// 检查订单状态
async function checkOrderStatus() {
try {
const response = await fetch('/api/query-order-status', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(orderParams)
});
const result = await response.json();
if (result.code === 200) {
const data = result.data;
switch (data.status) {
case 'success':
// 支付成功,跳转到成功页面
let successUrl = '/ok.html?orderNo=' + data.orderNo + '&faceMM=' + data.amount;
if (data.returnUrl) {
successUrl += '&returnUrl=' + encodeURIComponent(data.returnUrl);
}
window.location.href = successUrl;
break;
case 'fail':
// 支付失败,跳转到失败页面
let errorUrl = '/error.html?orderNo=' + data.orderNo + '&faceMM=' + data.amount + '&errorReason=' + encodeURIComponent(data.errorReason || '支付失败');
if (data.returnUrl) {
errorUrl += '&returnUrl=' + encodeURIComponent(data.returnUrl);
}
window.location.href = errorUrl;
break;
case 'wait':
case 'processing':
// 继续等待,不做处理
console.log('订单状态:', data.status);
break;
}
} else {
console.error('查询订单状态失败:', result.msg);
}
} catch (error) {
console.error('请求失败:', error);
}
}
// 每3秒检查一次订单状态
const statusInterval = setInterval(checkOrderStatus, 3000);
// 页面卸载时清除定时器
window.addEventListener('beforeunload', () => {
clearInterval(statusInterval);
});
// 页面加载完成后立即检查一次
document.addEventListener('DOMContentLoaded', () => {
checkOrderStatus();
});
</script>
</body>
</html>