diff --git a/.trae/documents/Implement Ticket Registration with WeChat H5 Payment.md b/.trae/documents/Implement Ticket Registration with WeChat H5 Payment.md new file mode 100644 index 0000000..21fdeb2 --- /dev/null +++ b/.trae/documents/Implement Ticket Registration with WeChat H5 Payment.md @@ -0,0 +1,52 @@ +I have analyzed the current project structure and designed a "Check-in Large Screen" (签到大屏) solution that integrates seamlessly with your existing system. + +### **Plan Overview** + +I will implement a **"Galaxy Spotlight"** style large screen: + +1. **Background (Danmaku/Floating Tags)**: + + * `Business Scope` and short `Vision` keywords will float elegantly in the background like stars or drifting clouds. + + * Uses the project's blue/cyan tech theme. + +2. **Foreground (Spotlight Card)**: + + * A central, animated card that cycles through checked-in guests. + + * Displays: **Company Name** (Prominent), **Guest Name/Position**, and **Vision 2026**. + + * Updates every 8-10 seconds to ensure everyone gets exposure. + +### **Implementation Steps** + +#### 1. Backend (`main.py`) + +* Add a new route `/wall` to serve the large screen page. + +* Add an API `/api/wall/data` to fetch approved check-in data (filtering out empty entries). + +#### 2. Frontend (`templates/wall.html`) + +* Create a new responsive HTML template. + +* **Visuals**: Reusing the `radial-gradient` background and neon aesthetics from `index.html`. + +* **Animation**: + + * JS-based floating animation for background tags. + + * CSS transitions for the central spotlight card. + +#### 3. Admin Integration (`templates/admin.html`) + +* Add a button "Open Large Screen" (打开签到大屏) in the Admin Dashboard for easy access. + +### **Technical Details** + +* **Tech Stack**: Native JS/CSS (no extra heavy libraries needed), integrated into the existing FastAPI app. + +* **Data Source**: Reads directly from `checkin_info` table. + +* **Compatibility**: Optimized for 1080p/4K displays (MacOS standard). + diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index 914a9bf..664b0f1 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 4a5a27a..939d34e 100644 --- a/config.json +++ b/config.json @@ -1,14 +1,24 @@ { - "event_title": "玉溪青年.玉见跨境", - "event_sub_title": "跨境电商企业交流沙龙", + "event_title": "AI共生大会", + "event_sub_title": "云南AI大会", "event_time": "等待输入", "event_location": "玉溪青花街三生咖啡酒吧", "event_content": "等待输入", "primary_color": "#00f2ff", "secondary_color": "#0066ff", "bg_color": "#050814", - "header_image": "/static/b5ccc4d50b6ce0aca8ddfef494edbbc0.jpg", + "header_image": "/static/image1.jpg", "enable_ticket_validation": false, + "enable_payment": false, + "payment_amount": 0.01, + "wechat_pay_config": { + "appid": "", + "mchid": "", + "api_v3_key": "", + "serial_no": "", + "private_key_path": "", + "notify_url": "" + }, "enable_seating": false, "total_tables": 14, "max_per_table": 10, @@ -56,9 +66,9 @@ "business_scope": true } }, - "db_host": "6.6.6.86", + "db_host": "6.6.6.66", "db_port": "5432", - "db_user": "zhao", - "db_password": "123zhao", - "db_name": "zhao" + "db_user": "AI_event", + "db_password": "123AI_event", + "db_name": "AI_event" } \ No newline at end of file diff --git a/diagnose_db.py b/diagnose_db.py new file mode 100644 index 0000000..2e14eaa --- /dev/null +++ b/diagnose_db.py @@ -0,0 +1,61 @@ + +import psycopg2 +import json +import os +from psycopg2.extras import RealDictCursor + +def load_config(): + if os.path.exists("config.json"): + with open("config.json", "r", encoding="utf-8") as f: + return json.load(f) + return {} + +def diagnose(): + config = load_config() + print(f"Config Loaded: Host={config.get('db_host')}, DB={config.get('db_name')}, User={config.get('db_user')}") + + try: + conn = psycopg2.connect( + host=config.get("db_host", "localhost"), + port=config.get("db_port", "5432"), + user=config.get("db_user", "gsdh"), + password=config.get("db_password", "123gsdh"), + database=config.get("db_name", "gsdh"), + connect_timeout=5 + ) + cur = conn.cursor() + + # 1. List Tables + print("\n=== Tables in 'public' schema ===") + cur.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + """) + tables = cur.fetchall() + for t in tables: + print(f"- {t[0]}") + + # 2. Detail Columns for our tables + target_tables = ['gsdh_data', 'checkin_info'] + for table in target_tables: + print(f"\n=== Columns for table '{table}' ===") + cur.execute(""" + SELECT column_name, data_type, is_nullable + FROM information_schema.columns + WHERE table_name = %s + """, (table,)) + columns = cur.fetchall() + if not columns: + print("(Table not found)") + else: + for col in columns: + print(f" - {col[0]}: {col[1]}") + + conn.close() + + except Exception as e: + print(f"\nConnection Failed: {e}") + +if __name__ == "__main__": + diagnose() diff --git a/main.py b/main.py index a91665d..c1dc8d0 100644 --- a/main.py +++ b/main.py @@ -58,6 +58,16 @@ DEFAULT_CONFIG = { "business_scope": True }, "bg_opacity": 0.3 + }, + "enable_payment": False, + "payment_amount": 0.01, + "wechat_pay_config": { + "mchid": "", + "appid": "", + "api_v3_key": "", + "serial_no": "", + "private_key_path": "cert/apiclient_key.pem", + "notify_url": "https://your-domain.com/api/payment/notify" } } @@ -1225,6 +1235,340 @@ def reset_database(): release_db_connection(conn) return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) +@app.post("/api/admin/init-db") +def init_database(): + """ + 初始化数据库:在不删除现有数据的情况下创建表结构(如果表不存在)。 + 适用于迁移到新数据库或修复表结构。 + """ + try: + # 1. 强制重新加载配置,确保使用磁盘上最新的 config.json + # 这避免了用户修改文件但未重启服务导致连接旧库的问题 + current_config = load_config() + + print(f"Initializing DB with config: {current_config.get('db_host')}:{current_config.get('db_port')} DB={current_config.get('db_name')}") + + # 2. 直接建立连接,不依赖全局连接池 + conn = psycopg2.connect( + host=current_config.get("db_host", "localhost"), + port=current_config.get("db_port", "5432"), + user=current_config.get("db_user", "gsdh"), + password=current_config.get("db_password", "123gsdh"), + database=current_config.get("db_name", "gsdh"), + connect_timeout=10 + ) + conn.autocommit = False # Ensure transaction + cur = conn.cursor() + + # 3. Create Tables (IF NOT EXISTS) + # gsdh_data + cur.execute(""" + CREATE TABLE IF NOT EXISTS gsdh_data ( + new_id VARCHAR(50) PRIMARY KEY, + name VARCHAR(100), + phone VARCHAR(20) UNIQUE, + industry_company VARCHAR(200), + fee VARCHAR(50), + payment_channel VARCHAR(50), + is_signed VARCHAR(10) DEFAULT 'FALSE' + ); + """) + + # checkin_info + cur.execute(""" + CREATE TABLE IF NOT EXISTS checkin_info ( + id SERIAL PRIMARY KEY, + gsdh_id VARCHAR(50) REFERENCES gsdh_data(new_id), + name VARCHAR(100), + phone VARCHAR(20), + company_name VARCHAR(200), + position VARCHAR(100), + business_scope TEXT, + vision_2026 TEXT, + location VARCHAR(50), + social_point INTEGER DEFAULT 5, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + """) + + # 4. Migration: Add missing columns if table exists but column doesn't + # Helper to add column safely + def add_column_if_not_exists(table, column, type_def): + try: + cur.execute(f"ALTER TABLE {table} ADD COLUMN {column} {type_def}") + print(f"Added column {column} to {table}") + except psycopg2.errors.DuplicateColumn: + conn.rollback() # Rollback the sub-transaction error + print(f"Column {column} already exists in {table}") + except Exception as e: + conn.rollback() + print(f"Error adding column {column}: {e}") + + # Ensure we are in a valid transaction state + conn.commit() + + # Check and add columns for gsdh_data + # We need to handle transaction carefully. ALTER TABLE is transactional in Postgres. + # But if it fails, the transaction is aborted. + # So we check information_schema first to avoid DuplicateColumn error which aborts transaction. + + def safe_add_column(table, col, dtype): + cur.execute(""" + SELECT column_name FROM information_schema.columns + WHERE table_name = %s AND column_name = %s + """, (table, col)) + if not cur.fetchone(): + cur.execute(f"ALTER TABLE {table} ADD COLUMN {col} {dtype}") + print(f"Added column {col} to {table}") + + safe_add_column('gsdh_data', 'fee', 'VARCHAR(50)') + safe_add_column('gsdh_data', 'payment_channel', 'VARCHAR(50)') + 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', 'business_scope', 'TEXT') + safe_add_column('checkin_info', 'vision_2026', 'TEXT') + + conn.commit() + cur.close() + conn.close() + + # Also update the global pool if needed, but not strictly necessary for this request + # But good practice to ensure subsequent requests use new config + global CONFIG + CONFIG = current_config + init_db_pool() + + return {"success": True, "message": f"数据库结构已初始化/迁移成功 (Host: {current_config.get('db_host')})"} + except Exception as e: + if 'conn' in locals() and conn: + conn.rollback() + conn.close() + return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) + +# ========================================== +# WeChat Pay V3 & Ticket Logic +# ========================================== + +class WeChatPayService: + def __init__(self, config): + self.mchid = config.get("mchid") + self.appid = config.get("appid") + self.api_v3_key = config.get("api_v3_key") + self.serial_no = config.get("serial_no") + self.private_key_path = config.get("private_key_path") + self.notify_url = config.get("notify_url") + self.private_key = self._load_private_key() + + def _load_private_key(self): + try: + if not self.private_key_path or not os.path.exists(self.private_key_path): + print(f"Warning: Private key not found at {self.private_key_path}") + return None + with open(self.private_key_path, "rb") as f: + return serialization.load_pem_private_key(f.read(), password=None) + except Exception as e: + print(f"Error loading private key: {e}") + return None + + def _generate_signature(self, method, url, timestamp, nonce_str, body): + if not self.private_key: + raise Exception("Private key not loaded") + + message = f"{method}\n{url}\n{timestamp}\n{nonce_str}\n{body}\n" + signature = self.private_key.sign( + message.encode("utf-8"), + padding.PKCS1v15(), + hashes.SHA256() + ) + return base64.b64encode(signature).decode("utf-8") + + def build_authorization_header(self, method, url, body): + timestamp = str(int(time.time())) + nonce_str = str(uuid.uuid4()).replace("-", "") + signature = self._generate_signature(method, url, timestamp, nonce_str, body) + + return ( + f'WECHATPAY2-SHA256-RSA2048 mchid="{self.mchid}",' + f'nonce_str="{nonce_str}",' + f'signature="{signature}",' + f'timestamp="{timestamp}",' + f'serial_no="{self.serial_no}"' + ) + + def h5_payment(self, description, out_trade_no, amount_fen, client_ip, payer_openid=None): + url = "https://api.mch.weixin.qq.com/v3/pay/transactions/h5" + path = "/v3/pay/transactions/h5" + + 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" + }, + "scene_info": { + "payer_client_ip": client_ip, + "h5_info": { + "type": "Wap" + } + } + } + + 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}") + +@app.get("/ticket", response_class=HTMLResponse) +async def ticket_page(request: Request): + global CONFIG + CONFIG = load_config() + + if not CONFIG.get("enable_payment", False): + return HTMLResponse(content="
+ 开启后,/ticket 页面将开放访问。 +
++ 提示:切换到新数据库后,点击“初始化/迁移结构”可自动创建表结构(不会删除现有数据)。 +
尚未购票?
+ + + +