Files
admin/app/database.py
T
2026-07-27 09:57:35 +08:00

453 lines
17 KiB
Python

import os
import sqlite3
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Annotated
from fastapi import Depends, FastAPI
from app.security import hash_password
DATABASE_PATH = Path(os.getenv("DATABASE_PATH", Path(__file__).parents[1] / "app.sqlite3"))
def ensure_user_schema(connection: sqlite3.Connection) -> None:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
registered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
name TEXT NOT NULL,
phone TEXT,
nickname TEXT,
avatar TEXT,
is_admin INTEGER NOT NULL DEFAULT 0,
total_spent REAL NOT NULL DEFAULT 0
)
"""
)
columns = {
row["name"] if isinstance(row, sqlite3.Row) else row[1]
for row in connection.execute("PRAGMA table_info(users)").fetchall()
}
additions = {
"registered_at": "TEXT",
"username": "TEXT",
"password_hash": "TEXT",
"failed_login_count": "INTEGER NOT NULL DEFAULT 0",
"locked_until": "TEXT",
"name": "TEXT",
"phone": "TEXT",
"nickname": "TEXT",
"avatar": "TEXT",
"is_admin": "INTEGER NOT NULL DEFAULT 0",
"total_spent": "REAL NOT NULL DEFAULT 0",
}
for column_name, definition in additions.items():
if column_name not in columns:
connection.execute(f"ALTER TABLE users ADD COLUMN {column_name} {definition}")
columns = {
row["name"] if isinstance(row, sqlite3.Row) else row[1]
for row in connection.execute("PRAGMA table_info(users)").fetchall()
}
connection.execute(
"UPDATE users SET registered_at = CURRENT_TIMESTAMP WHERE registered_at IS NULL OR registered_at = ''"
)
if "full_name" in columns:
connection.execute("UPDATE users SET name = full_name WHERE (name IS NULL OR name = '') AND full_name IS NOT NULL")
if "username" in columns:
connection.execute("UPDATE users SET name = username WHERE name IS NULL OR name = ''")
connection.execute("UPDATE users SET name = 'Unknown User' WHERE name IS NULL OR name = ''")
connection.execute("UPDATE users SET is_admin = 0 WHERE is_admin IS NULL")
connection.execute("UPDATE users SET total_spent = 0 WHERE total_spent IS NULL")
connection.execute("UPDATE users SET failed_login_count = 0 WHERE failed_login_count IS NULL")
connection.execute("UPDATE users SET username = lower(nickname) WHERE (username IS NULL OR username = '') AND nickname IS NOT NULL AND nickname != ''")
connection.execute(
"""
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_phone_unique
ON users(phone)
WHERE phone IS NOT NULL AND phone != ''
"""
)
connection.execute(
"""
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username_unique
ON users(username)
WHERE username IS NOT NULL AND username != ''
"""
)
def set_user_password(connection: sqlite3.Connection, username: str, password: str) -> None:
connection.execute(
"""
UPDATE users
SET username = ?, password_hash = ?
WHERE username = ? OR nickname = ? OR phone = ?
""",
(username, hash_password(password), username, username, username),
)
def ensure_default_user_credentials(connection: sqlite3.Connection) -> None:
admin = connection.execute(
"SELECT id, password_hash FROM users WHERE username = 'admin' OR nickname = 'admin' OR phone = '13800000000'"
).fetchone()
if admin is not None and not str(admin["password_hash"] or "").startswith("$2"):
set_user_password(connection, "admin", "admin123456")
demo = connection.execute(
"SELECT id, password_hash FROM users WHERE username = 'demo' OR nickname = 'demo' OR phone = '13900000000'"
).fetchone()
if demo is not None and not str(demo["password_hash"] or "").startswith("$2"):
set_user_password(connection, "demo", "demo123456")
rows = connection.execute(
"SELECT id FROM users WHERE username IS NULL OR username = '' ORDER BY id"
).fetchall()
for row in rows:
connection.execute("UPDATE users SET username = ? WHERE id = ?", (f"user{row['id']}", row["id"]))
def ensure_product_schema(connection: sqlite3.Connection) -> None:
columns = {
row["name"] if isinstance(row, sqlite3.Row) else row[1]
for row in connection.execute("PRAGMA table_info(products)").fetchall()
}
additions = {
"alias": "TEXT",
"description": "TEXT",
"payment_button_name": "TEXT NOT NULL DEFAULT '立即支付'",
"donation_button_name": "TEXT NOT NULL DEFAULT '随喜'",
"sort_order": "INTEGER NOT NULL DEFAULT 0",
"show_participant_count": "INTEGER NOT NULL DEFAULT 0",
"cover_image": "TEXT",
"images_json": "TEXT NOT NULL DEFAULT '[]'",
"listed_at": "TEXT",
"sale_starts_at": "TEXT",
"sale_ends_at": "TEXT",
"delisted_at": "TEXT",
"show_countdown": "INTEGER NOT NULL DEFAULT 0",
"show_on_home": "INTEGER NOT NULL DEFAULT 0",
"specs_json": "TEXT NOT NULL DEFAULT '[]'",
"details_html": "TEXT",
"share_title": "TEXT",
"share_description": "TEXT",
"share_image": "TEXT",
"merit_certificate_reserved": "INTEGER NOT NULL DEFAULT 0",
"auto_process": "INTEGER NOT NULL DEFAULT 0",
"sales_count": "INTEGER NOT NULL DEFAULT 0",
}
for column_name, definition in additions.items():
if column_name not in columns:
connection.execute(f"ALTER TABLE products ADD COLUMN {column_name} {definition}")
def init_db() -> None:
DATABASE_PATH.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(DATABASE_PATH) as connection:
connection.row_factory = sqlite3.Row
connection.execute(
"""
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0
)
"""
)
ensure_user_schema(connection)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS temples (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
location TEXT,
contact_phone TEXT,
description TEXT,
is_active INTEGER NOT NULL DEFAULT 1
)
"""
)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
temple_id INTEGER NOT NULL,
name TEXT NOT NULL,
price REAL NOT NULL DEFAULT 0,
stock INTEGER NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (temple_id) REFERENCES temples(id)
)
"""
)
ensure_product_schema(connection)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS ritual_services (
id INTEGER PRIMARY KEY AUTOINCREMENT,
temple_id INTEGER NOT NULL,
name TEXT NOT NULL,
description TEXT,
price REAL NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (temple_id) REFERENCES temples(id)
)
"""
)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
temple_id INTEGER NOT NULL,
customer_name TEXT NOT NULL,
customer_phone TEXT,
order_type TEXT NOT NULL,
item_name TEXT NOT NULL,
amount REAL NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (temple_id) REFERENCES temples(id)
)
"""
)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS payment_transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
temple_id INTEGER NOT NULL,
order_id INTEGER NOT NULL,
channel TEXT NOT NULL DEFAULT 'wechat',
out_trade_no TEXT NOT NULL UNIQUE,
amount INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'created',
prepay_id TEXT,
transaction_id TEXT,
raw_notify_json TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (temple_id) REFERENCES temples(id),
FOREIGN KEY (order_id) REFERENCES orders(id)
)
"""
)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS login_captchas (
id TEXT PRIMARY KEY,
answer_hash TEXT NOT NULL,
expires_at TEXT NOT NULL,
used_at TEXT
)
"""
)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS auth_sessions (
token_hash TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
revoked_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
)
"""
)
session_columns = {
row["name"] if isinstance(row, sqlite3.Row) else row[1]
for row in connection.execute("PRAGMA table_info(auth_sessions)").fetchall()
}
if "revoked_at" not in session_columns:
connection.execute("ALTER TABLE auth_sessions ADD COLUMN revoked_at TEXT")
connection.execute(
"""
CREATE TABLE IF NOT EXISTS login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
ip_address TEXT NOT NULL,
user_agent TEXT,
success INTEGER NOT NULL DEFAULT 0,
failure_reason TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS login_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
ip_address TEXT NOT NULL,
user_agent TEXT,
is_new_ip INTEGER NOT NULL DEFAULT 0,
is_new_device INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
"""
)
todo_count = connection.execute("SELECT COUNT(*) FROM todos").fetchone()[0]
if todo_count == 0:
connection.executemany(
"INSERT INTO todos (title, done) VALUES (?, ?)",
[
("Learn FastAPI routes", 0),
("Open interactive API docs", 0),
],
)
user_count = connection.execute("SELECT COUNT(*) FROM users").fetchone()[0]
if user_count == 0:
connection.executemany(
"""
INSERT INTO users
(name, phone, nickname, avatar, is_admin, total_spent)
VALUES (?, ?, ?, ?, ?, ?)
""",
[
("管理员", "13800000000", "admin", "", 1, 0),
("演示用户", "13900000000", "demo", "", 0, 128.5),
],
)
ensure_default_user_credentials(connection)
temple_count = connection.execute("SELECT COUNT(*) FROM temples").fetchone()[0]
if temple_count == 0:
connection.executemany(
"""
INSERT INTO temples (name, location, contact_phone, description, is_active)
VALUES (?, ?, ?, ?, ?)
""",
[
("福田寺", "深圳福田", "0755-10000001", "自在福田示例寺院", 1),
("南山寺", "深圳南山", "0755-10000002", "用于验证数据隔离的示例寺院", 1),
],
)
product_count = connection.execute("SELECT COUNT(*) FROM products").fetchone()[0]
if product_count == 0:
connection.executemany(
"""
INSERT INTO products
(
temple_id, name, alias, payment_button_name, donation_button_name,
price, stock, sort_order, cover_image, show_on_home, specs_json,
details_html, share_title, share_description, is_active, sales_count
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
[
(
1,
"平安香",
"平安",
"立即结缘",
"随喜功德",
29.9,
100,
10,
"",
1,
'[{"name":"单份","price":29.9,"stock":100,"sort_order":1,"is_chaodu":false}]',
"<p>祈愿平安顺遂。</p>",
"平安香",
"为家人祈福平安",
1,
32,
),
(
1,
"祈福灯",
"供灯",
"立即供灯",
"随喜供灯",
99.0,
50,
20,
"",
1,
'[{"name":"一盏","price":99,"stock":50,"sort_order":1,"is_chaodu":false}]',
"<p>供灯祈福,愿心光明。</p>",
"祈福灯",
"供灯祈福,照亮善愿",
1,
18,
),
(
2,
"南山香礼",
"香礼",
"立即支付",
"随喜",
39.9,
80,
10,
"",
1,
'[{"name":"单份","price":39.9,"stock":80,"sort_order":1,"is_chaodu":false}]',
"<p>南山寺香礼。</p>",
"南山香礼",
"南山寺祈福香礼",
1,
11,
),
],
)
ritual_count = connection.execute("SELECT COUNT(*) FROM ritual_services").fetchone()[0]
if ritual_count == 0:
connection.executemany(
"""
INSERT INTO ritual_services (temple_id, name, description, price, is_active)
VALUES (?, ?, ?, ?, ?)
""",
[
(1, "祈福法会", "为家人祈福平安", 199.0, 1),
(1, "超度法事", "超度追思法事", 399.0, 1),
(2, "供灯祈福", "南山寺供灯祈福", 129.0, 1),
],
)
order_count = connection.execute("SELECT COUNT(*) FROM orders").fetchone()[0]
if order_count == 0:
connection.executemany(
"""
INSERT INTO orders
(temple_id, customer_name, customer_phone, order_type, item_name, amount, status)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
[
(1, "张三", "13800138000", "product", "平安香", 29.9, "paid"),
(1, "李四", "13900139000", "ritual", "祈福法会", 199.0, "pending"),
(2, "王五", "13700137000", "product", "南山香礼", 39.9, "paid"),
],
)
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
yield
def get_db():
connection = sqlite3.connect(DATABASE_PATH, check_same_thread=False)
connection.row_factory = sqlite3.Row
try:
yield connection
finally:
connection.close()
Database = Annotated[sqlite3.Connection, Depends(get_db)]