完成页面签到一致性以及手机验证
This commit is contained in:
Binary file not shown.
19
config.json
19
config.json
@@ -8,7 +8,8 @@
|
|||||||
"secondary_color": "#0066ff",
|
"secondary_color": "#0066ff",
|
||||||
"bg_color": "#050814",
|
"bg_color": "#050814",
|
||||||
"header_image": "/static/image1.jpg",
|
"header_image": "/static/image1.jpg",
|
||||||
"enable_ticket_validation": false,
|
"enable_ticket_validation": true,
|
||||||
|
"enable_sms_verification": true,
|
||||||
"enable_payment": true,
|
"enable_payment": true,
|
||||||
"payment_amount": 0.01,
|
"payment_amount": 0.01,
|
||||||
"wechat_pay_config": {
|
"wechat_pay_config": {
|
||||||
@@ -53,21 +54,21 @@
|
|||||||
"company_name": {
|
"company_name": {
|
||||||
"label": "单位名称",
|
"label": "单位名称",
|
||||||
"show": true,
|
"show": true,
|
||||||
"required": false
|
"required": true
|
||||||
},
|
},
|
||||||
"position": {
|
"position": {
|
||||||
"label": "职务",
|
"label": "职务",
|
||||||
"show": true,
|
"show": true,
|
||||||
"required": false
|
"required": true
|
||||||
},
|
},
|
||||||
"business_scope": {
|
"business_scope": {
|
||||||
"label": "公司主要经营 / 业务",
|
"label": "公司主要经营 / 业务",
|
||||||
"show": true,
|
"show": true,
|
||||||
"required": false
|
"required": true
|
||||||
},
|
},
|
||||||
"vision_2026": {
|
"vision_2026": {
|
||||||
"label": "2026年业务愿景",
|
"label": "2026年业务愿景",
|
||||||
"show": true,
|
"show": false,
|
||||||
"required": false
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
"learn_more_url": "",
|
"learn_more_url": "",
|
||||||
"show_fields": {
|
"show_fields": {
|
||||||
"name": true,
|
"name": true,
|
||||||
"company_name": true,
|
"company_name": false,
|
||||||
"position": true,
|
"position": true,
|
||||||
"vision_2026": true,
|
"vision_2026": true,
|
||||||
"business_scope": true
|
"business_scope": true
|
||||||
@@ -85,7 +86,7 @@
|
|||||||
},
|
},
|
||||||
"db_host": "6.6.6.66",
|
"db_host": "6.6.6.66",
|
||||||
"db_port": "5432",
|
"db_port": "5432",
|
||||||
"db_user": "123",
|
"db_user": "AI_event",
|
||||||
"db_password": "1231",
|
"db_password": "123AI_event",
|
||||||
"db_name": "123"
|
"db_name": "AI_event"
|
||||||
}
|
}
|
||||||
154
main.py
154
main.py
@@ -7,6 +7,8 @@ import psycopg2
|
|||||||
from psycopg2 import pool
|
from psycopg2 import pool
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict
|
||||||
|
import datetime
|
||||||
|
from datetime import timedelta
|
||||||
import random
|
import random
|
||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
@@ -40,6 +42,11 @@ DEFAULT_CONFIG = {
|
|||||||
"db_user": os.getenv("DB_USER", "gsdh"),
|
"db_user": os.getenv("DB_USER", "gsdh"),
|
||||||
"db_password": os.getenv("DB_PASSWORD", "123gsdh"),
|
"db_password": os.getenv("DB_PASSWORD", "123gsdh"),
|
||||||
"db_name": os.getenv("DB_NAME", "gsdh"),
|
"db_name": os.getenv("DB_NAME", "gsdh"),
|
||||||
|
"enable_sms_verification": False,
|
||||||
|
"sms_verification_config": {
|
||||||
|
"template_code": "SMS_493295002",
|
||||||
|
"sign_name": "叠加态科技云南"
|
||||||
|
},
|
||||||
"enable_ticket_validation": True,
|
"enable_ticket_validation": True,
|
||||||
"enable_seating": True,
|
"enable_seating": True,
|
||||||
"total_tables": 14,
|
"total_tables": 14,
|
||||||
@@ -47,7 +54,7 @@ DEFAULT_CONFIG = {
|
|||||||
"ticket_field_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}
|
"industry_company": {"label": "单位名称/行业类型", "show": True, "required": True}
|
||||||
},
|
},
|
||||||
"checkin_field_config": {
|
"checkin_field_config": {
|
||||||
"name": {"label": "姓名", "show": True, "required": True},
|
"name": {"label": "姓名", "show": True, "required": True},
|
||||||
@@ -107,6 +114,9 @@ def save_config(config):
|
|||||||
|
|
||||||
CONFIG = load_config()
|
CONFIG = load_config()
|
||||||
|
|
||||||
|
# SMS Verification Storage (In-memory)
|
||||||
|
SMS_CODES = {} # phone -> {code: str, expires_at: datetime}
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
# 进程池全局变量
|
# 进程池全局变量
|
||||||
@@ -114,11 +124,29 @@ process_pool = None
|
|||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
|
import sys
|
||||||
|
print(f"DEBUG: Running from {__file__}")
|
||||||
|
print(f"DEBUG: Python executable: {sys.executable}")
|
||||||
|
|
||||||
global process_pool
|
global process_pool
|
||||||
# RK3588 有 8 个核心,预留一些给数据库和系统,使用 6 个核心进行计算
|
# RK3588 有 8 个核心,预留一些给数据库和系统,使用 6 个核心进行计算
|
||||||
process_pool = ProcessPoolExecutor(max_workers=6)
|
process_pool = ProcessPoolExecutor(max_workers=6)
|
||||||
print("ProcessPoolExecutor initialized with 6 workers")
|
print("ProcessPoolExecutor initialized with 6 workers")
|
||||||
|
|
||||||
|
# Debug: Print all registered routes
|
||||||
|
print("=== Registered Routes ===")
|
||||||
|
for route in app.routes:
|
||||||
|
if hasattr(route, "methods"):
|
||||||
|
print(f"{route.path} {route.methods}")
|
||||||
|
else:
|
||||||
|
print(f"{route.path}")
|
||||||
|
print("=========================")
|
||||||
|
|
||||||
|
@app.post("/api/test-sms")
|
||||||
|
async def test_sms_simple():
|
||||||
|
return {"message": "Test SMS route is working"}
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
async def shutdown_event():
|
async def shutdown_event():
|
||||||
if process_pool:
|
if process_pool:
|
||||||
@@ -306,6 +334,7 @@ class CheckinRequest(BaseModel):
|
|||||||
gsdh_id: str
|
gsdh_id: str
|
||||||
name: str
|
name: str
|
||||||
phone: str
|
phone: str
|
||||||
|
verification_code: Optional[str] = None
|
||||||
company_name: Optional[str] = None
|
company_name: Optional[str] = None
|
||||||
position: Optional[str] = None
|
position: Optional[str] = None
|
||||||
business_scope: Optional[str] = None
|
business_scope: Optional[str] = None
|
||||||
@@ -315,10 +344,113 @@ class CheckinRequest(BaseModel):
|
|||||||
class AddUserRequest(BaseModel):
|
class AddUserRequest(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
phone: str
|
phone: str
|
||||||
|
verification_code: Optional[str] = None
|
||||||
industry_company: Optional[str] = None
|
industry_company: Optional[str] = None
|
||||||
fee: Optional[str] = None
|
fee: Optional[str] = None
|
||||||
payment_channel: Optional[str] = None
|
payment_channel: Optional[str] = None
|
||||||
|
|
||||||
|
class SendSMSRequest(BaseModel):
|
||||||
|
phone: str
|
||||||
|
|
||||||
|
def verify_sms_code(phone: str, code: str) -> bool:
|
||||||
|
if not CONFIG.get("enable_sms_verification", False):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not code:
|
||||||
|
return False
|
||||||
|
|
||||||
|
record = SMS_CODES.get(phone)
|
||||||
|
if not record:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if datetime.datetime.now() > record["expires_at"]:
|
||||||
|
del SMS_CODES[phone]
|
||||||
|
return False
|
||||||
|
|
||||||
|
if record["code"] == code:
|
||||||
|
# Optional: Delete after successful verification to prevent replay
|
||||||
|
# del SMS_CODES[phone]
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
class VerifyCodeRequest(BaseModel):
|
||||||
|
phone: str
|
||||||
|
code: str
|
||||||
|
|
||||||
|
@app.post("/api/verify-sms-code")
|
||||||
|
def verify_sms_code_endpoint(req: VerifyCodeRequest):
|
||||||
|
if verify_sms_code(req.phone, req.code):
|
||||||
|
return JSONResponse({"success": True, "message": "验证成功"})
|
||||||
|
else:
|
||||||
|
return JSONResponse({"success": False, "message": "验证码错误或已过期"})
|
||||||
|
|
||||||
|
@app.api_route("/api/send-sms", methods=["GET", "POST"])
|
||||||
|
async def send_sms_endpoint(req: Request):
|
||||||
|
print(f"DEBUG: send_sms_endpoint called via {req.method}")
|
||||||
|
|
||||||
|
# Handle GET for testing
|
||||||
|
if req.method == "GET":
|
||||||
|
return JSONResponse({"message": "GET method works, please use POST"})
|
||||||
|
|
||||||
|
# Handle POST
|
||||||
|
try:
|
||||||
|
body = await req.json()
|
||||||
|
phone = body.get("phone")
|
||||||
|
except:
|
||||||
|
return JSONResponse({"success": False, "message": "Invalid JSON body"}, status_code=400)
|
||||||
|
|
||||||
|
print(f"DEBUG: Processing phone={phone}")
|
||||||
|
|
||||||
|
if not CONFIG.get("enable_sms_verification", False):
|
||||||
|
return JSONResponse({"success": False, "message": "短信验证未开启"})
|
||||||
|
|
||||||
|
if not phone or len(phone) != 11:
|
||||||
|
return JSONResponse({"success": False, "message": "手机号格式错误"})
|
||||||
|
|
||||||
|
# Generate 4 digit code
|
||||||
|
code = str(random.randint(1000, 9999))
|
||||||
|
|
||||||
|
# Store code (expires in 5 minutes)
|
||||||
|
SMS_CODES[phone] = {
|
||||||
|
"code": code,
|
||||||
|
"expires_at": datetime.datetime.now() + timedelta(minutes=5)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send SMS via external API
|
||||||
|
sms_config = CONFIG.get("sms_verification_config", {})
|
||||||
|
payload = {
|
||||||
|
"phone_number": phone,
|
||||||
|
"code": code,
|
||||||
|
"template_code": sms_config.get("template_code", "SMS_493295002"),
|
||||||
|
"sign_name": sms_config.get("sign_name", "叠加态科技云南")
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"DEBUG: Sending SMS payload: {payload}")
|
||||||
|
# Using a timeout to prevent hanging
|
||||||
|
res = requests.post(
|
||||||
|
'https://data.tangledup-ai.com/api/send-sms',
|
||||||
|
json=payload,
|
||||||
|
headers={'Content-Type': 'application/json', 'accept': 'application/json'},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"DEBUG: SMS API Response: {res.status_code} - {res.text}")
|
||||||
|
|
||||||
|
if res.status_code == 200:
|
||||||
|
data = res.json()
|
||||||
|
if data.get("status") == "success":
|
||||||
|
return JSONResponse({"success": True, "message": "验证码已发送"})
|
||||||
|
else:
|
||||||
|
return JSONResponse({"success": False, "message": data.get("message", "发送失败")})
|
||||||
|
else:
|
||||||
|
return JSONResponse({"success": False, "message": f"发送失败: {res.status_code}"})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"SMS Send Error: {e}")
|
||||||
|
return JSONResponse({"success": False, "message": "发送验证码请求失败"})
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
max_retries = 5
|
max_retries = 5
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
@@ -732,6 +864,15 @@ def search_user(query: str):
|
|||||||
else:
|
else:
|
||||||
user = users[0]
|
user = users[0]
|
||||||
|
|
||||||
|
# Normalize fee to float for frontend consistency
|
||||||
|
if user.get('fee'):
|
||||||
|
try:
|
||||||
|
user['fee'] = float(user['fee'])
|
||||||
|
except:
|
||||||
|
user['fee'] = 0.0
|
||||||
|
else:
|
||||||
|
user['fee'] = 0.0
|
||||||
|
|
||||||
# Check if already signed
|
# Check if already signed
|
||||||
if user.get('is_signed') == 'TRUE':
|
if user.get('is_signed') == 'TRUE':
|
||||||
# Check if already signed
|
# Check if already signed
|
||||||
@@ -776,6 +917,11 @@ def search_user(query: str):
|
|||||||
|
|
||||||
@app.post("/api/checkin")
|
@app.post("/api/checkin")
|
||||||
def checkin_user(checkin_data: CheckinRequest):
|
def checkin_user(checkin_data: CheckinRequest):
|
||||||
|
# Verify SMS Code if enabled
|
||||||
|
if CONFIG.get("enable_sms_verification", False):
|
||||||
|
if not verify_sms_code(checkin_data.phone, checkin_data.verification_code):
|
||||||
|
return JSONResponse(content={"success": False, "message": "手机验证码错误或已过期"}, status_code=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
@@ -1518,6 +1664,7 @@ async def success_page(request: Request):
|
|||||||
class TicketPaymentRequest(BaseModel):
|
class TicketPaymentRequest(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
phone: str
|
phone: str
|
||||||
|
verification_code: Optional[str] = None
|
||||||
company_name: Optional[str] = None
|
company_name: Optional[str] = None
|
||||||
position: Optional[str] = None
|
position: Optional[str] = None
|
||||||
business_scope: Optional[str] = None
|
business_scope: Optional[str] = None
|
||||||
@@ -1628,6 +1775,11 @@ async def create_native_payment(req: TicketPaymentRequest, request: Request):
|
|||||||
global CONFIG
|
global CONFIG
|
||||||
CONFIG = load_config()
|
CONFIG = load_config()
|
||||||
|
|
||||||
|
# Verify SMS Code if enabled
|
||||||
|
if CONFIG.get("enable_sms_verification", False):
|
||||||
|
if not verify_sms_code(req.phone, req.verification_code):
|
||||||
|
return JSONResponse(content={"success": False, "message": "手机验证码错误或已过期"}, status_code=400)
|
||||||
|
|
||||||
if not CONFIG.get("enable_payment", False):
|
if not CONFIG.get("enable_payment", False):
|
||||||
return JSONResponse(content={"success": False, "message": "支付未开启"}, status_code=403)
|
return JSONResponse(content={"success": False, "message": "支付未开启"}, status_code=403)
|
||||||
|
|
||||||
|
|||||||
@@ -363,6 +363,10 @@
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkbox-wrapper">
|
||||||
|
<input type="checkbox" id="enable_sms_verification">
|
||||||
|
<label for="enable_sms_verification">开启手机号短信验证 (SMS Verification)</label>
|
||||||
|
</div>
|
||||||
<p style="font-size: 0.9em; color: var(--text-muted); margin-left: 30px;">
|
<p style="font-size: 0.9em; color: var(--text-muted); margin-left: 30px;">
|
||||||
关闭后将跳过数据库核查,直接允许签到(若用户不存在将自动创建)。
|
关闭后将跳过数据库核查,直接允许签到(若用户不存在将自动创建)。
|
||||||
</p>
|
</p>
|
||||||
@@ -690,7 +694,7 @@
|
|||||||
renderFieldConfig('ticket_field_config_body', config.ticket_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}
|
"industry_company": {"label": "行业名称/单位名称", "show": true, "required": true}
|
||||||
}, ["name", "phone", "industry_company"]);
|
}, ["name", "phone", "industry_company"]);
|
||||||
|
|
||||||
renderFieldConfig('checkin_field_config_body', config.checkin_field_config || {
|
renderFieldConfig('checkin_field_config_body', config.checkin_field_config || {
|
||||||
@@ -797,6 +801,7 @@
|
|||||||
|
|
||||||
// Checkin Config
|
// Checkin Config
|
||||||
enable_ticket_validation: document.getElementById('enable_ticket_validation').checked,
|
enable_ticket_validation: document.getElementById('enable_ticket_validation').checked,
|
||||||
|
enable_sms_verification: document.getElementById('enable_sms_verification').checked,
|
||||||
|
|
||||||
// Payment Config
|
// Payment Config
|
||||||
enable_payment: document.getElementById('enable_payment').checked,
|
enable_payment: document.getElementById('enable_payment').checked,
|
||||||
|
|||||||
@@ -524,35 +524,33 @@
|
|||||||
<input type="text" id="form-phone" required>
|
<input type="text" id="form-phone" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set fc = config.field_config or {} %}
|
<div class="input-group hidden" id="sms-verification-group">
|
||||||
|
<label>验证码</label>
|
||||||
{% if fc.company_name and fc.company_name.show %}
|
<div style="display: flex; gap: 10px;">
|
||||||
<div class="input-group">
|
<input type="text" id="form-verification-code" placeholder="输入验证码" style="flex: 1;">
|
||||||
<label>{{ fc.company_name.label }}</label>
|
<button type="button" id="send-checkin-code-btn" onclick="sendCheckinSms()" style="width: auto; padding: 14px; background: var(--secondary-color); margin-bottom: 0; border: none; border-radius: 8px; color: white; cursor: pointer; font-weight: bold;">发送验证码</button>
|
||||||
<input type="text" id="form-company" placeholder="点此输入" {{ 'required' if fc.company_name.required else '' }}>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if fc.position and fc.position.show %}
|
<div class="input-group hidden" id="group-company">
|
||||||
<div class="input-group">
|
<label id="label-company">单位名称</label>
|
||||||
<label>{{ fc.position.label }}</label>
|
<input type="text" id="form-company" placeholder="点此输入">
|
||||||
<input type="text" id="form-position" placeholder="点此输入" {{ 'required' if fc.position.required else '' }}>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if fc.business_scope and fc.business_scope.show %}
|
<div class="input-group hidden" id="group-position">
|
||||||
<div class="input-group">
|
<label id="label-position">职务</label>
|
||||||
<label>{{ fc.business_scope.label }}</label>
|
<input type="text" id="form-position" placeholder="点此输入">
|
||||||
<textarea id="form-business" rows="2" placeholder="点此输入" {{ 'required' if fc.business_scope.required else '' }}></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if fc.vision_2026 and fc.vision_2026.show %}
|
<div class="input-group hidden" id="group-business">
|
||||||
<div class="input-group">
|
<label id="label-business">公司主要经营 / 业务</label>
|
||||||
<label>{{ fc.vision_2026.label }}</label>
|
<textarea id="form-business" rows="2" placeholder="点此输入"></textarea>
|
||||||
<textarea id="form-vision" rows="3" placeholder="点此输入 (为了业务更好对接,最好表述清晰)" {{ 'required' if fc.vision_2026.required else '' }}></textarea>
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group hidden" id="group-vision">
|
||||||
|
<label id="label-vision">2026年业务愿景</label>
|
||||||
|
<textarea id="form-vision" rows="3" placeholder="点此输入 (为了业务更好对接,最好表述清晰)"></textarea>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<input type="hidden" id="form-location" value="">
|
<input type="hidden" id="form-location" value="">
|
||||||
|
|
||||||
@@ -646,6 +644,138 @@
|
|||||||
// Inject Config
|
// Inject Config
|
||||||
const CONFIG = {{ config | tojson }};
|
const CONFIG = {{ config | tojson }};
|
||||||
|
|
||||||
|
let isCheckinCodeVerified = false;
|
||||||
|
|
||||||
|
async function refreshConfig() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/config');
|
||||||
|
if (res.ok) {
|
||||||
|
const newConfig = await res.json();
|
||||||
|
// Update global CONFIG object
|
||||||
|
Object.assign(CONFIG, newConfig);
|
||||||
|
applyConfigUI();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to refresh config", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyConfigUI() {
|
||||||
|
const smsGroup = document.getElementById('sms-verification-group');
|
||||||
|
if (smsGroup) {
|
||||||
|
if (CONFIG.enable_sms_verification) {
|
||||||
|
smsGroup.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
smsGroup.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply field config (Show/Hide/Required)
|
||||||
|
const fc = CONFIG.checkin_field_config || {};
|
||||||
|
const fields = [
|
||||||
|
{ key: 'company_name', groupId: 'group-company', inputId: 'form-company', labelId: 'label-company' },
|
||||||
|
{ key: 'position', groupId: 'group-position', inputId: 'form-position', labelId: 'label-position' },
|
||||||
|
{ key: 'business_scope', groupId: 'group-business', inputId: 'form-business', labelId: 'label-business' },
|
||||||
|
{ key: 'vision_2026', groupId: 'group-vision', inputId: 'form-vision', labelId: 'label-vision' }
|
||||||
|
];
|
||||||
|
|
||||||
|
fields.forEach(field => {
|
||||||
|
const conf = fc[field.key] || { show: false, required: false, label: '' };
|
||||||
|
const group = document.getElementById(field.groupId);
|
||||||
|
const input = document.getElementById(field.inputId);
|
||||||
|
const label = document.getElementById(field.labelId);
|
||||||
|
|
||||||
|
if (group && input) {
|
||||||
|
// Visibility
|
||||||
|
if (conf.show) {
|
||||||
|
group.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
group.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required
|
||||||
|
input.required = conf.required === true;
|
||||||
|
|
||||||
|
// Label
|
||||||
|
if (label && conf.label) {
|
||||||
|
label.textContent = conf.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCheckinButtonState() {
|
||||||
|
const btn = document.getElementById('submit-btn');
|
||||||
|
const codeInput = document.getElementById('form-verification-code');
|
||||||
|
// Check if loading (by checking loader class or text content)
|
||||||
|
if (btn.innerHTML.includes('loader')) return;
|
||||||
|
|
||||||
|
if (codeInput) {
|
||||||
|
if (isCheckinCodeVerified && codeInput.value.trim().length === 4) {
|
||||||
|
btn.disabled = false;
|
||||||
|
} else {
|
||||||
|
btn.disabled = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyCheckinCode() {
|
||||||
|
const codeInput = document.getElementById('form-verification-code');
|
||||||
|
const phoneInput = document.getElementById('form-phone');
|
||||||
|
const errorDiv = document.getElementById('submit-error');
|
||||||
|
|
||||||
|
if (!codeInput) return;
|
||||||
|
|
||||||
|
const code = codeInput.value.trim();
|
||||||
|
const phone = phoneInput ? phoneInput.value.trim() : '';
|
||||||
|
|
||||||
|
if (code.length === 4 && phone) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/verify-sms-code', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({phone: phone, code: code})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
isCheckinCodeVerified = true;
|
||||||
|
errorDiv.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
isCheckinCodeVerified = false;
|
||||||
|
errorDiv.textContent = "验证码错误";
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
isCheckinCodeVerified = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isCheckinCodeVerified = false;
|
||||||
|
if (code.length < 4) errorDiv.classList.add('hidden');
|
||||||
|
}
|
||||||
|
updateCheckinButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
applyConfigUI();
|
||||||
|
const codeInput = document.getElementById('form-verification-code');
|
||||||
|
const phoneInput = document.getElementById('form-phone');
|
||||||
|
|
||||||
|
if (codeInput) {
|
||||||
|
codeInput.addEventListener('input', verifyCheckinCode);
|
||||||
|
// Initial check
|
||||||
|
updateCheckinButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phoneInput) {
|
||||||
|
phoneInput.addEventListener('input', () => {
|
||||||
|
isCheckinCodeVerified = false;
|
||||||
|
updateCheckinButtonState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function searchUser() {
|
async function searchUser() {
|
||||||
const query = document.getElementById('search-input').value.trim();
|
const query = document.getElementById('search-input').value.trim();
|
||||||
const btn = document.getElementById('search-btn');
|
const btn = document.getElementById('search-btn');
|
||||||
@@ -672,6 +802,7 @@
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
if (data.allow_create) {
|
if (data.allow_create) {
|
||||||
|
await refreshConfig();
|
||||||
const u = data.user || {};
|
const u = data.user || {};
|
||||||
const tempId = 'TEMP_' + Date.now();
|
const tempId = 'TEMP_' + Date.now();
|
||||||
currentUser = { new_id: tempId };
|
currentUser = { new_id: tempId };
|
||||||
@@ -727,6 +858,7 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Single user found
|
// Single user found
|
||||||
|
await refreshConfig();
|
||||||
selectUser(data.user);
|
selectUser(data.user);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -743,11 +875,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectUser(user) {
|
async function selectUser(user) {
|
||||||
// Check if fee is 0 (unpaid)
|
// Check if fee is 0 (unpaid)
|
||||||
if (user.fee == 0) {
|
// Ensure fee is treated as number. If fee is missing or 0, block checkin.
|
||||||
|
// CONFIG.payment_amount is usually 0.01 or 26.
|
||||||
|
const fee = parseFloat(user.fee || 0);
|
||||||
|
if (fee <= 0) {
|
||||||
const errorDiv = document.getElementById('search-error');
|
const errorDiv = document.getElementById('search-error');
|
||||||
errorDiv.textContent = "⚠️ 无法签到:请先前往签到处付费26元";
|
const amount = CONFIG.payment_amount || 26;
|
||||||
|
errorDiv.textContent = `⚠️ 无法签到:请先前往签到处付费${amount}元`;
|
||||||
errorDiv.classList.remove('hidden');
|
errorDiv.classList.remove('hidden');
|
||||||
errorDiv.classList.add('important-warning');
|
errorDiv.classList.add('important-warning');
|
||||||
errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
@@ -758,18 +894,32 @@
|
|||||||
|
|
||||||
// Populate form
|
// Populate form
|
||||||
document.getElementById('gsdh-id').value = user.new_id;
|
document.getElementById('gsdh-id').value = user.new_id;
|
||||||
document.getElementById('form-name').value = user.name;
|
|
||||||
document.getElementById('form-phone').value = user.phone;
|
const setVal = (id, val) => {
|
||||||
document.getElementById('form-company').value = user.industry_company || ''; // Pre-fill if we have something roughly mapping
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.value = val || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
setVal('form-name', user.name);
|
||||||
|
setVal('form-phone', user.phone);
|
||||||
|
setVal('form-company', user.industry_company);
|
||||||
|
// Clear other optional fields to avoid carrying over data if user switches
|
||||||
|
setVal('form-position', user.position); // Note: user.position might not exist in search result
|
||||||
|
setVal('form-business', user.business_scope);
|
||||||
|
setVal('form-vision', user.vision_2026);
|
||||||
|
|
||||||
// Display preview
|
// Display preview
|
||||||
document.getElementById('display-name').textContent = user.name;
|
if (document.getElementById('display-name')) document.getElementById('display-name').textContent = user.name;
|
||||||
document.getElementById('display-phone').textContent = user.phone;
|
if (document.getElementById('display-phone')) document.getElementById('display-phone').textContent = user.phone;
|
||||||
document.getElementById('display-company').textContent = user.industry_company || '暂无单位信息';
|
if (document.getElementById('display-company')) document.getElementById('display-company').textContent = user.industry_company || '暂无单位信息';
|
||||||
|
|
||||||
|
// Ensure UI reflects current config (e.g. SMS button)
|
||||||
|
applyConfigUI();
|
||||||
|
|
||||||
// Switch steps
|
// Switch steps
|
||||||
document.getElementById('step-search').classList.add('hidden');
|
document.getElementById('step-search').classList.add('hidden');
|
||||||
document.getElementById('step-form').classList.remove('hidden');
|
document.getElementById('step-form').classList.remove('hidden');
|
||||||
|
updateCheckinButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitCheckin(e) {
|
async function submitCheckin(e) {
|
||||||
@@ -791,6 +941,7 @@
|
|||||||
gsdh_id: getVal('gsdh-id'),
|
gsdh_id: getVal('gsdh-id'),
|
||||||
name: getVal('form-name'),
|
name: getVal('form-name'),
|
||||||
phone: getVal('form-phone'),
|
phone: getVal('form-phone'),
|
||||||
|
verification_code: getVal('form-verification-code'),
|
||||||
company_name: getVal('form-company'),
|
company_name: getVal('form-company'),
|
||||||
position: getVal('form-position'),
|
position: getVal('form-position'),
|
||||||
business_scope: getVal('form-business'),
|
business_scope: getVal('form-business'),
|
||||||
@@ -840,8 +991,8 @@
|
|||||||
errorDiv.textContent = "网络错误,请稍后重试";
|
errorDiv.textContent = "网络错误,请稍后重试";
|
||||||
errorDiv.classList.remove('hidden');
|
errorDiv.classList.remove('hidden');
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = '确认签到';
|
btn.textContent = '确认签到';
|
||||||
|
updateCheckinButtonState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -903,6 +1054,57 @@
|
|||||||
document.getElementById('user-list').innerHTML = '';
|
document.getElementById('user-list').innerHTML = '';
|
||||||
document.getElementById('checkin-form').reset();
|
document.getElementById('checkin-form').reset();
|
||||||
currentUser = null;
|
currentUser = null;
|
||||||
|
updateCheckinButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendCheckinSms() {
|
||||||
|
const phone = document.getElementById('form-phone').value.trim();
|
||||||
|
const errorDiv = document.getElementById('submit-error');
|
||||||
|
|
||||||
|
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||||
|
errorDiv.textContent = '请输入有效的手机号码';
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
errorDiv.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('send-checkin-code-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
let count = 60;
|
||||||
|
const originalText = "发送验证码";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/send-sms', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({phone: phone})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
btn.innerText = `${count}s`;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
count--;
|
||||||
|
btn.innerText = `${count}s`;
|
||||||
|
if (count <= 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerText = originalText;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
errorDiv.textContent = data.message || '发送失败';
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errorDiv.textContent = '网络错误';
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -269,6 +269,12 @@
|
|||||||
<label>{{ field.label }}{% if field.required %} *{% endif %}</label>
|
<label>{{ field.label }}{% if field.required %} *{% endif %}</label>
|
||||||
{% if key == 'phone' %}
|
{% if key == 'phone' %}
|
||||||
<input type="tel" id="{{ key }}" {% if field.required %}required{% endif %} placeholder="请输入{{ field.label }}" pattern="[0-9]{11}">
|
<input type="tel" id="{{ key }}" {% if field.required %}required{% endif %} placeholder="请输入{{ field.label }}" pattern="[0-9]{11}">
|
||||||
|
{% if config.enable_sms_verification %}
|
||||||
|
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
||||||
|
<input type="text" id="verification_code" placeholder="请先发送验证码" style="flex: 1;" disabled>
|
||||||
|
<button type="button" id="send-code-btn" onclick="sendSmsCode()" style="width: auto; padding: 10px; font-size: 0.9rem; background: var(--secondary-color); margin-top: 0; border: none; border-radius: 8px; color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; min-width: 100px;">发送验证码</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<input type="text" id="{{ key }}" {% if field.required %}required{% endif %} placeholder="请输入{{ field.label }}">
|
<input type="text" id="{{ key }}" {% if field.required %}required{% endif %} placeholder="请输入{{ field.label }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -301,6 +307,57 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let pollTimer = null;
|
let pollTimer = null;
|
||||||
|
let isCodeVerified = false;
|
||||||
|
|
||||||
|
function updateSubmitButtonState() {
|
||||||
|
const btn = document.getElementById('submitBtn');
|
||||||
|
const codeInput = document.getElementById('verification_code');
|
||||||
|
// Only update if not in loading state (checking innerHTML for loading spinner)
|
||||||
|
if (btn.innerHTML.includes('loading')) return;
|
||||||
|
|
||||||
|
if (codeInput) {
|
||||||
|
if (isCodeVerified && codeInput.value.trim().length === 4) {
|
||||||
|
btn.disabled = false;
|
||||||
|
} else {
|
||||||
|
btn.disabled = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyCode() {
|
||||||
|
const codeInput = document.getElementById('verification_code');
|
||||||
|
const phoneInput = document.getElementById('phone');
|
||||||
|
|
||||||
|
if (!codeInput) return;
|
||||||
|
|
||||||
|
const code = codeInput.value.trim();
|
||||||
|
const phone = phoneInput ? phoneInput.value.trim() : '';
|
||||||
|
|
||||||
|
if (code.length === 4 && phone) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/verify-sms-code', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({phone: phone, code: code})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
isCodeVerified = true;
|
||||||
|
showMessage('验证码正确', 'success');
|
||||||
|
} else {
|
||||||
|
isCodeVerified = false;
|
||||||
|
showMessage('验证码错误', 'error');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
isCodeVerified = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isCodeVerified = false;
|
||||||
|
}
|
||||||
|
updateSubmitButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
document.getElementById('qrModal').style.display = 'none';
|
document.getElementById('qrModal').style.display = 'none';
|
||||||
@@ -309,10 +366,28 @@
|
|||||||
|
|
||||||
// Reset button state
|
// Reset button state
|
||||||
const btn = document.getElementById('submitBtn');
|
const btn = document.getElementById('submitBtn');
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = '立即支付报名';
|
btn.innerHTML = '立即支付报名';
|
||||||
|
updateSubmitButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize button state on load
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const codeInput = document.getElementById('verification_code');
|
||||||
|
const phoneInput = document.getElementById('phone');
|
||||||
|
|
||||||
|
if (codeInput) {
|
||||||
|
updateSubmitButtonState();
|
||||||
|
codeInput.addEventListener('input', verifyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phoneInput) {
|
||||||
|
phoneInput.addEventListener('input', () => {
|
||||||
|
isCodeVerified = false;
|
||||||
|
updateSubmitButtonState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function handlePayment(e) {
|
async function handlePayment(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -342,6 +417,7 @@
|
|||||||
const data = {
|
const data = {
|
||||||
name: name,
|
name: name,
|
||||||
phone: phone,
|
phone: phone,
|
||||||
|
verification_code: getVal('verification_code'),
|
||||||
company_name: getVal('industry_company')
|
company_name: getVal('industry_company')
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -383,14 +459,14 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showMessage(result.message || '支付请求失败', 'error');
|
showMessage(result.message || '支付请求失败', 'error');
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = originalText;
|
btn.innerHTML = originalText;
|
||||||
|
updateSubmitButtonState();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
showMessage(err.message || '网络请求错误,请重试', 'error');
|
showMessage(err.message || '网络请求错误,请重试', 'error');
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = originalText;
|
btn.innerHTML = originalText;
|
||||||
|
updateSubmitButtonState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,6 +509,65 @@
|
|||||||
msg.className = type;
|
msg.className = type;
|
||||||
msg.style.display = 'block';
|
msg.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendSmsCode() {
|
||||||
|
const phone = document.getElementById('phone').value.trim();
|
||||||
|
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||||
|
showMessage('请输入有效的手机号码', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('send-code-btn');
|
||||||
|
const codeInput = document.getElementById('verification_code');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
const originalText = "发送验证码";
|
||||||
|
// 添加loading效果
|
||||||
|
btn.innerHTML = '<span class="loading" style="width: 14px; height: 14px; border-width: 2px; margin-right: 6px;"></span>发送中';
|
||||||
|
|
||||||
|
let count = 60;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/send-sms', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({phone: phone})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showMessage('验证码已发送', 'success');
|
||||||
|
|
||||||
|
// 成功后才允许输入验证码
|
||||||
|
if (codeInput) {
|
||||||
|
codeInput.disabled = false;
|
||||||
|
codeInput.placeholder = "请输入验证码";
|
||||||
|
codeInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.innerText = `${count}s`;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
count--;
|
||||||
|
btn.innerText = `${count}s`;
|
||||||
|
if (count <= 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerText = originalText;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
showMessage(data.message || '发送失败', 'error');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerText = originalText;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showMessage('网络错误', 'error');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerText = originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user