#!/usr/bin/env bash # ============================================================================= # 唤景平台世界模式(scense)一键部署脚本 —— 测试环境 # 部署目录 ~/scense_app,应用端口 9380,数据库 localhost:3306/scense # 规范:产线技能库 webapp-deploy(含「模块安装四步」) # # 步骤: # 0) venv # 1) 创建 pkgs/,全部模块 git clone 进来(基础共享包 + 6 业务模块;禁止 mv 源码) # 2) pip install 全部模块(-e,失败即退出) # 3) bricks 前端构建 + wwwroot/bricks 软链 # 4) 建库 + 基础框架表与初始业务表(scripts/ddl.sql,幂等 IF NOT EXISTS) # 5) i18n 合并 # 6) 逐模块四步安装:① models/json2ddl 建表 ② json/build.sh(xls2ui) ③ wwwroot 软链 ④ 数据导入 # 7) 应用代码同步 + runtime 目录 # 8) 重启 + 健康检查(/healthz 必须 200,否则 exit 1) # 硬性约束:set -euo pipefail,任何一步失败立即终止,禁止带病启动。 # ============================================================================= set -euo pipefail DEPLOY_DIR="${DEPLOY_DIR:-$HOME/scense_app}" VENV="${DEPLOY_DIR}/venv" APP_PORT="${APP_PORT:-9380}" DB_HOST="${DB_HOST:-localhost}" DB_PORT="${DB_PORT:-3306}" DB_NAME="${DB_NAME:-scense}" DB_USER="${DB_USER:-test}" DB_PASSWORD="${DB_PASSWORD:-test123}" GIT_HTTPS="https://git.opencomputing.cn/yumoqing" FOUNDATION_PKGS="apppublic sqlor ahserver rbac xls2ddl appbase bricks-for-python bricks" BUSINESS_MODULES="world scene entity script_engine world_snapshot world_sync scense_demo scense_game scense_drag scense_runtime" APP_SRC_DIR="$(cd "$(dirname "$0")" && pwd)" mkdir -p "${DEPLOY_DIR}" export PATH="${VENV}/bin:${PATH}" echo "==> [0/9] venv" if [ ! -x "${VENV}/bin/python" ]; then python3 -m venv "${VENV}" fi "${VENV}/bin/pip" install -q --upgrade pip >/dev/null 2>&1 || true "${VENV}/bin/pip" install -q wheel >/dev/null 2>&1 || true echo "==> [1/9] clone modules into pkgs/ (git clone only, no mv)" cd "${DEPLOY_DIR}" mkdir -p pkgs for m in ${FOUNDATION_PKGS} ${BUSINESS_MODULES}; do if [ ! -d "pkgs/${m}/.git" ]; then rm -rf "pkgs/${m}" git clone -q "${GIT_HTTPS}/${m}.git" "pkgs/${m}" || { echo "ERROR: clone ${m} failed" >&2; exit 1; } echo " cloned ${m}" else git -C "pkgs/${m}" pull -q --ff-only || echo "WARN: pull ${m} failed, using existing" fi done echo "==> [2/9] pip install all modules" for m in ${FOUNDATION_PKGS} ${BUSINESS_MODULES}; do if [ -f "pkgs/${m}/pyproject.toml" ] || [ -f "pkgs/${m}/setup.py" ] || [ -f "pkgs/${m}/setup.cfg" ]; then "${VENV}/bin/pip" install -q -e "pkgs/${m}" || { echo "ERROR: pip install ${m} failed" >&2; exit 1; } echo " installed ${m}" else echo "WARN: ${m} has no packaging metadata, skipped pip install" fi done echo "==> [3/9] bricks frontend build + symlink" # dist/ 不入 git(gitignore),clone 后必须本地构建;内层 build.sh 用 cat 拼接,无需 node mkdir -p pkgs/bricks/dist if [ ! -f "pkgs/bricks/dist/bricks.js" ]; then (cd pkgs/bricks/bricks && bash build.sh) || { echo "ERROR: bricks build failed" >&2; exit 1; } fi [ -s "pkgs/bricks/dist/bricks.js" ] || { echo "ERROR: bricks.js missing/empty after build" >&2; exit 1; } mkdir -p wwwroot ln -sfn "../pkgs/bricks/dist" "wwwroot/bricks" echo "==> [4/9] database + foundation/initial tables + seed" mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" \ -e "CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci" \ || { echo "ERROR: create database failed" >&2; exit 1; } mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \ < "${APP_SRC_DIR}/scripts/ddl.sql" || { echo "ERROR: ddl.sql failed" >&2; exit 1; } # 种子:默认机构/角色/权限/超管(全新库无此数据则一切请求 401) mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \ < "${APP_SRC_DIR}/scripts/seed.sql" || { echo "ERROR: seed.sql failed" >&2; exit 1; } echo "==> [5/9] i18n merge" mkdir -p wwwroot/i18n/zh wwwroot/i18n/en "${VENV}/bin/python" - <<'PY' import json, os ok = 0 for lang in ("zh", "en"): merged = {} for m in ("apppublic","sqlor","ahserver","appbase","rbac","world","scene","entity","script_engine","world_snapshot","world_sync","scense_demo","scense_game","scense_drag","scense_runtime"): p = f"pkgs/{m}/i18n/{lang}/msg.txt" if os.path.isfile(p): for line in open(p, encoding="utf-8"): line = line.rstrip("\n") if "=" in line: k, v = line.split("=", 1) merged[k.strip()] = v.strip() out = f"wwwroot/i18n/{lang}/i18n.json" json.dump(merged, open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=2) ok += len(merged) print(f"i18n merged: {ok} keys") PY echo "==> [6/9] per-module install: ① json2ddl建表 ② json/build.sh ③ wwwroot软链 ④ 数据导入" for m in ${BUSINESS_MODULES}; do echo " -- ${m}" # ① 建表:模块 models 是唯一事实源;无 models/ 的纯引擎模块跳过 if [ -d "pkgs/${m}/models" ]; then (cd "pkgs/${m}/models" && "${VENV}/bin/json2ddl" mysql .) > "/tmp/${m}_ddl.sql" \ || { echo "ERROR: ${m} json2ddl failed" >&2; exit 1; } [ -s "/tmp/${m}_ddl.sql" ] || { echo "ERROR: ${m} DDL empty" >&2; exit 1; } mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \ < "/tmp/${m}_ddl.sql" || { echo "ERROR: ${m} create tables failed" >&2; exit 1; } else echo " (no models/, skip DDL)" fi # ② CRUD 页面(模块仓库自带 json/build.sh);无 json/ 跳过 if [ -d "pkgs/${m}/json" ]; then (cd "pkgs/${m}/json" && bash build.sh) || { echo "ERROR: ${m} json/build.sh failed" >&2; exit 1; } else echo " (no json/, skip CRUD gen)" fi # ③ wwwroot 软链(非 cp) ln -sfn "../pkgs/${m}/wwwroot" "wwwroot/${m}" # ④ 初始化数据(有 data/*.xlsx 才导) if ls "pkgs/${m}/data/"*.xlsx >/dev/null 2>&1; then (cd "pkgs/${m}/data" && for f in *.xlsx; do "${VENV}/bin/dbloader" "${DEPLOY_DIR}" "${DB_NAME}" "$f"; done) \ || { echo "ERROR: ${m} dbloader failed" >&2; exit 1; } fi done # 基础模块的 wwwroot 也要软链(rbac 登录页、appbase 页面),否则登录页 404 for m in appbase rbac; do if [ -d "pkgs/${m}/wwwroot" ]; then ln -sfn "../pkgs/${m}/wwwroot" "wwwroot/${m}" fi done echo "==> [7/9] sync app code + runtime dirs" for d in app conf scripts wwwroot; do if [ -d "${APP_SRC_DIR}/${d}" ] && [ "$(realpath "${APP_SRC_DIR}/${d}")" != "$(realpath "${DEPLOY_DIR}/${d}")" ]; then mkdir -p "${DEPLOY_DIR}/${d}" cp -a "${APP_SRC_DIR}/${d}/." "${DEPLOY_DIR}/${d}/" fi done mkdir -p logs files echo "==> [8/9] restart service" bash "${APP_SRC_DIR}/stop.sh" || true sleep 1 bash "${APP_SRC_DIR}/start.sh" echo "==> [9/9] health check: GET /healthz" ok="" code="" for i in $(seq 1 15); do code=$(curl -s -o /tmp/scense_healthz.json -w "%{http_code}" "http://127.0.0.1:${APP_PORT}/healthz" || true) if [ "${code}" = "200" ]; then ok=1; break; fi sleep 2 done if [ -z "${ok}" ]; then echo "ERROR: /healthz not 200 after 30s (last=${code:-000}); logs/scense.log tail:" >&2 tail -30 logs/scense.log 2>/dev/null >&2 || true exit 1 fi echo "healthz: $(cat /tmp/scense_healthz.json)" echo "==> deploy OK: app=scense port=${APP_PORT} healthz=200"