- backend(financial): 新增 /china/{ts_code}/snapshot API,返回昨日交易日的收盘价/市值/PE/PB/股息率等
- backend(schemas): 新增 TodaySnapshotResponse
- backend(main): 注册 orgs 路由 /api/v1/orgs
- backend(providers:finnhub): 归一化财报字段并计算 gross_margin/net_margin/ROA/ROE
- backend(providers:tushare): 股东户数报告期与财报期对齐
- backend(routers/financial): years 默认改为 10(最大 10)
- config: analysis-config.json 切换到 qwen-flash-2025-07-28
- frontend(report/[symbol]): 新增“昨日快照”卡片、限制展示期数为10、优化增长与阈值高亮、修正类名与标题处理
- frontend(reports/[id]): 统一 period 变量与计算,修正表格 key
- frontend(hooks): 新增 useChinaSnapshot 钩子与类型
- scripts: dev.sh 增加调试输出
186 lines
5.9 KiB
Bash
Executable File
186 lines
5.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Colors
|
|
RESET="\033[0m"
|
|
GREEN="\033[32m"
|
|
CYAN="\033[36m"
|
|
YELLOW="\033[33m"
|
|
RED="\033[31m"
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
|
|
BACKEND_DIR="$REPO_ROOT/backend"
|
|
FRONTEND_DIR="$REPO_ROOT/frontend"
|
|
CONFIG_FILE="$REPO_ROOT/config/config.json"
|
|
|
|
# Guard to ensure cleanup runs only once
|
|
__CLEANED_UP=0
|
|
|
|
# Port configuration
|
|
BACKEND_PORT=8000
|
|
FRONTEND_PORT=3001
|
|
|
|
# Kill process using specified port
|
|
kill_port() {
|
|
local port=$1
|
|
echo -e "${YELLOW}[DEBUG]${RESET} Checking port $port..."
|
|
local pids=$(lsof -nP -ti tcp:"$port" 2>/dev/null || true)
|
|
echo -e "${YELLOW}[DEBUG]${RESET} Done checking port $port. PIDs: '$pids'"
|
|
if [[ -n "$pids" ]]; then
|
|
echo -e "${YELLOW}[CLEANUP]${RESET} Killing process(es) using port $port: $pids"
|
|
echo "$pids" | xargs kill -9 2>/dev/null || true
|
|
sleep 1
|
|
fi
|
|
}
|
|
|
|
ensure_backend() {
|
|
cd "$BACKEND_DIR"
|
|
if [[ ! -d .venv ]]; then
|
|
echo -e "${YELLOW}[SETUP]${RESET} Creating Python venv and installing backend requirements..."
|
|
python3 -m venv .venv
|
|
source .venv/bin/activate
|
|
# Upgrade pip first
|
|
pip install --upgrade pip --timeout 100 -i https://pypi.tuna.tsinghua.edu.cn/simple || \
|
|
pip install --upgrade pip --timeout 100
|
|
# Install requirements with timeout and mirror
|
|
pip install -r requirements.txt --timeout 300 -i https://pypi.tuna.tsinghua.edu.cn/simple || \
|
|
pip install -r requirements.txt --timeout 300
|
|
else
|
|
source .venv/bin/activate
|
|
# Upgrade pip if needed
|
|
pip install --upgrade pip --timeout 100 -i https://pypi.tuna.tsinghua.edu.cn/simple 2>/dev/null || \
|
|
pip install --upgrade pip --timeout 100 2>/dev/null || true
|
|
# Check if key dependencies are installed
|
|
if ! python -c "import uvicorn" 2>/dev/null; then
|
|
echo -e "${YELLOW}[SETUP]${RESET} Installing missing backend requirements..."
|
|
pip install -r requirements.txt --timeout 300 -i https://pypi.tuna.tsinghua.edu.cn/simple || \
|
|
pip install -r requirements.txt --timeout 300
|
|
fi
|
|
fi
|
|
|
|
# Export TUSHARE_TOKEN from config if available (prefer jq, fallback to node)
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
if command -v jq >/dev/null 2>&1; then
|
|
TUSHARE_TOKEN_VAL=$(jq -r '.data_sources.tushare.api_key // empty' "$CONFIG_FILE" 2>/dev/null || true)
|
|
else
|
|
TUSHARE_TOKEN_VAL=$(node -e "const fs=require('fs');try{const c=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));const v=c?.data_sources?.tushare?.api_key||'';if(v)process.stdout.write(v)}catch{}" "$CONFIG_FILE" 2>/dev/null || true)
|
|
fi
|
|
if [[ -n "${TUSHARE_TOKEN_VAL:-}" ]]; then
|
|
export TUSHARE_TOKEN="$TUSHARE_TOKEN_VAL"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
run_backend() {
|
|
ensure_backend
|
|
cd "$BACKEND_DIR"
|
|
# Run and colorize output (avoid stdbuf on macOS)
|
|
UVICORN_CMD=(uvicorn app.main:app --reload --port "$BACKEND_PORT" --log-level info)
|
|
"${UVICORN_CMD[@]}" 2>&1 | while IFS= read -r line; do
|
|
printf "%b[%s] [BACKEND] %s%b\n" "$GREEN" "$(date '+%Y-%m-%d %H:%M:%S')" "$line" "$RESET"
|
|
done
|
|
}
|
|
|
|
ensure_frontend() {
|
|
cd "$FRONTEND_DIR"
|
|
if [[ ! -d node_modules ]]; then
|
|
echo -e "${YELLOW}[SETUP]${RESET} Installing frontend dependencies (npm install)..."
|
|
npm install --no-fund --no-audit
|
|
fi
|
|
}
|
|
|
|
run_frontend() {
|
|
ensure_frontend
|
|
cd "$FRONTEND_DIR"
|
|
npm run dev 2>&1 | while IFS= read -r line; do
|
|
printf "%b[%s] [FRONTEND] %s%b\n" "$CYAN" "$(date '+%Y-%m-%d %H:%M:%S')" "$line" "$RESET"
|
|
done
|
|
}
|
|
|
|
# Recursively kill a process tree (children first), with optional signal (default TERM)
|
|
kill_tree() {
|
|
local pid="$1"
|
|
local signal="${2:-TERM}"
|
|
if [[ -z "${pid:-}" ]]; then
|
|
return
|
|
fi
|
|
# Kill children first
|
|
local children
|
|
children=$(pgrep -P "$pid" 2>/dev/null || true)
|
|
if [[ -n "${children:-}" ]]; then
|
|
for child in $children; do
|
|
kill_tree "$child" "$signal"
|
|
done
|
|
fi
|
|
# Then the parent
|
|
kill -"$signal" "$pid" 2>/dev/null || true
|
|
}
|
|
|
|
cleanup() {
|
|
# Ensure this runs only once even if multiple signals (INT/TERM/EXIT) arrive
|
|
if [[ $__CLEANED_UP -eq 1 ]]; then
|
|
return
|
|
fi
|
|
__CLEANED_UP=1
|
|
|
|
echo -e "\n${YELLOW}[CLEANUP]${RESET} Stopping services..."
|
|
|
|
# Gracefully stop trees for backend and frontend, then escalate if needed
|
|
if [[ -n "${BACKEND_PID:-}" ]]; then
|
|
kill_tree "$BACKEND_PID" TERM
|
|
fi
|
|
if [[ -n "${FRONTEND_PID:-}" ]]; then
|
|
kill_tree "$FRONTEND_PID" TERM
|
|
fi
|
|
|
|
# Wait up to ~3s for graceful shutdown
|
|
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
|
|
local backend_alive=0 frontend_alive=0
|
|
if [[ -n "${BACKEND_PID:-}" ]] && kill -0 "$BACKEND_PID" 2>/dev/null; then backend_alive=1; fi
|
|
if [[ -n "${FRONTEND_PID:-}" ]] && kill -0 "$FRONTEND_PID" 2>/dev/null; then frontend_alive=1; fi
|
|
if [[ $backend_alive -eq 0 && $frontend_alive -eq 0 ]]; then
|
|
break
|
|
fi
|
|
sleep 0.2
|
|
done
|
|
|
|
# Escalate to KILL if still alive
|
|
if [[ -n "${BACKEND_PID:-}" ]] && kill -0 "$BACKEND_PID" 2>/dev/null; then
|
|
kill_tree "$BACKEND_PID" KILL
|
|
fi
|
|
if [[ -n "${FRONTEND_PID:-}" ]] && kill -0 "$FRONTEND_PID" 2>/dev/null; then
|
|
kill_tree "$FRONTEND_PID" KILL
|
|
fi
|
|
|
|
# As a final safeguard, free the ports
|
|
kill_port "$BACKEND_PORT"
|
|
kill_port "$FRONTEND_PORT"
|
|
|
|
echo -e "${GREEN}[CLEANUP]${RESET} All services stopped."
|
|
}
|
|
|
|
main() {
|
|
echo -e "${CYAN}Dev Launcher${RESET}: Starting Backend ($BACKEND_PORT) and Frontend ($FRONTEND_PORT)\n"
|
|
|
|
# Clean up ports before starting
|
|
kill_port "$BACKEND_PORT"
|
|
kill_port "$FRONTEND_PORT"
|
|
|
|
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] [BACKEND]${RESET} API: http://127.0.0.1:$BACKEND_PORT"
|
|
echo -e "${CYAN}[$(date '+%Y-%m-%d %H:%M:%S')] [FRONTEND]${RESET} APP: http://127.0.0.1:$FRONTEND_PORT\n"
|
|
|
|
run_backend & BACKEND_PID=$!
|
|
run_frontend & FRONTEND_PID=$!
|
|
|
|
trap cleanup INT TERM EXIT
|
|
|
|
# Wait on both; if either exits, we exit (portable)
|
|
while true; do
|
|
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then break; fi
|
|
if ! kill -0 "$FRONTEND_PID" 2>/dev/null; then break; fi
|
|
sleep 1
|
|
done
|
|
}
|
|
|
|
main "$@"
|