first commit
This commit is contained in:
commit
7a7a9759ca
11
.env.example
Normal file
11
.env.example
Normal file
@ -0,0 +1,11 @@
|
||||
# 环境变量配置文件
|
||||
# UPay 配置
|
||||
UPAY_APP_ID=E7c4dss9
|
||||
UPAY_APP_SECRET=Hwc56INsabRau2yn
|
||||
|
||||
# 数据库配置
|
||||
DB_PATH=./database/shop.db
|
||||
|
||||
# 服务器配置
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
94
.gitignore
vendored
Normal file
94
.gitignore
vendored
Normal file
@ -0,0 +1,94 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
database/*.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Linux
|
||||
*~
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Payment config files (if any)
|
||||
config/payment-keys.json
|
||||
config/secrets.json
|
||||
payment-config.json
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
117
README.md
Normal file
117
README.md
Normal file
@ -0,0 +1,117 @@
|
||||
# USDT 支付商城
|
||||
|
||||
这是一个基于 Node.js 和 UPay API 的 USDT 支付商城系统。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 📱 现代化响应式设计
|
||||
- 💰 USDT 加密货币支付
|
||||
- 🛒 简洁的购买流程
|
||||
- 📦 订单管理系统
|
||||
- 🔒 安全的支付处理
|
||||
- 📊 实时支付状态跟踪
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: Node.js + Express
|
||||
- **数据库**: SQLite3
|
||||
- **支付**: UPay API (USDT支付)
|
||||
- **前端**: 原生 JavaScript + CSS3
|
||||
- **样式**: CSS Grid + Flexbox
|
||||
|
||||
## 安装和运行
|
||||
|
||||
1. 克隆项目并安装依赖:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. 配置 UPay API:
|
||||
```bash
|
||||
# 设置环境变量
|
||||
export UPAY_APP_ID=your-upay-app-id
|
||||
export UPAY_APP_SECRET=your-upay-app-secret
|
||||
```
|
||||
|
||||
3. 启动服务器:
|
||||
```bash
|
||||
# 开发模式
|
||||
npm run dev
|
||||
|
||||
# 生产模式
|
||||
npm start
|
||||
```
|
||||
|
||||
4. 访问 http://localhost:3000
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
myusdtshop/
|
||||
├── server.js # 主服务器文件
|
||||
├── package.json # 项目配置
|
||||
├── database/ # SQLite 数据库文件
|
||||
├── public/ # 前端静态文件
|
||||
│ ├── index.html # 主页面
|
||||
│ ├── css/
|
||||
│ │ └── style.css # 样式文件
|
||||
│ ├── js/
|
||||
│ │ └── main.js # JavaScript 逻辑
|
||||
│ └── images/ # 图片资源
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
- `GET /api/products` - 获取产品信息
|
||||
- `POST /api/orders` - 创建订单
|
||||
- `POST /api/payment/create` - 创建支付
|
||||
- `POST /api/payment/callback` - 支付回调
|
||||
- `GET /api/orders/:order_id` - 获取订单状态
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 配置 UPay
|
||||
|
||||
1. 在 [UPay](https://upay.ink) 注册商户账户
|
||||
2. 获取 App ID 和 App Secret
|
||||
3. 设置环境变量或修改 server.js 中的配置
|
||||
|
||||
### 自定义产品
|
||||
|
||||
修改 server.js 中的 `PRODUCTS` 对象:
|
||||
|
||||
```javascript
|
||||
const PRODUCTS = {
|
||||
'your-product-id': {
|
||||
name: '您的产品名称',
|
||||
price: 99.99,
|
||||
description: '产品描述'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 支付流程
|
||||
|
||||
1. 用户选择产品和数量
|
||||
2. 填写收货信息
|
||||
3. 创建订单
|
||||
4. 跳转到支付页面完成 USDT 支付
|
||||
5. 系统通过回调自动确认支付状态
|
||||
|
||||
## UPay 集成特性
|
||||
|
||||
- 支持 USDT 直接转账到商户钱包
|
||||
- 实时支付状态回调通知
|
||||
- MD5 签名验证确保安全
|
||||
- 支持多种支付状态处理
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
- 请勿将 App Secret 提交到版本控制
|
||||
- 生产环境请使用 HTTPS
|
||||
- 定期备份数据库
|
||||
- 验证所有支付回调签名
|
||||
|
||||
## 许可证
|
||||
|
||||
ISC License
|
||||
24
package.json
Normal file
24
package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "usdt-shop",
|
||||
"version": "1.0.0",
|
||||
"description": "USDT Payment Product Sales Website",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"sqlite3": "^5.1.6",
|
||||
"axios": "^1.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"body-parser": "^1.20.2",
|
||||
"crypto": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
},
|
||||
"keywords": ["usdt", "payment", "shop", "nowpayments"],
|
||||
"author": "Aaron",
|
||||
"license": "ISC"
|
||||
}
|
||||
116
public/admin.html
Normal file
116
public/admin.html
Normal file
@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>订单管理 - USDT商城</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>订单管理系统</h1>
|
||||
<p>管理所有订单信息和发货状态</p>
|
||||
</header>
|
||||
|
||||
<div class="stats-section">
|
||||
<div class="stat-card">
|
||||
<h3>总订单数</h3>
|
||||
<span id="totalOrders">0</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>已支付未发货</h3>
|
||||
<span id="pendingOrders">0</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>已发货</h3>
|
||||
<span id="shippedOrders">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="orders-section">
|
||||
<div class="section-header">
|
||||
<h2>订单列表</h2>
|
||||
<div class="header-controls">
|
||||
<div class="search-box">
|
||||
<input type="text" id="searchOrder" placeholder="搜索订单号或客户姓名">
|
||||
<button onclick="searchOrders()" class="search-btn">搜索</button>
|
||||
</div>
|
||||
<select id="statusFilter" onchange="filterOrders()">
|
||||
<option value="">所有状态</option>
|
||||
<option value="pending">未支付</option>
|
||||
<option value="finished">已支付未发货</option>
|
||||
<option value="shipped">已发货</option>
|
||||
<option value="failed">支付失败</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="orders-table-container">
|
||||
<table class="orders-table" id="ordersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>客户信息</th>
|
||||
<th>产品</th>
|
||||
<th>金额</th>
|
||||
<th>支付状态</th>
|
||||
<th>发货状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ordersTableBody">
|
||||
<tr>
|
||||
<td colspan="8" class="loading">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发货模态框 -->
|
||||
<div id="shippingModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>订单发货</h3>
|
||||
<span class="close" onclick="closeShippingModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="shipping-form">
|
||||
<div class="form-group">
|
||||
<label for="trackingNumber">运单号:*</label>
|
||||
<input type="text" id="trackingNumber" placeholder="请输入快递运单号" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="shippingNotes">发货备注:</label>
|
||||
<textarea id="shippingNotes" rows="3" placeholder="选填:发货备注信息"></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button onclick="confirmShipping()" class="btn-primary">确认发货</button>
|
||||
<button onclick="closeShippingModal()" class="btn-secondary">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情模态框 -->
|
||||
<div id="orderDetailModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>订单详情</h3>
|
||||
<span class="close" onclick="closeOrderDetail()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="order-detail-content" id="orderDetailContent">
|
||||
<!-- 订单详情将在这里动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
455
public/css/admin.css
Normal file
455
public/css/admin.css
Normal file
@ -0,0 +1,455 @@
|
||||
/* 订单管理页面样式 */
|
||||
|
||||
/* 管理员导航 - 已移除 */
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(145deg, #1a1a1a, #2d2d2d);
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
border: 1px solid #444;
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #ffd700, #ffed4a);
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
color: #b0b0b0;
|
||||
font-size: 1.1em;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-card span {
|
||||
color: #ffd700;
|
||||
font-size: 2.2em;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
text-shadow: 0 0 20px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 订单表格区域 */
|
||||
.orders-section {
|
||||
background: linear-gradient(145deg, #1a1a1a, #2d2d2d);
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
border: 1px solid #444;
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #333;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
color: #ffffff;
|
||||
font-size: 1.5em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding: 10px 15px;
|
||||
background: #0a0a0a;
|
||||
border: 2px solid #444;
|
||||
border-radius: 8px;
|
||||
color: #e0e0e0;
|
||||
width: 250px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: #ffd700;
|
||||
box-shadow: 0 0 15px rgba(255, 215, 0, 0.2);
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 10px 18px;
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4a);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
.section-header select {
|
||||
padding: 10px 15px;
|
||||
background: #0a0a0a;
|
||||
border: 2px solid #444;
|
||||
border-radius: 8px;
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.section-header select:focus {
|
||||
outline: none;
|
||||
border-color: #ffd700;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.orders-table-container {
|
||||
overflow-x: auto;
|
||||
background: #0a0a0a;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.orders-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.orders-table th {
|
||||
background: linear-gradient(145deg, #2d2d2d, #3d3d3d);
|
||||
color: #ffd700;
|
||||
padding: 15px 12px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #444;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.orders-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #333;
|
||||
color: #e0e0e0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.orders-table tbody tr {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.orders-table tbody tr:hover {
|
||||
background-color: rgba(255, 215, 0, 0.05);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #b0b0b0;
|
||||
font-style: italic;
|
||||
padding: 50px !important;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: rgba(255, 193, 7, 0.2);
|
||||
color: #ffc107;
|
||||
border: 1px solid #ffc107;
|
||||
}
|
||||
|
||||
.status-finished {
|
||||
background: rgba(40, 167, 69, 0.2);
|
||||
color: #28a745;
|
||||
border: 1px solid #28a745;
|
||||
}
|
||||
|
||||
.status-shipped {
|
||||
background: rgba(0, 123, 255, 0.2);
|
||||
color: #007bff;
|
||||
border: 1px solid #007bff;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(108, 117, 125, 0.2);
|
||||
color: #6c757d;
|
||||
border: 1px solid #6c757d;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background: rgba(220, 53, 69, 0.2);
|
||||
color: #dc3545;
|
||||
border: 1px solid #dc3545;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 4px;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-copy {
|
||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||
color: #fff;
|
||||
border: 1px solid #3498db;
|
||||
}
|
||||
|
||||
.btn-copy:hover {
|
||||
background: linear-gradient(135deg, #2980b9, #1f639a);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.4);
|
||||
}
|
||||
|
||||
.btn-ship {
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4a);
|
||||
color: #000;
|
||||
border: 1px solid #ffd700;
|
||||
}
|
||||
|
||||
.btn-ship:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
/* 订单详情模态框 */
|
||||
.order-detail-content {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 25px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(145deg, #0a0a0a, #1a1a1a);
|
||||
border-radius: 10px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.detail-section h4 {
|
||||
color: #ffd700;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1em;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.detail-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: bold;
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 发货表单 */
|
||||
.shipping-form .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.shipping-form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.shipping-form input,
|
||||
.shipping-form select,
|
||||
.shipping-form textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #0a0a0a;
|
||||
border: 2px solid #444;
|
||||
border-radius: 8px;
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.shipping-form input:focus,
|
||||
.shipping-form select:focus,
|
||||
.shipping-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: #ffd700;
|
||||
box-shadow: 0 0 15px rgba(255, 215, 0, 0.2);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 12px 25px;
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4a);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 12px 25px;
|
||||
background: linear-gradient(145deg, #333, #444);
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(145deg, #444, #555);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 复制按钮样式 */
|
||||
.copy-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ffd700;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
opacity: 1;
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 复制成功提示 */
|
||||
.copy-toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: linear-gradient(135deg, #00ff88, #00cc6a);
|
||||
color: #000;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 8px 25px rgba(0, 255, 136, 0.3);
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.stats-section {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.orders-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.orders-table th,
|
||||
.orders-table td {
|
||||
padding: 8px 6px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
font-size: 10px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
6
public/css/spinner.css
Normal file
6
public/css/spinner.css
Normal file
@ -0,0 +1,6 @@
|
||||
<style>
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
611
public/css/style.css
Normal file
611
public/css/style.css
Normal file
@ -0,0 +1,611 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', 'Microsoft YaHei', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #e0e0e0;
|
||||
background: #0a0a0a;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 40px;
|
||||
align-items: start;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 订单管理按钮 */
|
||||
.admin-floating-btn {
|
||||
position: fixed;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
z-index: 1000;
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4a);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
padding: 12px 25px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
.admin-floating-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(255, 215, 0, 0.5);
|
||||
}
|
||||
|
||||
/* 产品卡片 */
|
||||
.product-section {
|
||||
background: linear-gradient(145deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
background: linear-gradient(135deg, #333 0%, #444 100%);
|
||||
border-radius: 15px;
|
||||
margin-bottom: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border: 2px solid #444;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.product-image::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(45deg, transparent 49%, rgba(255, 215, 0, 0.1) 50%, transparent 51%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.product-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.product-info h2 {
|
||||
color: #ffffff;
|
||||
margin-bottom: 15px;
|
||||
font-size: 2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
color: #b0b0b0;
|
||||
margin-bottom: 25px;
|
||||
line-height: 1.6;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 2.2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(45deg, #ffd700, #ffed4a);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-shadow: 0 0 20px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
.price .currency,
|
||||
.price .unit {
|
||||
font-size: 0.7em;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* 订单表单 */
|
||||
.order-section {
|
||||
background: linear-gradient(145deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
border: 2px solid #444;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
background: #0a0a0a;
|
||||
color: #e0e0e0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #ffd700;
|
||||
box-shadow: 0 0 20px rgba(255, 215, 0, 0.2);
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.form-group input::placeholder,
|
||||
.form-group textarea::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 数量控制 */
|
||||
.quantity-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quantity-controls button {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border: 2px solid #ffd700;
|
||||
background: linear-gradient(145deg, #1a1a1a, #2d2d2d);
|
||||
color: #ffd700;
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quantity-controls button:hover {
|
||||
background: linear-gradient(145deg, #ffd700, #ffed4a);
|
||||
color: #000;
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 8px 20px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
.quantity-controls input {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: linear-gradient(145deg, #0a0a0a, #1a1a1a);
|
||||
border-radius: 10px;
|
||||
border: 2px solid #333;
|
||||
background: linear-gradient(45deg, #ffd700, #ffed4a);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.order-btn {
|
||||
width: 100%;
|
||||
padding: 18px;
|
||||
background: linear-gradient(135deg, #ffd700 0%, #ffed4a 100%);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.order-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.order-btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.order-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 15px 35px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
.order-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 模态框 */
|
||||
.modal {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: linear-gradient(145deg, #1a1a1a, #2d2d2d);
|
||||
padding: 0;
|
||||
border-radius: 20px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
border: 1px solid #444;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, #ffd700 0%, #ffed4a 100%);
|
||||
color: #000;
|
||||
padding: 25px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.4em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #000;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 30px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.order-summary h4 {
|
||||
color: #ffffff;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.summary-item:last-child {
|
||||
border-bottom: none;
|
||||
font-weight: bold;
|
||||
color: #ffd700;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.payment-section {
|
||||
text-align: center;
|
||||
padding: 25px;
|
||||
background: linear-gradient(145deg, #0a0a0a, #1a1a1a);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.payment-section p {
|
||||
color: #b0b0b0;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.pay-btn {
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4a);
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 15px 35px;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.pay-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
/* 支付状态 */
|
||||
.payment-status {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0a0a0a;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
background: linear-gradient(145deg, #1a1a1a, #2d2d2d);
|
||||
border-radius: 20px;
|
||||
border: 1px solid #444;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 0 auto 30px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 6px solid #333;
|
||||
border-top: 6px solid #ffd700;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.status-content h3 {
|
||||
color: #ffffff;
|
||||
margin-bottom: 15px;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.status-content p {
|
||||
color: #b0b0b0;
|
||||
margin-bottom: 30px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.order-info {
|
||||
background: linear-gradient(145deg, #0a0a0a, #1a1a1a);
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.order-info p {
|
||||
margin-bottom: 12px;
|
||||
color: #e0e0e0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.status-btn {
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4a);
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 15px 35px;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.status-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
/* 成功/失败图标 */
|
||||
.success-icon {
|
||||
color: #00ff88;
|
||||
font-size: 100px;
|
||||
text-shadow: 0 0 30px rgba(0, 255, 136, 0.5);
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
color: #ff4757;
|
||||
font-size: 100px;
|
||||
text-shadow: 0 0 30px rgba(255, 71, 87, 0.5);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.product-section,
|
||||
.order-section {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
padding: 30px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.admin-floating-btn {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 10px 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 管理员登录表单样式 */
|
||||
.login-form .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-form input {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
background: #0a0a0a;
|
||||
border: 2px solid #444;
|
||||
border-radius: 8px;
|
||||
color: #e0e0e0;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.login-form input:focus {
|
||||
outline: none;
|
||||
border-color: #ffd700;
|
||||
box-shadow: 0 0 15px rgba(255, 215, 0, 0.2);
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.login-form .form-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.login-form .btn-primary {
|
||||
padding: 12px 25px;
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4a);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-form .btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
.login-form .btn-secondary {
|
||||
padding: 12px 25px;
|
||||
background: linear-gradient(145deg, #333, #444);
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-form .btn-secondary:hover {
|
||||
background: linear-gradient(145deg, #444, #555);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4757;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 71, 87, 0.1);
|
||||
border: 1px solid #ff4757;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
BIN
public/images/image01.jpg
Normal file
BIN
public/images/image01.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 407 KiB |
163
public/index.html
Normal file
163
public/index.html
Normal file
@ -0,0 +1,163 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>USDT 商城</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/spinner.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 浮动管理按钮 -->
|
||||
<button class="admin-floating-btn" onclick="showAdminLogin()">订单管理</button>
|
||||
|
||||
<main>
|
||||
<!-- 产品展示区 -->
|
||||
<section class="product-section">
|
||||
<div class="product-card" id="product-card">
|
||||
<div class="product-image">
|
||||
<img src="/images/image01.jpg" alt="产品图片" id="product-img">
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<h2 id="product-name">高级产品</h2>
|
||||
<p class="product-description" id="product-description">这是我们的高级产品,质量优秀</p>
|
||||
<div class="price">
|
||||
<span class="currency">$</span>
|
||||
<span id="product-price">99.99</span>
|
||||
<span class="unit">USDT</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 购买表单区 -->
|
||||
<section class="order-section">
|
||||
<form id="orderForm">
|
||||
<!-- 数量选择 -->
|
||||
<div class="form-group">
|
||||
<label for="quantity">购买数量:</label>
|
||||
<div class="quantity-controls">
|
||||
<button type="button" id="decrease-qty">-</button>
|
||||
<input type="number" id="quantity" name="quantity" value="1" min="1" max="999">
|
||||
<button type="button" id="increase-qty">+</button>
|
||||
</div>
|
||||
<div class="total-price">
|
||||
总价:<span id="total-price">$99.99</span> USDT
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 客户信息 -->
|
||||
<div class="form-group">
|
||||
<label for="customer_name">姓名:*</label>
|
||||
<input type="text" id="customer_name" name="customer_name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="customer_email">邮箱:</label>
|
||||
<input type="email" id="customer_email" name="customer_email">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="customer_phone">电话:</label>
|
||||
<input type="tel" id="customer_phone" name="customer_phone">
|
||||
</div>
|
||||
|
||||
<!-- 收货地址 -->
|
||||
<div class="form-group">
|
||||
<label for="shipping_address">收货地址:*</label>
|
||||
<textarea id="shipping_address" name="shipping_address" rows="3" required
|
||||
placeholder="请输入详细的收货地址,包括省市区县和具体地址"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<button type="submit" id="submit-order" class="order-btn">
|
||||
<span class="btn-text">立即下单</span>
|
||||
<span class="btn-loading" style="display: none;">处理中...</span>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 订单确认模态框 -->
|
||||
<div id="orderModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>订单确认</h3>
|
||||
<span class="close" id="closeModal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="order-summary">
|
||||
<h4>订单信息</h4>
|
||||
<div class="summary-item">
|
||||
<span>订单号:</span>
|
||||
<span id="modal-order-id"></span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>产品:</span>
|
||||
<span id="modal-product-name"></span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>数量:</span>
|
||||
<span id="modal-quantity"></span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>总价:</span>
|
||||
<span id="modal-total-price"></span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>收货地址:</span>
|
||||
<span id="modal-address"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="payment-section">
|
||||
<p>请使用USDT(TRC20)完成支付</p>
|
||||
<button id="pay-now-btn" class="pay-btn">前往支付</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 支付状态页面 -->
|
||||
<div id="paymentStatus" class="payment-status" style="display: none;">
|
||||
<div class="status-content">
|
||||
<div class="status-icon" id="status-icon">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
<h3 id="status-title">支付处理中...</h3>
|
||||
<p id="status-message">请完成USDT支付,我们正在确认您的交易</p>
|
||||
<div class="order-info">
|
||||
<p>订单号:<span id="status-order-id"></span></p>
|
||||
<p>金额:<span id="status-amount"></span> USDT</p>
|
||||
</div>
|
||||
<button id="check-status-btn" class="status-btn">检查支付状态</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理员登录模态框 -->
|
||||
<div id="adminLoginModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>管理员登录</h3>
|
||||
<span class="close" onclick="closeAdminLogin()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="adminPassword">请输入管理密码:</label>
|
||||
<input type="password" id="adminPassword" placeholder="输入密码" autofocus>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button onclick="verifyAdminPassword()" class="btn-primary">登录</button>
|
||||
<button onclick="closeAdminLogin()" class="btn-secondary">取消</button>
|
||||
</div>
|
||||
<div id="loginError" class="error-message" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
443
public/js/admin.js
Normal file
443
public/js/admin.js
Normal file
@ -0,0 +1,443 @@
|
||||
// 订单管理页面脚本
|
||||
let currentOrders = [];
|
||||
let currentOrderId = null;
|
||||
|
||||
// DOM 元素
|
||||
const ordersTableBody = document.getElementById('ordersTableBody');
|
||||
const searchInput = document.getElementById('searchOrder');
|
||||
const statusFilter = document.getElementById('statusFilter');
|
||||
const orderDetailModal = document.getElementById('orderDetailModal');
|
||||
const shippingModal = document.getElementById('shippingModal');
|
||||
|
||||
// 页面加载时初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadOrders();
|
||||
});
|
||||
|
||||
// 加载订单列表
|
||||
async function loadOrders(search = '', status = '') {
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (search) params.append('search', search);
|
||||
if (status) params.append('status', status);
|
||||
|
||||
const response = await fetch(`/api/admin/orders?${params}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
currentOrders = data.orders;
|
||||
updateOrdersTable(data.orders);
|
||||
updateStats(data.stats);
|
||||
} else {
|
||||
throw new Error(data.error || '加载订单失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load orders error:', error);
|
||||
showError('加载订单失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新订单表格
|
||||
function updateOrdersTable(orders) {
|
||||
if (!orders || orders.length === 0) {
|
||||
ordersTableBody.innerHTML = '<tr><td colspan="8" class="loading">暂无订单数据</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
ordersTableBody.innerHTML = orders.map(order => `
|
||||
<tr>
|
||||
<td>
|
||||
<strong>${order.order_id}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<div style="line-height: 1.4;">
|
||||
<strong>${order.customer_name}</strong>
|
||||
<button class="copy-btn" onclick="copyOrderInfo('${order.order_id}')" title="复制收货信息" style="margin-left: 5px; font-size: 12px;">📋</button><br>
|
||||
<small style="color: #666;">${order.customer_phone || '未提供电话'}</small><br>
|
||||
<small style="color: #666; font-size: 11px; max-width: 200px; display: inline-block; word-wrap: break-word;">${order.shipping_address ? (order.shipping_address.length > 30 ? order.shipping_address.substring(0, 30) + '...' : order.shipping_address) : '未提供地址'}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
${order.product_name}<br>
|
||||
<small style="color: #888;">数量: ${order.quantity}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<strong style="color: #ffd700;">$${order.total_amount.toFixed(2)}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge status-${order.payment_status}">
|
||||
${getStatusText(order.payment_status, 'payment')}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge status-${order.shipping_status}">
|
||||
${getStatusText(order.shipping_status, 'shipping')}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
${formatDate(order.created_at)}
|
||||
</td>
|
||||
<td>
|
||||
<button class="action-btn btn-ship" onclick="showShippingModal('${order.order_id}')">
|
||||
📦 发货
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 更新统计数据
|
||||
function updateStats(stats) {
|
||||
if (stats) {
|
||||
const totalElement = document.getElementById('totalOrders');
|
||||
const pendingElement = document.getElementById('pendingOrders');
|
||||
const shippedElement = document.getElementById('shippedOrders');
|
||||
|
||||
if (totalElement) totalElement.textContent = stats.total || 0;
|
||||
if (pendingElement) pendingElement.textContent = stats.pending_ship || 0;
|
||||
if (shippedElement) shippedElement.textContent = stats.shipped || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索订单
|
||||
function searchOrders() {
|
||||
const search = searchInput.value.trim();
|
||||
const status = statusFilter.value;
|
||||
loadOrders(search, status);
|
||||
}
|
||||
|
||||
// 筛选订单
|
||||
function filterOrders() {
|
||||
const search = searchInput.value.trim();
|
||||
const status = statusFilter.value;
|
||||
loadOrders(search, status);
|
||||
}
|
||||
|
||||
// 显示订单详情
|
||||
async function showOrderDetail(orderId) {
|
||||
try {
|
||||
const response = await fetch(`/api/orders/${orderId}`);
|
||||
const order = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
const detailContent = document.getElementById('orderDetailContent');
|
||||
detailContent.innerHTML = `
|
||||
<div class="detail-section">
|
||||
<h4>基本信息</h4>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">订单号:</span>
|
||||
<span class="detail-value">${order.order_id}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">支付ID:</span>
|
||||
<span class="detail-value">${order.payment_id || '未设置'}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">创建时间:</span>
|
||||
<span class="detail-value">${formatDate(order.created_at)}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">更新时间:</span>
|
||||
<span class="detail-value">${formatDate(order.updated_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>客户信息</h4>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">姓名:</span>
|
||||
<span class="detail-value">${order.customer_name}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">邮箱:</span>
|
||||
<span class="detail-value">${order.customer_email || '未提供'}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">电话:</span>
|
||||
<span class="detail-value">${order.customer_phone || '未提供'}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">收货地址:</span>
|
||||
<span class="detail-value">
|
||||
${order.shipping_address}
|
||||
<button class="copy-btn" onclick="copyText('${order.shipping_address.replace(/'/g, "\\'")}')">📋</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>商品信息</h4>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">产品名称:</span>
|
||||
<span class="detail-value">${order.product_name}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">单价:</span>
|
||||
<span class="detail-value">$${order.unit_price.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">数量:</span>
|
||||
<span class="detail-value">${order.quantity}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">总金额:</span>
|
||||
<span class="detail-value" style="color: #ffd700; font-weight: bold;">$${order.total_amount.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>状态信息</h4>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">支付状态:</span>
|
||||
<span class="detail-value">
|
||||
<span class="status-badge status-${order.payment_status}">
|
||||
${getStatusText(order.payment_status, 'payment')}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">发货状态:</span>
|
||||
<span class="detail-value">
|
||||
<span class="status-badge status-${order.shipping_status}">
|
||||
${getStatusText(order.shipping_status, 'shipping')}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
${order.tracking_number ? `
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">快递单号:</span>
|
||||
<span class="detail-value">${order.tracking_number}</span>
|
||||
</div>` : ''}
|
||||
${order.shipping_notes ? `
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">发货备注:</span>
|
||||
<span class="detail-value">${order.shipping_notes}</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
orderDetailModal.style.display = 'flex';
|
||||
} else {
|
||||
throw new Error(order.error || '获取订单详情失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Show order detail error:', error);
|
||||
alert('获取订单详情失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 复制订单收货信息
|
||||
function copyOrderInfo(orderId) {
|
||||
const order = currentOrders.find(o => o.order_id === orderId);
|
||||
if (!order) return;
|
||||
|
||||
// 组合收货信息:姓名, 电话, 地址
|
||||
let infoText = order.customer_name;
|
||||
if (order.customer_phone) {
|
||||
infoText += ', ' + order.customer_phone;
|
||||
}
|
||||
infoText += ', ' + order.shipping_address;
|
||||
|
||||
// 复制到剪贴板
|
||||
copyText(infoText);
|
||||
}
|
||||
|
||||
// 显示发货模态框
|
||||
function showShippingModal(orderId) {
|
||||
currentOrderId = orderId;
|
||||
const order = currentOrders.find(o => o.order_id === orderId);
|
||||
if (!order) return;
|
||||
|
||||
// 清空表单
|
||||
document.getElementById('trackingNumber').value = '';
|
||||
document.getElementById('shippingNotes').value = '';
|
||||
|
||||
// 显示模态框
|
||||
shippingModal.style.display = 'flex';
|
||||
document.getElementById('trackingNumber').focus();
|
||||
}
|
||||
|
||||
// 关闭发货模态框
|
||||
function closeShippingModal() {
|
||||
shippingModal.style.display = 'none';
|
||||
currentOrderId = null;
|
||||
}
|
||||
|
||||
// 确认发货
|
||||
async function confirmShipping() {
|
||||
const trackingNumber = document.getElementById('trackingNumber').value.trim();
|
||||
const shippingNotes = document.getElementById('shippingNotes').value.trim();
|
||||
|
||||
if (!trackingNumber) {
|
||||
alert('请输入运单号');
|
||||
document.getElementById('trackingNumber').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentOrderId) {
|
||||
alert('订单ID错误');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/orders/${currentOrderId}/shipping`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
shipping_status: 'shipped',
|
||||
tracking_number: trackingNumber,
|
||||
shipping_notes: shippingNotes
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
alert('订单已成功发货!');
|
||||
closeShippingModal();
|
||||
loadOrders(); // 重新加载订单列表
|
||||
} else {
|
||||
throw new Error(result.error || '发货失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Shipping error:', error);
|
||||
alert('发货失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 确认并标记为已发货
|
||||
function confirmMarkAsShipped(orderId) {
|
||||
if (confirm('确认标记此订单为已发货吗?')) {
|
||||
markAsShipped(orderId);
|
||||
}
|
||||
}
|
||||
|
||||
// 一键标记为已发货
|
||||
async function markAsShipped(orderId) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/orders/${orderId}/shipping`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
shipping_status: 'shipped'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
alert('订单已标记为发货!');
|
||||
loadOrders(); // 重新加载订单列表
|
||||
} else {
|
||||
throw new Error(result.error || '标记发货失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Mark as shipped error:', error);
|
||||
alert('标记发货失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 复制文本到剪贴板
|
||||
async function copyText(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
// 显示复制成功提示
|
||||
showCopySuccess();
|
||||
} catch (err) {
|
||||
// 降级方案:使用传统方法
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
showCopySuccess();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示复制成功提示
|
||||
function showCopySuccess() {
|
||||
// 创建提示元素
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'copy-toast';
|
||||
toast.textContent = '已复制到剪贴板';
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 3秒后移除提示
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 关闭订单详情模态框
|
||||
function closeOrderDetail() {
|
||||
orderDetailModal.style.display = 'none';
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status, type) {
|
||||
if (type === 'payment') {
|
||||
switch(status) {
|
||||
case 'pending': return '未支付';
|
||||
case 'finished': return '已支付';
|
||||
case 'failed': return '支付失败';
|
||||
case 'confirming': return '确认中';
|
||||
default: return status || '未知';
|
||||
}
|
||||
} else if (type === 'shipping') {
|
||||
switch(status) {
|
||||
case 'pending': return '未发货';
|
||||
case 'shipped': return '已发货';
|
||||
default: return status || '未知';
|
||||
}
|
||||
}
|
||||
return status || '未知';
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading() {
|
||||
ordersTableBody.innerHTML = '<tr><td colspan="8" class="loading">加载中...</td></tr>';
|
||||
}
|
||||
|
||||
// 显示错误信息
|
||||
function showError(message) {
|
||||
ordersTableBody.innerHTML = `<tr><td colspan="8" class="loading" style="color: #ff4757;">错误: ${message}</td></tr>`;
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '未设置';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 键盘事件监听
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeOrderDetail();
|
||||
closeShippingModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索框回车事件
|
||||
searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
searchOrders();
|
||||
}
|
||||
});
|
||||
369
public/js/main.js
Normal file
369
public/js/main.js
Normal file
@ -0,0 +1,369 @@
|
||||
// 全局变量
|
||||
let currentProduct = null;
|
||||
let currentOrder = null;
|
||||
|
||||
// DOM 元素
|
||||
const productPrice = document.getElementById('product-price');
|
||||
const quantityInput = document.getElementById('quantity');
|
||||
const totalPriceElement = document.getElementById('total-price');
|
||||
const orderForm = document.getElementById('orderForm');
|
||||
const orderModal = document.getElementById('orderModal');
|
||||
const paymentStatus = document.getElementById('paymentStatus');
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadProductData();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
// 加载产品数据
|
||||
async function loadProductData() {
|
||||
try {
|
||||
const response = await fetch('/api/products');
|
||||
const products = await response.json();
|
||||
|
||||
// 使用第一个产品作为展示产品
|
||||
const productId = Object.keys(products)[0];
|
||||
currentProduct = { id: productId, ...products[productId] };
|
||||
|
||||
// 更新界面
|
||||
document.getElementById('product-name').textContent = currentProduct.name;
|
||||
document.getElementById('product-description').textContent = currentProduct.description;
|
||||
document.getElementById('product-price').textContent = currentProduct.price.toFixed(2);
|
||||
|
||||
updateTotalPrice();
|
||||
} catch (error) {
|
||||
console.error('加载产品数据失败:', error);
|
||||
alert('加载产品信息失败,请刷新页面重试');
|
||||
}
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
function setupEventListeners() {
|
||||
// 数量控制
|
||||
document.getElementById('decrease-qty').addEventListener('click', () => {
|
||||
const current = parseInt(quantityInput.value) || 1;
|
||||
if (current > 1) {
|
||||
quantityInput.value = current - 1;
|
||||
updateTotalPrice();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('increase-qty').addEventListener('click', () => {
|
||||
const current = parseInt(quantityInput.value) || 1;
|
||||
if (current < 999) {
|
||||
quantityInput.value = current + 1;
|
||||
updateTotalPrice();
|
||||
}
|
||||
});
|
||||
|
||||
// 数量输入变化
|
||||
quantityInput.addEventListener('input', updateTotalPrice);
|
||||
quantityInput.addEventListener('change', validateQuantity);
|
||||
|
||||
// 表单提交
|
||||
orderForm.addEventListener('submit', handleOrderSubmit);
|
||||
|
||||
// 模态框控制
|
||||
document.getElementById('closeModal').addEventListener('click', closeOrderModal);
|
||||
document.getElementById('pay-now-btn').addEventListener('click', handlePayNow);
|
||||
|
||||
// 支付状态检查
|
||||
document.getElementById('check-status-btn').addEventListener('click', checkPaymentStatus);
|
||||
|
||||
// 点击模态框外部关闭
|
||||
orderModal.addEventListener('click', (e) => {
|
||||
if (e.target === orderModal) {
|
||||
closeOrderModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新总价
|
||||
function updateTotalPrice() {
|
||||
if (!currentProduct) return;
|
||||
|
||||
const quantity = parseInt(quantityInput.value) || 1;
|
||||
const total = (currentProduct.price * quantity).toFixed(2);
|
||||
totalPriceElement.textContent = `$${total}`;
|
||||
}
|
||||
|
||||
// 验证数量
|
||||
function validateQuantity() {
|
||||
const value = parseInt(quantityInput.value);
|
||||
if (isNaN(value) || value < 1) {
|
||||
quantityInput.value = 1;
|
||||
} else if (value > 999) {
|
||||
quantityInput.value = 999;
|
||||
}
|
||||
updateTotalPrice();
|
||||
}
|
||||
|
||||
// 处理订单提交
|
||||
async function handleOrderSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentProduct) {
|
||||
alert('产品信息加载中,请稍后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取表单数据
|
||||
const formData = new FormData(orderForm);
|
||||
const orderData = {
|
||||
product_id: currentProduct.id,
|
||||
quantity: parseInt(formData.get('quantity')),
|
||||
customer_name: formData.get('customer_name').trim(),
|
||||
customer_email: formData.get('customer_email').trim(),
|
||||
customer_phone: formData.get('customer_phone').trim(),
|
||||
shipping_address: formData.get('shipping_address').trim()
|
||||
};
|
||||
|
||||
// 验证必填字段
|
||||
if (!orderData.customer_name || !orderData.shipping_address) {
|
||||
alert('请填写姓名和收货地址');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用提交按钮
|
||||
const submitBtn = document.getElementById('submit-order');
|
||||
const btnText = submitBtn.querySelector('.btn-text');
|
||||
const btnLoading = submitBtn.querySelector('.btn-loading');
|
||||
|
||||
submitBtn.disabled = true;
|
||||
btnText.style.display = 'none';
|
||||
btnLoading.style.display = 'inline';
|
||||
|
||||
try {
|
||||
// 创建订单
|
||||
const response = await fetch('/api/orders', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(orderData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
currentOrder = {
|
||||
...orderData,
|
||||
order_id: result.order_id,
|
||||
total_amount: result.total_amount
|
||||
};
|
||||
|
||||
showOrderModal();
|
||||
} else {
|
||||
throw new Error(result.error || '订单创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建订单失败:', error);
|
||||
alert('创建订单失败: ' + error.message);
|
||||
} finally {
|
||||
// 恢复提交按钮
|
||||
submitBtn.disabled = false;
|
||||
btnText.style.display = 'inline';
|
||||
btnLoading.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示订单确认模态框
|
||||
function showOrderModal() {
|
||||
if (!currentOrder) return;
|
||||
|
||||
// 填充订单信息
|
||||
document.getElementById('modal-order-id').textContent = currentOrder.order_id;
|
||||
document.getElementById('modal-product-name').textContent = currentProduct.name;
|
||||
document.getElementById('modal-quantity').textContent = currentOrder.quantity;
|
||||
document.getElementById('modal-total-price').textContent = `$${currentOrder.total_amount.toFixed(2)} USDT`;
|
||||
document.getElementById('modal-address').textContent = currentOrder.shipping_address;
|
||||
|
||||
orderModal.style.display = 'flex';
|
||||
}
|
||||
|
||||
// 关闭订单模态框
|
||||
function closeOrderModal() {
|
||||
orderModal.style.display = 'none';
|
||||
}
|
||||
|
||||
// 处理支付
|
||||
async function handlePayNow() {
|
||||
if (!currentOrder) return;
|
||||
|
||||
try {
|
||||
// 创建支付
|
||||
const response = await fetch('/api/payment/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
order_id: currentOrder.order_id
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// 检查是否为手动支付模式
|
||||
if (result.manual_mode) {
|
||||
// 关闭模态框,跳转到手动支付页面
|
||||
closeOrderModal();
|
||||
window.location.href = result.payment_url;
|
||||
} else {
|
||||
// 关闭模态框,直接跳转到UPay支付页面
|
||||
closeOrderModal();
|
||||
|
||||
// 显示跳转提示
|
||||
const jumpTip = document.createElement('div');
|
||||
jumpTip.innerHTML = `
|
||||
<div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 9999; display: flex; align-items: center; justify-content: center;">
|
||||
<div style="background: white; padding: 30px; border-radius: 10px; text-align: center;">
|
||||
<h3>正在跳转到支付页面...</h3>
|
||||
<p>请在新页面完成USDT支付</p>
|
||||
<div style="margin: 20px 0;">
|
||||
<div style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #667eea; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
|
||||
</div>
|
||||
<button onclick="this.parentElement.parentElement.remove(); showPaymentStatus();" style="background: #667eea; color: white; border: none; padding: 8px 16px; border-radius: 5px; cursor: pointer;">我已完成支付</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(jumpTip);
|
||||
|
||||
// 3秒后跳转到UPay支付页面
|
||||
setTimeout(() => {
|
||||
window.location.href = result.payment_url;
|
||||
}, 3000);
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error || '创建支付失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建支付失败:', error);
|
||||
alert('创建支付失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示支付状态页面
|
||||
function showPaymentStatus() {
|
||||
if (!currentOrder) return;
|
||||
|
||||
document.getElementById('status-order-id').textContent = currentOrder.order_id;
|
||||
document.getElementById('status-amount').textContent = currentOrder.total_amount.toFixed(2);
|
||||
|
||||
paymentStatus.style.display = 'flex';
|
||||
|
||||
// 开始监听支付状态
|
||||
startPaymentStatusCheck();
|
||||
}
|
||||
|
||||
// 开始支付状态检查
|
||||
function startPaymentStatusCheck() {
|
||||
// 每10秒检查一次支付状态
|
||||
const checkInterval = setInterval(async () => {
|
||||
const status = await checkPaymentStatus();
|
||||
if (status === 'finished' || status === 'failed') {
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// 检查支付状态
|
||||
async function checkPaymentStatus() {
|
||||
if (!currentOrder) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/orders/${currentOrder.order_id}`);
|
||||
const order = await response.json();
|
||||
|
||||
const statusIcon = document.getElementById('status-icon');
|
||||
const statusTitle = document.getElementById('status-title');
|
||||
const statusMessage = document.getElementById('status-message');
|
||||
|
||||
switch (order.payment_status) {
|
||||
case 'finished':
|
||||
statusIcon.innerHTML = '<div class="success-icon">✓</div>';
|
||||
statusTitle.textContent = '支付成功!';
|
||||
statusMessage.textContent = '您的订单已确认,我们将尽快处理并发货。';
|
||||
break;
|
||||
case 'failed':
|
||||
case 'expired':
|
||||
statusIcon.innerHTML = '<div class="error-icon">✗</div>';
|
||||
statusTitle.textContent = '支付失败';
|
||||
statusMessage.textContent = '支付未完成或已过期,请重新下单。';
|
||||
break;
|
||||
case 'confirming':
|
||||
statusTitle.textContent = '支付确认中...';
|
||||
statusMessage.textContent = '我们已收到您的支付,正在等待区块链确认。';
|
||||
break;
|
||||
default:
|
||||
statusTitle.textContent = '等待支付...';
|
||||
statusMessage.textContent = '请完成USDT支付,我们正在等待您的交易。';
|
||||
}
|
||||
|
||||
return order.payment_status;
|
||||
} catch (error) {
|
||||
console.error('检查支付状态失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数:验证邮箱
|
||||
function validateEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// 工具函数:验证电话号码
|
||||
function validatePhone(phone) {
|
||||
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
|
||||
return phoneRegex.test(phone);
|
||||
}
|
||||
|
||||
// 管理员登录相关函数
|
||||
function showAdminLogin() {
|
||||
document.getElementById('adminLoginModal').style.display = 'flex';
|
||||
document.getElementById('adminPassword').focus();
|
||||
}
|
||||
|
||||
function closeAdminLogin() {
|
||||
document.getElementById('adminLoginModal').style.display = 'none';
|
||||
document.getElementById('adminPassword').value = '';
|
||||
document.getElementById('loginError').style.display = 'none';
|
||||
}
|
||||
|
||||
function verifyAdminPassword() {
|
||||
const password = document.getElementById('adminPassword').value;
|
||||
const errorDiv = document.getElementById('loginError');
|
||||
|
||||
if (password === '223388') {
|
||||
// 密码正确,跳转到管理页面
|
||||
window.location.href = '/admin.html';
|
||||
} else {
|
||||
// 密码错误
|
||||
errorDiv.textContent = '密码错误,请重新输入';
|
||||
errorDiv.style.display = 'block';
|
||||
document.getElementById('adminPassword').value = '';
|
||||
document.getElementById('adminPassword').focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘事件监听
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeAdminLogin();
|
||||
}
|
||||
});
|
||||
|
||||
// 管理员登录密码框回车事件
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const adminPasswordInput = document.getElementById('adminPassword');
|
||||
if (adminPasswordInput) {
|
||||
adminPasswordInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
verifyAdminPassword();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
222
public/manual-payment.html
Normal file
222
public/manual-payment.html
Normal file
@ -0,0 +1,222 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>手动支付确认 - USDT商城</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="manual-payment-page">
|
||||
<h1>手动支付确认</h1>
|
||||
<p>由于UPay API暂时不可用,请手动确认支付状态</p>
|
||||
|
||||
<div class="payment-info">
|
||||
<h3>支付信息</h3>
|
||||
<div class="info-item">
|
||||
<span>订单号:</span>
|
||||
<span id="orderId">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>支付金额:</span>
|
||||
<span id="amount">-</span> USDT
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>当前状态:</span>
|
||||
<span id="status" class="status-pending">待支付</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="manual-actions">
|
||||
<h3>模拟支付操作</h3>
|
||||
<button onclick="simulatePayment('finished')" class="btn-success">标记为支付成功</button>
|
||||
<button onclick="simulatePayment('failed')" class="btn-danger">标记为支付失败</button>
|
||||
<button onclick="checkStatus()" class="btn-info">刷新状态</button>
|
||||
</div>
|
||||
|
||||
<div class="back-action">
|
||||
<button onclick="window.location.href='/'" class="btn-primary">返回首页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const orderId = urlParams.get('order_id');
|
||||
|
||||
if (orderId) {
|
||||
document.getElementById('orderId').textContent = orderId;
|
||||
checkStatus();
|
||||
}
|
||||
|
||||
function checkStatus() {
|
||||
if (!orderId) return;
|
||||
|
||||
fetch(`/api/orders/${orderId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('amount').textContent = data.total_amount;
|
||||
const statusElement = document.getElementById('status');
|
||||
statusElement.textContent = getStatusText(data.payment_status);
|
||||
statusElement.className = `status-${data.payment_status}`;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function simulatePayment(status) {
|
||||
if (!orderId) return;
|
||||
|
||||
fetch('/api/payment/callback', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
merchantOrderNo: orderId,
|
||||
orderNo: 'SIM_' + orderId,
|
||||
amount: '100.00',
|
||||
currency: 'USDT',
|
||||
status: status === 'finished' ? '1' : '2',
|
||||
sign: 'simulated_sign'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
alert(`支付状态已更新为:${status === 'finished' ? '成功' : '失败'}`);
|
||||
setTimeout(() => {
|
||||
checkStatus();
|
||||
if (status === 'finished') {
|
||||
window.location.href = `/success.html?order_id=${orderId}`;
|
||||
}
|
||||
}, 1000);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('操作失败');
|
||||
});
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
switch(status) {
|
||||
case 'pending': return '待支付';
|
||||
case 'finished': return '支付成功';
|
||||
case 'failed': return '支付失败';
|
||||
case 'confirming': return '确认中';
|
||||
default: return status;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.manual-payment-page {
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.payment-info {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 30px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.payment-info h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
}
|
||||
|
||||
.manual-actions {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.manual-actions h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.manual-actions button {
|
||||
margin: 0 10px;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #219a52;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
.status-finished {
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.back-action {
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
159
public/success.html
Normal file
159
public/success.html
Normal file
@ -0,0 +1,159 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>支付成功 - USDT商城</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="success-page">
|
||||
<div class="success-icon">
|
||||
<div class="checkmark">✓</div>
|
||||
</div>
|
||||
<h1>支付成功!</h1>
|
||||
<p>感谢您的购买,我们已收到您的USDT支付</p>
|
||||
|
||||
<div class="order-details" id="orderDetails">
|
||||
<h3>订单详情</h3>
|
||||
<div class="detail-item">
|
||||
<span>订单号:</span>
|
||||
<span id="orderId">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span>支付状态:</span>
|
||||
<span class="status-success">已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button onclick="window.location.href='/'" class="btn-primary">继续购物</button>
|
||||
<button onclick="checkOrderStatus()" class="btn-secondary">查看订单状态</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 获取URL参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const orderId = urlParams.get('order_id');
|
||||
|
||||
if (orderId) {
|
||||
document.getElementById('orderId').textContent = orderId;
|
||||
}
|
||||
|
||||
function checkOrderStatus() {
|
||||
if (orderId) {
|
||||
fetch(`/api/orders/${orderId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
alert(`订单状态:${data.payment_status}`);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('查询失败');
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.success-page {
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
margin: 100px auto;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 0 auto 30px;
|
||||
background: #27ae60;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
color: white;
|
||||
font-size: 60px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.success-page h1 {
|
||||
color: #27ae60;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.success-page p {
|
||||
color: #7f8c8d;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.order-details {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.order-details h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: #27ae60;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e0e6ed;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #d1d9e0;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
384
server.js
Normal file
384
server.js
Normal file
@ -0,0 +1,384 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const bodyParser = require('body-parser');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// 中间件
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(express.static('public'));
|
||||
|
||||
// 数据库初始化
|
||||
const db = new sqlite3.Database('./database/shop.db');
|
||||
|
||||
// 创建订单表
|
||||
db.serialize(() => {
|
||||
db.run(`CREATE TABLE IF NOT EXISTS orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id TEXT UNIQUE NOT NULL,
|
||||
product_name TEXT NOT NULL,
|
||||
quantity INTEGER NOT NULL,
|
||||
unit_price REAL NOT NULL,
|
||||
total_amount REAL NOT NULL,
|
||||
customer_name TEXT NOT NULL,
|
||||
customer_email TEXT,
|
||||
customer_phone TEXT,
|
||||
shipping_address TEXT NOT NULL,
|
||||
payment_id TEXT,
|
||||
payment_status TEXT DEFAULT 'pending',
|
||||
shipping_status TEXT DEFAULT 'pending',
|
||||
tracking_number TEXT,
|
||||
shipping_notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
});
|
||||
|
||||
// UPay 配置
|
||||
const UPAY_APP_ID = process.env.UPAY_APP_ID || 'E7c4dss9';
|
||||
const UPAY_APP_SECRET = process.env.UPAY_APP_SECRET || 'Hwc56INsabRau2yn';
|
||||
const UPAY_API_URL = 'https://api.upay.ink/v1/api/open';
|
||||
// const UPAY_API_URL = 'https://api-test.upay.ink/v1/api/open';
|
||||
|
||||
// 产品配置
|
||||
const PRODUCTS = {
|
||||
'premium-product': {
|
||||
name: '比特币彩票抽奖机',
|
||||
price: 100,
|
||||
description: '这是我们的比特币彩票抽奖机,质量优秀'
|
||||
}
|
||||
};
|
||||
|
||||
// 路由
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
});
|
||||
|
||||
// 获取产品信息
|
||||
app.get('/api/products', (req, res) => {
|
||||
res.json(PRODUCTS);
|
||||
});
|
||||
|
||||
// 创建订单
|
||||
app.post('/api/orders', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
product_id,
|
||||
quantity,
|
||||
customer_name,
|
||||
customer_email,
|
||||
customer_phone,
|
||||
shipping_address
|
||||
} = req.body;
|
||||
|
||||
// 验证产品
|
||||
if (!PRODUCTS[product_id]) {
|
||||
return res.status(400).json({ error: '无效的产品ID' });
|
||||
}
|
||||
|
||||
const product = PRODUCTS[product_id];
|
||||
const total_amount = product.price * quantity;
|
||||
const order_id = 'ORDER_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// 保存订单到数据库
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO orders (order_id, product_name, quantity, unit_price, total_amount,
|
||||
customer_name, customer_email, customer_phone, shipping_address)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run([
|
||||
order_id, product.name, quantity, product.price, total_amount,
|
||||
customer_name, customer_email, customer_phone, shipping_address
|
||||
], function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ error: '订单创建失败' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
order_id,
|
||||
total_amount,
|
||||
message: '订单创建成功'
|
||||
});
|
||||
});
|
||||
|
||||
stmt.finalize();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: '服务器错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// UPay 签名生成函数
|
||||
function generateUpaySignature(params, appSecret) {
|
||||
// 1. 过滤需要签名的参数,排除signature字段
|
||||
const signParams = {};
|
||||
Object.keys(params).forEach(key => {
|
||||
if (key !== 'signature' && params[key] !== null && params[key] !== undefined && params[key] !== '') {
|
||||
signParams[key] = params[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 按ASCII字典序排序参数
|
||||
const sortedKeys = Object.keys(signParams).sort();
|
||||
|
||||
// 3. 拼接参数字符串 key1=value1&key2=value2
|
||||
let signStr = '';
|
||||
sortedKeys.forEach((key, index) => {
|
||||
signStr += `${key}=${signParams[key]}`;
|
||||
if (index < sortedKeys.length - 1) {
|
||||
signStr += '&';
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 在末尾追加&appSecret=密钥
|
||||
signStr += `&appSecret=${appSecret}`;
|
||||
|
||||
console.log('Signature string:', signStr);
|
||||
|
||||
// 5. MD5加密并转大写
|
||||
return crypto.createHash('md5').update(signStr, 'utf8').digest('hex').toUpperCase();
|
||||
}
|
||||
|
||||
// 创建支付
|
||||
app.post('/api/payment/create', async (req, res) => {
|
||||
try {
|
||||
const { order_id } = req.body;
|
||||
|
||||
// 从数据库获取订单信息
|
||||
db.get('SELECT * FROM orders WHERE order_id = ?', [order_id], async (err, order) => {
|
||||
if (err || !order) {
|
||||
return res.status(404).json({ error: '订单未找到' });
|
||||
}
|
||||
|
||||
// 创建 UPay 支付订单
|
||||
const paymentData = {
|
||||
appId: UPAY_APP_ID,
|
||||
merchantOrderNo: order_id,
|
||||
chainType: '1', // USDT TRC20
|
||||
fiatAmount: order.total_amount.toFixed(2),
|
||||
fiatCurrency: 'USD',
|
||||
notifyUrl: `${req.protocol}://${req.get('host')}/api/payment/callback`
|
||||
};
|
||||
|
||||
// 生成签名
|
||||
paymentData.signature = generateUpaySignature(paymentData, UPAY_APP_SECRET);
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${UPAY_API_URL}/order/apply`, paymentData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('UPay API Response:', response.data);
|
||||
|
||||
if (response.data.code === '1' && response.data.message === 'success') {
|
||||
// 更新订单支付信息
|
||||
db.run(
|
||||
'UPDATE orders SET payment_id = ?, updated_at = CURRENT_TIMESTAMP WHERE order_id = ?',
|
||||
[response.data.data.orderNo, order_id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
payment_url: response.data.data.payUrl,
|
||||
payment_id: response.data.data.orderNo,
|
||||
order_no: response.data.data.orderNo
|
||||
});
|
||||
} else {
|
||||
throw new Error(response.data.message || '创建支付订单失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('UPay API Error:', error.response?.data || error.message);
|
||||
// 如果API失败,返回手动支付页面
|
||||
const manualPaymentUrl = `/manual-payment.html?order_id=${order_id}`;
|
||||
res.json({
|
||||
success: true,
|
||||
payment_url: manualPaymentUrl,
|
||||
payment_id: 'MANUAL_' + order_id,
|
||||
message: 'API不可用,请使用手动支付模式',
|
||||
manual_mode: true
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: '服务器错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// UPay 支付回调验证
|
||||
function verifyUpayCallback(params, signature, appSecret) {
|
||||
const expectedSignature = generateUpaySignature(params, appSecret);
|
||||
console.log('Expected signature:', expectedSignature);
|
||||
console.log('Received signature:', signature);
|
||||
return expectedSignature === signature;
|
||||
}
|
||||
|
||||
// 支付回调
|
||||
app.post('/api/payment/callback', (req, res) => {
|
||||
try {
|
||||
console.log('Received callback:', req.body);
|
||||
const callbackData = req.body;
|
||||
const { signature, merchantOrderNo, orderNo, status } = callbackData;
|
||||
|
||||
// 验证签名
|
||||
const paramsForSign = { ...callbackData };
|
||||
delete paramsForSign.signature;
|
||||
|
||||
if (!verifyUpayCallback(paramsForSign, signature, UPAY_APP_SECRET)) {
|
||||
console.error('UPay callback signature verification failed');
|
||||
return res.status(400).send('FAIL');
|
||||
}
|
||||
|
||||
// 映射支付状态
|
||||
let paymentStatus = 'pending';
|
||||
switch (status.toString()) {
|
||||
case '1': // 订单完成
|
||||
paymentStatus = 'finished';
|
||||
break;
|
||||
case '2': // 订单超时
|
||||
case '3': // 订单失败
|
||||
paymentStatus = 'failed';
|
||||
break;
|
||||
case '0': // 处理中
|
||||
default:
|
||||
paymentStatus = 'pending';
|
||||
}
|
||||
|
||||
// 更新订单支付状态
|
||||
db.run(
|
||||
'UPDATE orders SET payment_status = ?, payment_id = ?, updated_at = CURRENT_TIMESTAMP WHERE order_id = ?',
|
||||
[paymentStatus, orderNo, merchantOrderNo],
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error('Database update error:', err);
|
||||
return res.status(500).send('FAIL');
|
||||
}
|
||||
|
||||
console.log(`Order ${merchantOrderNo} payment status updated to: ${paymentStatus}`);
|
||||
res.send('OK');
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Callback error:', error);
|
||||
res.status(500).send('FAIL');
|
||||
}
|
||||
});
|
||||
|
||||
// 获取订单状态
|
||||
app.get('/api/orders/:order_id', (req, res) => {
|
||||
const { order_id } = req.params;
|
||||
|
||||
db.get('SELECT * FROM orders WHERE order_id = ?', [order_id], (err, order) => {
|
||||
if (err || !order) {
|
||||
return res.status(404).json({ error: '订单未找到' });
|
||||
}
|
||||
|
||||
res.json(order);
|
||||
});
|
||||
});
|
||||
|
||||
// 获取所有订单列表(管理员)
|
||||
app.get('/api/admin/orders', (req, res) => {
|
||||
const { status, search, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
let query = 'SELECT * FROM orders';
|
||||
let params = [];
|
||||
let conditions = [];
|
||||
|
||||
if (status) {
|
||||
conditions.push('payment_status = ? OR shipping_status = ?');
|
||||
params.push(status, status);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
conditions.push('(order_id LIKE ? OR customer_name LIKE ? OR customer_email LIKE ?)');
|
||||
const searchTerm = `%${search}%`;
|
||||
params.push(searchTerm, searchTerm, searchTerm);
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query += ' WHERE ' + conditions.join(' AND ');
|
||||
}
|
||||
|
||||
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||
params.push(parseInt(limit), parseInt(offset));
|
||||
|
||||
db.all(query, params, (err, orders) => {
|
||||
if (err) {
|
||||
console.error('Database error:', err);
|
||||
return res.status(500).json({ error: '查询订单失败' });
|
||||
}
|
||||
|
||||
// 获取订单统计
|
||||
db.all(`
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN payment_status = 'finished' AND shipping_status = 'pending' THEN 1 ELSE 0 END) as pending_ship,
|
||||
SUM(CASE WHEN shipping_status = 'shipped' THEN 1 ELSE 0 END) as shipped,
|
||||
SUM(CASE WHEN shipping_status = 'completed' THEN 1 ELSE 0 END) as completed
|
||||
FROM orders
|
||||
`, [], (err, stats) => {
|
||||
if (err) {
|
||||
console.error('Stats error:', err);
|
||||
return res.json({ orders, stats: null });
|
||||
}
|
||||
|
||||
res.json({
|
||||
orders,
|
||||
stats: stats[0] || { total: 0, pending_ship: 0, shipped: 0, completed: 0 }
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 更新订单发货状态
|
||||
app.put('/api/admin/orders/:order_id/shipping', (req, res) => {
|
||||
const { order_id } = req.params;
|
||||
const { shipping_status, tracking_number, shipping_notes } = req.body;
|
||||
|
||||
if (!['pending', 'shipped', 'completed'].includes(shipping_status)) {
|
||||
return res.status(400).json({ error: '无效的发货状态' });
|
||||
}
|
||||
|
||||
db.run(
|
||||
`UPDATE orders SET
|
||||
shipping_status = ?,
|
||||
tracking_number = ?,
|
||||
shipping_notes = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE order_id = ?`,
|
||||
[shipping_status, tracking_number || null, shipping_notes || null, order_id],
|
||||
function(err) {
|
||||
if (err) {
|
||||
console.error('Update shipping error:', err);
|
||||
return res.status(500).json({ error: '更新发货状态失败' });
|
||||
}
|
||||
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: '订单未找到' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '发货状态更新成功',
|
||||
changes: this.changes
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`服务器运行在 http://localhost:${PORT}`);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user