支付功能
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
I will optimize the registration and payment flow with the specific requirement to record the actual payment amount.
|
||||||
|
|
||||||
|
### 1. Backend Updates (`main.py`)
|
||||||
|
- **Database Schema**: Add `out_trade_no` column to `gsdh_data` to link users with payment orders.
|
||||||
|
- **Pre-payment Validation**:
|
||||||
|
- Update `create_payment` (H5) and `create_native_payment`.
|
||||||
|
- Check if phone exists in `gsdh_data`.
|
||||||
|
- **Logic**:
|
||||||
|
- If `fee` has a valid amount (not empty, '0', or 'PENDING'), reject as "Already Registered".
|
||||||
|
- If `fee` is empty/PENDING, allow payment.
|
||||||
|
- Upsert user with `fee` = 'PENDING' and `payment_channel` = '..._pending'.
|
||||||
|
- **Payment Status Sync**:
|
||||||
|
- Update `check_payment_status`:
|
||||||
|
- When WeChat returns `SUCCESS`:
|
||||||
|
- **Update `fee`** to the actual configured payment amount (e.g., "0.01").
|
||||||
|
- Update `payment_channel` to the final channel (removing '_pending').
|
||||||
|
- Update `out_trade_no` logic to ensure we find the correct user.
|
||||||
|
|
||||||
|
### 2. Frontend Optimization (`templates/ticket.html`)
|
||||||
|
- **Dynamic Form Fields**:
|
||||||
|
- Replace hardcoded inputs with a loop that renders fields based on `config.field_config` (managed in Admin).
|
||||||
|
- Respect "Show", "Required", and "Label" settings.
|
||||||
|
- **QR Code Modal**:
|
||||||
|
- Add explicit text: "请使用微信扫一扫完成支付" (Please use WeChat Scan to pay).
|
||||||
|
- Optimize layout.
|
||||||
|
- **Payment Logic**:
|
||||||
|
- Handle "Already Registered" error.
|
||||||
|
- Redirect to Success Page on completion.
|
||||||
|
|
||||||
|
### 3. New Success Page
|
||||||
|
- Create `templates/success.html` to show a "Registration Successful" message.
|
||||||
|
- Add route in `main.py`.
|
||||||
|
|
||||||
|
### 4. Admin Interface
|
||||||
|
- Verify `templates/admin.html` field configuration works with the new `ticket.html` dynamic rendering.
|
||||||
|
|
||||||
|
This ensures that successful payments overwrite the 'PENDING' status with the actual amount paid.
|
||||||
Binary file not shown.
53
config.json
53
config.json
@@ -9,20 +9,37 @@
|
|||||||
"bg_color": "#050814",
|
"bg_color": "#050814",
|
||||||
"header_image": "/static/image1.jpg",
|
"header_image": "/static/image1.jpg",
|
||||||
"enable_ticket_validation": false,
|
"enable_ticket_validation": false,
|
||||||
"enable_payment": false,
|
"enable_payment": true,
|
||||||
"payment_amount": 0.01,
|
"payment_amount": 0.01,
|
||||||
"wechat_pay_config": {
|
"wechat_pay_config": {
|
||||||
"appid": "",
|
"appid": "wxdf2ca73e6c0929f0",
|
||||||
"mchid": "",
|
"mchid": "1723685511",
|
||||||
"api_v3_key": "",
|
"api_v3_key": "XishanquBanzhanweixinzhifumiyao3",
|
||||||
"serial_no": "",
|
"serial_no": "49E299FC2F01841ECD6658D8B037C5338BD170BA",
|
||||||
"private_key_path": "",
|
"private_key_path": "static/cert/apiclient_key.pem",
|
||||||
"notify_url": ""
|
"notify_url": "https://event.quant-speed.com/ticket/finish/"
|
||||||
},
|
},
|
||||||
"enable_seating": false,
|
"enable_seating": false,
|
||||||
"total_tables": 14,
|
"total_tables": 14,
|
||||||
"max_per_table": 10,
|
"max_per_table": 10,
|
||||||
"field_config": {
|
"ticket_field_config": {
|
||||||
|
"name": {
|
||||||
|
"label": "姓名",
|
||||||
|
"show": true,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"label": "手机号码",
|
||||||
|
"show": true,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"industry_company": {
|
||||||
|
"label": "单位名称",
|
||||||
|
"show": true,
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkin_field_config": {
|
||||||
"name": {
|
"name": {
|
||||||
"label": "姓名",
|
"label": "姓名",
|
||||||
"show": true,
|
"show": true,
|
||||||
@@ -36,31 +53,31 @@
|
|||||||
"company_name": {
|
"company_name": {
|
||||||
"label": "单位名称",
|
"label": "单位名称",
|
||||||
"show": true,
|
"show": true,
|
||||||
"required": true
|
"required": false
|
||||||
},
|
},
|
||||||
"position": {
|
"position": {
|
||||||
"label": "职务",
|
"label": "职务",
|
||||||
"show": true,
|
"show": true,
|
||||||
"required": true
|
"required": false
|
||||||
},
|
},
|
||||||
"business_scope": {
|
"business_scope": {
|
||||||
"label": "公司主要经营 / 业务",
|
"label": "公司主要经营 / 业务",
|
||||||
"show": true,
|
"show": true,
|
||||||
"required": true
|
"required": false
|
||||||
},
|
},
|
||||||
"vision_2026": {
|
"vision_2026": {
|
||||||
"label": "2026年业务愿景",
|
"label": "2026年业务愿景",
|
||||||
"show": false,
|
"show": true,
|
||||||
"required": false
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"wall_config": {
|
"wall_config": {
|
||||||
"bg_opacity": 0.9,
|
"bg_opacity": 0.3,
|
||||||
"show_title": true,
|
"show_title": true,
|
||||||
"learn_more_url": "https://www.example.com",
|
"learn_more_url": "",
|
||||||
"show_fields": {
|
"show_fields": {
|
||||||
"name": true,
|
"name": true,
|
||||||
"company_name": false,
|
"company_name": true,
|
||||||
"position": true,
|
"position": true,
|
||||||
"vision_2026": true,
|
"vision_2026": true,
|
||||||
"business_scope": true
|
"business_scope": true
|
||||||
@@ -68,7 +85,7 @@
|
|||||||
},
|
},
|
||||||
"db_host": "6.6.6.66",
|
"db_host": "6.6.6.66",
|
||||||
"db_port": "5432",
|
"db_port": "5432",
|
||||||
"db_user": "AI_event",
|
"db_user": "123",
|
||||||
"db_password": "123AI_event",
|
"db_password": "1231",
|
||||||
"db_name": "AI_event"
|
"db_name": "123"
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,16 @@
|
|||||||
# SERVER_PORT="6222"
|
# SERVER_PORT="6222"
|
||||||
|
|
||||||
# 配置局域网变量 - 公司局域网上传方法
|
# 配置局域网变量 - 公司局域网上传方法
|
||||||
SERVER_HOST="6.6.6.86" # 服务器IP地址
|
# SERVER_HOST="6.6.6.86" # 服务器IP地址
|
||||||
SERVER_USER="ubuntu" # 服务器用户名
|
# SERVER_USER="ubuntu" # 服务器用户名
|
||||||
SERVER_PASSWORD="qweasdzxc1" # 服务器密码
|
# SERVER_PASSWORD="qweasdzxc1" # 服务器密码
|
||||||
SERVER_PORT="22" # SSH端口,默认22
|
# SERVER_PORT="22" # SSH端口,默认22
|
||||||
|
|
||||||
|
|
||||||
|
SERVER_HOST="6.6.6.66" # 服务器IP地址
|
||||||
|
SERVER_USER="quant" # 服务器用户名
|
||||||
|
SERVER_PASSWORD="123quant-speed" # 服务器密码
|
||||||
|
SERVER_PORT="22"
|
||||||
|
|
||||||
|
|
||||||
IMAGE_NAME="checkin_sys" # Docker镜像名称
|
IMAGE_NAME="checkin_sys" # Docker镜像名称
|
||||||
@@ -163,27 +169,32 @@ deploy_on_server() {
|
|||||||
sshpass -p "$SERVER_PASSWORD" ssh -p "$SERVER_PORT" -o StrictHostKeyChecking=no "${SERVER_USER}@${SERVER_HOST}" << EOF
|
sshpass -p "$SERVER_PASSWORD" ssh -p "$SERVER_PORT" -o StrictHostKeyChecking=no "${SERVER_USER}@${SERVER_HOST}" << EOF
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# 定义 sudo 包装函数以处理密码输入
|
||||||
|
run_sudo() {
|
||||||
|
echo "$SERVER_PASSWORD" | sudo -S -p '' "\$@"
|
||||||
|
}
|
||||||
|
|
||||||
echo "[INFO] 开始服务器端部署..."
|
echo "[INFO] 开始服务器端部署..."
|
||||||
|
|
||||||
# 检查并停止现有容器
|
# 检查并停止现有容器
|
||||||
if sudo docker ps -a --format 'table {{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
if run_sudo docker ps -a --format 'table {{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
echo "[INFO] 发现现有容器 ${CONTAINER_NAME},正在停止并删除..."
|
echo "[INFO] 发现现有容器 ${CONTAINER_NAME},正在停止并删除..."
|
||||||
sudo docker stop ${CONTAINER_NAME} || true
|
run_sudo docker stop ${CONTAINER_NAME} || true
|
||||||
sudo docker rm ${CONTAINER_NAME} || true
|
run_sudo docker rm ${CONTAINER_NAME} || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查并删除现有镜像
|
# 检查并删除现有镜像
|
||||||
if sudo docker images --format 'table {{.Repository}}:{{.Tag}}' | grep -q "^${IMAGE_NAME}:${IMAGE_TAG}$"; then
|
if run_sudo docker images --format 'table {{.Repository}}:{{.Tag}}' | grep -q "^${IMAGE_NAME}:${IMAGE_TAG}$"; then
|
||||||
echo "[INFO] 发现现有镜像 ${IMAGE_NAME}:${IMAGE_TAG},正在删除..."
|
echo "[INFO] 发现现有镜像 ${IMAGE_NAME}:${IMAGE_TAG},正在删除..."
|
||||||
sudo docker rmi ${IMAGE_NAME}:${IMAGE_TAG} || true
|
run_sudo docker rmi ${IMAGE_NAME}:${IMAGE_TAG} || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 加载新镜像
|
# 加载新镜像
|
||||||
echo "[INFO] 加载新镜像..."
|
echo "[INFO] 加载新镜像..."
|
||||||
sudo docker load -i /tmp/${TAR_FILE}
|
run_sudo docker load -i /tmp/${TAR_FILE}
|
||||||
|
|
||||||
# 验证镜像是否加载成功
|
# 验证镜像是否加载成功
|
||||||
if sudo docker images | grep -q "${IMAGE_NAME}"; then
|
if run_sudo docker images | grep -q "${IMAGE_NAME}"; then
|
||||||
echo "[SUCCESS] 镜像加载成功"
|
echo "[SUCCESS] 镜像加载成功"
|
||||||
else
|
else
|
||||||
echo "[ERROR] 镜像加载失败"
|
echo "[ERROR] 镜像加载失败"
|
||||||
@@ -194,21 +205,21 @@ deploy_on_server() {
|
|||||||
echo "[INFO] 启动新容器..."
|
echo "[INFO] 启动新容器..."
|
||||||
# 使用 host 网络模式,让容器直接使用宿主机网络栈,从而可以通过 localhost:5432 访问宿主机数据库
|
# 使用 host 网络模式,让容器直接使用宿主机网络栈,从而可以通过 localhost:5432 访问宿主机数据库
|
||||||
# 同时覆盖环境变量,强制使用本地数据库配置
|
# 同时覆盖环境变量,强制使用本地数据库配置
|
||||||
sudo docker run -d --network host --name ${CONTAINER_NAME} \
|
run_sudo docker run -d --network host --name ${CONTAINER_NAME} \
|
||||||
--env-file /tmp/.env \
|
--env-file /tmp/.env \
|
||||||
-e DB_HOST=localhost \
|
-e DB_HOST=localhost \
|
||||||
-e DB_PORT=5432 \
|
-e DB_PORT=5432 \
|
||||||
${IMAGE_NAME}:${IMAGE_TAG}
|
${IMAGE_NAME}:${IMAGE_TAG}
|
||||||
|
|
||||||
# 验证容器是否启动成功
|
# 验证容器是否启动成功
|
||||||
if sudo docker ps | grep -q "${CONTAINER_NAME}"; then
|
if run_sudo docker ps | grep -q "${CONTAINER_NAME}"; then
|
||||||
echo "[SUCCESS] 容器启动成功"
|
echo "[SUCCESS] 容器启动成功"
|
||||||
echo "[INFO] 容器状态:"
|
echo "[INFO] 容器状态:"
|
||||||
sudo docker ps | grep "${CONTAINER_NAME}"
|
run_sudo docker ps | grep "${CONTAINER_NAME}"
|
||||||
else
|
else
|
||||||
echo "[ERROR] 容器启动失败"
|
echo "[ERROR] 容器启动失败"
|
||||||
echo "[INFO] 查看容器日志:"
|
echo "[INFO] 查看容器日志:"
|
||||||
sudo docker logs ${CONTAINER_NAME}
|
run_sudo docker logs ${CONTAINER_NAME}
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
230
main.py
230
main.py
@@ -13,6 +13,11 @@ import os
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import difflib
|
import difflib
|
||||||
|
import time
|
||||||
|
import base64
|
||||||
|
import requests
|
||||||
|
from cryptography.hazmat.primitives import serialization, hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
@@ -39,7 +44,12 @@ DEFAULT_CONFIG = {
|
|||||||
"enable_seating": True,
|
"enable_seating": True,
|
||||||
"total_tables": 14,
|
"total_tables": 14,
|
||||||
"max_per_table": 10,
|
"max_per_table": 10,
|
||||||
"field_config": {
|
"ticket_field_config": {
|
||||||
|
"name": {"label": "姓名", "show": True, "required": True},
|
||||||
|
"phone": {"label": "手机号码", "show": True, "required": True},
|
||||||
|
"industry_company": {"label": "单位名称", "show": True, "required": True}
|
||||||
|
},
|
||||||
|
"checkin_field_config": {
|
||||||
"name": {"label": "姓名", "show": True, "required": True},
|
"name": {"label": "姓名", "show": True, "required": True},
|
||||||
"phone": {"label": "手机号码", "show": True, "required": True},
|
"phone": {"label": "手机号码", "show": True, "required": True},
|
||||||
"company_name": {"label": "单位名称", "show": True, "required": False},
|
"company_name": {"label": "单位名称", "show": True, "required": False},
|
||||||
@@ -1198,6 +1208,7 @@ def reset_database():
|
|||||||
industry_company VARCHAR(200),
|
industry_company VARCHAR(200),
|
||||||
fee VARCHAR(50),
|
fee VARCHAR(50),
|
||||||
payment_channel VARCHAR(50),
|
payment_channel VARCHAR(50),
|
||||||
|
out_trade_no VARCHAR(100),
|
||||||
is_signed VARCHAR(10) DEFAULT 'FALSE'
|
is_signed VARCHAR(10) DEFAULT 'FALSE'
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
@@ -1270,6 +1281,7 @@ def init_database():
|
|||||||
industry_company VARCHAR(200),
|
industry_company VARCHAR(200),
|
||||||
fee VARCHAR(50),
|
fee VARCHAR(50),
|
||||||
payment_channel VARCHAR(50),
|
payment_channel VARCHAR(50),
|
||||||
|
out_trade_no VARCHAR(100),
|
||||||
is_signed VARCHAR(10) DEFAULT 'FALSE'
|
is_signed VARCHAR(10) DEFAULT 'FALSE'
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
@@ -1323,6 +1335,7 @@ def init_database():
|
|||||||
|
|
||||||
safe_add_column('gsdh_data', 'fee', 'VARCHAR(50)')
|
safe_add_column('gsdh_data', 'fee', 'VARCHAR(50)')
|
||||||
safe_add_column('gsdh_data', 'payment_channel', 'VARCHAR(50)')
|
safe_add_column('gsdh_data', 'payment_channel', 'VARCHAR(50)')
|
||||||
|
safe_add_column('gsdh_data', 'out_trade_no', 'VARCHAR(100)')
|
||||||
safe_add_column('gsdh_data', 'is_signed', "VARCHAR(10) DEFAULT 'FALSE'")
|
safe_add_column('gsdh_data', 'is_signed', "VARCHAR(10) DEFAULT 'FALSE'")
|
||||||
|
|
||||||
safe_add_column('checkin_info', 'social_point', 'INTEGER DEFAULT 5')
|
safe_add_column('checkin_info', 'social_point', 'INTEGER DEFAULT 5')
|
||||||
@@ -1434,6 +1447,58 @@ class WeChatPayService:
|
|||||||
else:
|
else:
|
||||||
raise Exception(f"WeChat Pay Error: {response.text}")
|
raise Exception(f"WeChat Pay Error: {response.text}")
|
||||||
|
|
||||||
|
def native_payment(self, description, out_trade_no, amount_fen, client_ip=None):
|
||||||
|
url = "https://api.mch.weixin.qq.com/v3/pay/transactions/native"
|
||||||
|
path = "/v3/pay/transactions/native"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"appid": self.appid,
|
||||||
|
"mchid": self.mchid,
|
||||||
|
"description": description,
|
||||||
|
"out_trade_no": out_trade_no,
|
||||||
|
"notify_url": self.notify_url,
|
||||||
|
"amount": {
|
||||||
|
"total": amount_fen,
|
||||||
|
"currency": "CNY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body = json.dumps(data)
|
||||||
|
auth = self.build_authorization_header("POST", path, body)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Authorization": auth
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, data=body, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code in [200, 202]:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"WeChat Pay Error: {response.text}")
|
||||||
|
|
||||||
|
def query_order(self, out_trade_no):
|
||||||
|
url = f"https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={self.mchid}"
|
||||||
|
path = f"/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={self.mchid}"
|
||||||
|
|
||||||
|
auth = self.build_authorization_header("GET", path, "")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Authorization": auth
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
elif response.status_code == 404:
|
||||||
|
return {"trade_state": "NOTPAY"} # Or handle as not found
|
||||||
|
else:
|
||||||
|
raise Exception(f"WeChat Query Error: {response.text}")
|
||||||
|
|
||||||
@app.get("/ticket", response_class=HTMLResponse)
|
@app.get("/ticket", response_class=HTMLResponse)
|
||||||
async def ticket_page(request: Request):
|
async def ticket_page(request: Request):
|
||||||
global CONFIG
|
global CONFIG
|
||||||
@@ -1444,6 +1509,12 @@ async def ticket_page(request: Request):
|
|||||||
|
|
||||||
return templates.TemplateResponse("ticket.html", {"request": request, "config": CONFIG})
|
return templates.TemplateResponse("ticket.html", {"request": request, "config": CONFIG})
|
||||||
|
|
||||||
|
@app.get("/success", response_class=HTMLResponse)
|
||||||
|
async def success_page(request: Request):
|
||||||
|
global CONFIG
|
||||||
|
CONFIG = load_config()
|
||||||
|
return templates.TemplateResponse("success.html", {"request": request, "config": CONFIG})
|
||||||
|
|
||||||
class TicketPaymentRequest(BaseModel):
|
class TicketPaymentRequest(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
phone: str
|
phone: str
|
||||||
@@ -1488,23 +1559,31 @@ async def create_payment(req: TicketPaymentRequest, request: Request):
|
|||||||
new_id = str(max_id + 1)
|
new_id = str(max_id + 1)
|
||||||
|
|
||||||
# Upsert user (if phone exists, update; else insert)
|
# Upsert user (if phone exists, update; else insert)
|
||||||
# Actually, if phone exists, we should update.
|
cur.execute("SELECT new_id, fee FROM gsdh_data WHERE phone = %s", (req.phone,))
|
||||||
cur.execute("SELECT new_id FROM gsdh_data WHERE phone = %s", (req.phone,))
|
|
||||||
existing = cur.fetchone()
|
existing = cur.fetchone()
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
user_id = existing[0]
|
user_id = existing[0]
|
||||||
|
current_fee = existing[1]
|
||||||
|
|
||||||
|
# Validation: Check if already paid
|
||||||
|
# Conditions for "Unpaid": None, Empty String, '0', 'PENDING'
|
||||||
|
if current_fee and current_fee not in ['0', 'PENDING']:
|
||||||
|
cur.close()
|
||||||
|
release_db_connection(conn)
|
||||||
|
return JSONResponse(content={"success": False, "message": "该手机号已完成报名,无需重复支付"}, status_code=400)
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE gsdh_data
|
UPDATE gsdh_data
|
||||||
SET name=%s, industry_company=%s, fee='PENDING', payment_channel='wechat_h5_pending'
|
SET name=%s, industry_company=%s, fee='PENDING', payment_channel='wechat_h5_pending', out_trade_no=%s
|
||||||
WHERE new_id=%s
|
WHERE new_id=%s
|
||||||
""", (req.name, req.company_name, user_id))
|
""", (req.name, req.company_name, out_trade_no, user_id))
|
||||||
else:
|
else:
|
||||||
user_id = new_id
|
user_id = new_id
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO gsdh_data (new_id, name, phone, industry_company, fee, payment_channel, is_signed)
|
INSERT INTO gsdh_data (new_id, name, phone, industry_company, fee, payment_channel, is_signed, out_trade_no)
|
||||||
VALUES (%s, %s, %s, %s, 'PENDING', 'wechat_h5_pending', 'FALSE')
|
VALUES (%s, %s, %s, %s, 'PENDING', 'wechat_h5_pending', 'FALSE', %s)
|
||||||
""", (user_id, req.name, req.phone, req.company_name))
|
""", (user_id, req.name, req.phone, req.company_name, out_trade_no))
|
||||||
|
|
||||||
# Also store extra info in checkin_info?
|
# Also store extra info in checkin_info?
|
||||||
# Requirement: "If registration paid then add to gsdh_data"
|
# Requirement: "If registration paid then add to gsdh_data"
|
||||||
@@ -1540,6 +1619,141 @@ async def create_payment(req: TicketPaymentRequest, request: Request):
|
|||||||
release_db_connection(conn)
|
release_db_connection(conn)
|
||||||
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
|
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
@app.post("/api/payment/native")
|
||||||
|
async def create_native_payment(req: TicketPaymentRequest, request: Request):
|
||||||
|
"""
|
||||||
|
处理 Native 支付请求 (扫码支付)。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
global CONFIG
|
||||||
|
CONFIG = load_config()
|
||||||
|
|
||||||
|
if not CONFIG.get("enable_payment", False):
|
||||||
|
return JSONResponse(content={"success": False, "message": "支付未开启"}, status_code=403)
|
||||||
|
|
||||||
|
wc_config = CONFIG.get("wechat_pay_config", {})
|
||||||
|
service = WeChatPayService(wc_config)
|
||||||
|
|
||||||
|
# 1. Generate Order ID
|
||||||
|
out_trade_no = f"TICKET_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
||||||
|
|
||||||
|
# 2. Amount (Convert to Fen)
|
||||||
|
amount_yuan = float(CONFIG.get("payment_amount", 0.01))
|
||||||
|
amount_fen = int(amount_yuan * 100)
|
||||||
|
|
||||||
|
# 3. Save Temporary User Data (Pending Payment)
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Calculate new ID
|
||||||
|
cur.execute("SELECT MAX(CAST(new_id AS INTEGER)) FROM gsdh_data WHERE new_id ~ '^[0-9]+$'")
|
||||||
|
row = cur.fetchone()
|
||||||
|
max_id = row[0] if row and row[0] is not None else 0
|
||||||
|
new_id = str(max_id + 1)
|
||||||
|
|
||||||
|
# Upsert user
|
||||||
|
cur.execute("SELECT new_id, fee FROM gsdh_data WHERE phone = %s", (req.phone,))
|
||||||
|
existing = cur.fetchone()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
user_id = existing[0]
|
||||||
|
current_fee = existing[1]
|
||||||
|
|
||||||
|
# Validation: Check if already paid
|
||||||
|
if current_fee and current_fee not in ['0', 'PENDING']:
|
||||||
|
cur.close()
|
||||||
|
release_db_connection(conn)
|
||||||
|
return JSONResponse(content={"success": False, "message": "该手机号已完成报名,无需重复支付"}, status_code=400)
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE gsdh_data
|
||||||
|
SET name=%s, industry_company=%s, fee='PENDING', payment_channel='微信线上支付_pending', out_trade_no=%s
|
||||||
|
WHERE new_id=%s
|
||||||
|
""", (req.name, req.company_name, out_trade_no, user_id))
|
||||||
|
else:
|
||||||
|
user_id = new_id
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO gsdh_data (new_id, name, phone, industry_company, fee, payment_channel, is_signed, out_trade_no)
|
||||||
|
VALUES (%s, %s, %s, %s, 'PENDING', '微信线上支付_pending', 'FALSE', %s)
|
||||||
|
""", (user_id, req.name, req.phone, req.company_name, out_trade_no))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
release_db_connection(conn)
|
||||||
|
|
||||||
|
# 4. Call WeChat Pay
|
||||||
|
try:
|
||||||
|
res = service.native_payment(
|
||||||
|
description=f"{CONFIG.get('event_title', 'Event')} Ticket",
|
||||||
|
out_trade_no=out_trade_no,
|
||||||
|
amount_fen=amount_fen,
|
||||||
|
client_ip=request.client.host
|
||||||
|
)
|
||||||
|
code_url = res.get("code_url")
|
||||||
|
return {"success": True, "code_url": code_url, "out_trade_no": out_trade_no}
|
||||||
|
|
||||||
|
except Exception as wx_e:
|
||||||
|
print(f"WeChat Pay Failed: {wx_e}")
|
||||||
|
if "Private key not loaded" in str(wx_e):
|
||||||
|
return JSONResponse(content={"success": False, "message": "服务端未配置支付证书,无法发起支付"}, status_code=500)
|
||||||
|
return JSONResponse(content={"success": False, "message": f"支付请求失败: {str(wx_e)}"}, status_code=500)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if 'conn' in locals() and conn:
|
||||||
|
release_db_connection(conn)
|
||||||
|
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
@app.get("/api/payment/check/{out_trade_no}")
|
||||||
|
async def check_payment_status(out_trade_no: str):
|
||||||
|
"""
|
||||||
|
查询订单支付状态。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
global CONFIG
|
||||||
|
CONFIG = load_config()
|
||||||
|
wc_config = CONFIG.get("wechat_pay_config", {})
|
||||||
|
service = WeChatPayService(wc_config)
|
||||||
|
|
||||||
|
# Call WeChat Query API
|
||||||
|
try:
|
||||||
|
res = service.query_order(out_trade_no)
|
||||||
|
trade_state = res.get("trade_state")
|
||||||
|
|
||||||
|
if trade_state == "SUCCESS":
|
||||||
|
# Update DB to mark as paid
|
||||||
|
conn = get_db_connection()
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Get actual amount from Config
|
||||||
|
amount = str(CONFIG.get("payment_amount", 0.01))
|
||||||
|
|
||||||
|
# Update gsdh_data
|
||||||
|
# Remove '_pending' suffix from payment_channel and set actual fee
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE gsdh_data
|
||||||
|
SET fee = %s,
|
||||||
|
payment_channel = REPLACE(payment_channel, '_pending', '')
|
||||||
|
WHERE out_trade_no = %s
|
||||||
|
""", (amount, out_trade_no))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
finally:
|
||||||
|
release_db_connection(conn)
|
||||||
|
|
||||||
|
return {"success": True, "trade_state": trade_state}
|
||||||
|
|
||||||
|
except Exception as wx_e:
|
||||||
|
# Mock for testing without keys
|
||||||
|
if "Private key not loaded" in str(wx_e):
|
||||||
|
# Simulate success for testing? No, stick to error.
|
||||||
|
return {"success": False, "message": "配置错误"}
|
||||||
|
return {"success": False, "message": str(wx_e)}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
|
||||||
|
|
||||||
@app.post("/api/payment/notify")
|
@app.post("/api/payment/notify")
|
||||||
async def payment_notify(request: Request):
|
async def payment_notify(request: Request):
|
||||||
# Handle WeChat Pay Callback
|
# Handle WeChat Pay Callback
|
||||||
|
|||||||
BIN
static/cert/apiclient_cert.p12
Normal file
BIN
static/cert/apiclient_cert.p12
Normal file
Binary file not shown.
25
static/cert/apiclient_cert.pem
Normal file
25
static/cert/apiclient_cert.pem
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEITCCAwmgAwIBAgIUSeKZ/C8BhB7NZljYsDfFM4vRcLowDQYJKoZIhvcNAQEL
|
||||||
|
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
|
||||||
|
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
|
||||||
|
Q0EwHhcNMjYwMTI4MTQxNTE4WhcNMzEwMTI3MTQxNTE4WjB7MRMwEQYDVQQDDAox
|
||||||
|
NzIzNjg1NTExMRswGQYDVQQKDBLlvq7kv6HllYbmiLfns7vnu58xJzAlBgNVBAsM
|
||||||
|
HuS6keWNl+mHj+i/ueenkeaKgOaciemZkOWFrOWPuDELMAkGA1UEBhMCQ04xETAP
|
||||||
|
BgNVBAcMCFNoZW5aaGVuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
||||||
|
xxKpTgk0MAbX/cU2zXn9T6uCWToRJzNXiGf45RIcP9mD7wlXdr3p2HQ6Vv3zhDKE
|
||||||
|
goj980THhoaqX3mWXs9jxMNtdXPsjrFXk0I0e0O7BqFl/5pVicoDv3GZ/LASdqwC
|
||||||
|
rLFWZRmmkB45ehb5qoFBTFBmE3SS5qm8v2UbrrRMuu07vM1AHmtw36YyAlGRKaMj
|
||||||
|
fQVr5l0u9769j2F8Rd8/HlyiyjUZjhpRiGuv/n1eEWoL6ZIFGA/BCPgvU+Q+Xm3q
|
||||||
|
6aZd8odO5p2c1knlV/dJfRv2vC9Q8fXLalr3UQF0bM0vFCKn2sWLRYFo9Z+Nep51
|
||||||
|
/S9eUZn+7yhpxoImKBGycQIDAQABo4G5MIG2MAkGA1UdEwQCMAAwCwYDVR0PBAQD
|
||||||
|
AgP4MIGbBgNVHR8EgZMwgZAwgY2ggYqggYeGgYRodHRwOi8vZXZjYS5pdHJ1cy5j
|
||||||
|
b20uY24vcHVibGljL2l0cnVzY3JsP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFEMzk3
|
||||||
|
NTQ5ODQ2QzAxQzNFOEVCRDImc2c9SEFDQzQ3MUI2NTQyMkUxMkIyN0E5RDMzQTg3
|
||||||
|
QUQxQ0RGNTkyNkUxNDAzNzEwDQYJKoZIhvcNAQELBQADggEBAEqNn5h/g+PNkbwM
|
||||||
|
2sIl8aSgtB1tOgBAQH0sI2zqJbP/nEkeqAS1PL7Eo8EwbVrxXQV7rv75+iF8g3ry
|
||||||
|
DiCGGgooO4F/Ohh3X1BwiSKokzl8sZV6rB/ojHKkfwA0GE9xualqo2sikGpNQyUt
|
||||||
|
BeDp9OC8puBd7LR8KK8fJ7+3e3xcnH+cWZ6IZCRXgVtlofvM3OuzHQs0Bq31B4fp
|
||||||
|
yhuX1Dig/gldeKt2S624tUfOaVVdFROikYFKx8vxcsWmKMZnK8M5UBiJJIE/txvK
|
||||||
|
TT+vRUXdjZ737j4GQ86T6vedn5S88rFFiBv7LsQTIa69CRdD2dGajoTexgh1m+l9
|
||||||
|
LsHf65A=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
28
static/cert/apiclient_key.pem
Normal file
28
static/cert/apiclient_key.pem
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHEqlOCTQwBtf9
|
||||||
|
xTbNef1Pq4JZOhEnM1eIZ/jlEhw/2YPvCVd2venYdDpW/fOEMoSCiP3zRMeGhqpf
|
||||||
|
eZZez2PEw211c+yOsVeTQjR7Q7sGoWX/mlWJygO/cZn8sBJ2rAKssVZlGaaQHjl6
|
||||||
|
FvmqgUFMUGYTdJLmqby/ZRuutEy67Tu8zUAea3DfpjICUZEpoyN9BWvmXS73vr2P
|
||||||
|
YXxF3z8eXKLKNRmOGlGIa6/+fV4RagvpkgUYD8EI+C9T5D5eberppl3yh07mnZzW
|
||||||
|
SeVX90l9G/a8L1Dx9ctqWvdRAXRszS8UIqfaxYtFgWj1n416nnX9L15Rmf7vKGnG
|
||||||
|
giYoEbJxAgMBAAECggEAf6NzcllsYK7Cpi9gp/ZjeEWbWeJlRSEiKo+fgiBOhJHO
|
||||||
|
ieEPd07lNKexED3beRN66sczLFsZIyQM8RJgW4HVkj9LW1dsgUEryXtVcwNGU0YA
|
||||||
|
xBYMakBgjssj1GSAMh6vyFIb9s6vgRAgAivhAHXHjEqwaCECX2rNXlpRmX55BBAd
|
||||||
|
nMudGbbnb/jE9EUVOEvovrYqfCCxloHejCEY2/XwmU2GpGkDQIklOv2gnsvAWvDu
|
||||||
|
V+jx8OlMDeyoAZ65yIdRzgrX20JmxqyXZPOfbleNmR3F03sgpuv5cZSZVgLU8Hd+
|
||||||
|
CPVDGOV66kupFmsMi5R2rvrExhTp4I0V6s5s+wwNAQKBgQDu7G8BQcUB4NXsKemf
|
||||||
|
D89e5di0BKK5+MvMDGYTRmEMJJ9Ag1iCD11/4eg6iEei1h6TRTxmvDWm3e20KnrK
|
||||||
|
8Ug3oJQf8kYYUGgmIDhvWegfbAw1RNoZ9+I7UYofUzFk4pAdJMMqolc0kXIoOPX7
|
||||||
|
uym/FiDgIqstjNRr+kbLtU8/qQKBgQDVTRU/cE3R0eZMDZ3EvbqD00BzPmK3h3Hs
|
||||||
|
9358oh3v0Xf69p2TH58gVpbJPbHydeUvbmjPc6QeRFT9pVA7dFqXi3lJ4WOu9vu+
|
||||||
|
l2Sn0NX+g7khDzig8lj27RWxmAVc5H//VqeSxi8Z87JWGKGn1bkttN8MGvCSn8UA
|
||||||
|
W3LLfRw5iQKBgQCAmI1etdGdozBA/oYZ8N6Ci1/J9LzJMBow53OBaF4PtR5qEMfw
|
||||||
|
qOiATk06Q+Oax3iJd7h860xNViH82Ohilt9x4WIYl8QWjiCgdLMra86+Kt+fREHH
|
||||||
|
vF3t44NePN7XGALNTN4m8l3Rk39IGvB631Am43gqaz6LM8OZoom8VwgikQKBgEFT
|
||||||
|
4nYTh0ID72zLns5q7X0CbnLdYI6lRRc2ld7GKDUTCpQqhAhTXwEgg/4OjzmbMh5c
|
||||||
|
ymz/FfzPINiukOwkrrpLVVJzXXxw2Jl+9K0RIPlajpC5gLaKgwTdiA9kaAATW8Bw
|
||||||
|
YdJqMHLaWHxV/uzQYG7HPqkOdy/xDv0VBQudvc9BAoGBANIzo192Da7c7BrtDetc
|
||||||
|
C+/sc8B4Y/1mkTDc/LS3b53VfGA+otsCAKZF53ezK8xQrtWONFKF9dWkk/fPERsV
|
||||||
|
qgfg3WQIiaobDpLbjgbIOkcP/PX2H0HqhrRofDBt6QiTANBuh6FK4KOPnklXuYPp
|
||||||
|
8Sj4YnidN9RxTxPdIQuoQKaV
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
1
static/js/qrcode.min.js
vendored
Normal file
1
static/js/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -73,6 +73,41 @@
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2::after {
|
||||||
|
content: '▼';
|
||||||
|
font-size: 0.8em;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.collapsed h2::after {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.collapsed h2 {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
max-height: 3000px; /* Ensure enough height for content */
|
||||||
|
opacity: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.collapsed .card-content {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
@@ -297,6 +332,7 @@
|
|||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>基本信息设置</h2>
|
<h2>基本信息设置</h2>
|
||||||
|
<div class="card-content">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>活动标题</label>
|
<label>活动标题</label>
|
||||||
<input type="text" id="event_title">
|
<input type="text" id="event_title">
|
||||||
@@ -318,9 +354,11 @@
|
|||||||
<textarea id="event_content" rows="4"></textarea>
|
<textarea id="event_content" rows="4"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card collapsed">
|
||||||
<h2>签到设置</h2>
|
<h2>签到设置</h2>
|
||||||
|
<div class="card-content">
|
||||||
<div class="checkbox-wrapper">
|
<div class="checkbox-wrapper">
|
||||||
<input type="checkbox" id="enable_ticket_validation">
|
<input type="checkbox" id="enable_ticket_validation">
|
||||||
<label for="enable_ticket_validation">开启验票验证 (核对数据库与付款记录)</label>
|
<label for="enable_ticket_validation">开启验票验证 (核对数据库与付款记录)</label>
|
||||||
@@ -329,9 +367,11 @@
|
|||||||
关闭后将跳过数据库核查,直接允许签到(若用户不存在将自动创建)。
|
关闭后将跳过数据库核查,直接允许签到(若用户不存在将自动创建)。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card collapsed">
|
||||||
<h2>座位设置</h2>
|
<h2>座位设置</h2>
|
||||||
|
<div class="card-content">
|
||||||
<div class="checkbox-wrapper">
|
<div class="checkbox-wrapper">
|
||||||
<input type="checkbox" id="enable_seating" onchange="toggleSeatingInputs()">
|
<input type="checkbox" id="enable_seating" onchange="toggleSeatingInputs()">
|
||||||
<label for="enable_seating">开启分桌功能</label>
|
<label for="enable_seating">开启分桌功能</label>
|
||||||
@@ -347,9 +387,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card collapsed">
|
||||||
<h2>表单字段设置</h2>
|
<h2>报名输入字段设置 (Registration Fields)</h2>
|
||||||
|
<div class="card-content">
|
||||||
|
<p style="font-size: 0.9em; color: var(--text-muted); margin-bottom: 15px;">对应 gsdh_data 表 (ticket.html)</p>
|
||||||
<table style="width: 100%; border-collapse: collapse; color: white;">
|
<table style="width: 100%; border-collapse: collapse; color: white;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="border-bottom: 1px solid rgba(255,255,255,0.1); text-align: left;">
|
<tr style="border-bottom: 1px solid rgba(255,255,255,0.1); text-align: left;">
|
||||||
@@ -358,14 +401,35 @@
|
|||||||
<th style="padding: 10px;">是否必填</th>
|
<th style="padding: 10px;">是否必填</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="field_config_body">
|
<tbody id="ticket_field_config_body">
|
||||||
<!-- Fields will be injected here -->
|
<!-- Ticket Fields will be injected here -->
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card collapsed">
|
||||||
|
<h2>签到/大屏字段设置 (Check-in Fields)</h2>
|
||||||
|
<div class="card-content">
|
||||||
|
<p style="font-size: 0.9em; color: var(--text-muted); margin-bottom: 15px;">对应 checkin_info 表 (wall/checkin)</p>
|
||||||
|
<table style="width: 100%; border-collapse: collapse; color: white;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 1px solid rgba(255,255,255,0.1); text-align: left;">
|
||||||
|
<th style="padding: 10px;">字段名称</th>
|
||||||
|
<th style="padding: 10px;">是否显示</th>
|
||||||
|
<th style="padding: 10px;">是否必填</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="checkin_field_config_body">
|
||||||
|
<!-- Checkin Fields will be injected here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card collapsed">
|
||||||
<h2>支付设置 (Payment)</h2>
|
<h2>支付设置 (Payment)</h2>
|
||||||
|
<div class="card-content">
|
||||||
<div class="checkbox-wrapper">
|
<div class="checkbox-wrapper">
|
||||||
<input type="checkbox" id="enable_payment">
|
<input type="checkbox" id="enable_payment">
|
||||||
<label for="enable_payment">开启付费报名 (Enable Paid Registration)</label>
|
<label for="enable_payment">开启付费报名 (Enable Paid Registration)</label>
|
||||||
@@ -408,9 +472,11 @@
|
|||||||
<input type="text" id="wx_notify_url" placeholder="https://your-domain.com/api/payment/notify">
|
<input type="text" id="wx_notify_url" placeholder="https://your-domain.com/api/payment/notify">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card collapsed">
|
||||||
<h2>数据库设置</h2>
|
<h2>数据库设置</h2>
|
||||||
|
<div class="card-content">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>数据库地址 (Host)</label>
|
<label>数据库地址 (Host)</label>
|
||||||
<input type="text" id="db_host" placeholder="localhost">
|
<input type="text" id="db_host" placeholder="localhost">
|
||||||
@@ -440,9 +506,11 @@
|
|||||||
提示:切换到新数据库后,点击“初始化/迁移结构”可自动创建表结构(不会删除现有数据)。
|
提示:切换到新数据库后,点击“初始化/迁移结构”可自动创建表结构(不会删除现有数据)。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card collapsed">
|
||||||
<h2>大屏设置 (Wall Config)</h2>
|
<h2>大屏设置 (Wall Config)</h2>
|
||||||
|
<div class="card-content">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>背景图片透明度 (0-1)</label>
|
<label>背景图片透明度 (0-1)</label>
|
||||||
<input type="number" id="wall_bg_opacity" step="0.1" min="0" max="1" value="0.3">
|
<input type="number" id="wall_bg_opacity" step="0.1" min="0" max="1" value="0.3">
|
||||||
@@ -473,9 +541,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card collapsed">
|
||||||
<h2>主题设置</h2>
|
<h2>主题设置</h2>
|
||||||
|
<div class="card-content">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>主色调 (Primary Color)</label>
|
<label>主色调 (Primary Color)</label>
|
||||||
<input type="color" id="primary_color">
|
<input type="color" id="primary_color">
|
||||||
@@ -489,9 +559,11 @@
|
|||||||
<input type="color" id="bg_color">
|
<input type="color" id="bg_color">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card collapsed">
|
||||||
<h2>图片设置</h2>
|
<h2>图片设置</h2>
|
||||||
|
<div class="card-content">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>顶部头图</label>
|
<label>顶部头图</label>
|
||||||
<input type="file" id="header_image_file" accept="image/*" style="padding: 10px 0;">
|
<input type="file" id="header_image_file" accept="image/*" style="padding: 10px 0;">
|
||||||
@@ -499,24 +571,36 @@
|
|||||||
</div>
|
</div>
|
||||||
<button onclick="uploadImage()">上传图片</button>
|
<button onclick="uploadImage()">上传图片</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 25px; text-align: center;">
|
<div style="margin-bottom: 25px; text-align: center;">
|
||||||
<button onclick="saveConfig()" class="btn-block btn-lg">保存所有设置</button>
|
<button onclick="saveConfig()" class="btn-block btn-lg">保存所有设置</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card danger-zone">
|
<div class="card danger-zone collapsed">
|
||||||
<h2>危险区域</h2>
|
<h2>危险区域</h2>
|
||||||
|
<div class="card-content">
|
||||||
<p style="color: #ffcccc; margin-bottom: 15px;">此处操作不可逆,请谨慎操作。</p>
|
<p style="color: #ffcccc; margin-bottom: 15px;">此处操作不可逆,请谨慎操作。</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button class="btn-danger" onclick="resetDatabase()">重置/创建数据库</button>
|
<button class="btn-danger" onclick="resetDatabase()">重置/创建数据库</button>
|
||||||
<p style="font-size: 0.9em; color: #ffcccc; margin-top: 10px;">这将清空所有签到数据并重新创建表结构。</p>
|
<p style="font-size: 0.9em; color: #ffcccc; margin-top: 10px;">这将清空所有签到数据并重新创建表结构。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="message"></div>
|
<div id="message"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Collapsible Cards Logic
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('.card h2').forEach(header => {
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
header.parentElement.classList.toggle('collapsed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Use secret from backend
|
// Use secret from backend
|
||||||
const ACCESS_SECRET = "{{ secret }}";
|
const ACCESS_SECRET = "{{ secret }}";
|
||||||
|
|
||||||
@@ -602,15 +686,21 @@
|
|||||||
document.getElementById('max_per_table').value = config.max_per_table || 10;
|
document.getElementById('max_per_table').value = config.max_per_table || 10;
|
||||||
toggleSeatingInputs();
|
toggleSeatingInputs();
|
||||||
|
|
||||||
// Field Config
|
// Field Configs
|
||||||
renderFieldConfig(config.field_config || {
|
renderFieldConfig('ticket_field_config_body', config.ticket_field_config || {
|
||||||
"name": {"label": "姓名", "show": true, "required": true},
|
"name": {"label": "姓名", "show": true, "required": true},
|
||||||
"phone": {"label": "手机号码", "show": true, "required": true},
|
"phone": {"label": "手机号码", "show": true, "required": true},
|
||||||
|
"industry_company": {"label": "单位名称", "show": true, "required": true}
|
||||||
|
}, ["name", "phone", "industry_company"]);
|
||||||
|
|
||||||
|
renderFieldConfig('checkin_field_config_body', config.checkin_field_config || {
|
||||||
|
"name": {"label": "姓名", "show": true, "required": true},
|
||||||
|
"phone": {"label": "手机号码", "show": True, "required": True},
|
||||||
"company_name": {"label": "单位名称", "show": true, "required": false},
|
"company_name": {"label": "单位名称", "show": true, "required": false},
|
||||||
"position": {"label": "职务", "show": true, "required": false},
|
"position": {"label": "职务", "show": true, "required": false},
|
||||||
"business_scope": {"label": "公司主要经营 / 业务", "show": true, "required": false},
|
"business_scope": {"label": "公司主要经营 / 业务", "show": true, "required": false},
|
||||||
"vision_2026": {"label": "2026年业务愿景", "show": true, "required": false}
|
"vision_2026": {"label": "2026年业务愿景", "show": true, "required": false}
|
||||||
});
|
}, ["name", "phone", "company_name", "position", "business_scope", "vision_2026"]);
|
||||||
|
|
||||||
// DB Config
|
// DB Config
|
||||||
document.getElementById('db_host').value = config.db_host || '';
|
document.getElementById('db_host').value = config.db_host || '';
|
||||||
@@ -631,12 +721,14 @@
|
|||||||
document.getElementById('wall_show_scope').checked = show.business_scope !== false;
|
document.getElementById('wall_show_scope').checked = show.business_scope !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderFieldConfig(fieldConfig) {
|
function renderFieldConfig(tbodyId, fieldConfig, orderedKeys) {
|
||||||
const tbody = document.getElementById('field_config_body');
|
const tbody = document.getElementById(tbodyId);
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
// Order of fields
|
// If orderedKeys not provided, use default order or Object.keys
|
||||||
const orderedKeys = ["name", "phone", "company_name", "position", "business_scope", "vision_2026"];
|
if (!orderedKeys) {
|
||||||
|
orderedKeys = Object.keys(fieldConfig);
|
||||||
|
}
|
||||||
|
|
||||||
orderedKeys.forEach(key => {
|
orderedKeys.forEach(key => {
|
||||||
if (!fieldConfig[key]) return;
|
if (!fieldConfig[key]) return;
|
||||||
@@ -644,9 +736,7 @@
|
|||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
|
tr.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
|
||||||
|
|
||||||
// Name/Phone lock logic (can modify required/show but maybe should lock show for logic safety?
|
// Name/Phone lock logic
|
||||||
// User asked to set displayed/required, so I will allow it but maybe warn or just allow it.)
|
|
||||||
// Actually, name/phone are critical. I will disable the 'show' checkbox for them to prevent system break.
|
|
||||||
const isLocked = (key === 'name' || key === 'phone');
|
const isLocked = (key === 'name' || key === 'phone');
|
||||||
|
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
@@ -662,16 +752,14 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFieldConfig() {
|
function getFieldConfig(tbodyId) {
|
||||||
const config = {};
|
const config = {};
|
||||||
const rows = document.querySelectorAll('#field_config_body tr');
|
const rows = document.querySelectorAll(`#${tbodyId} tr`);
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
const showCb = row.querySelector('.field-show');
|
const showCb = row.querySelector('.field-show');
|
||||||
const reqCb = row.querySelector('.field-required');
|
const reqCb = row.querySelector('.field-required');
|
||||||
const key = showCb.dataset.key;
|
const key = showCb.dataset.key;
|
||||||
|
|
||||||
// Retrieve label from somewhere or just hardcode/store in data attr.
|
|
||||||
// Simple way: text content
|
|
||||||
const labelText = row.cells[0].textContent.split(' (')[0];
|
const labelText = row.cells[0].textContent.split(' (')[0];
|
||||||
|
|
||||||
config[key] = {
|
config[key] = {
|
||||||
@@ -680,7 +768,7 @@
|
|||||||
required: reqCb.checked
|
required: reqCb.checked
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
// Ensure locked fields are true/true if disabled
|
// Ensure locked fields are true/true
|
||||||
if (config.name) { config.name.show = true; config.name.required = true; }
|
if (config.name) { config.name.show = true; config.name.required = true; }
|
||||||
if (config.phone) { config.phone.show = true; config.phone.required = true; }
|
if (config.phone) { config.phone.show = true; config.phone.required = true; }
|
||||||
|
|
||||||
@@ -728,7 +816,8 @@
|
|||||||
max_per_table: parseInt(document.getElementById('max_per_table').value),
|
max_per_table: parseInt(document.getElementById('max_per_table').value),
|
||||||
|
|
||||||
// Field Config
|
// Field Config
|
||||||
field_config: getFieldConfig(),
|
ticket_field_config: getFieldConfig('ticket_field_config_body'),
|
||||||
|
checkin_field_config: getFieldConfig('checkin_field_config_body'),
|
||||||
|
|
||||||
// Wall Config
|
// Wall Config
|
||||||
wall_config: {
|
wall_config: {
|
||||||
|
|||||||
104
templates/success.html
Normal file
104
templates/success.html
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>报名成功 - {{ config.event_title }}</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: {{ config.primary_color }};
|
||||||
|
--secondary-color: {{ config.secondary_color }};
|
||||||
|
--bg-color: {{ config.bg_color }};
|
||||||
|
--card-bg: rgba(12, 24, 50, 0.7);
|
||||||
|
--text-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 50% 0%, #1a3a75 0%, #050814 60%),
|
||||||
|
radial-gradient(circle at 85% 30%, rgba(0, 242, 255, 0.1) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 15% 70%, rgba(0, 102, 255, 0.15) 0%, transparent 40%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.3);
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||||
|
animation: fadeIn 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-success {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 40px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
box-shadow: 0 0 20px rgba(40, 167, 69, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #ccc;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 30px;
|
||||||
|
background: linear-gradient(90deg, #0051ff 0%, #00f2ff 100%);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 242, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 25px rgba(0, 242, 255, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon-success">✓</div>
|
||||||
|
<h1>报名成功</h1>
|
||||||
|
<p>您已成功支付并完成报名!<br>请届时凭手机号或二维码入场。</p>
|
||||||
|
<a href="/" class="btn">返回首页</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>购票报名 - {{ config.event_title }}</title>
|
<title>购票报名 - {{ config.event_title }}</title>
|
||||||
|
<script src="/static/js/qrcode.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary-color: {{ config.primary_color }};
|
--primary-color: {{ config.primary_color }};
|
||||||
@@ -191,6 +192,60 @@
|
|||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
display: none;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 90%;
|
||||||
|
width: 400px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 242, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 15px;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#qrcode {
|
||||||
|
margin: 20px auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-tip {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -206,26 +261,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="paymentForm" onsubmit="handlePayment(event)">
|
<form id="paymentForm" onsubmit="handlePayment(event)">
|
||||||
|
{% set ordered_keys = ['name', 'phone', 'industry_company'] %}
|
||||||
|
{% for key in ordered_keys %}
|
||||||
|
{% set field = config.ticket_field_config.get(key) %}
|
||||||
|
{% if field and field.show %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>姓名 *</label>
|
<label>{{ field.label }}{% if field.required %} *{% endif %}</label>
|
||||||
<input type="text" id="name" required placeholder="请输入您的姓名">
|
{% if key == 'phone' %}
|
||||||
</div>
|
<input type="tel" id="{{ key }}" {% if field.required %}required{% endif %} placeholder="请输入{{ field.label }}" pattern="[0-9]{11}">
|
||||||
<div class="form-group">
|
{% else %}
|
||||||
<label>手机号码 *</label>
|
<input type="text" id="{{ key }}" {% if field.required %}required{% endif %} placeholder="请输入{{ field.label }}">
|
||||||
<input type="tel" id="phone" required placeholder="请输入您的手机号码" pattern="[0-9]{11}">
|
{% endif %}
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>单位名称</label>
|
|
||||||
<input type="text" id="company_name" placeholder="请输入单位名称">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>职务</label>
|
|
||||||
<input type="text" id="position" placeholder="请输入您的职务">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>业务范围</label>
|
|
||||||
<input type="text" id="business_scope" placeholder="简述主要业务">
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<button type="submit" class="submit-btn" id="submitBtn">
|
<button type="submit" class="submit-btn" id="submitBtn">
|
||||||
立即支付报名
|
立即支付报名
|
||||||
@@ -236,17 +285,49 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- QR Code Modal -->
|
||||||
|
<div id="qrModal" class="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="closeModal()">×</span>
|
||||||
|
<div class="modal-title">微信扫码支付</div>
|
||||||
|
<div style="margin: 15px 0; color: #28a745; font-weight: bold; font-size: 1.1rem;">
|
||||||
|
<span style="display:inline-block; margin-right:5px; vertical-align:middle;">📱</span>
|
||||||
|
请使用微信“扫一扫”
|
||||||
|
</div>
|
||||||
|
<div id="qrcode"></div>
|
||||||
|
<div id="payment-status" style="color: #666; font-size: 14px; margin-top: 15px;">等待支付...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
let pollTimer = null;
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('qrModal').style.display = 'none';
|
||||||
|
if (pollTimer) clearInterval(pollTimer);
|
||||||
|
document.getElementById('qrcode').innerHTML = '';
|
||||||
|
|
||||||
|
// Reset button state
|
||||||
|
const btn = document.getElementById('submitBtn');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '立即支付报名';
|
||||||
|
}
|
||||||
|
|
||||||
async function handlePayment(e) {
|
async function handlePayment(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const btn = document.getElementById('submitBtn');
|
const btn = document.getElementById('submitBtn');
|
||||||
const msg = document.getElementById('message');
|
const msg = document.getElementById('message');
|
||||||
const originalText = btn.innerHTML;
|
const originalText = "立即支付报名"; // Hardcoded or get from element
|
||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
const name = document.getElementById('name').value.trim();
|
const getVal = (id) => {
|
||||||
const phone = document.getElementById('phone').value.trim();
|
const el = document.getElementById(id);
|
||||||
|
return el ? el.value.trim() : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const name = getVal('name');
|
||||||
|
const phone = getVal('phone');
|
||||||
|
|
||||||
if (!name || !phone) {
|
if (!name || !phone) {
|
||||||
showMessage('请填写必填项', 'error');
|
showMessage('请填写必填项', 'error');
|
||||||
@@ -261,13 +342,15 @@
|
|||||||
const data = {
|
const data = {
|
||||||
name: name,
|
name: name,
|
||||||
phone: phone,
|
phone: phone,
|
||||||
company_name: document.getElementById('company_name').value.trim(),
|
company_name: getVal('industry_company')
|
||||||
position: document.getElementById('position').value.trim(),
|
|
||||||
business_scope: document.getElementById('business_scope').value.trim()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check Screen Width
|
||||||
|
const isWideScreen = window.innerWidth > 768;
|
||||||
|
const apiEndpoint = isWideScreen ? '/api/payment/native' : '/api/payment/h5';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/payment/h5', {
|
const res = await fetch(apiEndpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -277,10 +360,27 @@
|
|||||||
|
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
|
||||||
if (result.success && result.h5_url) {
|
if (result.success) {
|
||||||
|
if (isWideScreen && result.code_url) {
|
||||||
|
// Show Native Pay QR Code
|
||||||
|
document.getElementById('qrModal').style.display = 'flex';
|
||||||
|
document.getElementById('qrcode').innerHTML = ''; // Clear previous
|
||||||
|
new QRCode(document.getElementById("qrcode"), {
|
||||||
|
text: result.code_url,
|
||||||
|
width: 200,
|
||||||
|
height: 200
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start Polling
|
||||||
|
startPolling(result.out_trade_no);
|
||||||
|
|
||||||
|
} else if (result.h5_url) {
|
||||||
|
// H5 Redirect
|
||||||
showMessage('正在跳转支付...', 'success');
|
showMessage('正在跳转支付...', 'success');
|
||||||
// Redirect to WeChat Pay
|
|
||||||
window.location.href = result.h5_url;
|
window.location.href = result.h5_url;
|
||||||
|
} else {
|
||||||
|
throw new Error('未获取到支付链接');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showMessage(result.message || '支付请求失败', 'error');
|
showMessage(result.message || '支付请求失败', 'error');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
@@ -288,12 +388,45 @@
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
showMessage('网络请求错误,请重试', 'error');
|
showMessage(err.message || '网络请求错误,请重试', 'error');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = originalText;
|
btn.innerHTML = originalText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startPolling(out_trade_no) {
|
||||||
|
const statusDiv = document.getElementById('payment-status');
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 600; // 30 mins max
|
||||||
|
|
||||||
|
if (pollTimer) clearInterval(pollTimer);
|
||||||
|
|
||||||
|
pollTimer = setInterval(async () => {
|
||||||
|
attempts++;
|
||||||
|
if (attempts > maxAttempts) {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
statusDiv.textContent = '支付超时,请刷新重试';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/payment/check/${out_trade_no}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success && data.trade_state === 'SUCCESS') {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
statusDiv.textContent = '支付成功!正在跳转...';
|
||||||
|
statusDiv.style.color = '#28a745';
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/success';
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Polling error", e);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
function showMessage(text, type) {
|
function showMessage(text, type) {
|
||||||
const msg = document.getElementById('message');
|
const msg = document.getElementById('message');
|
||||||
msg.textContent = text;
|
msg.textContent = text;
|
||||||
|
|||||||
Reference in New Issue
Block a user