zlwu 5 месяцев назад
Родитель
Сommit
2e66e03792

+ 5 - 2
AGENTS.md

@@ -98,8 +98,11 @@ source venv/bin/activate
 pip install -r requirements.txt
 cp .env.example .env.local   # fill in DB, Redis, MinIO, etc.
 ./start-local.sh              # uvicorn --reload on 127.0.0.1:8000
-./start-prod.sh start         # production (reads .env.production, PID-managed)
+./start-prod.sh start         # production on :8111 (reads .env.production, PID-managed)
 ./start-prod.sh status|stop|restart|logs
+./start-test.sh start         # test on :8112 (reads .env.test, PID-managed)
+./start-test.sh activate      # source .venv/bin/activate (interactive shell)
+./start-test.sh status|stop|restart|logs
 ```
 
 ### Entrypoint & Structure
@@ -117,7 +120,7 @@ cp .env.example .env.local   # fill in DB, Redis, MinIO, etc.
 - API prefix: `/api` (set via `API_V1_PREFIX` in config). Routes: `/api/public/*`, `/api/admin/*`, `/api/supplier/*`, `/api/client/*`, `/api/deliver/*`
 - Auth: JWT + RSA-encrypted login. Login RSA key auto-generated or set via `LOGIN_RSA_PRIVATE_KEY_PEM` env var.
 - API encryption middleware: symmetric crypto layer controlled by `API_ENCRYPT_*` env vars. Excluded paths listed in `API_ENCRYPT_EXCLUDE_PATHS`.
-- Config: pydantic-settings (`app/core/config.py`), reads from `.env` / `.env.local` / `.env.production`.
+- Config: pydantic-settings (`app/core/config.py`), reads from `.env` / `.env.local` / `.env.production` / `.env.test` (via `ENV_FILE`).
 - Database init / schema baseline: `docs/sql/zkxw.sql`(从当前库导出的全量 SQL)。Default admin: `admin` / `111111`, role `super_admin`.
 - Swagger docs at `/docs`, ReDoc at `/redoc`.
 - Tests: minimal, only `tests/test_api_crypto.py`. Run with `pytest`.


+ 1 - 1
backend/app.pid

@@ -1 +1 @@
-69671
+11399

+ 20 - 5
backend/start-prod.sh

@@ -2,7 +2,16 @@
 
 # ============================================
 # 陇耘鲜达 Backend 生产环境启动脚本
-# 适配: python3.6+ (显式不使用 python2)
+# 适配: python3.9+(推荐 venv;显式不使用 python2)
+#
+# 生产服务器目录示例: /root/zkxw-backend/backend
+# 默认监听: 0.0.0.0:8111(可通过环境变量 PORT 覆盖)
+#
+# 首次部署:
+#   cd /root/zkxw-backend/backend
+#   cp .env.example .env.production   # 填写数据库、Redis、MinIO、SECRET_KEY 等
+#   python3 -m venv venv && ./venv/bin/pip install -r requirements.txt
+#   ./start-prod.sh start
 # ============================================
 
 set -euo pipefail
@@ -12,7 +21,7 @@ cd "$BASE_DIR"
 
 APP_MODULE="${APP_MODULE:-app.main:app}"
 HOST="${HOST:-0.0.0.0}"
-PORT="${PORT:-8000}"
+PORT="${PORT:-8111}"
 WORKERS="${WORKERS:-2}"
 LOG_LEVEL="${LOG_LEVEL:-info}"
 ENV_FILE="${ENV_FILE:-$BASE_DIR/.env.production}"
@@ -56,12 +65,17 @@ show_help() {
 
 可选环境变量:
   HOST=0.0.0.0
-  PORT=8000
+  PORT=8111
   WORKERS=2
   LOG_LEVEL=info
   APP_MODULE=app.main:app
   VENV_DIR=./venv
   ENV_FILE=./.env.production
+
+生产部署目录(示例):
+  /root/zkxw-backend/backend
+
+  cd /root/zkxw-backend/backend && ./start-prod.sh start
 EOF
 }
 
@@ -71,7 +85,7 @@ ensure_dirs() {
 
 check_python() {
   if ! command -v python3 >/dev/null 2>&1; then
-    log_error "未找到 python3,请安装 python3.6+"
+    log_error "未找到 python3,请安装 python3.9+"
     exit 1
   fi
 
@@ -159,7 +173,8 @@ start_server() {
     exit 1
   fi
 
-  ENV_FILE="$ENV_FILE" nohup "$UVICORN_BIN" "$APP_MODULE" \
+  export ENV_FILE
+  nohup "$UVICORN_BIN" "$APP_MODULE" \
     --host "$HOST" \
     --port "$PORT" \
     --workers "$WORKERS" \

+ 292 - 0
backend/start-test.sh

@@ -0,0 +1,292 @@
+#!/bin/bash
+
+# ============================================
+# 陇耘鲜达 Backend 测试环境启动脚本
+# 适配: python3.9+(推荐 venv;显式不使用 python2)
+#
+# 测试服务器目录示例: /root/zkxw-backend/backend
+# 默认监听: 0.0.0.0:8112(生产为 8111,同机部署勿冲突)
+#
+# 首次部署:
+#   cd /root/zkxw-backend/backend
+#   cp .env.test.example .env.test   # 或从 .env.example 复制后按测试库填写
+#   python3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt
+#   ./start-test.sh activate   # source .venv/bin/activate
+#   ./start-test.sh start
+# ============================================
+
+set -euo pipefail
+
+BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$BASE_DIR"
+
+APP_MODULE="${APP_MODULE:-app.main:app}"
+HOST="${HOST:-0.0.0.0}"
+PORT="${PORT:-8112}"
+WORKERS="${WORKERS:-1}"
+LOG_LEVEL="${LOG_LEVEL:-debug}"
+ENV_FILE="${ENV_FILE:-$BASE_DIR/.env.test}"
+
+VENV_DIR="${VENV_DIR:-$BASE_DIR/.venv}"
+ACTIVATE_SCRIPT="$VENV_DIR/bin/activate"
+PYTHON_BIN="${PYTHON_BIN:-$VENV_DIR/bin/python}"
+UVICORN_BIN="${UVICORN_BIN:-$VENV_DIR/bin/uvicorn}"
+
+LOG_DIR="$BASE_DIR/logs"
+LOG_FILE="$LOG_DIR/backend-test.log"
+PID_FILE="$BASE_DIR/backend-test.pid"
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+log_info() {
+  echo -e "${GREEN}[INFO]${NC} $1"
+}
+
+log_warn() {
+  echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+log_error() {
+  echo -e "${RED}[ERROR]${NC} $1"
+}
+
+show_help() {
+  cat << 'EOF'
+用法:
+  ./start-test.sh [start|stop|restart|status|logs|activate]
+
+命令:
+  start     启动服务(默认)
+  stop      停止服务
+  restart   重启服务
+  status    查看状态
+  logs      查看日志(最近100行)
+  activate  激活 .venv 并进入交互 shell(等价 source .venv/bin/activate)
+
+可选环境变量:
+  HOST=0.0.0.0
+  PORT=8112
+  WORKERS=1
+  LOG_LEVEL=debug
+  APP_MODULE=app.main:app
+  VENV_DIR=./.venv
+  ENV_FILE=./.env.test
+
+测试部署目录(示例):
+  /root/zkxw-backend/backend
+
+  cd /root/zkxw-backend/backend && ./start-test.sh start
+
+说明:
+  - 读取 .env.test(非 .env.production)
+  - 默认端口 8112;生产 start-prod.sh 使用 8111
+  - 可与生产共用 venv,但 PID/日志文件独立,勿混用 start/stop 命令
+EOF
+}
+
+ensure_dirs() {
+  mkdir -p "$LOG_DIR"
+}
+
+check_python() {
+  if ! command -v python3 >/dev/null 2>&1; then
+    log_error "未找到 python3,请安装 python3.9+"
+    exit 1
+  fi
+
+  local py_ver
+  py_ver="$(python3 -c 'import sys; print("{}.{}".format(sys.version_info[0], sys.version_info[1]))')"
+  log_info "系统 python3 版本: $py_ver"
+}
+
+ensure_venv_and_runtime() {
+  if [ ! -d "$VENV_DIR" ]; then
+    log_warn "未找到虚拟环境,开始自动创建: $VENV_DIR"
+    if ! python3 -m venv "$VENV_DIR" >/dev/null 2>&1; then
+      log_error "自动创建 venv 失败,请先安装 python3-venv 或手动创建虚拟环境"
+      log_warn "可尝试: yum install -y python3-venv 或 python3 -m ensurepip --upgrade"
+      exit 1
+    fi
+  fi
+
+  if [ ! -x "$PYTHON_BIN" ]; then
+    log_error "虚拟环境 python 不存在: $PYTHON_BIN"
+    exit 1
+  fi
+
+  if [ ! -x "$VENV_DIR/bin/pip" ]; then
+    log_warn "虚拟环境 pip 不存在,尝试引导安装"
+    "$PYTHON_BIN" -m ensurepip --upgrade >/dev/null 2>&1 || true
+  fi
+
+  if [ ! -x "$UVICORN_BIN" ]; then
+    log_warn "未检测到 uvicorn,开始安装依赖 requirements.txt"
+    if [ ! -x "$VENV_DIR/bin/pip" ]; then
+      log_error "pip 仍不可用,请手动执行依赖初始化"
+      exit 1
+    fi
+    "$VENV_DIR/bin/pip" install -r "$BASE_DIR/requirements.txt"
+    if [ ! -x "$UVICORN_BIN" ]; then
+      log_error "依赖安装后仍未找到 uvicorn: $UVICORN_BIN"
+      exit 1
+    fi
+  fi
+}
+
+is_running() {
+  if [ ! -f "$PID_FILE" ]; then
+    return 1
+  fi
+
+  local pid
+  pid="$(cat "$PID_FILE")"
+  if [ -z "$pid" ]; then
+    return 1
+  fi
+
+  if ps -p "$pid" >/dev/null 2>&1; then
+    return 0
+  fi
+
+  return 1
+}
+
+start_server() {
+  check_python
+  ensure_venv_and_runtime
+  ensure_dirs
+
+  if is_running; then
+    local running_pid
+    running_pid="$(cat "$PID_FILE")"
+    log_warn "测试服务已在运行 (PID: $running_pid)"
+    exit 0
+  fi
+
+  if [ -f "$PID_FILE" ]; then
+    rm -f "$PID_FILE"
+  fi
+
+  log_info "启动测试环境服务..."
+  log_info "监听地址: $HOST:$PORT"
+  log_info "工作进程: $WORKERS"
+  log_info "日志文件: $LOG_FILE"
+  log_info "环境文件: $ENV_FILE"
+
+  if [ ! -f "$ENV_FILE" ]; then
+    log_error "环境文件不存在: $ENV_FILE"
+    log_warn "可先执行: cp .env.test.example .env.test 并按测试库修改"
+    exit 1
+  fi
+
+  export ENV_FILE
+  nohup "$UVICORN_BIN" "$APP_MODULE" \
+    --host "$HOST" \
+    --port "$PORT" \
+    --workers "$WORKERS" \
+    --log-level "$LOG_LEVEL" \
+    >> "$LOG_FILE" 2>&1 &
+
+  echo $! > "$PID_FILE"
+  sleep 2
+
+  if is_running; then
+    log_info "测试服务启动成功 (PID: $(cat "$PID_FILE"))"
+    log_info "健康检查: http://127.0.0.1:$PORT/health"
+    log_info "API 文档: http://127.0.0.1:$PORT/docs"
+  else
+    log_error "测试服务启动失败,请检查日志: $LOG_FILE"
+    rm -f "$PID_FILE"
+    exit 1
+  fi
+}
+
+stop_server() {
+  if ! is_running; then
+    log_warn "测试服务未运行"
+    [ -f "$PID_FILE" ] && rm -f "$PID_FILE"
+    exit 0
+  fi
+
+  local pid
+  pid="$(cat "$PID_FILE")"
+  log_info "正在停止测试服务 (PID: $pid)"
+  kill -TERM "$pid" >/dev/null 2>&1 || true
+
+  for _ in 1 2 3 4 5 6 7 8 9 10; do
+    if ! ps -p "$pid" >/dev/null 2>&1; then
+      break
+    fi
+    sleep 1
+  done
+
+  if ps -p "$pid" >/dev/null 2>&1; then
+    log_warn "优雅停止超时,执行强制停止"
+    kill -9 "$pid" >/dev/null 2>&1 || true
+  fi
+
+  rm -f "$PID_FILE"
+  log_info "测试服务已停止"
+}
+
+status_server() {
+  if is_running; then
+    echo -e "${GREEN}RUNNING (test)${NC} PID=$(cat "$PID_FILE") HOST=$HOST PORT=$PORT"
+  else
+    echo -e "${RED}STOPPED (test)${NC}"
+  fi
+}
+
+show_logs() {
+  ensure_dirs
+  if [ ! -f "$LOG_FILE" ]; then
+    log_warn "暂无日志文件: $LOG_FILE"
+    exit 0
+  fi
+  tail -n 100 "$LOG_FILE"
+}
+
+activate_venv() {
+  if [ ! -f "$ACTIVATE_SCRIPT" ]; then
+    log_error "虚拟环境激活脚本不存在: $ACTIVATE_SCRIPT"
+    log_warn "可先执行: python3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt"
+    exit 1
+  fi
+
+  log_info "激活虚拟环境: source $ACTIVATE_SCRIPT"
+  exec "$SHELL" -i -c "cd '$BASE_DIR' && source '$ACTIVATE_SCRIPT' && exec -i '$SHELL'"
+}
+
+ACTION="${1:-start}"
+case "$ACTION" in
+  start)
+    start_server
+    ;;
+  stop)
+    stop_server
+    ;;
+  restart)
+    stop_server || true
+    start_server
+    ;;
+  status)
+    status_server
+    ;;
+  logs)
+    show_logs
+    ;;
+  activate)
+    activate_venv
+    ;;
+  -h|--help|help)
+    show_help
+    ;;
+  *)
+    log_error "未知命令: $ACTION"
+    show_help
+    exit 1
+    ;;
+esac

+ 1 - 1
web-admin/graphify-out/GRAPH_REPORT.md

@@ -1,7 +1,7 @@
 # Graph Report - src  (2026-05-28)
 
 ## Corpus Check
-- Large corpus: 672 files · ~418,149 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
+- Large corpus: 672 files · ~418,385 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
 
 ## Summary
 - 1934 nodes · 2494 edges · 50 communities detected

+ 2 - 2
web-admin/graphify-out/manifest.json

@@ -182,7 +182,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/product-view-dialog/index.vue": 1779790325.4147334,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-production-info-cards/index.vue": 1779790326.1909053,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-storage-release-info/index.vue": 1779542542.2434525,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/infusion-schedule-outbound-chain-view/index.vue": 1779830492.0922873,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/infusion-schedule-outbound-chain-view/index.vue": 1779970046.2722821,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/work-order-remark-dialog/index.vue": 1779965478.2090704,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/clinical-schedule-payment-detail/index.vue": 1779944059.476768,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/outbound-material-form-sections/index.vue": 1779969249.1980019,
@@ -355,7 +355,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue": 1779790354.1964018,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/index.vue": 1779941177.032692,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue": 1779830388.4782345,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleViewDialog.vue": 1779944277.8142326,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleViewDialog.vue": 1779970072.929038,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleCancelDialog.vue": 1779785733.3155978,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleEditDialog.vue": 1779790355.024449,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/index.vue": 1779865135.982138,

+ 1 - 0
web-admin/package.json

@@ -10,6 +10,7 @@
   "scripts": {
     "dev": "vite --open",
     "build": "vite build",
+    "build:test": "vite build --mode test",
     "build:typecheck": "vue-tsc --noEmit && vite build",
     "serve": "vite preview",
     "lint": "eslint",

+ 1 - 0
web-admin/src/graphify-out/cache/ast/00c5e4d6405463ec797191c984247454ef9181377a359b2a6afb12886625e535.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_infusion_schedule_outbound_chain_view_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/infusion-schedule-outbound-chain-view/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
web-admin/src/graphify-out/cache/ast/fc2da720378640ff0f9168914cbb41764b005f648df7deb63ae7d62c2860329e.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_scheduleviewdialog_vue", "label": "ScheduleViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleViewDialog.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 370 - 0
测试环境部署/README.md

@@ -0,0 +1,370 @@
+# 后端测试环境部署说明
+
+> 本文档汇总测试环境相关结论与操作步骤,便于后续追查。  
+> 代码仓库路径:`backend/`;服务器部署目录示例:`/root/zkxw-backend/backend`。
+
+---
+
+## 1. 环境与脚本对照
+
+| 项 | 测试环境 | 生产环境 |
+|----|----------|----------|
+| 启动脚本 | `backend/start-test.sh` | `backend/start-prod.sh` |
+| 环境变量文件 | `.env.test` | `.env.production` |
+| 环境变量模板 | `backend/.env.test.example` | `backend/.env.example`(复制为 `.env.production`) |
+| 默认端口 | **8112** | **8111** |
+| 默认监听 | `0.0.0.0` | `0.0.0.0` |
+| 工作进程数 | 1 | 2 |
+| 日志级别 | `debug` | `info` |
+| 虚拟环境目录 | **`.venv`** | **`venv`** |
+| PID 文件 | `backend-test.pid` | `backend-prod.pid` |
+| 运行日志 | `logs/backend-test.log` | `logs/backend-prod.log` |
+
+**同机部署**:测试用 8112、生产用 8111,避免端口冲突;启停命令必须分开,勿混用 `start-test` / `start-prod`。
+
+应用通过环境变量 `ENV_FILE` 加载配置(见 `backend/app/core/config.py`),启动脚本会 `export ENV_FILE` 后再拉起 uvicorn。
+
+---
+
+## 2. 服务器目录
+
+```text
+/root/zkxw-backend/backend/
+├── start-test.sh          # 测试启停
+├── start-prod.sh          # 生产启停(勿与测试混用命令)
+├── .env.test              # 测试配置(勿提交敏感信息到 git)
+├── .env.production        # 生产配置
+├── .env.test.example      # 测试配置模板
+├── .venv/                 # 测试脚本默认虚拟环境
+├── venv/                  # 生产脚本默认虚拟环境
+├── backend-test.pid
+├── backend-prod.pid
+└── logs/
+    ├── backend-test.log
+    └── backend-prod.log
+```
+
+---
+
+## 3. 首次部署(测试)
+
+```bash
+cd /root/zkxw-backend/backend
+
+# 1) 测试环境变量
+cp .env.test.example .env.test
+# 编辑 .env.test:测试库、Redis DB、MinIO bucket、SECRET_KEY、CORS、API 加密等与前端一致
+
+# 2) 虚拟环境与依赖
+python3 -m venv .venv
+./.venv/bin/pip install -r requirements.txt
+
+# 3) 赋予脚本执行权限
+chmod +x start-test.sh
+
+# 4) 启动
+./start-test.sh start
+./start-test.sh status
+curl -s http://127.0.0.1:8112/health
+```
+
+健康检查与文档:
+
+- 健康检查:`http://127.0.0.1:8112/health`
+- Swagger:`http://127.0.0.1:8112/docs`
+
+---
+
+## 4. `start-test.sh` 命令
+
+```bash
+./start-test.sh [start|stop|restart|status|logs|activate|help]
+```
+
+| 命令 | 说明 |
+|------|------|
+| `start` | 后台启动(默认) |
+| `stop` | 停止 |
+| `restart` | 重启 |
+| `status` | 查看运行状态 |
+| `logs` | 最近 100 行日志 |
+| `activate` | 激活 `.venv` 并进入交互 shell(等价 `source .venv/bin/activate`) |
+| `help` | 帮助 |
+
+### 激活虚拟环境
+
+```bash
+./start-test.sh activate
+# 或手动:
+source .venv/bin/activate
+```
+
+`activate` 会进入已激活 venv 的交互 shell,输入 `exit` 退出。
+
+### 可选环境变量
+
+```bash
+HOST=0.0.0.0 PORT=8112 WORKERS=1 LOG_LEVEL=debug \
+ENV_FILE=./.env.test VENV_DIR=./.venv ./start-test.sh start
+```
+
+若仍使用旧目录 `venv` 而非 `.venv`:
+
+```bash
+VENV_DIR=./venv ./start-test.sh start
+```
+
+若测试也必须占用 **8111**(需确保未启动生产实例):
+
+```bash
+PORT=8111 ./start-test.sh start
+```
+
+---
+
+## 5. 生产环境(对照,便于追查)
+
+生产脚本:`backend/start-prod.sh`,默认端口 **8111**,读取 `.env.production`。
+
+```bash
+cd /root/zkxw-backend/backend
+cp .env.example .env.production   # 按生产填写
+python3 -m venv venv && ./venv/bin/pip install -r requirements.txt
+chmod +x start-prod.sh
+./start-prod.sh start
+curl -s http://127.0.0.1:8111/health
+```
+
+生产命令:`start` / `stop` / `restart` / `status` / `logs`(无 `activate` 子命令)。
+
+---
+
+## 6. `.env.test` 配置要点
+
+模板文件:`backend/.env.test.example`。
+
+| 配置项 | 建议 |
+|--------|------|
+| `DEBUG` | 测试可为 `true` |
+| `DATABASE_URL` | **独立测试库**,勿直连生产库 |
+| `SECRET_KEY` | 与生产区分,勿提交仓库 |
+| `REDIS_DB` | 与生产区分 DB 编号 |
+| `MINIO_BUCKET` | 建议独立 bucket |
+| `API_ENCRYPT_*` | 与测试环境前端 `VITE_API_ENCRYPT_*` 一致 |
+| `BACKEND_CORS_ORIGINS` | 测试管理端 / 小程序 H5 域名(JSON 数组) |
+| 微信支付 | 测试可用 `WXPAY_SANDBOX=true` 或留空 |
+
+`.env.test`、`.env.production` 含密钥,**不要提交到 git**。
+
+---
+
+## 7. Web 管理端(web-admin)测试打包与部署
+
+### 7.1 环境拓扑
+
+| 角色 | 地址 | 说明 |
+|------|------|------|
+| 前端 Nginx | `47.121.120.79:8112` | 静态资源 + `/api` 反代 |
+| 前端静态目录 | `/root/zkxw/dist` | `pnpm build:test` 产物上传至此 |
+| 测试后端 | `47.99.164.66:8112` | `backend/start-test.sh` 启动 |
+
+浏览器访问:`http://47.121.120.79:8112`  
+API 请求:`http://47.121.120.79:8112/api/...` → Nginx 转发 → `http://47.99.164.66:8112/api/...`
+
+### 7.2 本地打包命令
+
+```bash
+cd web-admin
+
+# 首次或依赖变更
+pnpm install
+
+# 测试环境打包(读取 web-admin/.env.test)
+pnpm build:test
+
+# 产物目录
+ls dist/
+```
+
+等价命令:
+
+```bash
+pnpm exec vite build --mode test
+```
+
+需要类型检查后再打包:
+
+```bash
+pnpm build:typecheck --mode test   # 若需单独 typecheck,可先 vue-tsc --noEmit
+```
+
+### 7.3 上传到前端服务器
+
+```bash
+# 在 web-admin 目录执行(示例)
+rsync -avz --delete dist/ root@47.121.120.79:/root/zkxw/dist/
+```
+
+或使用 scp:
+
+```bash
+scp -r dist/* root@47.121.120.79:/root/zkxw/dist/
+```
+
+### 7.4 前端环境变量(`web-admin/.env.test`)
+
+| 变量 | 测试值 | 说明 |
+|------|--------|------|
+| `VITE_BASE_URL` | `/` | 部署在 8112 根路径 |
+| `VITE_API_URL` | `/api` | 同域 Nginx 反代,避免跨域 |
+| `VITE_API_ENCRYPT_ENABLED` | `true` | 与后端 `API_ENCRYPT_ENABLED` 一致 |
+| `VITE_API_ENCRYPT_FIXED_SECRET` | 与后端相同 | 须与 `backend/.env.test` 一致 |
+| `VITE_MINIO_BASE_URL` | `/minio/zkxw` | 由 Nginx 反代 MinIO |
+
+### 7.5 Nginx 配置
+
+配置文件:`测试环境部署/nginx.conf`(已新增 **8112** 的 `ZKXW` server 块)。
+
+部署到服务器:
+
+```bash
+# 备份后覆盖或合并 include
+nginx -t && nginx -s reload
+```
+
+关键配置摘要:
+
+- `listen 8112`
+- `root /root/zkxw/dist`
+- `location ^~ /api/` → `http://47.99.164.66:8112`(完整 URI 转发)
+- `location ^~ /minio/zkxw/` → `http://47.99.164.66:20003/zkxw/`(MinIO 端口按后端实际映射调整)
+
+### 7.6 后端 CORS(若直连后端 API 时需要)
+
+推荐走 Nginx 同域反代,一般**无需**额外 CORS。若前端改为直连 `http://47.99.164.66:8112`,须在 `backend/.env.test` 增加:
+
+```env
+BACKEND_CORS_ORIGINS=["http://47.121.120.79:8112"]
+```
+
+---
+
+## 8. 常见问题
+
+### API 请求地址是 47.121.120.79 而不是 47.99.164.66(正常)
+
+测试环境使用 **Nginx 同域反代**,浏览器 Network 里应看到:
+
+```text
+http://47.121.120.79:8112/api/web/auth/login-rsa-public-key
+```
+
+**不会**显示 `47.99.164.66`——由 Nginx 在服务端转发到后端,这是预期行为。
+
+### API 返回 504 Gateway Time-out(当前最可能问题)
+
+已实测:`http://47.121.120.79:8112/` 静态页 **200**,但 `/api/...` 返回 **504**,且从外网直连 `47.99.164.66:8112` **连接超时**。
+
+说明 **Nginx 反代配置已生效**,但 **前端服务器连不上后端 8112**,请在后端机 `47.99.164.66` 与网络侧排查:
+
+**① 后端服务是否已启动并监听 8112**
+
+```bash
+cd /root/zkxw-backend/backend
+./start-test.sh status
+ss -lntp | grep 8112          # 应看到 0.0.0.0:8112
+curl -s http://127.0.0.1:8112/health
+curl -s http://127.0.0.1:8112/api/web/auth/login-rsa-public-key
+```
+
+未启动则:
+
+```bash
+cp .env.test.example .env.test   # 若尚未配置
+./start-test.sh start
+./start-test.sh logs
+```
+
+**② 从前端 Nginx 服务器测试能否访问后端(关键)**
+
+在 **47.121.120.79** 上执行:
+
+```bash
+curl -v --connect-timeout 5 http://47.99.164.66:8112/health
+```
+
+- 若此处 **超时/拒绝**:不是前端打包问题,需放通 **云安全组 / 防火墙**,允许 **47.121.120.79 → 47.99.164.66:8112**(无需对公网全网开放 8112)。
+
+阿里云安全组示例:入方向 TCP 8112,源 IP `47.121.120.79/32`。
+
+本机 firewalld 示例(仅允许 Nginx 服务器):
+
+```bash
+firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="47.121.120.79/32" port port="8112" protocol="tcp" accept'
+firewall-cmd --reload
+```
+
+**③ Nginx 配置是否已 reload**
+
+```bash
+nginx -t && nginx -s reload
+tail -f /var/log/nginx/zkxw_admin_8112_error.log
+```
+
+**④ 确认前端用测试包打包**
+
+须使用 `pnpm build:test`(读取 `.env.test`,`VITE_API_URL=/api`)。若误用 `pnpm build` 会走 `.env.production`,API 会打到生产域名。
+
+### 启动报「环境文件不存在」
+
+```bash
+cp .env.test.example .env.test
+# 编辑后重试
+./start-test.sh start
+```
+
+### 脚本语法错误(`syntax error near unexpected token`)
+
+`start-*.sh` 须为 **Unix 换行(LF)**,不能是 CRLF。在服务器上可执行:
+
+```bash
+sed -i 's/\r$//' start-test.sh start-prod.sh
+```
+
+### 测试与生产同时运行
+
+- 端口:8112 / 8111  
+- PID:`backend-test.pid` / `backend-prod.pid`  
+- 停止测试勿执行 `./start-prod.sh stop`,反之亦然。
+
+### 虚拟环境未找到
+
+```bash
+python3 -m venv .venv
+./.venv/bin/pip install -r requirements.txt
+```
+
+---
+
+## 9. 仓库内相关文件索引
+
+| 路径 | 说明 |
+|------|------|
+| `backend/start-test.sh` | 测试环境启动脚本 |
+| `backend/start-prod.sh` | 生产环境启动脚本 |
+| `backend/.env.test.example` | 测试环境变量模板 |
+| `backend/.env.example` | 通用环境变量示例 |
+| `backend/app/core/config.py` | `ENV_FILE` 加载逻辑 |
+| `web-admin/.env.test` | 测试环境前端打包变量 |
+| `web-admin/package.json` | `pnpm build:test` |
+| `测试环境部署/nginx.conf` | 前端 8112 + API/MinIO 反代 |
+| `AGENTS.md` | 开发说明中的 `start-test` / `start-prod` 命令摘要 |
+
+---
+
+## 10. 修订记录
+
+| 日期 | 说明 |
+|------|------|
+| 2026-06-01 | 初版:测试脚本 `start-test.sh`(8112、`.env.test`、`.venv`)、`activate` 命令、与生产对照及部署步骤 |
+| 2026-06-01 | 补充:`web-admin` 测试打包(`pnpm build:test`)、Nginx 8112、`/root/zkxw/dist`、API 反代 `47.99.164.66:8112` |

+ 448 - 0
测试环境部署/nginx.conf

@@ -0,0 +1,448 @@
+user root;
+worker_processes auto;
+
+events {
+    worker_connections 8192;
+    use epoll;
+    multi_accept on;
+}
+
+http {
+    include       mime.types;
+    default_type  application/octet-stream;
+    charset utf-8;
+
+    sendfile on;
+    tcp_nopush on;
+    tcp_nodelay on;
+    keepalive_timeout 65;
+    types_hash_max_size 2048;
+    server_tokens off;
+
+    # 全局 gzip
+    gzip on;
+    gzip_vary on;
+    gzip_min_length 1024;
+    gzip_proxied any;
+    gzip_comp_level 6;
+    gzip_types
+        text/plain
+        text/css
+        text/js
+        text/xml
+        text/javascript
+        application/javascript
+        application/xml+rss
+        application/json
+        image/svg+xml;
+
+# =========================
+    # ZKXW 管理端(测试环境 8112)
+    # 静态目录: /root/zkxw/dist
+    # API 反代: 47.99.164.66:8112
+    # =========================
+    server {
+        listen 8112;
+        listen [::]:8112;
+        server_name 47.121.120.79 localhost 127.0.0.1;
+
+        root /root/zkxw/dist;
+        index index.html;
+
+        client_max_body_size 100M;
+
+        access_log /var/log/nginx/zkxw_admin_8112_access.log;
+        error_log  /var/log/nginx/zkxw_admin_8112_error.log;
+
+        # 健康检查 / Swagger(须在 location / 之前;否则 /health 会走 SPA 返回 index.html)
+        location = /health {
+            proxy_pass http://47.99.164.66:8112/health;
+            proxy_http_version 1.1;
+            proxy_set_header Host $host;
+            proxy_connect_timeout 10s;
+            proxy_read_timeout 30s;
+        }
+
+        location ^~ /docs {
+            proxy_pass http://47.99.164.66:8112;
+            proxy_http_version 1.1;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto $scheme;
+        }
+
+        location = /openapi.json {
+            proxy_pass http://47.99.164.66:8112/openapi.json;
+            proxy_http_version 1.1;
+            proxy_set_header Host $host;
+        }
+
+        # API -> 测试后端(47.99.164.66:8112)
+        location ^~ /api/ {
+            proxy_pass http://47.99.164.66:8112;
+            proxy_http_version 1.1;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto $scheme;
+            proxy_connect_timeout 10s;
+            proxy_send_timeout 120s;
+            proxy_read_timeout 120s;
+        }
+
+        # MinIO 附件直链(bucket=zkxw;端口与后端 MINIO_ENDPOINT 映射一致,默认 20003)
+        location ^~ /minio/zkxw/ {
+            proxy_pass http://47.99.164.66:20003/zkxw/;
+            proxy_http_version 1.1;
+            proxy_set_header Host $http_host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto $scheme;
+            proxy_request_buffering off;
+            proxy_buffering off;
+            proxy_read_timeout 600s;
+            proxy_send_timeout 600s;
+            proxy_connect_timeout 10s;
+
+            add_header Access-Control-Allow-Origin * always;
+            add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
+            add_header Access-Control-Allow-Headers "DNT,User-Agent,Keep-Alive,Content-Type,Authorization,Range,If-Modified-Since,Cache-Control" always;
+
+            if ($request_method = OPTIONS) {
+                add_header Content-Length 0;
+                add_header Content-Type text/plain;
+                return 204;
+            }
+        }
+
+        # SPA 路由(放最后,避免吞掉 /health、/api 等)
+        location / {
+            try_files $uri $uri/ /index.html;
+        }
+
+        # 静态资源缓存
+        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+            expires 30d;
+            add_header Cache-Control "public, immutable" always;
+            add_header X-Content-Type-Options nosniff always;
+            access_log off;
+        }
+
+        # HTML 禁缓存,便于发版后立刻生效
+        location ~* \.html$ {
+            add_header Cache-Control "no-cache, no-store, must-revalidate" always;
+            add_header Pragma "no-cache" always;
+            add_header Expires "0" always;
+        }
+
+        # 禁止访问隐藏文件
+        location ~ /\. {
+            deny all;
+            access_log off;
+            log_not_found off;
+        }
+    }
+    # =========================
+    # SQXP Admin 静态服务(本机 3006)
+    # =========================
+    server {
+        listen 3006;
+        listen [::]:3006;
+        server_name 127.0.0.1 localhost 47.121.120.79;
+
+        root /root/sqxp-dist/dist;
+        index index.html;
+
+        access_log /var/log/nginx/sqxp_admin_3006_access.log;
+        error_log  /var/log/nginx/sqxp_admin_3006_error.log;
+
+        # SPA 路由支持
+        location / {
+            try_files $uri $uri/ /index.html;
+        }
+
+        # 静态资源缓存
+        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+            expires 30d;
+            add_header Cache-Control "public, immutable" always;
+            add_header X-Content-Type-Options nosniff always;
+            access_log off;
+        }
+
+        # HTML 禁缓存,便于发版后立刻生效
+        location ~* \.html$ {
+            add_header Cache-Control "no-cache, no-store, must-revalidate" always;
+            add_header Pragma "no-cache" always;
+            add_header Expires "0" always;
+        }
+
+        # 禁止访问隐藏文件
+        location ~ /\. {
+            deny all;
+            access_log off;
+            log_not_found off;
+        }
+    }
+
+    # =========================
+    # 主站:https://www.chengyantek.cn
+    # =========================
+    server {
+        listen 443 ssl http2;
+        listen [::]:443 ssl http2;
+        server_name www.chengyantek.cn;
+
+        ssl_certificate "/root/cert/www.chengyantek.cn.pem";
+        ssl_certificate_key "/root/cert/www.chengyantek.cn.key";
+
+        ssl_session_cache shared:SSL:10m;
+        ssl_session_timeout 1h;
+        ssl_protocols TLSv1.2 TLSv1.3;
+        ssl_ciphers HIGH:!aNULL:!MD5;
+        ssl_prefer_server_ciphers on;
+
+        access_log /var/log/nginx/ahchengyan_access.log;
+        error_log  /var/log/nginx/ahchengyan_error.log;
+
+        client_max_body_size 100M;
+
+        # =========================
+        # SQXP Admin 外部访问入口
+        # =========================
+        location ^~ /sqxp-admin/ {
+            proxy_pass http://127.0.0.1:3006/;
+            proxy_http_version 1.1;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto $scheme;
+            proxy_connect_timeout 10s;
+            proxy_send_timeout 60s;
+            proxy_read_timeout 60s;
+        }
+
+        # =========================
+        # SQXP API 同域代理 -> 47.99.164.66:8000/api
+        # 前端请求 /sqxp-api/web/... -> 后端 /api/web/...
+        # =========================
+        location ^~ /sqxp-api/ {
+            proxy_pass http://47.99.164.66:8000/api/;
+            proxy_http_version 1.1;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto $scheme;
+            proxy_connect_timeout 10s;
+            proxy_send_timeout 60s;
+            proxy_read_timeout 60s;
+        }
+
+        # 直接把 bucket 路径代理到 MinIO(bucket= sanqiang)
+        location ^~ /sanqiang/ {
+            proxy_pass http://127.0.0.1:20004/sanqiang/;
+            proxy_http_version 1.1;
+
+            proxy_set_header Host $http_host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto https;
+
+            proxy_request_buffering off;
+            proxy_buffering off;
+            proxy_read_timeout 600s;
+            proxy_send_timeout 600s;
+            proxy_connect_timeout 10s;
+        }
+
+        # ===== MinIO 直链访问(新增,推荐)=====
+        location ^~ /minio/ {
+            proxy_pass http://127.0.0.1:20004/;
+            proxy_http_version 1.1;
+
+            proxy_set_header Host $http_host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto https;
+
+            proxy_request_buffering off;
+            proxy_buffering off;
+            proxy_read_timeout 600s;
+            proxy_send_timeout 600s;
+            proxy_connect_timeout 10s;
+
+            add_header Access-Control-Allow-Origin * always;
+            add_header Access-Control-Allow-Methods "GET, PUT, POST, DELETE, HEAD, OPTIONS" always;
+            add_header Access-Control-Allow-Headers "Authorization,Content-Type,Content-Length,Content-MD5,Date,ETag,Range,X-Amz-Date,X-Amz-Conte
+nt-Sha256,X-Amz-Security-Token,X-Amz-User-Agent" always;
+            add_header Access-Control-Expose-Headers "ETag,Content-Length,Content-Range,Accept-Ranges,x-amz-request-id,x-amz-id-2" always;
+
+            if ($request_method = OPTIONS) {
+                add_header Content-Length 0;
+                add_header Content-Type text/plain;
+                return 204;
+            }
+        }
+
+        # ===== 小程序静态资源 =====
+        location ^~ /sqxp-static/ {
+            alias /root/sqxp/;
+            access_log off;
+
+            if ($request_method = OPTIONS) {
+                add_header Access-Control-Allow-Origin * always;
+                add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
+                add_header Access-Control-Allow-Headers "DNT,User-Agent,Keep-Alive,Content-Type,Authorization,Range,If-Modified-Since,Cache-Contr
+ol" always;
+                add_header Access-Control-Max-Age 86400 always;
+                add_header Content-Length 0;
+                add_header Content-Type text/plain;
+                return 204;
+            }
+
+            add_header Access-Control-Allow-Origin * always;
+            add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
+            add_header Access-Control-Allow-Headers "DNT,User-Agent,Keep-Alive,Content-Type,Authorization,Range,If-Modified-Since,Cache-Control" 
+always;
+
+            expires 30d;
+            add_header Cache-Control "public";
+            add_header Vary "Accept-Encoding";
+
+            try_files $uri =404;
+        }
+
+        # favicon
+        location = /favicon.ico {
+            root /root/Lyrria/frontend/dist;
+            access_log off;
+            log_not_found off;
+            expires 1y;
+            add_header Cache-Control "public, immutable";
+        }
+
+        # robots
+        location = /robots.txt {
+            access_log off;
+            log_not_found off;
+            return 200 "User-agent: *\nDisallow:\n";
+        }
+
+        # WebSocket 代理
+        location /ws {
+            proxy_pass http://127.0.0.1:8766;
+            proxy_http_version 1.1;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection "upgrade";
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto $scheme;
+            proxy_read_timeout 3600s;
+            proxy_send_timeout 3600s;
+            proxy_connect_timeout 10s;
+        }
+
+        # API 代理(Lyrria)
+        location ^~ /api/ {
+            proxy_pass http://127.0.0.1:8766$request_uri;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto $scheme;
+            proxy_read_timeout 60s;
+            proxy_send_timeout 60s;
+            proxy_connect_timeout 10s;
+        }
+
+        # 主站静态资源缓存(Lyrria)
+        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+            root /root/Lyrria/frontend/dist;
+            expires 30d;
+            add_header Cache-Control "public, immutable" always;
+            add_header X-Content-Type-Options nosniff always;
+        }
+
+        # SPA 路由支持(Lyrria)
+        location / {
+            root /root/Lyrria/frontend/dist;
+            index index.html;
+            try_files $uri $uri/ /index.html;
+
+            add_header X-Frame-Options "SAMEORIGIN" always;
+            add_header X-Content-Type-Options "nosniff" always;
+            add_header X-XSS-Protection "1; mode=block" always;
+        }
+
+        # HTML 不缓存
+        location ~* \.html$ {
+            root /root/Lyrria/frontend/dist;
+            add_header Cache-Control "no-cache, no-store, must-revalidate" always;
+            add_header Pragma "no-cache" always;
+            add_header Expires "0" always;
+            try_files $uri =404;
+        }
+
+        # 禁止访问隐藏文件
+        location ~ /\. {
+            deny all;
+            access_log off;
+            log_not_found off;
+        }
+
+        location ~ ~$ {
+            deny all;
+            access_log off;
+            log_not_found off;
+        }
+    }
+
+    # 非www在443跳转到www
+    server {
+        listen 443 ssl http2;
+        listen [::]:443 ssl http2;
+        server_name chengyantek.cn;
+
+        ssl_certificate "/root/cert/www.chengyantek.cn.pem";
+        ssl_certificate_key "/root/cert/www.chengyantek.cn.key";
+
+        return 301 https://www.chengyantek.cn$request_uri;
+    }
+
+    # =========================
+    # 未来如果给 img.chengyantek.cn 单独申请好证书,再加这个 server
+    # =========================
+    # server {
+    #     listen 443 ssl http2;
+    #     listen [::]:443 ssl http2;
+    #     server_name img.chengyantek.cn;
+    #
+    #     ssl_certificate "/root/.acme.sh/img.chengyantek.cn_ecc/fullchain.cer";
+    #     ssl_certificate_key "/root/.acme.sh/img.chengyantek.cn_ecc/img.chengyantek.cn.key";
+    #
+    #     ssl_session_cache shared:SSL:10m;
+    #     ssl_session_timeout 1h;
+    #     ssl_protocols TLSv1.2 TLSv1.3;
+    #     ssl_ciphers HIGH:!aNULL:!MD5;
+    #     ssl_prefer_server_ciphers on;
+    #
+    #     access_log /var/log/nginx/minio_img_access.log;
+    #     error_log  /var/log/nginx/minio_img_error.log;
+    #
+    #     client_max_body_size 100M;
+    #
+    #     location / {
+    #         proxy_pass http://127.0.0.1:20004;
+    #         proxy_http_version 1.1;
+    #         proxy_set_header Host $http_host;
+    #         proxy_set_header X-Real-IP $remote_addr;
+    #         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    #         proxy_set_header X-Forwarded-Proto https;
+    #         proxy_request_buffering off;
+    #         proxy_buffering off;
+    #         proxy_read_timeout 600s;
+    #         proxy_send_timeout 600s;
+    #         proxy_connect_timeout 10s;
+    #     }
+    # }
+}