From 2b94be2048aec56e47dd529031481733bb9197e2 Mon Sep 17 00:00:00 2001 From: mavis Date: Mon, 15 Jun 2026 18:30:19 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20add=20deploy=5Fsearch=5Fsuggestions.sh?= =?UTF-8?q?=20(=E7=AB=AF=E5=88=B0=E7=AB=AF=E9=83=A8=E7=BD=B2+=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=84=9A=E6=9C=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/deploy_search_suggestions.sh | 147 +++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 scripts/deploy_search_suggestions.sh diff --git a/scripts/deploy_search_suggestions.sh b/scripts/deploy_search_suggestions.sh new file mode 100644 index 0000000..5f1d85f --- /dev/null +++ b/scripts/deploy_search_suggestions.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# 部署 search_suggestions 到远程服务器(在能 ssh 进服务器的机器上跑) +# +# 用法: bash scripts/deploy_search_suggestions.sh +# +# 假设: +# - 你已经能 ssh news@207.57.129.228 免密登录 +# - 远程路径 /srv/news 是部署根 +# - docker compose 服务名: api / worker / frontend / postgres / redis + +set -euo pipefail + +REMOTE="news@207.57.129.228" +APP_DIR="/srv/news" +COMPOSE_DIR="$APP_DIR" + +log() { echo -e "\033[1;36m[$(date +'%H:%M:%S')]\033[0m $*"; } +ok() { echo -e "\033[1;32m ✓\033[0m $*"; } +err() { echo -e "\033[1;31m ✗\033[0m $*" >&2; } +warn(){ echo -e "\033[1;33m !\033[0m $*"; } + +dc() { ssh "$REMOTE" "cd $APP_DIR && sg docker -c 'docker compose $*'"; } + +log "=== 1/9 拉代码 ===" +ssh "$REMOTE" "cd $APP_DIR && git pull origin main" +ok "git pull done" + +log "=== 2/9 跑 alembic 迁移 ===" +dc exec -T api alembic upgrade head +ok "alembic upgrade head" + +log "=== 3/9 验证 schema ===" +dc exec -T postgres psql -U "\$POSTGRES_USER" -d "\$POSTGRES_DB" -c " + SELECT table_name FROM information_schema.tables + WHERE table_name IN ('search_keywords', 'search_title_suggestions') + ORDER BY table_name; +" +# 预期: 2 行 + +log "=== 4/9 验证 trigger 挂上 ===" +dc exec -T postgres psql -U "\$POSTGRES_USER" -d "\$POSTGRES_DB" -c " + SELECT tgname, tgenabled + FROM pg_trigger + WHERE tgrelid = 'articles'::regclass + AND NOT tgisinternal + AND tgname LIKE 'trg_articles%'; +" +# 预期: trg_articles_rebuild_title_suggestions | O + +log "=== 5/9 重建 worker(让新加的 _refresh_search_keywords job 生效) ===" +dc up -d --no-deps --force-recreate worker +sleep 5 +dc ps worker +ok "worker recreated" + +log "=== 6/9 重建 api(让 search router 生效) ===" +dc up -d --no-deps --force-recreate api +sleep 5 +dc ps api +ok "api recreated" + +log "=== 7/9 重建 frontend(让 NAutoComplete 生效) ===" +dc up -d --no-deps --build frontend +sleep 10 +dc ps frontend +ok "frontend rebuilt" + +log "=== 8/9 回灌历史 articles ===" +dc exec -T api python -m app.scripts.backfill_search_suggestions +# 预期: backfill start: N ... backfill done: N rows in X.Xs ... refresh_search_keywords() done + +log "=== 9/9 端到端 API 测试 ===" +# 读 owner 密码 +OWNER_PASS=$(ssh "$REMOTE" "sudo cat /root/.owner_pass 2>/dev/null || echo ''") +if [ -z "$OWNER_PASS" ]; then + warn "找不到 /root/.owner_pass,改用交互式输入 owner 密码" + read -s -p "owner password: " OWNER_PASS + echo +fi + +# 登录拿 token +log " 登录拿 token..." +TOKEN=$(curl -s --max-time 10 -X POST "http://127.0.0.1/api/v1/auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"owner\",\"password\":\"$OWNER_PASS\"}" \ + | python -c "import sys,json; d=json.load(sys.stdin); print(d.get('access_token',''))") + +if [ -z "$TOKEN" ]; then + err "登录失败,跳过 API 测试(检查密码)" + exit 1 +fi +ok "got token (${#TOKEN} chars)" + +log " 测试 1: 空 q (应当 422 参数校验)" +curl -s -o /dev/null -w " HTTP %{http_code}\n" \ + "http://127.0.0.1/api/v1/search/suggestions" \ + -H "Authorization: Bearer $TOKEN" + +log " 测试 2: 正常 q='美'" +curl -s --max-time 10 "http://127.0.0.1/api/v1/search/suggestions?q=美&limit=10" \ + -H "Authorization: Bearer $TOKEN" | python -m json.tool + +log " 测试 3: 正常 q='美联储'" +curl -s --max-time 10 "http://127.0.0.1/api/v1/search/suggestions?q=%E7%BE%8E%E8%81%94%E5%82%A8&limit=10" \ + -H "Authorization: Bearer $TOKEN" | python -m json.tool + +log " 测试 4: 长 prefix (>20 字符,应当 422)" +curl -s -o /dev/null -w " HTTP %{http_code}\n" \ + "http://127.0.0.1/api/v1/search/suggestions?q=this_is_a_very_long_prefix_x" \ + -H "Authorization: Bearer $TOKEN" + +log " 测试 5: 不存在的字符 q='zzzzz' (应当返回空)" +curl -s --max-time 10 "http://127.0.0.1/api/v1/search/suggestions?q=zzzzz&limit=10" \ + -H "Authorization: Bearer $TOKEN" | python -m json.tool + +log " 测试 6: 未鉴权 (应当 401)" +curl -s -o /dev/null -w " HTTP %{http_code}\n" \ + "http://127.0.0.1/api/v1/search/suggestions?q=美" + +log "=== 10/10 worker 日志看 search_keywords 刷新 ===" +dc logs worker --tail 50 2>&1 | grep -i "search_keywords" || warn "没找到 search_keywords 相关日志(可能要等 10s)" + +log "=== 11/11 trigger 实际工作: 拿最新文章看 search_title_suggestions ===" +ARTICLE_ID=$(dc exec -T postgres psql -U "\$POSTGRES_USER" -d "\$POSTGRES_DB" -t -c \ + "SELECT id FROM articles WHERE title_zh IS NOT NULL ORDER BY id DESC LIMIT 1;" \ + | tr -d ' ' | tr -d '\r') +echo " 最新带 title_zh 的文章 id: $ARTICLE_ID" +if [ -n "$ARTICLE_ID" ]; then + dc exec -T postgres psql -U "\$POSTGRES_USER" -d "\$POSTGRES_DB" -c " + SELECT article_id, title_lang, + array_length(prefix_keys, 1) AS prefix_count, + published_at + FROM search_title_suggestions + WHERE article_id = $ARTICLE_ID; + " + # 预期: 一行,prefix_count 应该是标题字符数 +fi + +log "=== 12/12 search_keywords 表有数据吗 ===" +dc exec -T postgres psql -U "\$POSTGRES_USER" -d "\$POSTGRES_DB" -c \ + "SELECT source, count(*) FROM search_keywords GROUP BY source;" +# 预期: 1 行,ts_stat | N + +echo +echo "================================================" +echo -e "\033[1;32m 部署 + 测试完成!请把上面输出贴回去排查\033[0m" +echo "================================================"