From 1b5faf00ccedcf220ac2bc58c5b87d68d62b6537 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Thu, 26 Mar 2026 20:15:10 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/main.cpython-312.pyc | Bin 92090 -> 92129 bytes main.py | 186 ++++++++- templates/admin.html | 681 +++++++++++++++++++++++++++++-- templates/ticket.html | 145 +++++-- 4 files changed, 926 insertions(+), 86 deletions(-) diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index acda3f55534b84bbf008b8b797e9f559e3b2ea55..20cdd5e86bcd4669736ebe07e542a79fa3ff34b6 100644 GIT binary patch delta 72 zcmdmWp7r5*R_@chyj%=GuuJCXM(%kM8hZL+`8lPzsm1y!i6x2pfu)IgCAz@{si`UY b>BT7-@yQvf$=R8C`pv5(wy%<46z~TCj@%hA delta 33 ocmaEOo^{uGR_@chyj%=GpvAa%BlkQBM)u~565B6IFbens0JhT#3;+NC diff --git a/main.py b/main.py index 70eacc3..b91b0d6 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse, JSONResponse from pydantic import BaseModel +from contextlib import asynccontextmanager import psycopg2 from psycopg2 import pool from psycopg2.extras import RealDictCursor @@ -13,6 +14,7 @@ import random import uuid import os import json +import re import shutil import difflib import time @@ -117,13 +119,12 @@ CONFIG = load_config() # SMS Verification Storage (In-memory) SMS_CODES = {} # phone -> {code: str, expires_at: datetime} -app = FastAPI() - # 进程池全局变量 process_pool = None -@app.on_event("startup") -async def startup_event(): +@asynccontextmanager +async def lifespan(app: FastAPI): + """应用生命周期管理:启动时初始化进程池,关闭时释放资源""" import sys print(f"DEBUG: Running from {__file__}") print(f"DEBUG: Python executable: {sys.executable}") @@ -141,18 +142,20 @@ async def startup_event(): else: print(f"{route.path}") print("=========================") + + yield + + # 关闭时清理进程池 + if process_pool: + process_pool.shutdown() + print("ProcessPoolExecutor shutdown") + +app = FastAPI(lifespan=lifespan) @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(): - if process_pool: - process_pool.shutdown() - print("ProcessPoolExecutor shutdown") - # Database connection parameters - Moved to CONFIG # 商业领域同义词库 (Business Thesaurus) - 用于解决模糊语义匹配 @@ -1548,6 +1551,167 @@ def init_database(): conn.close() return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) +class FieldAddRequest(BaseModel): + """添加字段请求模型""" + table: str # 'ticket' or 'checkin' + key: str # 字段标识 + label: str # 显示名称 + show: bool = True + required: bool = False + +class FieldDeleteRequest(BaseModel): + """删除字段请求模型""" + table: str # 'ticket' or 'checkin' + key: str # 字段标识 + +@app.post("/api/admin/field/add") +async def add_field(req: FieldAddRequest): + """ + 添加自定义字段: + 1. 在数据库表中添加新列 + 2. 在 config.json 中更新字段配置 + """ + global CONFIG + + try: + # 确定表名 + table_name = 'gsdh_data' if req.table == 'ticket' else 'checkin_info' + + # 验证字段标识格式 + if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', req.key): + return JSONResponse( + content={"success": False, "message": "字段标识格式不正确(只能包含字母、数字和下划线,不能以数字开头)"}, + status_code=400 + ) + + # 防止删除锁定字段 + locked_fields = ['name', 'phone', 'new_id', 'gsdh_id'] + if req.key.lower() in locked_fields: + return JSONResponse( + content={"success": False, "message": f"不能添加锁定字段 {req.key}"}, + status_code=400 + ) + + # 获取当前配置 + CONFIG = load_config() + + # 获取当前字段配置 + field_config_key = 'ticket_field_config' if req.table == 'ticket' else 'checkin_field_config' + current_fields = CONFIG.get(field_config_key, {}) + + # 检查字段是否已存在 + if req.key in current_fields: + return JSONResponse( + content={"success": False, "message": f"字段 {req.key} 已存在"}, + status_code=400 + ) + + # 在数据库中添加列 + conn = get_db_connection() + cur = conn.cursor() + + try: + # 添加新列(使用 TEXT 类型) + cur.execute(f'ALTER TABLE {table_name} ADD COLUMN IF NOT EXISTS {req.key} TEXT') + conn.commit() + except Exception as db_e: + conn.rollback() + cur.close() + release_db_connection(conn) + return JSONResponse( + content={"success": False, "message": f"数据库操作失败: {str(db_e)}"}, + status_code=500 + ) + + cur.close() + release_db_connection(conn) + + # 更新配置 + current_fields[req.key] = { + "label": req.label, + "show": req.show, + "required": req.required + } + CONFIG[field_config_key] = current_fields + save_config(CONFIG) + + return {"success": True, "message": f"字段 '{req.label}' 添加成功"} + + except Exception as e: + import traceback + traceback.print_exc() + return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) + +@app.post("/api/admin/field/delete") +async def delete_field(req: FieldDeleteRequest): + """ + 删除自定义字段: + 1. 从 config.json 中移除字段配置 + 2. 从数据库表中删除列 + """ + global CONFIG + + try: + # 确定表名 + table_name = 'gsdh_data' if req.table == 'ticket' else 'checkin_info' + + # 防止删除锁定字段 + locked_fields = ['name', 'phone', 'new_id', 'gsdh_id'] + if req.key.lower() in locked_fields: + return JSONResponse( + content={"success": False, "message": f"不能删除锁定字段 {req.key}"}, + status_code=400 + ) + + # 获取当前配置 + CONFIG = load_config() + + # 获取当前字段配置 + field_config_key = 'ticket_field_config' if req.table == 'ticket' else 'checkin_field_config' + current_fields = CONFIG.get(field_config_key, {}) + + # 检查字段是否存在 + if req.key not in current_fields: + return JSONResponse( + content={"success": False, "message": f"字段 {req.key} 不存在"}, + status_code=400 + ) + + # 获取字段标签(用于返回消息) + field_label = current_fields[req.key].get('label', req.key) + + # 从数据库中删除列 + conn = get_db_connection() + cur = conn.cursor() + + try: + # 删除列 + cur.execute(f'ALTER TABLE {table_name} DROP COLUMN IF EXISTS {req.key}') + conn.commit() + except Exception as db_e: + conn.rollback() + cur.close() + release_db_connection(conn) + return JSONResponse( + content={"success": False, "message": f"数据库操作失败: {str(db_e)}"}, + status_code=500 + ) + + cur.close() + release_db_connection(conn) + + # 更新配置(移除字段) + del current_fields[req.key] + CONFIG[field_config_key] = current_fields + save_config(CONFIG) + + return {"success": True, "message": f"字段 '{field_label}' 已删除"} + + except Exception as e: + import traceback + traceback.print_exc() + return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) + # ========================================== # WeChat Pay V3 & Ticket Logic # ========================================== diff --git a/templates/admin.html b/templates/admin.html index fc6699f..e49d87c 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -303,6 +303,188 @@ color: white; cursor: pointer; } + + /* 字段管理样式 */ + .field-item { + display: flex; + align-items: center; + padding: 12px; + border-bottom: 1px solid rgba(255,255,255,0.05); + gap: 10px; + } + + .field-item:last-child { + border-bottom: none; + } + + .field-key { + min-width: 120px; + font-weight: bold; + color: var(--primary-color); + } + + .field-key.locked { + color: #888; + } + + .field-input { + flex: 1; + padding: 8px 12px; + background: rgba(0,0,0,0.4); + border: 1px solid rgba(0,242,255,0.3); + border-radius: 6px; + color: white; + font-size: 0.9rem; + } + + .field-input:focus { + border-color: var(--primary-color); + outline: none; + box-shadow: 0 0 8px rgba(0,242,255,0.2); + } + + .field-checkbox { + display: flex; + align-items: center; + gap: 5px; + white-space: nowrap; + } + + .field-checkbox input { + width: 18px; + height: 18px; + accent-color: var(--primary-color); + } + + .field-checkbox label { + margin: 0; + font-size: 0.85rem; + color: var(--text-muted); + } + + .field-actions { + display: flex; + gap: 8px; + } + + .btn-sm { + padding: 6px 12px; + font-size: 0.85rem; + border-radius: 6px; + } + + .btn-danger-sm { + background: linear-gradient(90deg, #cc0000 0%, #ff4d4d 100%); + padding: 6px 12px; + font-size: 0.85rem; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(255,0,0,0.3); + } + + .btn-danger-sm:hover { + box-shadow: 0 4px 12px rgba(255,0,0,0.5); + } + + .btn-success-sm { + background: linear-gradient(90deg, #28a745 0%, #20c997 100%); + padding: 6px 12px; + font-size: 0.85rem; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(32,201,151,0.35); + } + + .add-field-section { + margin-top: 15px; + padding: 15px; + background: rgba(0,242,255,0.05); + border-radius: 8px; + border: 1px dashed rgba(0,242,255,0.3); + } + + .add-field-section h4 { + margin-bottom: 10px; + color: var(--primary-color); + font-size: 0.95rem; + } + + .add-field-form { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; + } + + .add-field-form input { + padding: 8px 12px; + font-size: 0.9rem; + } + + .add-field-form input[type="text"] { + flex: 1; + min-width: 100px; + } + + .field-help { + font-size: 0.8em; + color: var(--text-muted); + margin-left: 5px; + } + + .modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.7); + display: none; + justify-content: center; + align-items: center; + z-index: 1000; + } + + .modal-content { + background: var(--card-bg); + padding: 25px; + border-radius: 12px; + max-width: 400px; + width: 90%; + border: 1px solid rgba(0,242,255,0.3); + } + + .modal-title { + margin-bottom: 15px; + font-size: 1.1rem; + } + + .modal-buttons { + display: flex; + gap: 10px; + justify-content: flex-end; + margin-top: 20px; + } + + .field-table-header { + display: flex; + padding: 10px; + background: rgba(0,0,0,0.3); + border-radius: 8px 8px 0 0; + font-weight: bold; + font-size: 0.85rem; + color: var(--text-muted); + } + + .field-table-row { + display: flex; + padding: 10px; + align-items: center; + border-bottom: 1px solid rgba(255,255,255,0.05); + font-size: 0.9rem; + } + + .field-table-row:hover { + background: rgba(255,255,255,0.03); + } @@ -417,38 +599,80 @@ @@ -616,6 +840,19 @@
+ + +