diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index acda3f5..20cdd5e 100644 Binary files a/__pycache__/main.cpython-312.pyc and b/__pycache__/main.cpython-312.pyc differ 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 @@对应 gsdh_data 表 (ticket.html)
-| 字段名称 | -是否显示 | -是否必填 | -
|---|
对应 gsdh_data 表 (ticket.html),可编辑显示名称、添加或删除字段
+ + +💡 字段标识建议使用英文下划线格式,如 company_name,系统会自动同步到数据库表结构
+对应 checkin_info 表 (wall/checkin)
-| 字段名称 | -是否显示 | -是否必填 | -
|---|
对应 checkin_info 表 (wall/checkin),可编辑显示名称、添加或删除字段
+ + +💡 字段标识建议使用英文下划线格式,如 wechat_id,系统会自动同步到数据库表结构
+