From da2f7b34ea7087a009955b56042035ffe1967a7d Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Wed, 28 Jan 2026 23:43:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E9=A1=B5=E9=9D=A2=E7=AD=BE?= =?UTF-8?q?=E5=88=B0=E4=B8=80=E8=87=B4=E6=80=A7=E4=BB=A5=E5=8F=8A=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/main.cpython-312.pyc | Bin 72243 -> 80358 bytes config.json | 19 +-- main.py | 154 +++++++++++++++++- templates/admin.html | 7 +- templates/index.html | 266 +++++++++++++++++++++++++++---- templates/ticket.html | 141 +++++++++++++++- 6 files changed, 541 insertions(+), 46 deletions(-) diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index d272258ef279c306b179e59730a1ea027544fe13..c66f396355bbae3a7d506e39d3ac0b6b5e9eabe2 100644 GIT binary patch delta 23590 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'!gzNC>=#sqO^1!k_2&79B+<-hNoQgy4G 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 bd_X-|c7wxT#2g`J`T6$W#FHH|yv6#z$GFl8 delta 16622 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 ZCl+57@E1vkt3|!kcf)vcmlo4<{|}>V?A-tW 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