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 ensure_ad_slot_schema(connection: sqlite3.Connection) -> None: columns = { row["name"] if isinstance(row, sqlite3.Row) else row[1] for row in connection.execute("PRAGMA table_info(ad_slots)").fetchall() } additions = { "name": "TEXT", "code": "TEXT", "description": "TEXT", "title": "TEXT", "image_url": "TEXT", "target_type": "TEXT NOT NULL DEFAULT 'link'", "target_url": "TEXT", "product_id": "INTEGER", "sort_order": "INTEGER NOT NULL DEFAULT 0", "is_active": "INTEGER NOT NULL DEFAULT 1", } for column_name, definition in additions.items(): if column_name not in columns: connection.execute(f"ALTER TABLE ad_slots ADD COLUMN {column_name} {definition}") connection.execute("UPDATE ad_slots SET name = title WHERE (name IS NULL OR name = '') AND title IS NOT NULL") connection.execute("UPDATE ad_slots SET name = '广告位' || id WHERE name IS NULL OR name = ''") connection.execute("UPDATE ad_slots SET code = 'slot_' || id WHERE code IS NULL OR code = ''") connection.execute("UPDATE ad_slots SET sort_order = 0 WHERE sort_order IS NULL") connection.execute("UPDATE ad_slots SET is_active = 1 WHERE is_active IS NULL") connection.execute( """ CREATE UNIQUE INDEX IF NOT EXISTS idx_ad_slots_temple_code_unique ON ad_slots(temple_id, code) WHERE code IS NOT NULL AND code != '' """ ) 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 feature_icons ( id INTEGER PRIMARY KEY AUTOINCREMENT, temple_id INTEGER NOT NULL, title TEXT NOT NULL, icon_url TEXT NOT NULL, target_type TEXT NOT NULL DEFAULT 'link', target_url TEXT, product_id INTEGER, sort_order INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (temple_id) REFERENCES temples(id), FOREIGN KEY (product_id) REFERENCES products(id) ) """ ) connection.execute( """ CREATE TABLE IF NOT EXISTS ad_slots ( id INTEGER PRIMARY KEY AUTOINCREMENT, temple_id INTEGER NOT NULL, name TEXT, code TEXT, description TEXT, title TEXT, image_url TEXT, target_type TEXT NOT NULL DEFAULT 'link', target_url TEXT, product_id INTEGER, sort_order INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (temple_id) REFERENCES temples(id), FOREIGN KEY (product_id) REFERENCES products(id) ) """ ) ensure_ad_slot_schema(connection) connection.execute( """ CREATE TABLE IF NOT EXISTS ads ( id INTEGER PRIMARY KEY AUTOINCREMENT, temple_id INTEGER NOT NULL, slot_id INTEGER NOT NULL, title TEXT NOT NULL, image_url TEXT NOT NULL, target_type TEXT NOT NULL DEFAULT 'link', target_url TEXT, product_id INTEGER, sort_order INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (temple_id) REFERENCES temples(id), FOREIGN KEY (slot_id) REFERENCES ad_slots(id), FOREIGN KEY (product_id) REFERENCES products(id) ) """ ) 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}]', "

祈愿平安顺遂。

", "平安香", "为家人祈福平安", 1, 32, ), ( 1, "祈福灯", "供灯", "立即供灯", "随喜供灯", 99.0, 50, 20, "", 1, '[{"name":"一盏","price":99,"stock":50,"sort_order":1,"is_chaodu":false}]', "

供灯祈福,愿心光明。

", "祈福灯", "供灯祈福,照亮善愿", 1, 18, ), ( 2, "南山香礼", "香礼", "立即支付", "随喜", 39.9, 80, 10, "", 1, '[{"name":"单份","price":39.9,"stock":80,"sort_order":1,"is_chaodu":false}]', "

南山寺香礼。

", "南山香礼", "南山寺祈福香礼", 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)]