ソースを参照

feat:新增 Shell 脚本并修复迁移兼容性

leon 1 ヶ月 前
コミット
2f151b3f18

+ 17 - 35
README.md

@@ -52,25 +52,22 @@ pnpm install --frozen-lockfile
 Set-Location ..
 ```
 
-macOS:
+macOS / Linux
 
 ```bash
-test -f backend/.env
-uv run --project backend python -c 'import sys; assert sys.version_info[:2] == (3, 12); print(sys.version)'
-nc -z 127.0.0.1 3306
-nc -z 127.0.0.1 6379
+./scripts/check-env.sh
+./scripts/init-databases.sh
+./scripts/migrate.sh
+./scripts/seed.sh
 
 cd backend
-uv run python -m zbt.commands.init_databases --root-user root
-uv run alembic -c alembic.ini --name alembic_core upgrade head
-uv run alembic -c alembic.ini --name alembic_agent upgrade head
-uv run alembic -c alembic.ini --name alembic_analytics upgrade head
-uv run python -m zbt.commands.seed
 uv run python -m zbt.commands.seed_business_data
-uv run python -m zbt.commands.build_analytics
-uv run python -m zbt.commands.verify_seed
+cd ..
+
+./scripts/build-analytics.sh
+./scripts/verify-seed.sh
 
-cd ../frontend
+cd frontend
 pnpm install --frozen-lockfile
 cd ..
 ```
@@ -85,18 +82,10 @@ cd ..
 .\scripts\dev.ps1
 ```
 
-macOS(两个终端):
-
-```bash
-# 终端一
-cd backend
-uv run uvicorn zbt.main:app --host 127.0.0.1 --port 8000 --reload
-```
+macOS / Linux:
 
 ```bash
-# 终端二
-cd frontend
-pnpm dev
+./scripts/dev.sh
 ```
 
 - H5:http://127.0.0.1:5173
@@ -126,7 +115,7 @@ uv run python -m zbt.commands.evaluate_agents --suite smoke
 uv run python -m zbt.commands.evaluate_agents --suite full
 ```
 
-macOS:
+macOS / Linux
 
 ```bash
 uv run python -m zbt.commands.evaluate_agents --suite smoke
@@ -141,19 +130,12 @@ uv run python -m zbt.commands.evaluate_agents --suite full
 .\scripts\test.ps1
 ```
 
-macOS:
+macOS / Linux
 
 ```bash
-cd backend
-uv sync --locked
-uv run pytest
-uv run ruff check .
-uv run mypy src/zbt
-
-cd ../frontend
-pnpm install --frozen-lockfile
-pnpm typecheck
-pnpm build
+./scripts/test.sh
 ```
 
 该脚本执行后端测试、Ruff、mypy、前端类型检查与双端生产构建。真实 MySQL/Redis 集成测试以及真实模型评测单独执行,避免普通代码检查意外消耗模型额度。
+
+所有 `scripts/*.ps1` 均提供同名的 `scripts/*.sh`,分别用于 Windows PowerShell 与 macOS/Linux。数据库迁移脚本可安全处理字段已存在但 Alembic 版本记录尚未更新的中断状态。

+ 31 - 0
backend/.env.example

@@ -0,0 +1,31 @@
+APP_ENV=development
+APP_HOST=127.0.0.1
+APP_PORT=8000
+APP_TIMEZONE=Asia/Shanghai
+APP_CORS_ORIGINS=["http://127.0.0.1:5173","http://127.0.0.1:5174"]
+
+MYSQL_HOST=127.0.0.1
+MYSQL_PORT=3306
+MYSQL_USER=root
+MYSQL_PASSWORD=123456
+MYSQL_CORE_DATABASE=insurance_s1_core
+MYSQL_AGENT_DATABASE=insurance_s1_agent
+MYSQL_ANALYTICS_DATABASE=insurance_s1_analytics
+
+REDIS_URL=redis://127.0.0.1:6379/0
+REDIS_PREFIX=ins:s1:
+
+JWT_ACCESS_SECRET=replace-with-at-least-32-characters
+JWT_REFRESH_SECRET=replace-with-another-at-least-32-characters
+FIELD_ENCRYPTION_KEY=replace-with-at-least-32-characters
+DEV_FIXED_OTP=147258
+DEV_TOOLS_ENABLED=true
+
+DEEPSEEK_API_KEY=xxxxxxxxxxxxxxxxxxxxx
+DEEPSEEK_BASE_URL=https://api.deepseek.com
+DEEPSEEK_MODEL=deepseek-v4-flash
+
+LANGSMITH_TRACING=true
+LANGSMITH_API_KEY=xxxxxxxxxxxxxxxxxxxxxx
+LANGSMITH_ENDPOINT=https://api.smith.langchain.com
+LANGSMITH_PROJECT=智保通-第一阶段-智能投保

+ 22 - 17
backend/migrations/core/versions/core_0004_product_management.py

@@ -12,33 +12,38 @@ depends_on = None
 
 
 def upgrade() -> None:
-    op.add_column(
-        "plans",
+    bind = op.get_bind()
+    inspector = sa.inspect(bind)
+    existing_columns = {column["name"] for column in inspector.get_columns("plans")}
+    columns = (
         sa.Column("premium_cents", sa.BigInteger(), nullable=False, server_default="0"),
-    )
-    op.add_column(
-        "plans",
         sa.Column(
             "coverage_amount_cents",
             sa.BigInteger(),
             nullable=False,
             server_default="0",
         ),
-    )
-    op.add_column(
-        "plans",
         sa.Column("min_age", sa.Integer(), nullable=False, server_default="0"),
-    )
-    op.add_column(
-        "plans",
         sa.Column("max_age", sa.Integer(), nullable=False, server_default="100"),
     )
-    CoreBase.metadata.tables["product_change_logs"].create(bind=op.get_bind())
+    for column in columns:
+        if column.name not in existing_columns:
+            op.add_column("plans", column)
+
+    CoreBase.metadata.tables["product_change_logs"].create(bind=bind, checkfirst=True)
 
 
 def downgrade() -> None:
-    CoreBase.metadata.tables["product_change_logs"].drop(bind=op.get_bind())
-    op.drop_column("plans", "max_age")
-    op.drop_column("plans", "min_age")
-    op.drop_column("plans", "coverage_amount_cents")
-    op.drop_column("plans", "premium_cents")
+    bind = op.get_bind()
+    CoreBase.metadata.tables["product_change_logs"].drop(bind=bind, checkfirst=True)
+    existing_columns = {
+        column["name"] for column in sa.inspect(bind).get_columns("plans")
+    }
+    for column_name in (
+        "max_age",
+        "min_age",
+        "coverage_amount_cents",
+        "premium_cents",
+    ):
+        if column_name in existing_columns:
+            op.drop_column("plans", column_name)

+ 44 - 0
backend/tests/test_migrations.py

@@ -1,9 +1,24 @@
+import importlib.util
 from pathlib import Path
+from unittest.mock import Mock
 
+import sqlalchemy as sa
 from alembic.config import Config
 from alembic.script import ScriptDirectory
 
 
+def load_core_0004():
+    migration_path = (
+        Path(__file__).parents[1]
+        / "migrations/core/versions/core_0004_product_management.py"
+    )
+    spec = importlib.util.spec_from_file_location("core_0004_product_management", migration_path)
+    assert spec is not None and spec.loader is not None
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    return module
+
+
 def test_each_database_has_the_expected_independent_migration_head() -> None:
     backend_dir = Path(__file__).parents[1]
     expected_heads = {
@@ -16,3 +31,32 @@ def test_each_database_has_the_expected_independent_migration_head() -> None:
         config = Config(backend_dir / "alembic.ini", ini_section=section)
         scripts = ScriptDirectory.from_config(config)
         assert scripts.get_current_head() == expected_head
+
+
+def test_core_0004_skips_columns_that_already_exist(monkeypatch) -> None:
+    core_0004_product_management = load_core_0004()
+    bind = Mock()
+    inspector = Mock()
+    inspector.get_columns.return_value = [
+        {"name": "premium_cents"},
+        {"name": "min_age"},
+    ]
+    add_column = Mock()
+    create_table = Mock()
+
+    monkeypatch.setattr(sa, "inspect", lambda connection: inspector)
+    monkeypatch.setattr(core_0004_product_management.op, "get_bind", lambda: bind)
+    monkeypatch.setattr(core_0004_product_management.op, "add_column", add_column)
+    monkeypatch.setattr(
+        core_0004_product_management.CoreBase.metadata.tables["product_change_logs"],
+        "create",
+        create_table,
+    )
+
+    core_0004_product_management.upgrade()
+
+    assert [call.args[1].name for call in add_column.call_args_list] == [
+        "coverage_amount_cents",
+        "max_age",
+    ]
+    create_table.assert_called_once_with(bind=bind, checkfirst=True)

+ 6 - 0
scripts/build-analytics.sh

@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$project_root/backend"
+uv run python -m zbt.commands.build_analytics

+ 17 - 0
scripts/check-env.sh

@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+backend_root="$project_root/backend"
+
+[[ -f "$backend_root/.env" ]] || { echo "缺少 backend/.env。请先创建该文件并配置后端环境变量。" >&2; exit 1; }
+command -v uv >/dev/null 2>&1 || { echo "未找到 uv。" >&2; exit 1; }
+
+cd "$project_root"
+uv run --project backend python -c 'import sys; assert sys.version_info[:2] == (3, 12); print(sys.version)'
+
+command -v nc >/dev/null 2>&1 || { echo "未找到 nc,无法检查 MySQL 和 Redis 端口。" >&2; exit 1; }
+nc -z 127.0.0.1 3306 || { echo "MySQL 127.0.0.1:3306 未监听。" >&2; exit 1; }
+nc -z 127.0.0.1 6379 || { echo "Redis 127.0.0.1:6379 未监听。" >&2; exit 1; }
+
+echo "环境检查通过:uv、Python 3.12、MySQL、Redis。"

+ 22 - 0
scripts/dev.sh

@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+frontend_pid=""
+
+cleanup() {
+    if [[ -n "$frontend_pid" ]] && kill -0 "$frontend_pid" 2>/dev/null; then
+        kill "$frontend_pid"
+        wait "$frontend_pid" 2>/dev/null || true
+    fi
+}
+trap cleanup EXIT INT TERM
+
+(cd "$project_root/frontend" && exec pnpm dev) &
+frontend_pid=$!
+
+echo "H5:http://127.0.0.1:5173"
+echo "管理后台:http://127.0.0.1:5174"
+echo "后端API:http://127.0.0.1:8000/docs"
+cd "$project_root/backend"
+uv run uvicorn zbt.main:app --host 127.0.0.1 --port 8000 --reload

+ 8 - 0
scripts/init-databases.sh

@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+root_user="${1:-root}"
+
+cd "$project_root/backend"
+uv run python -m zbt.commands.init_databases --root-user "$root_user"

+ 9 - 0
scripts/migrate.sh

@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$project_root/backend"
+
+uv run alembic -c alembic.ini --name alembic_core upgrade head
+uv run alembic -c alembic.ini --name alembic_agent upgrade head
+uv run alembic -c alembic.ini --name alembic_analytics upgrade head

+ 23 - 0
scripts/reset-dev-data.sh

@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+project_root="$(cd "$script_dir/.." && pwd)"
+expected="RESET insurance_s1"
+confirmation="${1:-}"
+
+echo "即将重建数据库:insurance_s1_core、insurance_s1_agent、insurance_s1_analytics"
+echo "即将删除 Redis 前缀:ins:s1:*"
+if [[ -z "${confirmation//[[:space:]]/}" ]]; then
+    read -r -p "请输入 $expected 继续: " confirmation
+fi
+[[ "$confirmation" == "$expected" ]] || { echo "确认文本不匹配,已取消。" >&2; exit 1; }
+
+cd "$project_root/backend"
+uv run alembic -c alembic.ini --name alembic_analytics downgrade base
+uv run alembic -c alembic.ini --name alembic_agent downgrade base
+uv run alembic -c alembic.ini --name alembic_core downgrade base
+uv run python -m zbt.commands.reset_cache
+"$script_dir/migrate.sh"
+"$script_dir/seed.sh"
+"$script_dir/build-analytics.sh"

+ 6 - 0
scripts/seed.sh

@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$project_root/backend"
+uv run python -m zbt.commands.seed

+ 15 - 0
scripts/test.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+cd "$project_root/backend"
+uv sync --locked
+uv run pytest
+uv run ruff check .
+uv run mypy src/zbt
+
+cd "$project_root/frontend"
+pnpm install --frozen-lockfile
+pnpm typecheck
+pnpm build

+ 6 - 0
scripts/verify-seed.sh

@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$project_root/backend"
+uv run python -m zbt.commands.verify_seed