You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
11 KiB
303 lines
11 KiB
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
|
|
import click
|
|
from flask import Flask, current_app
|
|
from flask.cli import with_appcontext
|
|
from sqlalchemy import inspect
|
|
|
|
from app.extensions import db
|
|
from app.models import Account, Household, HouseholdMember, OptionItem
|
|
from app.services import demo_login_lines, seed_demo_accounts, seed_demo_households, seed_stress_households
|
|
|
|
DEFAULT_ADMIN_USERNAME = "admin"
|
|
DEFAULT_ADMIN_DISPLAY_NAME = "系统管理员"
|
|
DEFAULT_ADMIN_PASSWORD = "ChangeMe123!"
|
|
|
|
BASE_OPTION_ITEMS: tuple[dict[str, str | int | bool | None], ...] = (
|
|
{"option_group": "relation_category", "option_code": "relative", "option_label": "亲戚", "parent_code": None, "sort_order": 10},
|
|
{"option_group": "relation_category", "option_code": "friend", "option_label": "朋友", "parent_code": None, "sort_order": 20},
|
|
{"option_group": "relation_category", "option_code": "colleague", "option_label": "同事", "parent_code": None, "sort_order": 30},
|
|
{"option_group": "gift_method", "option_code": "cash", "option_label": "现金", "parent_code": None, "sort_order": 10},
|
|
{"option_group": "gift_method", "option_code": "transfer", "option_label": "转账", "parent_code": None, "sort_order": 20},
|
|
{"option_group": "gift_scene", "option_code": "pre_wedding", "option_label": "婚前", "parent_code": None, "sort_order": 10},
|
|
{"option_group": "gift_scene", "option_code": "wedding_day", "option_label": "婚礼当天", "parent_code": None, "sort_order": 20},
|
|
{"option_group": "gift_scene", "option_code": "post_wedding", "option_label": "婚后补记", "parent_code": None, "sort_order": 30},
|
|
)
|
|
|
|
|
|
def register_commands(app: Flask) -> None:
|
|
app.cli.add_command(init_db_command)
|
|
app.cli.add_command(seed_admin_command)
|
|
app.cli.add_command(seed_options_command)
|
|
app.cli.add_command(seed_all_command)
|
|
app.cli.add_command(seed_demo_command)
|
|
app.cli.add_command(seed_stress_command)
|
|
app.cli.add_command(dev_bootstrap_command)
|
|
|
|
|
|
@click.command("init-db")
|
|
@with_appcontext
|
|
def init_db_command() -> None:
|
|
db.create_all()
|
|
click.echo("Database tables created.")
|
|
|
|
|
|
def ensure_seed_tables_exist() -> None:
|
|
inspector = inspect(db.engine)
|
|
required_tables = {Account.__tablename__, OptionItem.__tablename__}
|
|
existing_tables = set(inspector.get_table_names())
|
|
|
|
if required_tables.issubset(existing_tables):
|
|
return
|
|
|
|
missing_tables = ", ".join(sorted(required_tables - existing_tables))
|
|
raise click.ClickException(
|
|
f"Missing database tables: {missing_tables}. Run 'flask db upgrade' or 'flask init-db' first.",
|
|
)
|
|
|
|
|
|
def ensure_demo_seed_tables_exist() -> None:
|
|
inspector = inspect(db.engine)
|
|
required_tables = {
|
|
Account.__tablename__,
|
|
OptionItem.__tablename__,
|
|
Household.__tablename__,
|
|
HouseholdMember.__tablename__,
|
|
}
|
|
existing_tables = set(inspector.get_table_names())
|
|
|
|
if required_tables.issubset(existing_tables):
|
|
return
|
|
|
|
missing_tables = ", ".join(sorted(required_tables - existing_tables))
|
|
raise click.ClickException(
|
|
f"Missing database tables: {missing_tables}. Run 'flask db upgrade' or 'flask init-db' first.",
|
|
)
|
|
|
|
|
|
@click.command("seed-admin")
|
|
@click.option("--username", default=DEFAULT_ADMIN_USERNAME, show_default=True)
|
|
@click.option("--display-name", default=DEFAULT_ADMIN_DISPLAY_NAME, show_default=True)
|
|
@click.option("--password", default=DEFAULT_ADMIN_PASSWORD, show_default=True)
|
|
@with_appcontext
|
|
def seed_admin_command(username: str, display_name: str, password: str) -> None:
|
|
ensure_seed_tables_exist()
|
|
|
|
existing_account = db.session.execute(
|
|
db.select(Account).where(Account.username == username),
|
|
).scalar_one_or_none()
|
|
if existing_account is not None:
|
|
click.echo(f"Admin account '{username}' already exists.")
|
|
return
|
|
|
|
account = Account()
|
|
account.username = username
|
|
account.display_name = display_name
|
|
account.set_password(password)
|
|
account.role = "admin"
|
|
account.status = "active"
|
|
db.session.add(account)
|
|
db.session.commit()
|
|
click.echo(f"Admin account '{username}' created.")
|
|
|
|
|
|
@click.command("seed-options")
|
|
@with_appcontext
|
|
def seed_options_command() -> None:
|
|
ensure_seed_tables_exist()
|
|
|
|
created_count = seed_option_items(BASE_OPTION_ITEMS)
|
|
db.session.commit()
|
|
click.echo(f"Seeded {created_count} option items.")
|
|
|
|
|
|
@click.command("seed-all")
|
|
@click.option("--username", default=DEFAULT_ADMIN_USERNAME, show_default=True)
|
|
@click.option("--display-name", default=DEFAULT_ADMIN_DISPLAY_NAME, show_default=True)
|
|
@click.option("--password", default=DEFAULT_ADMIN_PASSWORD, show_default=True)
|
|
@with_appcontext
|
|
def seed_all_command(username: str, display_name: str, password: str) -> None:
|
|
ensure_seed_tables_exist()
|
|
|
|
created_options = seed_option_items(BASE_OPTION_ITEMS)
|
|
|
|
existing_account = db.session.execute(
|
|
db.select(Account).where(Account.username == username),
|
|
).scalar_one_or_none()
|
|
if existing_account is None:
|
|
admin_account = Account()
|
|
admin_account.username = username
|
|
admin_account.display_name = display_name
|
|
admin_account.set_password(password)
|
|
admin_account.role = "admin"
|
|
admin_account.status = "active"
|
|
db.session.add(admin_account)
|
|
admin_created = True
|
|
else:
|
|
admin_created = False
|
|
|
|
db.session.commit()
|
|
|
|
if admin_created:
|
|
click.echo(f"Admin account '{username}' created.")
|
|
else:
|
|
click.echo(f"Admin account '{username}' already exists.")
|
|
click.echo(f"Seeded {created_options} option items.")
|
|
|
|
|
|
@click.command("seed-demo")
|
|
@with_appcontext
|
|
def seed_demo_command() -> None:
|
|
ensure_demo_seed_tables_exist()
|
|
|
|
created_accounts = seed_demo_accounts()
|
|
created_households = seed_demo_households()
|
|
db.session.commit()
|
|
|
|
click.echo(f"Seeded {created_accounts} demo accounts.")
|
|
click.echo(f"Seeded {created_households} demo households.")
|
|
click.echo("Demo login accounts:")
|
|
for login_line in demo_login_lines():
|
|
click.echo(f"- {login_line}")
|
|
|
|
|
|
@click.command("seed-stress")
|
|
@click.option("--household-count", default=1000, show_default=True, type=int)
|
|
@click.option("--members-per-household", default=4, show_default=True, type=int)
|
|
@click.option("--prefix", default="STRESS-", show_default=True)
|
|
@with_appcontext
|
|
def seed_stress_command(household_count: int, members_per_household: int, prefix: str) -> None:
|
|
ensure_demo_seed_tables_exist()
|
|
|
|
try:
|
|
created_households, created_members = seed_stress_households(
|
|
household_count=household_count,
|
|
members_per_household=members_per_household,
|
|
household_code_prefix=prefix,
|
|
)
|
|
except ValueError as exc:
|
|
raise click.ClickException(str(exc)) from exc
|
|
|
|
db.session.commit()
|
|
click.echo(f"Seeded {created_households} stress households.")
|
|
click.echo(f"Seeded {created_members} stress household members.")
|
|
|
|
|
|
@click.command("dev-bootstrap")
|
|
@click.option("--host", default="127.0.0.1", show_default=True)
|
|
@click.option("--port", default=5000, show_default=True, type=int)
|
|
@click.option("--skip-server", is_flag=True, default=False)
|
|
@with_appcontext
|
|
def dev_bootstrap_command(host: str, port: int, skip_server: bool) -> None:
|
|
app = current_app._get_current_object()
|
|
project_root = os.fspath(Path(app.root_path).parent)
|
|
python_executable = sys.executable
|
|
env = os.environ.copy()
|
|
env["FLASK_APP"] = "run.py"
|
|
|
|
_run_python_command(
|
|
[python_executable, "-m", "flask", "db", "upgrade"],
|
|
env=env,
|
|
cwd=project_root,
|
|
label="Applying database migrations",
|
|
)
|
|
_run_python_command(
|
|
[python_executable, "-m", "flask", "seed-all"],
|
|
env=env,
|
|
cwd=project_root,
|
|
label="Seeding baseline admin and options",
|
|
)
|
|
_run_python_command(
|
|
[python_executable, "-m", "flask", "seed-demo"],
|
|
env=env,
|
|
cwd=project_root,
|
|
label="Seeding demo accounts and households",
|
|
)
|
|
|
|
click.echo(f"Service URL: http://{host}:{port}")
|
|
click.echo("Available demo logins:")
|
|
for login_line in demo_login_lines():
|
|
click.echo(f"- {login_line}")
|
|
|
|
if skip_server:
|
|
click.echo("Skipped starting the Flask development server.")
|
|
return
|
|
|
|
click.echo("Starting Flask development server...")
|
|
app.run(host=host, port=port, debug=True)
|
|
|
|
|
|
def seed_option_items(option_rows: Iterable[dict[str, str | int | bool | None]]) -> int:
|
|
created_count = 0
|
|
created_items_by_code: dict[tuple[str, str], OptionItem] = {}
|
|
|
|
for row in option_rows:
|
|
option_group = str(row["option_group"])
|
|
option_code = str(row["option_code"])
|
|
existing_item = db.session.execute(
|
|
db.select(OptionItem).where(
|
|
OptionItem.option_group == option_group,
|
|
OptionItem.option_code == option_code,
|
|
),
|
|
).scalar_one_or_none()
|
|
if existing_item is not None:
|
|
created_items_by_code[(option_group, option_code)] = existing_item
|
|
continue
|
|
|
|
parent_id = None
|
|
parent_code = row.get("parent_code")
|
|
if parent_code is not None:
|
|
parent_item = created_items_by_code.get((option_group.replace("_detail", "_category"), str(parent_code)))
|
|
if parent_item is None:
|
|
parent_item = db.session.execute(
|
|
db.select(OptionItem).where(OptionItem.option_code == str(parent_code)),
|
|
).scalar_one_or_none()
|
|
if parent_item is not None:
|
|
parent_id = parent_item.id
|
|
|
|
option_item = OptionItem()
|
|
option_item.option_group = option_group
|
|
option_item.option_code = option_code
|
|
option_item.option_label = str(row["option_label"])
|
|
option_item.parent_id = parent_id
|
|
option_item.sort_order = int(row.get("sort_order", 0) or 0)
|
|
option_item.is_enabled = bool(row.get("is_enabled", True))
|
|
option_item.is_system = bool(row.get("is_system", True))
|
|
db.session.add(option_item)
|
|
db.session.flush()
|
|
created_items_by_code[(option_group, option_code)] = option_item
|
|
created_count += 1
|
|
|
|
return created_count
|
|
|
|
|
|
def _run_python_command(
|
|
args: list[str],
|
|
*,
|
|
env: dict[str, str],
|
|
cwd: str,
|
|
label: str,
|
|
) -> None:
|
|
click.echo(label)
|
|
completed_process = subprocess.run(
|
|
args,
|
|
cwd=cwd,
|
|
env=env,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if completed_process.stdout:
|
|
click.echo(completed_process.stdout.rstrip())
|
|
if completed_process.returncode == 0:
|
|
return
|
|
|
|
if completed_process.stderr:
|
|
click.echo(completed_process.stderr.rstrip(), err=True)
|
|
raise click.ClickException(f"Command failed: {' '.join(args)}")
|
|
|