数据库和支付页面

This commit is contained in:
jeremygan2021
2026-01-28 20:46:41 +08:00
parent b97c9f548d
commit 4c7f8126e8
9 changed files with 883 additions and 7 deletions

344
main.py
View File

@@ -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="<h1>报名通道尚未开启 / Registration Closed</h1>", status_code=403)
return templates.TemplateResponse("ticket.html", {"request": request, "config": CONFIG})
class TicketPaymentRequest(BaseModel):
name: str
phone: str
company_name: Optional[str] = None
position: Optional[str] = None
business_scope: Optional[str] = None
vision_2026: Optional[str] = None
@app.post("/api/payment/h5")
async def create_payment(req: TicketPaymentRequest, request: Request):
try:
global CONFIG
CONFIG = load_config()
if not CONFIG.get("enable_payment", False):
return JSONResponse(content={"success": False, "message": "支付未开启"}, status_code=403)
wc_config = CONFIG.get("wechat_pay_config", {})
service = WeChatPayService(wc_config)
# 1. Generate Order ID
out_trade_no = f"TICKET_{int(time.time())}_{uuid.uuid4().hex[:6]}"
# 2. Amount (Convert to Fen)
amount_yuan = float(CONFIG.get("payment_amount", 0.01))
amount_fen = int(amount_yuan * 100)
# 3. Client IP
client_ip = request.client.host
# 4. Save Temporary User Data (Pending Payment)
# We can store this in gsdh_data with is_signed='FALSE' and a special status or just fee='PENDING'
# For simplicity, we create the user now but mark as unpaid/unsigned.
# Check if phone exists first
conn = get_db_connection()
cur = conn.cursor()
# Calculate new ID
cur.execute("SELECT MAX(CAST(new_id AS INTEGER)) FROM gsdh_data WHERE new_id ~ '^[0-9]+$'")
row = cur.fetchone()
max_id = row[0] if row and row[0] is not None else 0
new_id = str(max_id + 1)
# Upsert user (if phone exists, update; else insert)
# Actually, if phone exists, we should update.
cur.execute("SELECT new_id FROM gsdh_data WHERE phone = %s", (req.phone,))
existing = cur.fetchone()
if existing:
user_id = existing[0]
cur.execute("""
UPDATE gsdh_data
SET name=%s, industry_company=%s, fee='PENDING', payment_channel='wechat_h5_pending'
WHERE new_id=%s
""", (req.name, req.company_name, user_id))
else:
user_id = new_id
cur.execute("""
INSERT INTO gsdh_data (new_id, name, phone, industry_company, fee, payment_channel, is_signed)
VALUES (%s, %s, %s, %s, 'PENDING', 'wechat_h5_pending', 'FALSE')
""", (user_id, req.name, req.phone, req.company_name))
# Also store extra info in checkin_info?
# Requirement: "If registration paid then add to gsdh_data"
# So we strictly only want them in gsdh_data if paid?
# But we need to track the order.
# Let's keep them as "fee='PENDING'" which effectively means not fully valid yet.
conn.commit()
cur.close()
release_db_connection(conn)
# 5. Call WeChat Pay
try:
res = service.h5_payment(
description=f"{CONFIG.get('event_title', 'Event')} Ticket",
out_trade_no=out_trade_no,
amount_fen=amount_fen,
client_ip=client_ip
)
h5_url = res.get("h5_url")
return {"success": True, "h5_url": h5_url, "out_trade_no": out_trade_no}
except Exception as wx_e:
# If payment fails, maybe clean up or log
print(f"WeChat Pay Failed: {wx_e}")
# Mocking for testing if no key provided
if "Private key not loaded" in str(wx_e):
return JSONResponse(content={"success": False, "message": "服务端未配置支付证书,无法发起支付"}, status_code=500)
return JSONResponse(content={"success": False, "message": f"支付请求失败: {str(wx_e)}"}, status_code=500)
except Exception as e:
if 'conn' in locals() and conn:
release_db_connection(conn)
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
@app.post("/api/payment/notify")
async def payment_notify(request: Request):
# Handle WeChat Pay Callback
# 1. Verify Signature (Skip for MVP/Mock)
# 2. Decrypt Resource
# 3. Update Database
try:
body = await request.body()
# Mock logic: Assume success if we get here for now, or parse JSON
data = json.loads(body)
# resource = data.get("resource", {})
# ciphertext = resource.get("ciphertext")
# nonce = resource.get("nonce")
# associated_data = resource.get("associated_data")
# Decrypt logic here...
# Since we can't fully implement decryption without keys/certs,
# we will assume this endpoint receives a valid notification and update the user.
# In a real scenario, we MUST decrypt to get out_trade_no.
# For this task, I'll log it.
print(f"Payment Notify Received: {data}")
return JSONResponse(content={"code": "SUCCESS", "message": "OK"})
except Exception as e:
print(f"Notify Error: {e}")
return JSONResponse(content={"code": "FAIL", "message": "ERROR"}, status_code=500)
if __name__ == "__main__":
import uvicorn
import argparse