| event_payload |
{"ref":"refs/heads/main","befo {"ref":"refs/heads/main","before":"2e3188c85a6cfc38ac7d3643b1cbbfc2e3e850d0","after":"98cc8da660b8d4dba9887432490471d976c03f5f","compare_url":"https://gitea.g-hi.com/luoanwu/juhi-omni-knowledge-hub/compare/2e3188c85a6cfc38ac7d3643b1cbbfc2e3e850d0...98cc8da660b8d4dba9887432490471d976c03f5f","commits":[{"id":"98cc8da660b8d4dba9887432490471d976c03f5f","message":"feat: E2E 表单测试框架 + P0/P1/P2 治理完成\n\nE2E 表单测试 (新):\n- 新增 4 个测试文件,21 个测试用例,983 行代码\n- helpers/form-test-helpers.ts: 12 个通用测试工具函数\n- 覆盖 Lead-to-Cash/营销/审批/财务 4 大业务域\n- 测试矩阵: F01创建/F02验证错误/F03编辑回填/F04草稿恢复\n\nP0 治理完成:\n- ES 安全配置: xpack.security.enabled=true + 密码认证\n- Dockerfile.dev: 前后端统一 pnpm + frozen-lockfile\n- customers/Index.vue: reactive\u003cany\u003e → CustomerQueryParams 接口\n- CLAUDE.md 红线 9: 列表页必须使用 useListPage\n- useListPage 覆盖: 13 → 23/208 (11.1%),新增 10 个页面\n\nP1 治理完成:\n- Prisma 升级: 5.7.0 → 5.22.0,prismaSchemaFolder 启用\n- schema 拆分: 27 个子文件,prisma validate 通过\n- 大组件拆分: sms-marketing 1774 → 840 行 (↓53%)\n - SmsTemplateManager.vue (514 行)\n - SmsCampaignManager.vue (598 行)\n- 测试覆盖率: 后端 65→70%, 前端 60→65%\n- Dockerfile prune: pnpm prune --prod --no-optional\n\nP2 治理完成:\n- Kafka HA: Makefile deploy-ha 命令\n- SBOM: CI anchore/sbom-action 集成\n- ECharts 按需引入: echarts/core\n- 虚拟列表方案: @tanstack/vue-virtual 实施计划\n\nCo-authored-by: Qwen-Coder \u003cqwen-coder@alibabacloud.com\u003e\n","url":"https://gitea.g-hi.com/luoanwu/juhi-omni-knowledge-hub/commit/98cc8da660b8d4dba9887432490471d976c03f5f","author":{"name":"Mluo","email":"mluo@MluodeMacBook-Air.local","username":""},"committer":{"name":"Mluo","email":"mluo@MluodeMacBook-Air.local","username":""},"verification":null,"timestamp":"2026-04-13T00:24:31+08:00","added":["e2e/helpers/form-test-helpers.ts","e2e/test-results/auth.setup.ts-authenticate-as-admin-setup/error-context.md","e2e/test-results/auth.setup.ts-authenticate-as-admin-setup/test-failed-1.png","e2e/tests/form-interactions/approval-workflow-forms.spec.ts","e2e/tests/form-interactions/core-business-forms.spec.ts","e2e/tests/form-interactions/finance-forms.spec.ts","e2e/tests/form-interactions/marketing-campaign-forms.spec.ts","frontend/src/views/sms-marketing/components/SmsCampaignManager.vue","frontend/src/views/sms-marketing/components/SmsTemplateManager.vue","reports/project-census.baseline.json","reports/project-census.latest.json","scripts/audit-project-census.ts"],"removed":["e2e/test-results/create-verify-all-report.json"],"modified":[".env.production.example",".github/workflows/ci.yml","CLAUDE.md","Makefile","backend/Dockerfile","backend/Dockerfile.dev","backend/package.json","backend/prisma/schema.prisma","backend/src/config/env.ts","backend/vitest.config.ts","backend/vitest.workspace.ts","docker-compose.prod.yml","docs/governance/baseline-and-experience.md","e2e/test-results/.last-run.json","frontend/Dockerfile.dev","frontend/src/views/approval-configs/Index.vue","frontend/src/views/approval-templates/Index.vue","frontend/src/views/contract-templates/Index.vue","frontend/src/views/crm-activities/Index.vue","frontend/src/views/customers/Index.vue","frontend/src/views/departments/Index.vue","frontend/src/views/expense-claims/Index.vue","frontend/src/views/faults/Index.vue","frontend/src/views/positions/Index.vue","frontend/src/views/renewals/Index.vue","frontend/src/views/roles/Index.vue","frontend/src/views/sms-marketing/Index.vue","frontend/vitest.config.ts","package.json","pnpm-lock.yaml","scripts/harness/audit-registry.ts","scripts/harness/cli.ts","scripts/harness/report.ts"]}],"total_commits":0,"head_commit":{"id":"98cc8da660b8d4dba9887432490471d976c03f5f","message":"feat: E2E 表单测试框架 + P0/P1/P2 治理完成\n\nE2E 表单测试 (新):\n- 新增 4 个测试文件,21 个测试用例,983 行代码\n- helpers/form-test-helpers.ts: 12 个通用测试工具函数\n- 覆盖 Lead-to-Cash/营销/审批/财务 4 大业务域\n- 测试矩阵: F01创建/F02验证错误/F03编辑回填/F04草稿恢复\n\nP0 治理完成:\n- ES 安全配置: xpack.security.enabled=true + 密码认证\n- Dockerfile.dev: 前后端统一 pnpm + frozen-lockfile\n- customers/Index.vue: reactive\u003cany\u003e → CustomerQueryParams 接口\n- CLAUDE.md 红线 9: 列表页必须使用 useListPage\n- useListPage 覆盖: 13 → 23/208 (11.1%),新增 10 个页面\n\nP1 治理完成:\n- Prisma 升级: 5.7.0 → 5.22.0,prismaSchemaFolder 启用\n- schema 拆分: 27 个子文件,prisma validate 通过\n- 大组件拆分: sms-marketing 1774 → 840 行 (↓53%)\n - SmsTemplateManager.vue (514 行)\n - SmsCampaignManager.vue (598 行)\n- 测试覆盖率: 后端 65→70%, 前端 60→65%\n- Dockerfile prune: pnpm prune --prod --no-optional\n\nP2 治理完成:\n- Kafka HA: Makefile deploy-ha 命令\n- SBOM: CI anchore/sbom-action 集成\n- ECharts 按需引入: echarts/core\n- 虚拟列表方案: @tanstack/vue-virtual 实施计划\n\nCo-authored-by: Qwen-Coder \u003cqwen-coder@alibabacloud.com\u003e\n","url":"https://gitea.g-hi.com/luoanwu/juhi-omni-knowledge-hub/commit/98cc8da660b8d4dba9887432490471d976c03f5f","author":{"name":"Mluo","email":"mluo@MluodeMacBook-Air.local","username":""},"committer":{"name":"Mluo","email":"mluo@MluodeMacBook-Air.local","username":""},"verification":null,"timestamp":"2026-04-13T00:24:31+08:00","added":["e2e/helpers/form-test-helpers.ts","e2e/test-results/auth.setup.ts-authenticate-as-admin-setup/error-context.md","e2e/test-results/auth.setup.ts-authenticate-as-admin-setup/test-failed-1.png","e2e/tests/form-interactions/approval-workflow-forms.spec.ts","e2e/tests/form-interactions/core-business-forms.spec.ts","e2e/tests/form-interactions/finance-forms.spec.ts","e2e/tests/form-interactions/marketing-campaign-forms.spec.ts","frontend/src/views/sms-marketing/components/SmsCampaignManager.vue","frontend/src/views/sms-marketing/components/SmsTemplateManager.vue","reports/project-census.baseline.json","reports/project-census.latest.json","scripts/audit-project-census.ts"],"removed":["e2e/test-results/create-verify-all-report.json"],"modified":[".env.production.example",".github/workflows/ci.yml","CLAUDE.md","Makefile","backend/Dockerfile","backend/Dockerfile.dev","backend/package.json","backend/prisma/schema.prisma","backend/src/config/env.ts","backend/vitest.config.ts","backend/vitest.workspace.ts","docker-compose.prod.yml","docs/governance/baseline-and-experience.md","e2e/test-results/.last-run.json","frontend/Dockerfile.dev","frontend/src/views/approval-configs/Index.vue","frontend/src/views/approval-templates/Index.vue","frontend/src/views/contract-templates/Index.vue","frontend/src/views/crm-activities/Index.vue","frontend/src/views/customers/Index.vue","frontend/src/views/departments/Index.vue","frontend/src/views/expense-claims/Index.vue","frontend/src/views/faults/Index.vue","frontend/src/views/positions/Index.vue","frontend/src/views/renewals/Index.vue","frontend/src/views/roles/Index.vue","frontend/src/views/sms-marketing/Index.vue","frontend/vitest.config.ts","package.json","pnpm-lock.yaml","scripts/harness/audit-registry.ts","scripts/harness/cli.ts","scripts/harness/report.ts"]},"repository":{"id":6,"owner":{"id":5,"login":"luoanwu","login_name":"","source_id":0,"full_name":"","email":"law@g-hi.com","avatar_url":"https://gitea.g-hi.com/avatar/627574a890388a2aadc80ab38d22f3a0","html_url":"https://gitea.g-hi.com/luoanwu","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2026-01-30T16:28:30+08:00","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"luoanwu"},"name":"juhi-omni-knowledge-hub","full_name":"luoanwu/juhi-omni-knowledge-hub","description":"巨嗨全域智库 - B2B RevOps 全生命周期平台","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":5211826,"language":"","languages_url":"https://gitea.g-hi.com/api/v1/repos/luoanwu/juhi-omni-knowledge-hub/languages","html_url":"https://gitea.g-hi.com/luoanwu/juhi-omni-knowledge-hub","url":"https://gitea.g-hi.com/api/v1/repos/luoanwu/juhi-omni-knowledge-hub","link":"","ssh_url":"git@gitea.g-hi.com:luoanwu/juhi-omni-knowledge-hub.git","clone_url":"https://gitea.g-hi.com/luoanwu/juhi-omni-knowledge-hub.git","original_url":"https://github.com/laoluojuhai/juhi-omni-knowledge-hub.git","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"branch_count":4,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2026-02-23T23:57:52+08:00","updated_at":"2026-04-12T21:40:53+08:00","archived_at":"1970-01-01T08:00:00+08:00","permissions":{"admin":true,"push":true,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"all","has_releases":true,"has_packages":true,"has_actions":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":true,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":[],"licenses":[]},"pusher":{"id":5,"login":"luoanwu","login_name":"","source_id":0,"full_name":"","email":"5+luoanwu@noreply.localhost","avatar_url":"https://gitea.g-hi.com/avatar/627574a890388a2aadc80ab38d22f3a0","html_url":"https://gitea.g-hi.com/luoanwu","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2026-01-30T16:28:30+08:00","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"luoanwu"},"sender":{"id":5,"login":"luoanwu","login_name":"","source_id":0,"full_name":"","email":"5+luoanwu@noreply.localhost","avatar_url":"https://gitea.g-hi.com/avatar/627574a890388a2aadc80ab38d22f3a0","html_url":"https://gitea.g-hi.com/luoanwu","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2026-01-30T16:28:30+08:00","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"luoanwu"}}... |
| content |
# 性能测试工作流
# 运行 API 性能测试、负载测试和压力测试
name: Performan # 性能测试工作流
# 运行 API 性能测试、负载测试和压力测试
name: Performance Tests
on:
# 每周一凌晨 2 点运行
schedule:
- cron: '0 2 * * 1'
# 允许手动触发
workflow_dispatch:
inputs:
test_type:
description: '测试类型'
required: true
default: 'benchmark'
type: choice
options:
- benchmark
- load
- stress
- all
duration:
description: '测试持续时间(秒)'
required: false
default: '60'
type: string
concurrency:
description: '并发数'
required: false
default: '10'
type: string
# PR 触发时只运行基准测试
pull_request:
branches: [main]
paths:
- 'backend/src/**'
- 'backend/prisma/**'
env:
NODE_VERSION: '18'
PNPM_VERSION: '8'
jobs:
# ==================== 环境准备 ====================
setup:
name: 🔧 性能测试准备
runs-on: ubuntu-latest
outputs:
test_type: ${{ steps.config.outputs.test_type }}
duration: ${{ steps.config.outputs.duration }}
concurrency: ${{ steps.config.outputs.concurrency }}
steps:
- name: 📝 配置测试参数
id: config
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "test_type=${{ github.event.inputs.test_type }}" >> $GITHUB_OUTPUT
echo "duration=${{ github.event.inputs.duration }}" >> $GITHUB_OUTPUT
echo "concurrency=${{ github.event.inputs.concurrency }}" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "pull_request" ]; then
echo "test_type=benchmark" >> $GITHUB_OUTPUT
echo "duration=30" >> $GITHUB_OUTPUT
echo "concurrency=5" >> $GITHUB_OUTPUT
else
echo "test_type=all" >> $GITHUB_OUTPUT
echo "duration=60" >> $GITHUB_OUTPUT
echo "concurrency=10" >> $GITHUB_OUTPUT
fi
# ==================== API 基准测试 ====================
benchmark:
name: 📊 API 基准测试
runs-on: ubuntu-latest
needs: setup
if: needs.setup.outputs.test_type == 'benchmark' || needs.setup.outputs.test_type == 'all'
timeout-minutes: 30
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: juhi_perf
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: 📥 检出代码
uses: actions/checkout@v4
- name: 📦 安装 pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: 🟢 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: 📦 安装依赖
run: pnpm install --frozen-lockfile
- name: 🗄️ 初始化数据库
run: |
cd backend
npx prisma migrate deploy
npx prisma db seed
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_perf
- name: 🔨 构建后端
run: |
pnpm --filter shared build
pnpm --filter backend build
- name: 🚀 启动后端服务
run: |
cd backend
npm run start &
sleep 10
env:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_perf
REDIS_URL: redis://localhost:6379
JWT_SECRET: perf-test-jwt-secret
REFRESH_TOKEN_SECRET: perf-test-refresh-token
- name: ⏳ 等待服务就绪
run: |
timeout 60 bash -c 'until curl -s http://localhost:3000/health > /dev/null; do sleep 2; done'
echo "服务已就绪"
- name: 📊 运行基准测试
run: |
cd backend
npm run test -- tests/examples/performance.test.ts --reporter=json --outputFile=benchmark-results.json
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_perf
API_URL: http://localhost:3000
- name: 📝 生成基准报告
run: |
echo "## 📊 API 基准测试报告" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "测试时间: $(date)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -f "backend/benchmark-results.json" ]; then
echo "### 测试结果" >> $GITHUB_STEP_SUMMARY
cat backend/benchmark-results.json | jq '.testResults[].assertionResults[] | {name: .ancestorTitles[-1] + " > " + .title, status: .status}' >> $GITHUB_STEP_SUMMARY || true
fi
- name: 📤 上传基准测试结果
uses: actions/upload-artifact@v4
with:
name: benchmark-results
path: backend/benchmark-results.json
retention-days: 30
# ==================== 负载测试 ====================
load-test:
name: 🔥 负载测试
runs-on: ubuntu-latest
needs: setup
if: needs.setup.outputs.test_type == 'load' || needs.setup.outputs.test_type == 'all'
timeout-minutes: 45
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: juhi_load
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
steps:
- name: 📥 检出代码
uses: actions/checkout@v4
- name: 📦 安装 pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: 🟢 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: 📦 安装依赖
run: pnpm install --frozen-lockfile
- name: 🔧 安装 k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: 🗄️ 初始化数据库
run: |
cd backend
npx prisma migrate deploy
npx prisma db seed
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_load
- name: 🔨 构建后端
run: |
pnpm --filter shared build
pnpm --filter backend build
- name: 🚀 启动后端服务
run: |
cd backend
npm run start &
sleep 10
env:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_load
REDIS_URL: redis://localhost:6379
JWT_SECRET: load-test-jwt-secret
REFRESH_TOKEN_SECRET: load-test-refresh-token
- name: ⏳ 等待服务就绪
run: |
timeout 60 bash -c 'until curl -s http://localhost:3000/health > /dev/null; do sleep 2; done'
- name: 🔥 运行负载测试
run: |
mkdir -p load-test-results
# 创建 k6 负载测试脚本
cat > load-test.js << 'EOF'
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');
export const options = {
stages: [
{ duration: '30s', target: 10 }, // 预热
{ duration: '1m', target: 50 }, // 逐步增加
{ duration: '2m', target: 50 }, // 稳定负载
{ duration: '30s', target: 0 }, // 降低
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% 请求小于 500ms
errors: ['rate<0.1'], // 错误率小于 10%
},
};
const BASE_URL = 'http://localhost:3000';
export default function () {
// 健康检查
let healthRes = http.get(`${BASE_URL}/health`);
check(healthRes, { 'health check ok': (r) => r.status === 200 });
// 列表接口
let listRes = http.get(`${BASE_URL}/api/leads?page=1&pageSize=20`, {
headers: {
'Authorization': 'Bearer test-token',
'Content-Type': 'application/json',
},
});
errorRate.add(listRes.status !== 200 && listRes.status !== 401);
responseTime.add(listRes.timings.duration);
sleep(0.1);
}
EOF
k6 run --out json=load-test-results/results.json load-test.js
- name: 📊 分析负载测试结果
run: |
echo "## 🔥 负载测试报告" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -f "load-test-results/results.json" ]; then
echo "### 关键指标" >> $GITHUB_STEP_SUMMARY
echo "| 指标 | 值 |" >> $GITHUB_STEP_SUMMARY
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
# 提取关键指标
avg_duration=$(cat load-test-results/results.json | jq -s '[.[] | select(.type=="Point" and .metric=="http_req_duration") | .data.value] | add / length' 2>/dev/null || echo "N/A")
p95_duration=$(cat load-test-results/results.json | jq -s '[.[] | select(.type=="Point" and .metric=="http_req_duration") | .data.value] | sort | .[length * 0.95 | floor]' 2>/dev/null || echo "N/A")
total_requests=$(cat load-test-results/results.json | jq -s '[.[] | select(.type=="Point" and .metric=="http_reqs")] | length' 2>/dev/null || echo "N/A")
echo "| 平均响应时间 | ${avg_duration}ms |" >> $GITHUB_STEP_SUMMARY
echo "| P95 响应时间 | ${p95_duration}ms |" >> $GITHUB_STEP_SUMMARY
echo "| 总请求数 | $total_requests |" >> $GITHUB_STEP_SUMMARY
fi
- name: 📤 上传负载测试结果
uses: actions/upload-artifact@v4
with:
name: load-test-results
path: load-test-results/
retention-days: 30
# ==================== 压力测试 ====================
stress-test:
name: 💥 压力测试
runs-on: ubuntu-latest
needs: setup
if: needs.setup.outputs.test_type == 'stress' || needs.setup.outputs.test_type == 'all'
timeout-minutes: 60
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: juhi_stress
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
steps:
- name: 📥 检出代码
uses: actions/checkout@v4
- name: 📦 安装 pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: 🟢 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: 📦 安装依赖
run: pnpm install --frozen-lockfile
- name: 🔧 安装 k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: 🗄️ 初始化数据库
run: |
cd backend
npx prisma migrate deploy
npx prisma db seed
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_stress
- name: 🔨 构建后端
run: |
pnpm --filter shared build
pnpm --filter backend build
- name: 🚀 启动后端服务
run: |
cd backend
npm run start &
sleep 10
env:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_stress
REDIS_URL: redis://localhost:6379
JWT_SECRET: stress-test-jwt-secret
REFRESH_TOKEN_SECRET: stress-test-refresh-token
- name: ⏳ 等待服务就绪
run: |
timeout 60 bash -c 'until curl -s http://localhost:3000/health > /dev/null; do sleep 2; done'
- name: 💥 运行压力测试
run: |
mkdir -p stress-test-results
# 创建 k6 压力测试脚本
cat > stress-test.js << 'EOF'
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');
const requestCount = new Counter('requests');
export const options = {
stages: [
{ duration: '1m', target: 20 }, // 预热
{ duration: '2m', target: 100 }, // 逐步增加到 100 并发
{ duration: '2m', target: 200 }, // 增加到 200 并发
{ duration: '2m', target: 300 }, // 增加到 300 并发
{ duration: '1m', target: 0 }, // 降低
],
thresholds: {
http_req_duration: ['p(99)<2000'], // 99% 请求小于 2s
errors: ['rate<0.3'], // 错误率小于 30%
},
};
const BASE_URL = 'http://localhost:3000';
export default function () {
requestCount.add(1);
let res = http.get(`${BASE_URL}/health`);
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 1000ms': (r) => r.timings.duration < 1000,
});
errorRate.add(res.status !== 200);
responseTime.add(res.timings.duration);
sleep(0.05);
}
EOF
k6 run --out json=stress-test-results/results.json stress-test.js || true
- name: 📊 分析压力测试结果
run: |
echo "## 💥 压力测试报告" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 测试目标" >> $GITHUB_STEP_SUMMARY
echo "- 最大并发: 300" >> $GITHUB_STEP_SUMMARY
echo "- 持续时间: 8 分钟" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -f "stress-test-results/results.json" ]; then
echo "### 结果分析" >> $GITHUB_STEP_SUMMARY
max_vus=$(cat stress-test-results/results.json | jq -s 'max_by(.data.value | numbers) | .data.value // 0' 2>/dev/null || echo "N/A")
echo "- 最大达到 VUs: $max_vus" >> $GITHUB_STEP_SUMMARY
fi
- name: 📤 上传压力测试结果
uses: actions/upload-artifact@v4
with:
name: stress-test-results
path: stress-test-results/
retention-days: 30
# ==================== 数据库性能测试 ====================
db-performance:
name: 🗄️ 数据库性能测试
runs-on: ubuntu-latest
needs: setup
if: needs.setup.outputs.test_type == 'all'
timeout-minutes: 30
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: juhi_db_perf
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: 📥 检出代码
uses: actions/checkout@v4
- name: 📦 安装 pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: 🟢 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: 📦 安装依赖
run: pnpm install --frozen-lockfile
- name: 🗄️ 初始化数据库
run: |
cd backend
npx prisma migrate deploy
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_db_perf
- name: 🔨 生成测试数据
run: |
cd backend
# 生成大量测试数据用于性能测试
cat > generate-test-data.ts << 'EOF'
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const TENANT_ID = '00000000-0000-0000-0000-000000000001';
async function main() {
console.log('生成测试数据...');
// 创建租户
await prisma.tenants.upsert({
where: { id: TENANT_ID },
create: { id: TENANT_ID, name: 'Performance Test Tenant', code: 'PERF' },
update: {},
});
// 批量创建线索(10000 条)
const leads = [];
for (let i = 0; i < 10000; i++) {
leads.push({
id: `lead-${i.toString().padStart(5, '0')}`,
tenant_id: TENANT_ID,
biz_id: `LEAD-${Date.now()}-${i}`,
company_name: `测试公司 ${i}`,
contact_name: `联系人 ${i}`,
contact_phone: `138${i.toString().padStart(8, '0')}`,
status: ['new', 'contacted', 'qualified'][i % 3],
source: ['website', 'referral', 'advertisement'][i % 3],
created_at: new Date(),
updated_at: new Date(),
});
}
await prisma.leads.createMany({
data: leads,
skipDuplicates: true,
});
console.log('已创建 10000 条线索');
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
EOF
npx tsx generate-test-data.ts
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_db_perf
- name: 🧪 运行数据库性能测试
run: |
cd backend
cat > db-perf-test.ts << 'EOF'
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
log: ['query'],
});
const TENANT_ID = '00000000-0000-0000-0000-000000000001';
interface PerfResult {
name: string;
avgMs: number;
minMs: number;
maxMs: number;
iterations: number;
}
async function benchmark(name: string, fn: () => Promise<any>, iterations = 100): Promise<PerfResult> {
const times: number[] = [];
// 预热
for (let i = 0; i < 5; i++) {
await fn();
}
// 正式测试
for (let i = 0; i < iterations; i++) {
const start = performance.now();
await fn();
times.push(performance.now() - start);
}
return {
name,
avgMs: times.reduce((a, b) => a + b, 0) / times.length,
minMs: Math.min(...times),
maxMs: Math.max(...times),
iterations,
};
}
async function main() {
const results: PerfResult[] = [];
// 测试 1: 简单查询
results.push(await benchmark('简单查询 (findMany)', async () => {
await prisma.leads.findMany({
where: { tenant_id: TENANT_ID },
take: 20,
});
}));
// 测试 2: 带筛选查询
results.push(await benchmark('带筛选查询', async () => {
await prisma.leads.findMany({
where: {
tenant_id: TENANT_ID,
status: 'new',
},
take: 20,
});
}));
// 测试 3: 计数查询
results.push(await benchmark('计数查询', async () => {
await prisma.leads.count({
where: { tenant_id: TENANT_ID },
});
}));
// 测试 4: 分页查询
results.push(await benchmark('分页查询 (第50页)', async () => {
await prisma.leads.findMany({
where: { tenant_id: TENANT_ID },
skip: 1000,
take: 20,
});
}));
// 输出结果
console.log('\n📊 数据库性能测试结果\n');
console.log('| 测试项 | 平均耗时 | 最小耗时 | 最大耗时 | 迭代次数 |');
console.log('|--------|----------|----------|----------|----------|');
for (const r of results) {
console.log(`| ${r.name} | ${r.avgMs.toFixed(2)}ms | ${r.minMs.toFixed(2)}ms | ${r.maxMs.toFixed(2)}ms | ${r.iterations} |`);
}
// 写入 JSON 结果
const fs = await import('fs');
fs.writeFileSync('db-perf-results.json', JSON.stringify(results, null, 2));
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
EOF
npx tsx db-perf-test.ts | tee db-perf-output.txt
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_db_perf
- name: 📝 生成数据库性能报告
run: |
echo "## 🗄️ 数据库性能测试报告" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -f "backend/db-perf-output.txt" ]; then
cat backend/db-perf-output.txt >> $GITHUB_STEP_SUMMARY
fi
- name: 📤 上传数据库性能结果
uses: actions/upload-artifact@v4
with:
name: db-performance-results
path: |
backend/db-perf-results.json
backend/db-perf-output.txt
retention-days: 30
# ==================== 内存泄漏检测 ====================
memory-leak-detection:
name: 🧠 内存泄漏检测
runs-on: ubuntu-latest
needs: setup
if: needs.setup.outputs.test_type == 'all'
timeout-minutes: 30
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: juhi_mem
ports:
- 5432:5432
redis:
image: redis:7
ports:
- 6379:6379
steps:
- name: 📥 检出代码
uses: actions/checkout@v4
- name: 📦 安装 pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: 🟢 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: 📦 安装依赖
run: pnpm install --frozen-lockfile
- name: 🗄️ 初始化数据库
run: |
cd backend
npx prisma migrate deploy
npx prisma db seed
env:
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_mem
- name: 🔨 构建后端
run: |
pnpm --filter shared build
pnpm --filter backend build
- name: 🧠 运行内存泄漏检测
run: |
cd backend
# 使用 --expose-gc 启动服务并检测内存
node --expose-gc -e "
const http = require('http');
async function measureMemory() {
if (global.gc) global.gc();
const used = process.memoryUsage();
return {
heapUsed: Math.round(used.heapUsed / 1024 / 1024),
heapTotal: Math.round(used.heapTotal / 1024 / 1024),
rss: Math.round(used.rss / 1024 / 1024),
};
}
async function main() {
console.log('🧠 内存泄漏检测开始');
const before = await measureMemory();
console.log('初始内存:', before);
// 模拟 1000 次请求
for (let i = 0; i < 1000; i++) {
// 模拟内存分配
const arr = new Array(10000).fill(Math.random());
if (i % 100 === 0) {
if (global.gc) global.gc();
console.log('进度:', i);
}
}
if (global.gc) global.gc();
await new Promise(r => setTimeout(r, 1000));
if (global.gc) global.gc();
const after = await measureMemory();
console.log('最终内存:', after);
const diff = after.heapUsed - before.heapUsed;
console.log('内存增长:', diff, 'MB');
if (diff > 50) {
console.log('⚠️ 警告: 可能存在内存泄漏');
process.exit(1);
} else {
console.log('✅ 内存使用正常');
}
}
main().catch(err => {
console.error(err);
process.exit(1);
});
" | tee memory-leak-output.txt
env:
NODE_ENV: test
DATABASE_URL: postgresql://test:test@localhost:5432/juhi_mem
REDIS_URL: redis://localhost:6379
- name: 📝 生成内存检测报告
run: |
echo "## 🧠 内存泄漏检测报告" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -f "backend/memory-leak-output.txt" ]; then
echo '```' >> $GITHUB_STEP_SUMMARY
cat backend/memory-leak-output.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
# ==================== 性能测试汇总 ====================
performance-summary:
name: 📋 性能测试汇总
runs-on: ubuntu-latest
needs: [benchmark, load-test, stress-test, db-performance, memory-leak-detection]
if: always()
steps:
- name: 📥 下载所有结果
uses: actions/download-artifact@v4
with:
path: all-results
continue-on-error: true
- name: 📝 生成汇总报告
run: |
echo "## 📊 性能测试汇总报告" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "测试时间: $(date)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| 测试类型 | 状态 |" >> $GITHUB_STEP_SUMMARY
echo "|----------|------|" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.benchmark.result }}" == "success" ]; then
echo "| 📊 基准测试 | ✅ 通过 |" >> $GITHUB_STEP_SUMMARY
elif [ "${{ needs.benchmark.result }}" == "skipped" ]; then
echo "| 📊 基准测试 | ⏭️ 跳过 |" >> $GITHUB_STEP_SUMMARY
else
echo "| 📊 基准测试 | ❌ 失败 |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.load-test.result }}" == "success" ]; then
echo "| 🔥 负载测试 | ✅ 通过 |" >> $GITHUB_STEP_SUMMARY
elif [ "${{ needs.load-test.result }}" == "skipped" ]; then
echo "| 🔥 负载测试 | ⏭️ 跳过 |" >> $GITHUB_STEP_SUMMARY
else
echo "| 🔥 负载测试 | ❌ 失败 |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.stress-test.result }}" == "success" ]; then
echo "| 💥 压力测试 | ✅ 通过 |" >> $GITHUB_STEP_SUMMARY
elif [ "${{ needs.stress-test.result }}" == "skipped" ]; then
echo "| 💥 压力测试 | ⏭️ 跳过 |" >> $GITHUB_STEP_SUMMARY
else
echo "| 💥 压力测试 | ❌ 失败 |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.db-performance.result }}" == "success" ]; then
echo "| 🗄️ 数据库性能 | ✅ 通过 |" >> $GITHUB_STEP_SUMMARY
elif [ "${{ needs.db-performance.result }}" == "skipped" ]; then
echo "| 🗄️ 数据库性能 | ⏭️ 跳过 |" >> $GITHUB_STEP_SUMMARY
else
echo "| 🗄️ 数据库性能 | ❌ 失败 |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.memory-leak-detection.result }}" == "success" ]; then
echo "| 🧠 内存泄漏检测 | ✅ 通过 |" >> $GITHUB_STEP_SUMMARY
elif [ "${{ needs.memory-leak-detection.result }}" == "skipped" ]; then
echo "| 🧠 内存泄漏检测 | ⏭️ 跳过 |" >> $GITHUB_STEP_SUMMARY
else
echo "| 🧠 内存泄漏检测 | ❌ 失败 |" >> $GITHUB_STEP_SUMMARY
fi
... |