diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index d272258..c66f396 100644 Binary files a/__pycache__/main.cpython-312.pyc and b/__pycache__/main.cpython-312.pyc differ diff --git a/config.json b/config.json index 602effb..8d7a633 100644 --- a/config.json +++ b/config.json @@ -8,7 +8,8 @@ "secondary_color": "#0066ff", "bg_color": "#050814", "header_image": "/static/image1.jpg", - "enable_ticket_validation": false, + "enable_ticket_validation": true, + "enable_sms_verification": true, "enable_payment": true, "payment_amount": 0.01, "wechat_pay_config": { @@ -53,21 +54,21 @@ "company_name": { "label": "单位名称", "show": true, - "required": false + "required": true }, "position": { "label": "职务", "show": true, - "required": false + "required": true }, "business_scope": { "label": "公司主要经营 / 业务", "show": true, - "required": false + "required": true }, "vision_2026": { "label": "2026年业务愿景", - "show": true, + "show": false, "required": false } }, @@ -77,7 +78,7 @@ "learn_more_url": "", "show_fields": { "name": true, - "company_name": true, + "company_name": false, "position": true, "vision_2026": true, "business_scope": true @@ -85,7 +86,7 @@ }, "db_host": "6.6.6.66", "db_port": "5432", - "db_user": "123", - "db_password": "1231", - "db_name": "123" + "db_user": "AI_event", + "db_password": "123AI_event", + "db_name": "AI_event" } \ No newline at end of file diff --git a/main.py b/main.py index 7677b78..dd9f633 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,8 @@ import psycopg2 from psycopg2 import pool from psycopg2.extras import RealDictCursor from typing import Optional, List, Dict +import datetime +from datetime import timedelta import random import uuid import os @@ -40,6 +42,11 @@ DEFAULT_CONFIG = { "db_user": os.getenv("DB_USER", "gsdh"), "db_password": os.getenv("DB_PASSWORD", "123gsdh"), "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_seating": True, "total_tables": 14, @@ -47,7 +54,7 @@ DEFAULT_CONFIG = { "ticket_field_config": { "name": {"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": { "name": {"label": "姓名", "show": True, "required": True}, @@ -107,6 +114,9 @@ def save_config(config): CONFIG = load_config() +# SMS Verification Storage (In-memory) +SMS_CODES = {} # phone -> {code: str, expires_at: datetime} + app = FastAPI() # 进程池全局变量 @@ -114,10 +124,28 @@ process_pool = None @app.on_event("startup") async def startup_event(): + import sys + print(f"DEBUG: Running from {__file__}") + print(f"DEBUG: Python executable: {sys.executable}") + global process_pool # RK3588 有 8 个核心,预留一些给数据库和系统,使用 6 个核心进行计算 process_pool = ProcessPoolExecutor(max_workers=6) 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") async def shutdown_event(): @@ -306,6 +334,7 @@ class CheckinRequest(BaseModel): gsdh_id: str name: str phone: str + verification_code: Optional[str] = None company_name: Optional[str] = None position: Optional[str] = None business_scope: Optional[str] = None @@ -315,10 +344,113 @@ class CheckinRequest(BaseModel): class AddUserRequest(BaseModel): name: str phone: str + verification_code: Optional[str] = None industry_company: Optional[str] = None fee: 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(): max_retries = 5 for attempt in range(max_retries): @@ -732,6 +864,15 @@ def search_user(query: str): else: 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 if user.get('is_signed') == 'TRUE': # Check if already signed @@ -776,6 +917,11 @@ def search_user(query: str): @app.post("/api/checkin") 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: conn = get_db_connection() cur = conn.cursor() @@ -1518,6 +1664,7 @@ async def success_page(request: Request): class TicketPaymentRequest(BaseModel): name: str phone: str + verification_code: Optional[str] = None company_name: Optional[str] = None position: Optional[str] = None business_scope: Optional[str] = None @@ -1628,6 +1775,11 @@ async def create_native_payment(req: TicketPaymentRequest, request: Request): global 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): return JSONResponse(content={"success": False, "message": "支付未开启"}, status_code=403) diff --git a/templates/admin.html b/templates/admin.html index a5e420e..a65936e 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -363,6 +363,10 @@ +
+ + +

关闭后将跳过数据库核查,直接允许签到(若用户不存在将自动创建)。

@@ -690,7 +694,7 @@ renderFieldConfig('ticket_field_config_body', config.ticket_field_config || { "name": {"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"]); renderFieldConfig('checkin_field_config_body', config.checkin_field_config || { @@ -797,6 +801,7 @@ // Checkin Config enable_ticket_validation: document.getElementById('enable_ticket_validation').checked, + enable_sms_verification: document.getElementById('enable_sms_verification').checked, // Payment Config enable_payment: document.getElementById('enable_payment').checked, diff --git a/templates/index.html b/templates/index.html index c9a3362..eb8fc5d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -524,35 +524,33 @@ - {% set fc = config.field_config or {} %} - - {% if fc.company_name and fc.company_name.show %} -
- - + - {% endif %} - {% if fc.position and fc.position.show %} -
- - + - {% endif %} - {% if fc.business_scope and fc.business_scope.show %} -
- - + - {% endif %} - {% if fc.vision_2026 and fc.vision_2026.show %} -
- - + + + - {% endif %} @@ -646,6 +644,138 @@ // Inject Config 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() { const query = document.getElementById('search-input').value.trim(); const btn = document.getElementById('search-btn'); @@ -672,6 +802,7 @@ if (response.ok) { if (data.allow_create) { + await refreshConfig(); const u = data.user || {}; const tempId = 'TEMP_' + Date.now(); currentUser = { new_id: tempId }; @@ -727,6 +858,7 @@ } } else { // Single user found + await refreshConfig(); selectUser(data.user); } } else { @@ -743,11 +875,15 @@ } } - function selectUser(user) { + async function selectUser(user) { // 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'); - errorDiv.textContent = "⚠️ 无法签到:请先前往签到处付费26元"; + const amount = CONFIG.payment_amount || 26; + errorDiv.textContent = `⚠️ 无法签到:请先前往签到处付费${amount}元`; errorDiv.classList.remove('hidden'); errorDiv.classList.add('important-warning'); errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' }); @@ -758,18 +894,32 @@ // Populate form document.getElementById('gsdh-id').value = user.new_id; - document.getElementById('form-name').value = user.name; - document.getElementById('form-phone').value = user.phone; - document.getElementById('form-company').value = user.industry_company || ''; // Pre-fill if we have something roughly mapping + const setVal = (id, val) => { + 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 - document.getElementById('display-name').textContent = user.name; - document.getElementById('display-phone').textContent = user.phone; - document.getElementById('display-company').textContent = user.industry_company || '暂无单位信息'; + if (document.getElementById('display-name')) document.getElementById('display-name').textContent = user.name; + if (document.getElementById('display-phone')) document.getElementById('display-phone').textContent = user.phone; + 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 document.getElementById('step-search').classList.add('hidden'); document.getElementById('step-form').classList.remove('hidden'); + updateCheckinButtonState(); } async function submitCheckin(e) { @@ -791,6 +941,7 @@ gsdh_id: getVal('gsdh-id'), name: getVal('form-name'), phone: getVal('form-phone'), + verification_code: getVal('form-verification-code'), company_name: getVal('form-company'), position: getVal('form-position'), business_scope: getVal('form-business'), @@ -840,8 +991,8 @@ errorDiv.textContent = "网络错误,请稍后重试"; errorDiv.classList.remove('hidden'); } finally { - btn.disabled = false; btn.textContent = '确认签到'; + updateCheckinButtonState(); } } @@ -903,6 +1054,57 @@ document.getElementById('user-list').innerHTML = ''; document.getElementById('checkin-form').reset(); 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; + } } diff --git a/templates/ticket.html b/templates/ticket.html index 588a54d..8a568cd 100644 --- a/templates/ticket.html +++ b/templates/ticket.html @@ -269,6 +269,12 @@ {% if key == 'phone' %} + {% if config.enable_sms_verification %} +
+ + +
+ {% endif %} {% else %} {% endif %} @@ -301,6 +307,57 @@ \ No newline at end of file