diff --git a/AGENTS.md b/AGENTS.md index e7641147..a85cd5b1 100644 --- a/AGENTS.md +++ b/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`. diff --git a/backend.zip b/backend.zip new file mode 100644 index 00000000..0c1969cc Binary files /dev/null and b/backend.zip differ diff --git a/backend/app.pid b/backend/app.pid index b230bd83..9d6893fd 100644 --- a/backend/app.pid +++ b/backend/app.pid @@ -1 +1 @@ -69671 +11399 diff --git a/backend/start-prod.sh b/backend/start-prod.sh index 0b337f76..6af30468 100755 --- a/backend/start-prod.sh +++ b/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" \ diff --git a/backend/start-test.sh b/backend/start-test.sh new file mode 100755 index 00000000..116c7085 --- /dev/null +++ b/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 diff --git a/web-admin/graphify-out/GRAPH_REPORT.md b/web-admin/graphify-out/GRAPH_REPORT.md index 7b44f8b6..bd305b5f 100644 --- a/web-admin/graphify-out/GRAPH_REPORT.md +++ b/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 diff --git a/web-admin/graphify-out/manifest.json b/web-admin/graphify-out/manifest.json index 1e899e91..b4220453 100644 --- a/web-admin/graphify-out/manifest.json +++ b/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, diff --git a/web-admin/package.json b/web-admin/package.json index 75456326..24a4c9c7 100644 --- a/web-admin/package.json +++ b/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", diff --git a/web-admin/src/graphify-out/cache/ast/00c5e4d6405463ec797191c984247454ef9181377a359b2a6afb12886625e535.json b/web-admin/src/graphify-out/cache/ast/00c5e4d6405463ec797191c984247454ef9181377a359b2a6afb12886625e535.json new file mode 100644 index 00000000..3b6bf189 --- /dev/null +++ b/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": []} \ No newline at end of file diff --git a/web-admin/src/graphify-out/cache/ast/fc2da720378640ff0f9168914cbb41764b005f648df7deb63ae7d62c2860329e.json b/web-admin/src/graphify-out/cache/ast/fc2da720378640ff0f9168914cbb41764b005f648df7deb63ae7d62c2860329e.json new file mode 100644 index 00000000..48f12aa9 --- /dev/null +++ b/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": []} \ No newline at end of file diff --git a/测试环境部署/README.md b/测试环境部署/README.md new file mode 100644 index 00000000..a79a75a3 --- /dev/null +++ b/测试环境部署/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` | diff --git a/测试环境部署/nginx.conf b/测试环境部署/nginx.conf new file mode 100644 index 00000000..2aa2b8df --- /dev/null +++ b/测试环境部署/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; + # } + # } +} \ No newline at end of file