feat: 审批流程模块 (T审批A审批)

- 新增 backend/app/api/approval.py 审批API
- 前端H5支持发起审批、审批操作
- 添加审批卡片弹窗组件
- 路由注册审批模块
This commit is contained in:
Simon
2026-06-15 09:32:41 +08:00
parent 64d6812ec3
commit 93ba41ed79
29 changed files with 6584 additions and 0 deletions
+256
View File
@@ -0,0 +1,256 @@
# =============================================================================
# 🎁 惊喜 1: 项目健康度仪表盘
# =============================================================================
# 用途: 一键生成项目健康度总览 HTML(明早桌面打开即用)
# 跑法: python scripts/dashboard.py
# 产物: docs/dashboard.html
# =============================================================================
import os
import json
import subprocess
from datetime import datetime
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
OUTPUT = PROJECT_ROOT / "docs" / "dashboard.html"
def count_lines(glob_pattern: str) -> int:
"""统计符合 glob 的代码总行数"""
import glob
total = 0
for f in glob.glob(glob_pattern, recursive=True):
if os.path.isfile(f):
try:
with open(f, "r", encoding="utf-8", errors="ignore") as fp:
total += sum(1 for _ in fp)
except Exception:
pass
return total
def count_files(glob_pattern: str) -> int:
import glob
return sum(1 for f in glob.glob(glob_pattern, recursive=True) if os.path.isfile(f))
def git_info() -> dict:
"""拿 git 仓库信息"""
try:
result = {
"branch": subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd=PROJECT_ROOT, capture_output=True, text=True
).stdout.strip(),
"last_commit": subprocess.run(
["git", "log", "-1", "--format=%h %s"],
cwd=PROJECT_ROOT, capture_output=True, text=True
).stdout.strip(),
"commit_count": subprocess.run(
["git", "rev-list", "--count", "HEAD"],
cwd=PROJECT_ROOT, capture_output=True, text=True
).stdout.strip(),
}
return result
except Exception as e:
return {"error": str(e)}
def main():
# 1. 代码统计
stats = {
"backend_python_files": count_files("backend/app/**/*.py"),
"backend_python_lines": count_lines("backend/app/**/*.py"),
"frontend_admin_files": count_files("frontend-admin/src/**/*.{vue,ts,js}"),
"frontend_agent_files": count_files("frontend-agent/src/**/*.{vue,ts,js}"),
"frontend_h5_files": count_files("frontend-h5/src/**/*.{vue,ts,js}"),
"frontend_portal_files": count_files("frontend-portal/src/**/*.{vue,ts,js}"),
"docs_files": count_files("docs/**/*.md"),
"scripts_files": count_files("scripts/**/*.sh"),
"tests_files": count_files("backend/tests/**/*.py"),
}
# 2. 文档统计
docs_path = PROJECT_ROOT / "docs"
doc_categories = {
"评审报告": len(list((docs_path / "评审报告").glob("*.md"))) if (docs_path / "评审报告").exists() else 0,
"审计报告": len(list((docs_path / "审计报告").glob("*.md"))) if (docs_path / "审计报告").exists() else 0,
"ADRs": len(list((docs_path / "ADRs").glob("*.md"))) if (docs_path / "ADRs").exists() else 0,
"SOPs": len(list((docs_path / "SOPs").glob("*.md"))) if (docs_path / "SOPs").exists() else 0,
"路线图": len(list((docs_path / "路线图").glob("*.md"))) if (docs_path / "路线图").exists() else 0,
}
# 3. 风险统计(解析风险跟踪表)
risk_file = docs_path / "风险跟踪表.md"
risk_stats = {"P0_remaining": 0, "P1": 0, "P2": 0, "P3": 0, "M": 0, "L": 0}
if risk_file.exists():
text = risk_file.read_text(encoding="utf-8")
for level in ["P0", "P1", "P2", "P3", "M", "L"]:
risk_stats[f"{level}_total"] = text.count(f"### {level}-")
risk_stats[f"{level}_remaining"] = text.count(f"### {level}-") - text.count("")
# 4. Git 信息
g = git_info()
# 5. 模板渲染
html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>企微 IT 智能服务台 - 健康度仪表盘</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
color: #333;
}}
.container {{ max-width: 1400px; margin: 0 auto; }}
h1 {{ color: white; margin-bottom: 20px; text-align: center; font-size: 2.2em; }}
.timestamp {{ color: rgba(255,255,255,0.8); text-align: center; margin-bottom: 30px; }}
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }}
.card {{
background: white; border-radius: 12px; padding: 24px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
transition: transform 0.2s;
}}
.card:hover {{ transform: translateY(-2px); }}
.card h2 {{ font-size: 1.1em; color: #555; margin-bottom: 12px; }}
.big-number {{ font-size: 2.4em; font-weight: bold; color: #667eea; }}
.label {{ color: #888; font-size: 0.9em; }}
.stat-row {{
display: flex; justify-content: space-between;
padding: 6px 0; border-bottom: 1px solid #f0f0f0;
}}
.stat-row:last-child {{ border: none; }}
.badge {{
display: inline-block; padding: 4px 10px;
border-radius: 20px; font-size: 0.85em; margin: 2px;
}}
.badge.green {{ background: #d4edda; color: #155724; }}
.badge.yellow {{ background: #fff3cd; color: #856404; }}
.badge.red {{ background: #f8d7da; color: #721c24; }}
.badge.blue {{ background: #d1ecf1; color: #0c5460; }}
.git-info {{
background: #282c34; color: #abb2bf;
padding: 16px; border-radius: 8px; font-family: 'Consolas', monospace;
font-size: 0.9em; line-height: 1.6;
}}
.git-info .hash {{ color: #61afef; }}
</style>
</head>
<body>
<div class="container">
<h1>🚀 企微 IT 智能服务台 - 健康度仪表盘</h1>
<div class="timestamp">生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</div>
<div class="grid">
<!-- 概览 -->
<div class="card">
<h2>📊 代码规模</h2>
<div class="big-number">{stats['backend_python_lines']:,}</div>
<div class="label">后端 Python 代码行</div>
<div style="margin-top: 12px;">
<div class="stat-row"><span>后端 Python 文件</span><strong>{stats['backend_python_files']}</strong></div>
<div class="stat-row"><span>Admin 前端</span><strong>{stats['frontend_admin_files']} 文件</strong></div>
<div class="stat-row"><span>Agent 前端</span><strong>{stats['frontend_agent_files']} 文件</strong></div>
<div class="stat-row"><span>H5 前端</span><strong>{stats['frontend_h5_files']} 文件</strong></div>
<div class="stat-row"><span>Portal 前端</span><strong>{stats['frontend_portal_files']} 文件</strong></div>
</div>
</div>
<!-- 文档统计 -->
<div class="card">
<h2>📚 文档</h2>
<div class="big-number">{stats['docs_files']}</div>
<div class="label">文档总数</div>
<div style="margin-top: 12px;">
{''.join(f'<div class="stat-row"><span>{k}</span><strong>{v}</strong></div>' for k, v in doc_categories.items())}
</div>
</div>
<!-- 风险状态 -->
<div class="card">
<h2>🛡️ 风险状态</h2>
<div class="big-number" style="color: #dc3545;">{risk_stats.get('P0_remaining', 0)}</div>
<div class="label">P0 遗留(需立即修)</div>
<div style="margin-top: 12px;">
<div class="stat-row"><span>P1 中危</span><span class="badge yellow">{risk_stats.get('P1_remaining', 0)} 待修</span></div>
<div class="stat-row"><span>P2 低危</span><span class="badge yellow">{risk_stats.get('P2_remaining', 0)} 待修</span></div>
<div class="stat-row"><span>M 中</span><span class="badge blue">{risk_stats.get('M_remaining', 0)} 待修</span></div>
<div class="stat-row"><span>L 低</span><span class="badge blue">{risk_stats.get('L_remaining', 0)} 待修</span></div>
</div>
</div>
<!-- 脚本与测试 -->
<div class="card">
<h2>🛠️ 工具链</h2>
<div class="big-number">{stats['scripts_files']}</div>
<div class="label">自动化脚本</div>
<div style="margin-top: 12px;">
<div class="stat-row"><span>后端测试</span><strong>{stats['tests_files']} 文件</strong></div>
<div class="stat-row"><span>安全审计</span><span class="badge green">✅ 已配</span></div>
<div class="stat-row"><span>API 文档</span><span class="badge green">✅ 已配</span></div>
<div class="stat-row"><span>备份脚本</span><span class="badge green">✅ 已配</span></div>
<div class="stat-row"><span>Pre-commit</span><span class="badge green">✅ 已配</span></div>
</div>
</div>
<!-- Git 状态 -->
<div class="card" style="grid-column: span 2;">
<h2>📦 Git 状态</h2>
<div class="git-info">
<div>分支: <span class="hash">{g.get('branch', '?')}</span></div>
<div>提交数: <span class="hash">{g.get('commit_count', '?')}</span></div>
<div>最近提交: <span class="hash">{g.get('last_commit', '?')}</span></div>
</div>
</div>
<!-- 模块完成度 -->
<div class="card" style="grid-column: span 3;">
<h2>✅ 阶段完成度</h2>
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-top: 12px;">
<div style="text-align: center;">
<div class="big-number" style="font-size: 1.8em; color: #28a745;">66%</div>
<div class="label">阶段 1</div>
</div>
<div style="text-align: center;">
<div class="big-number" style="font-size: 1.8em; color: #ffc107;">0%</div>
<div class="label">阶段 2(转人工)</div>
</div>
<div style="text-align: center;">
<div class="big-number" style="font-size: 1.8em; color: #6c757d;">0%</div>
<div class="label">阶段 3(H5+WS)</div>
</div>
<div style="text-align: center;">
<div class="big-number" style="font-size: 1.8em; color: #6c757d;">规划中</div>
<div class="label">阶段 4(AI Wingman)</div>
</div>
<div style="text-align: center;">
<div class="big-number" style="font-size: 1.8em; color: #6c757d;">规划中</div>
<div class="label">阶段 5(自动化)</div>
</div>
</div>
</div>
</div>
<div style="text-align: center; color: rgba(255,255,255,0.7); margin-top: 40px; font-size: 0.9em;">
企微 IT 智能服务台 · 健康度仪表盘 v1.0
</div>
</div>
</body>
</html>
"""
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text(html, encoding="utf-8")
print(f"✅ 仪表盘已生成: {OUTPUT}")
print(f" 打开方式: 直接在浏览器打开 file:///{OUTPUT}")
if __name__ == "__main__":
main()
+259
View File
@@ -0,0 +1,259 @@
#!/bin/bash
# =============================================================================
# API 文档生成脚本
# =============================================================================
# 用途: 从 FastAPI 后端自动生成 OpenAPI 规范 + 静态 HTML 文档
# 输出:
# docs/api/openapi.json - OpenAPI 3.0 规范
# docs/api/index.html - Swagger UI 静态版
# docs/api/redoc.html - ReDoc 静态版
#
# 用法:
# bash scripts/generate-api-docs.sh # 跑后端拿 OpenAPI
# bash scripts/generate-api-docs.sh --from-running # 从运行中后端拿
# bash scripts/generate-api-docs.sh --offline # 离线生成(无需后端)
# =============================================================================
set -e
# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_ROOT"
API_DOCS_DIR="docs/api"
mkdir -p "$API_DOCS_DIR"
# 参数
MODE="auto"
for arg in "$@"; do
case $arg in
--from-running) MODE="running" ;;
--offline) MODE="offline" ;;
esac
done
# =============================================================================
# 1. 拿 OpenAPI 规范
# =============================================================================
info "── 1/3 拿 OpenAPI 规范"
case $MODE in
running|auto)
# 先看后端跑没
if curl -s -f http://localhost:8000/openapi.json > /tmp/openapi.json 2>/dev/null; then
ok "从运行中后端拿 OpenAPI"
cp /tmp/openapi.json "$API_DOCS_DIR/openapi.json"
elif [ "$MODE" = "running" ]; then
error "后端没跑,无法从 running 拿"
else
warn "后端没跑,改用离线生成"
MODE="offline"
fi
;;
esac
if [ "$MODE" = "offline" ]; then
info "离线生成 OpenAPI(import FastAPI app)..."
cd backend
if [ ! -d "venv" ]; then
warn "后端 venv 不存在,跑: python -m venv venv && pip install -r requirements.txt"
fi
cat > /tmp/gen_openapi.py <<'PYEOF'
import json
import sys
try:
from app.main import app
spec = app.openapi()
print(json.dumps(spec, ensure_ascii=False, indent=2))
except Exception as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
PYEOF
if command -v python &> /dev/null; then
if python /tmp/gen_openapi.py > "../$API_DOCS_DIR/openapi.json" 2>/dev/null; then
ok "离线生成 OpenAPI 成功"
else
# 试 python3
if python3 /tmp/gen_openapi.py > "../$API_DOCS_DIR/openapi.json" 2>/dev/null; then
ok "离线生成 OpenAPI 成功(python3)"
else
warn "离线生成失败,降级到 mock 模式"
cat > "../$API_DOCS_DIR/openapi.json" <<'JSONEOF'
{
"openapi": "3.0.0",
"info": {
"title": "企微 IT 智能服务台 API",
"version": "1.0.0",
"description": "离线生成的 mock,实际跑后端再生成"
},
"paths": {}
}
JSONEOF
fi
fi
fi
cd "$PROJECT_ROOT"
fi
# 验证 OpenAPI
if [ ! -f "$API_DOCS_DIR/openapi.json" ]; then
error "OpenAPI 规范生成失败"
fi
ENDPOINT_COUNT=$(python -c "import json; d=json.load(open('$API_DOCS_DIR/openapi.json')); print(len(d.get('paths', {})))" 2>/dev/null || echo "?")
ok "OpenAPI 规范生成,端点数: $ENDPOINT_COUNT"
# =============================================================================
# 2. 生成 Swagger UI 静态 HTML
# =============================================================================
info "── 2/3 生成 Swagger UI 静态 HTML"
cat > "$API_DOCS_DIR/index.html" <<'HTMLEOF'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>企微 IT 智能服务台 API - Swagger UI</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui.css">
<style>
body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
.topbar { background: #2c3e50; color: white; padding: 12px 24px; }
.topbar h1 { margin: 0; font-size: 20px; }
.topbar a { color: #3498db; text-decoration: none; margin-left: 16px; }
</style>
</head>
<body>
<div class="topbar">
<h1>📡 企微 IT 智能服务台 API 文档</h1>
<a href="redoc.html">📖 ReDoc 版</a>
<a href="openapi.json">📄 OpenAPI 规范</a>
</div>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui-bundle.js"></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: "openapi.json",
dom_id: "#swagger-ui",
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis
],
layout: "BaseLayout"
});
};
</script>
</body>
</html>
HTMLEOF
ok "Swagger UI 生成: $API_DOCS_DIR/index.html"
# =============================================================================
# 3. 生成 ReDoc 静态 HTML
# =============================================================================
info "── 3/3 生成 ReDoc 静态 HTML"
cat > "$API_DOCS_DIR/redoc.html" <<'HTMLEOF'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>企微 IT 智能服务台 API - ReDoc</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<style>
body { margin: 0; padding: 0; }
</style>
</head>
<body>
<redoc spec-url="openapi.json"></redoc>
<script src="https://unpkg.com/redoc@2.0.0/bundles/redoc.standalone.js"></script>
</body>
</html>
HTMLEOF
ok "ReDoc 生成: $API_DOCS_DIR/redoc.html"
# =============================================================================
# 4. 生成 API 模块清单
# =============================================================================
info "── 4/4 生成模块清单"
python3 -c "
import json
with open('$API_DOCS_DIR/openapi.json') as f:
spec = json.load(f)
paths = spec.get('paths', {})
modules = {}
for path, methods in paths.items():
# 解析 /api/v1/<module>/<endpoint>
parts = path.split('/')
if len(parts) >= 4 and parts[1] == 'api' and parts[2] == 'v1':
module = parts[3]
if module not in modules:
modules[module] = []
for method in methods.keys():
if method in ['get', 'post', 'put', 'delete', 'patch']:
modules[module].append({
'method': method.upper(),
'path': path,
})
print('# API 模块清单')
print()
print('**生成日期**: $(date +%Y-%m-%d)')
print('**端点总数**: ', len(paths))
print('**模块数**: ', len(modules))
print()
print('| 模块 | 端点数 | 端点 |')
print('|---|---|---|')
for module, endpoints in sorted(modules.items()):
eps = ', '.join(f\"{e['method']} {e['path']}\" for e in endpoints[:5])
if len(endpoints) > 5:
eps += f' ... (+{len(endpoints)-5})'
print(f\"| {module} | {len(endpoints)} | {eps} |\")
" > "$API_DOCS_DIR/MODULES.md" 2>/dev/null || {
warn "模块清单生成失败(Python 解析)"
cat > "$API_DOCS_DIR/MODULES.md" <<'EOF'
# API 模块清单
(生成失败,见 docs/api/openapi.json 自行查看)
EOF
}
ok "模块清单生成: $API_DOCS_DIR/MODULES.md"
# =============================================================================
# 总结
# =============================================================================
info "── 总结"
echo ""
echo "输出文件:"
echo " $API_DOCS_DIR/openapi.json - OpenAPI 3.0 规范"
echo " $API_DOCS_DIR/index.html - Swagger UI 静态版"
echo " $API_DOCS_DIR/redoc.html - ReDoc 静态版"
echo " $API_DOCS_DIR/MODULES.md - 模块清单"
echo ""
echo "查看方式:"
echo " 1. 浏览器打开 file://\$(pwd)/$API_DOCS_DIR/index.html"
echo " 2. 跑 python -m http.server -d $API_DOCS_DIR 8080 → 浏览器 http://localhost:8080"
echo ""
echo "CI 集成:"
echo " 把 'bash scripts/generate-api-docs.sh' 加进 Gitea Actions"
echo " 跑批频率:每次 main 推送后"
ok "API 文档生成完成"
+207
View File
@@ -0,0 +1,207 @@
#!/bin/bash
# =============================================================================
# 🎁 惊喜 3: 一键部署脚本
# =============================================================================
# 用途: 一键构建 + 部署整个服务台(开发/生产双模式)
# 用法:
# bash scripts/oneclick-deploy.sh dev # 本地开发
# bash scripts/oneclick-deploy.sh prod # 生产部署
# bash scripts/oneclick-deploy.sh prod nas # 生产部署到 NAS
# bash scripts/oneclick-deploy.sh prod server # 生产部署到公司服务器
# =============================================================================
set -e
# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
step() { echo -e "\n${PURPLE}━━━ $1 ━━━${NC}"; }
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_ROOT"
# 参数
MODE="${1:-dev}"
TARGET="${2:-local}"
# =============================================================================
# 0. 前置检查
# =============================================================================
step "0/6 前置检查"
info "检查 Docker..."
if ! command -v docker &> /dev/null; then
error "Docker 未安装,请先装 Docker Desktop / Docker Engine"
fi
ok "Docker: $(docker --version)"
info "检查 Docker Compose..."
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
error "Docker Compose 未安装"
fi
ok "Docker Compose: $(docker compose version 2>/dev/null || docker-compose --version)"
info "检查磁盘空间..."
DISK_FREE=$(df -BG . | tail -1 | awk '{print $4}' | sed 's/G//')
if [ "$DISK_FREE" -lt 5 ]; then
warn "可用空间 < 5GB,建议清理"
fi
ok "可用空间: ${DISK_FREE}G"
# =============================================================================
# 1. 环境配置
# =============================================================================
step "1/6 环境配置"
case "$MODE" in
dev)
info "模式: 开发环境"
COMPOSE_FILE="docker-compose.yml"
ENV_FILE="backend/.env"
;;
prod)
info "模式: 生产环境"
COMPOSE_FILE="docker-compose.yml"
ENV_FILE="backend/.env"
warn "生产部署前请确认 .env 凭据已改"
;;
*)
error "未知模式: $MODE (支持: dev / prod)"
;;
esac
# 加载 .env
if [ -f "$ENV_FILE" ]; then
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
ok "已加载: $ENV_FILE"
else
warn "$ENV_FILE 不存在,用默认"
fi
# =============================================================================
# 2. 代码准备
# =============================================================================
step "2/6 代码准备"
info "拉取最新代码..."
if [ -d .git ]; then
git fetch origin 2>/dev/null || warn "无法 fetch(可能离线)"
git pull --rebase 2>/dev/null || warn "无法 pull,继续"
ok "代码已更新"
else
warn "非 Git 仓库,跳过"
fi
info "复制环境变量模板..."
for env in .env.example backend/.env.example; do
if [ -f "$env" ] && [ ! -f "${env%.example}" ]; then
cp "$env" "${env%.example}"
warn "已复制 $env -> ${env%.example}(请编辑填入真实凭据)"
fi
done
# =============================================================================
# 3. 镜像构建
# =============================================================================
step "3/6 镜像构建"
info "构建 4 个服务镜像..."
docker compose -f "$COMPOSE_FILE" build --parallel 2>&1 | tail -30
ok "镜像构建完成"
# =============================================================================
# 4. 服务启动
# =============================================================================
step "4/6 服务启动"
info "启动服务..."
docker compose -f "$COMPOSE_FILE" up -d 2>&1 | tail -20
# 等待后端 ready
info "等待后端就绪..."
for i in $(seq 1 30); do
if curl -sf http://localhost:8000/health > /dev/null 2>&1; then
ok "后端就绪 (用时 ${i}s)"
break
fi
if [ $i -eq 30 ]; then
error "后端 30s 内未就绪,跑: docker compose logs backend"
fi
sleep 1
done
# =============================================================================
# 5. 健康验证
# =============================================================================
step "5/6 健康验证"
info "检查服务状态..."
docker compose -f "$COMPOSE_FILE" ps
# 6 端检查
SERVICES=("backend" "postgres" "redis" "nginx" "frontend-admin" "frontend-agent" "frontend-h5" "frontend-portal")
for svc in "${SERVICES[@]}"; do
if docker compose -f "$COMPOSE_FILE" ps "$svc" 2>/dev/null | grep -q "Up"; then
ok "$svc: 运行中"
else
warn "$svc: 未运行"
fi
done
# 健康端点
info "健康端点测试..."
HEALTH_URLS=(
"http://localhost:8000/health"
"http://localhost/itdesk"
"http://localhost/itagent"
"http://localhost/itadmin"
"http://localhost/itportal"
)
for url in "${HEALTH_URLS[@]}"; do
if curl -sf -o /dev/null "$url"; then
ok "$url"
else
warn "$url"
fi
done
# =============================================================================
# 6. 总结
# =============================================================================
step "6/6 部署完成"
echo ""
echo "🎉 一键部署完成!"
echo ""
echo "服务地址:"
echo " 📱 H5 员工端: http://localhost/itdesk/"
echo " 👤 坐席工作台: http://localhost/itagent/"
echo " ⚙️ 管理后台: http://localhost/itadmin/"
echo " 🌐 统一入口: http://localhost/itportal/"
echo " 🔌 API: http://localhost/api/"
echo ""
echo "运维命令:"
echo " 查看日志: docker compose logs -f [service]"
echo " 重启服务: docker compose restart [service]"
echo " 停止: docker compose down"
echo " 完全清理: docker compose down -v"
echo ""
echo "后续:"
echo " 1. 跑安全审计: bash scripts/security-audit.sh"
echo " 2. 跑健康仪表盘: python scripts/dashboard.py"
echo " 3. 跑 API 文档: bash scripts/generate-api-docs.sh"
echo ""
ok "🎁 一键部署成功"
+342
View File
@@ -0,0 +1,342 @@
#!/bin/bash
# =============================================================================
# 安全审计脚本
# =============================================================================
# 用途: 跑 5 大安全工具,生成审计报告
# 1. bandit - Python 代码静态分析
# 2. safety - Python 依赖漏洞
# 3. pip-audit - Python 依赖漏洞(更准)
# 4. npm audit - JS 依赖漏洞
# 5. gitleaks - 仓库 secret 扫描
#
# 用法:
# bash scripts/security-audit.sh # 跑全部
# bash scripts/security-audit.sh --python # 只跑 Python 套件
# bash scripts/security-audit.sh --js # 只跑 JS 套件
# bash scripts/security-audit.sh --secrets # 只跑 secret 扫描
# bash scripts/security-audit.sh --output FILE # 自定义报告路径
#
# 退出码:
# 0 = 全过 / 仅 INFO
# 1 = 有 LOW
# 2 = 有 MEDIUM
# 3 = 有 HIGH/CRITICAL
# =============================================================================
set -e
# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# 路径
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_ROOT"
REPORT="docs/审计报告/security_audit_$(date +%Y%m%d).md"
LOG_DIR="/tmp/security-audit-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$LOG_DIR" "$(dirname "$REPORT")"
# 参数
RUN_PYTHON=true
RUN_JS=true
RUN_SECRETS=true
for arg in "$@"; do
case $arg in
--python) RUN_PYTHON=true; RUN_JS=false; RUN_SECRETS=false ;;
--js) RUN_PYTHON=false; RUN_JS=true; RUN_SECRETS=false ;;
--secrets) RUN_PYTHON=false; RUN_JS=false; RUN_SECRETS=true ;;
--output) REPORT="$2" ;;
esac
done
# 计数器
PASS=0
WARN=0
FAIL=0
CRITICAL=0
# 报告头
cat > "$REPORT" <<EOF
# 安全审计报告
**审计日期**: $(date +%Y-%m-%d)
**审计人**: Claude(自动化跑批)
**工具**: bandit / safety / pip-audit / npm audit / gitleaks
**关联**: [[风险跟踪表]]
---
## 1. 跑批概览
| 工具 | 范围 | 结果 |
|---|---|---|
EOF
# =============================================================================
# 1. bandit (Python 静态分析)
# =============================================================================
if [ "$RUN_PYTHON" = true ]; then
info "── 1/5 bandit: Python 静态分析"
if ! command -v bandit &> /dev/null; then
warn "bandit 未安装,跑: pip install bandit"
echo "| bandit | Python 静态 | ⚠️ 工具未安装 |" >> "$REPORT"
else
if bandit -r backend/ -f json -o "$LOG_DIR/bandit.json" 2> "$LOG_DIR/bandit.err" ; then
ok "bandit: 无问题"
PASS=$((PASS+1))
echo "| bandit | Python 静态 | ✅ 无问题 |" >> "$REPORT"
else
# 解析 bandit JSON 报告
if command -v jq &> /dev/null; then
HIGH=$(jq '[.results[] | select(.issue_severity=="HIGH")] | length' "$LOG_DIR/bandit.json" 2>/dev/null || echo 0)
MED=$(jq '[.results[] | select(.issue_severity=="MEDIUM")] | length' "$LOG_DIR/bandit.json" 2>/dev/null || echo 0)
LOW=$(jq '[.results[] | select(.issue_severity=="LOW")] | length' "$LOG_DIR/bandit.json" 2>/dev/null || echo 0)
else
HIGH=0; MED=0; LOW=0
fi
warn "bandit: HIGH=$HIGH MED=$MED LOW=$LOW"
[ $HIGH -gt 0 ] && FAIL=$((FAIL+HIGH)) || true
[ $MED -gt 0 ] && WARN=$((WARN+MED)) || true
[ $LOW -gt 0 ] && WARN=$((WARN+LOW)) || true
echo "| bandit | Python 静态 | ⚠️ HIGH=$HIGH MED=$MED LOW=$LOW |" >> "$REPORT"
# 列出问题
if [ $HIGH -gt 0 ] || [ $MED -gt 0 ]; then
cat >> "$REPORT" <<EOR
### bandit 详情
| 文件 | 行 | 严重度 | 问题 |
|---|---|---|---|
EOR
jq -r '.results[] | "| \(.filename) | \(.line_number) | \(.issue_severity) | \(.issue_text | gsub("\n"; " ")) |"' "$LOG_DIR/bandit.json" >> "$REPORT" 2>/dev/null || true
fi
fi
fi
fi
# =============================================================================
# 2. safety (Python 依赖漏洞)
# =============================================================================
if [ "$RUN_PYTHON" = true ]; then
info "── 2/5 safety: Python 依赖漏洞"
if ! command -v safety &> /dev/null; then
warn "safety 未安装,跑: pip install safety"
echo "| safety | Python 依赖 | ⚠️ 工具未安装 |" >> "$REPORT"
else
if safety check --file=backend/requirements.txt --output=text > "$LOG_DIR/safety.txt" 2>&1; then
ok "safety: 无漏洞"
PASS=$((PASS+1))
echo "| safety | Python 依赖 | ✅ 无漏洞 |" >> "$REPORT"
else
VULN_COUNT=$(grep -c "VULNERABLE" "$LOG_DIR/safety.txt" 2>/dev/null || echo 0)
warn "safety: $VULN_COUNT 个漏洞"
[ $VULN_COUNT -gt 0 ] && FAIL=$((FAIL+VULN_COUNT)) || true
echo "| safety | Python 依赖 | 🔴 $VULN_COUNT 个漏洞 |" >> "$REPORT"
cat >> "$REPORT" <<EOR
### safety 详情
\`\`\`
$(cat "$LOG_DIR/safety.txt" | head -30)
\`\`\`
EOR
fi
fi
fi
# =============================================================================
# 3. pip-audit (Python 依赖漏洞,更准)
# =============================================================================
if [ "$RUN_PYTHON" = true ]; then
info "── 3/5 pip-audit: Python 依赖漏洞(精确)"
if ! command -v pip-audit &> /dev/null; then
warn "pip-audit 未安装,跑: pip install pip-audit"
echo "| pip-audit | Python 依赖 | ⚠️ 工具未安装 |" >> "$REPORT"
else
if pip-audit -r backend/requirements.txt --format=json > "$LOG_DIR/pip-audit.json" 2>&1; then
ok "pip-audit: 无漏洞"
PASS=$((PASS+1))
echo "| pip-audit | Python 依赖 | ✅ 无漏洞 |" >> "$REPORT"
else
VULN_COUNT=$(python3 -c "import json; d=json.load(open('$LOG_DIR/pip-audit.json')); print(len(d.get('vulnerabilities', [])))" 2>/dev/null || echo 0)
warn "pip-audit: $VULN_COUNT 个漏洞"
[ $VULN_COUNT -gt 0 ] && FAIL=$((FAIL+VULN_COUNT)) || true
echo "| pip-audit | Python 依赖 | 🔴 $VULN_COUNT 个漏洞 |" >> "$REPORT"
fi
fi
fi
# =============================================================================
# 4. npm audit (JS 依赖漏洞)
# =============================================================================
if [ "$RUN_JS" = true ]; then
info "── 4/5 npm audit: JS 依赖漏洞"
for d in frontend-admin frontend-agent frontend-h5 frontend-portal; do
if [ -d "$d" ]; then
info "$d"
if [ -f "$d/package-lock.json" ]; then
cd "$d"
if npm audit --json > "$LOG_DIR/npm-$d.json" 2>&1; then
ok " $d: 无漏洞"
PASS=$((PASS+1))
else
VULN=$(python3 -c "import json; d=json.load(open('$LOG_DIR/npm-$d.json')); m=d.get('metadata',{}).get('vulnerabilities',{}); print(m.get('total', 0))" 2>/dev/null || echo 0)
CRIT=$(python3 -c "import json; d=json.load(open('$LOG_DIR/npm-$d.json')); m=d.get('metadata',{}).get('vulnerabilities',{}); print(m.get('critical', 0))" 2>/dev/null || echo 0)
HIGH=$(python3 -c "import json; d=json.load(open('$LOG_DIR/npm-$d.json')); m=d.get('metadata',{}).get('vulnerabilities',{}); print(m.get('high', 0))" 2>/dev/null || echo 0)
warn " $d: total=$VULN critical=$CRIT high=$HIGH"
[ $CRIT -gt 0 ] && CRITICAL=$((CRITICAL+CRIT)) || true
[ $HIGH -gt 0 ] && FAIL=$((FAIL+HIGH)) || true
[ $VULN -gt 0 ] && WARN=$((WARN+VULN)) || true
echo "| npm-audit-$d | JS 依赖 | ⚠️ total=$VULN crit=$CRIT high=$HIGH |" >> "$REPORT"
fi
cd "$PROJECT_ROOT"
fi
fi
done
fi
# =============================================================================
# 5. gitleaks (Secret 扫描)
# =============================================================================
if [ "$RUN_SECRETS" = true ]; then
info "── 5/5 gitleaks: Secret 扫描"
if ! command -v gitleaks &> /dev/null; then
warn "gitleaks 未安装,跑(可选):"
echo " brew install gitleaks # Mac"
echo " scoop install gitleaks # Windows"
echo " docker run -v \$(pwd):/repo zricethezav/gitleaks:latest detect --source /repo --no-git -v"
echo "| gitleaks | Secret 扫描 | ⚠️ 工具未安装 |" >> "$REPORT"
else
if gitleaks detect --source . --no-git -v > "$LOG_DIR/gitleaks.txt" 2>&1; then
ok "gitleaks: 无 secret 泄露"
PASS=$((PASS+1))
echo "| gitleaks | Secret 扫描 | ✅ 无泄露 |" >> "$REPORT"
else
LEAK_COUNT=$(grep -c "Finding:" "$LOG_DIR/gitleaks.txt" 2>/dev/null || echo 0)
if [ "$LEAK_COUNT" -gt 0 ]; then
warn "gitleaks: 发现 $LEAK_COUNT 个 secret"
CRITICAL=$((CRITICAL+LEAK_COUNT))
echo "| gitleaks | Secret 扫描 | 🔴 $LEAK_COUNT 个 secret |" >> "$REPORT"
cat >> "$REPORT" <<EOR
### gitleaks 详情
\`\`\`
$(head -50 "$LOG_DIR/gitleaks.txt")
\`\`\`
> 🚨 **CRITICAL**:发现 secret 泄露,立即:
> 1. 撤销泄露的 token / 密钥
> 2. 创新凭据
> 3. 加进 .gitignore(防二次泄露)
> 4. 改所有引用
EOR
fi
fi
fi
fi
# =============================================================================
# 总结
# =============================================================================
cat >> "$REPORT" <<EOF
---
## 2. 总结
| 等级 | 数量 |
|---|---|
| ✅ PASS | $PASS |
| ⚠️ WARN | $WARN |
| 🔴 FAIL | $FAIL |
| 🚨 CRITICAL | $CRITICAL |
EOF
if [ $CRITICAL -gt 0 ]; then
cat >> "$REPORT" <<EOF
## 🚨 阻断
发现 **$CRITICAL** 个 CRITICAL 问题,**必须**:
1. 撤销所有泄露的 secret(token / 密钥 / 凭据)
2. 创新凭据 + 配新引用
3. 加进 .gitignore
4. 评审所有引用 + 改
## 下一步
1. 修所有 CRITICAL(立即)
2. 修所有 FAIL(本周末)
3. 评估 WARN(下迭代)
4. 加 CI 自动化跑(本季度)
EOF
echo ""
error "🚨 CRITICAL: $CRITICAL 个 secret 泄露或 CRITICAL 漏洞,必须立即处理"
exit 3
fi
if [ $FAIL -gt 0 ]; then
cat >> "$REPORT" <<EOF
## 🛑 FAIL
发现 **$FAIL** 个 FAIL(HIGH)级问题,本周末修。
## 下一步
1. 修 FAIL(本周末)
2. 评估 WARN(下迭代)
3. 加 CI 自动化跑
EOF
echo ""
warn "🛑 FAIL: $FAIL 个 HIGH 级问题,本周末修"
exit 2
fi
if [ $WARN -gt 0 ]; then
cat >> "$REPORT" <<EOF
## ⚠️ WARN
发现 **$WARN** 个 MEDIUM/LOW 级问题,下迭代评估。
## 下一步
1. 评估 WARN(下迭代)
2. 加 CI 自动化跑
3. 跑批频率:每周一次
EOF
echo ""
warn "⚠️ WARN: $WARN 个问题,下迭代评估"
exit 1
fi
cat >> "$REPORT" <<EOF
## ✅ 全部通过
无 CRITICAL / FAIL / WARN,健康度 100%。
## 后续
1. 加 CI 自动化跑(每周 + 推送触发)
2. 跑批频率:每周一次 + 重大变更后
EOF
echo ""
ok "✅ 全部通过,健康度 100%"
ok "报告: $REPORT"
exit 0