基于 OpenClaw + Claude Code 的端到端研发自动化系统任务拆解
从需求 → PRD 设计 → 技术方案设计 → API 接口开发 → AI Coding → 测试 → CI/CD 自动部署 → UI 自动化验收
在 2025-2026 年 AI Agent 技术迅猛发展的背景下,OpenClaw 作为一个本地优先(Local-First)的 AI 助手框架,与 Claude Code 智能编程助手深度整合,构建了端到端的研发自动化系统。本系统实现了从需求分析到最终部署验收的全流程自动化,支持多角色 Agents 协同工作,并在关键节点提供人机协同能力。
本系统定义了 12 个核心研发角色 Agents,每个 Agent 都有明确的职责边界和协作接口,形成完整的研发价值链。
系统支持多种需求输入方式:
# 产品需求文档 (PRD) ## 1. 文档信息 - **产品名称**: [产品名称] - **版本号**: v1.0 - **创建日期**: 2026-03-13 - **最后更新**: 2026-03-13 - **负责人**: [产品经理姓名] ## 2. 产品概述 ### 2.1 背景与目标 [描述产品产生的业务背景、要解决的核心问题、预期达成的业务目标] ### 2.2 目标用户 - 主要用户群体:[用户画像描述] - 使用场景:[典型使用场景] ### 2.3 核心价值主张 [产品为用户提供的独特价值] ## 3. 功能需求 ### 3.1 功能清单 | 功能模块 | 功能点 | 优先级 | 复杂度 | 依赖关系 | |---------|--------|--------|--------|----------| | 用户管理 | 注册登录 | P0 | 中 | 无 | | 订单管理 | 创建订单 | P0 | 高 | 用户管理 | ### 3.2 详细功能说明 #### 3.2.1 [功能名称] - **用户故事**: As a [角色], I want [功能], So that [价值] - **前置条件**: [使用该功能前需满足的条件] - **基本流程**: 1. 步骤一 2. 步骤二 3. ... - **后置条件**: [操作完成后的系统状态] - **异常流程**: [可能的异常情况与处理方式] ## 4. 非功能性需求 ### 4.1 性能要求 - 响应时间:≤ 500ms (P95) - 并发用户数:≥ 10,000 - 吞吐量:≥ 1000 TPS ### 4.2 可用性要求 - 系统可用性:≥ 99.9% - 容灾能力:支持跨区域容灾 ### 4.3 安全性要求 - 数据加密:传输层 TLS 1.3,存储层 AES-256 - 认证方式:OAuth 2.0 + JWT ## 5. 界面原型 [嵌入 Figma/MasterGo 原型链接或截图] ## 6. 验收标准 ### 6.1 功能验收清单 - [ ] 功能点 1 可正常使用 - [ ] 功能点 2 符合预期 - ... ### 6.2 性能验收指标 - [ ] 压力测试通过 - [ ] 负载测试达标 - ... ## 7. 附录 ### 7.1 术语表 [专业术语解释] ### 7.2 参考资料 [相关文档链接]
# 技术方案设计文档
## 1. 架构概览
### 1.1 系统架构图
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │────▶│ API Gateway │────▶│ Services │
│ (Web/Mobile)│ │ (Kong) │ │ (Microserv.)│
└─────────────┘ └──────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ Database │
│ (MySQL+Redis)│
└─────────────┘
```
### 1.2 技术栈选型
| 层级 | 技术选型 | 版本 | 选型理由 |
|------|---------|------|----------|
| 前端框架 | React | 18.2 | 生态丰富、性能优秀 |
| 后端框架 | Spring Boot | 3.2 | 企业级支持、成熟稳定 |
| 数据库 | PostgreSQL | 15 | ACID 兼容、JSON 支持 |
| 缓存 | Redis | 7.2 | 高性能、数据结构丰富 |
## 2. 数据库设计
### 2.1 ER 图
[嵌入 ER 图]
### 2.2 核心表结构
```sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
```
## 3. 接口设计原则
- RESTful 风格
- 版本控制:/api/v1/...
- 统一响应格式
- 标准化错误码
## 4. 安全设计
### 4.1 认证授权
- JWT Token 认证
- RBAC 权限模型
- OAuth 2.0 第三方登录
### 4.2 数据安全
- 敏感数据加密存储
- HTTPS 强制启用
- SQL 注入防护
## 5. 性能优化策略
### 5.1 缓存策略
- 多级缓存:CDN → Nginx → Redis → 本地缓存
- 缓存穿透/击穿/雪崩解决方案
### 5.2 数据库优化
- 读写分离
- 分库分表策略
- 慢查询监控
## 6. 部署架构
### 6.1 Kubernetes 资源配置
- Deployment 副本数:3
- HPA 配置:CPU 利用率 > 70% 自动扩容
- 资源限制:CPU 2C, Memory 4Gi
## 7. 风险评估与应对
| 风险项 | 可能性 | 影响程度 | 应对措施 |
|--------|--------|----------|----------|
| 数据库单点故障 | 低 | 高 | 主从复制 + 自动切换 |
| 缓存雪崩 | 中 | 中 | 随机过期时间 + 多级缓存 |
openapi: 3.0.3
info:
title: 用户管理系统 API
description: 提供用户注册、登录、信息管理等功能
version: 1.0.0
contact:
name: API Support
email: api-support@example.com
servers:
- url: https://api.example.com/v1
description: 生产环境
- url: https://staging-api.example.com/v1
description: 预发布环境
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
User:
type: object
required:
- username
- email
- password
properties:
id:
type: integer
format: int64
example: 1
username:
type: string
minLength: 3
maxLength: 50
example: "john_doe"
email:
type: string
format: email
example: "john@example.com"
role:
type: string
enum: [USER, ADMIN]
default: USER
createdAt:
type: string
format: date-time
Error:
type: object
properties:
code:
type: string
example: "USER_NOT_FOUND"
message:
type: string
example: "用户不存在"
details:
type: object
paths:
/users:
post:
summary: 创建新用户
tags: [Users]
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/User'
responses:
'201':
description: 用户创建成功
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: 请求参数错误
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
get:
summary: 获取用户列表
tags: [Users]
security:
- bearerAuth: []
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
'200':
description: 成功返回用户列表
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
/users/{id}:
get:
summary: 获取指定用户详情
tags: [Users]
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: 成功返回用户信息
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: 用户不存在
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# 使用 Prism Mock Server
# 安装:npm install -g @stoplight/prism-cli
# 启动 Mock 服务
prism mock openapi.yaml --port 4010
# 前端开发时可配置代理
# vite.config.ts
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:4010',
changeOrigin: true
}
}
}
}
# 初始化项目 $ claude "基于 Spring Boot 3.2 创建一个用户管理微服务项目" ✨ 正在分析需求... 📁 创建项目结构... ✅ 项目初始化完成! # 生成实体类 $ claude "创建 User 实体类,包含 id、username、email、passwordHash、role、createdAt 字段" ✨ 生成实体代码... ✅ src/main/java/com/example/user/entity/User.java 已创建! # 生成 Repository 层 $ claude "为 User 实体创建 Spring Data JPA Repository" ✨ 生成 Repository 代码... ✅ src/main/java/com/example/user/repository/UserRepository.java 已创建! # 生成 Service 层 $ claude "创建 UserService,实现用户注册、登录、查询功能" ✨ 生成 Service 代码... ✅ src/main/java/com/example/user/service/UserService.java 已创建! # 生成 Controller 层 $ claude "创建 UserController,实现 RESTful API 端点" ✨ 生成 Controller 代码... ✅ src/main/java/com/example/user/controller/UserController.java 已创建! # 生成单元测试 $ claude "为 UserService 编写单元测试,覆盖正常流程和异常场景" ✨ 生成测试代码... ✅ src/test/java/com/example/user/service/UserServiceTest.java 已创建! # 运行测试 $ claude "运行所有单元测试并生成覆盖率报告" 🧪 执行测试中... ✅ 测试通过率:100% | 代码覆盖率:87% # 代码重构 $ claude "重构 UserService,应用策略模式支持多种登录方式" ♻️ 重构代码中... ✅ 重构完成!代码复杂度降低 35% # Git 操作 $ claude "提交当前更改并创建 feature/user-management 分支" 📦 创建分支 feature/user-management... 📝 生成 commit message... ✅ 提交成功!commit: feat(user): 实现用户管理核心功能
# 创建 React 组件
$ claude "创建一个用户登录表单组件,包含邮箱和密码输入,使用 Ant Design"
✨ 分析需求...
📝 生成组件代码...
✅ src/components/LoginForm.tsx 已创建!
```tsx
import React, { useState } from 'react';
import { Form, Input, Button, message } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
interface LoginFormProps {
onSubmit?: (values: LoginValues) => void;
}
interface LoginValues {
email: string;
password: string;
}
export default function LoginForm({ onSubmit }: LoginFormProps) {
const [loading, setLoading] = useState(false);
const onFinish = async (values: LoginValues) => {
setLoading(true);
try {
await onSubmit?.(values);
message.success('登录成功');
} catch (error) {
message.error('登录失败');
} finally {
setLoading(false);
}
};
return (
<Form name="login" onFinish={onFinish}>
<Form.Item
name="email"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '邮箱格式不正确' }
]}>
<Input prefix={<UserOutlined />} placeholder="邮箱" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>
登录
</Button>
</Form.Item>
</Form>
);
}
```
package com.example.user.service;
import com.example.user.entity.User;
import com.example.user.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserService userService;
private User testUser;
@BeforeEach
void setUp() {
testUser = new User();
testUser.setId(1L);
testUser.setUsername("test_user");
testUser.setEmail("test@example.com");
testUser.setPasswordHash("encoded_password");
}
@Test
void testRegisterUser_Success() {
// Arrange
when(passwordEncoder.encode(any())).thenReturn("encoded_password");
when(userRepository.save(any(User.class))).thenReturn(testUser);
// Act
User result = userService.registerUser(
"test_user",
"test@example.com",
"raw_password"
);
// Assert
assertNotNull(result);
assertEquals("test_user", result.getUsername());
verify(userRepository, times(1)).save(any(User.class));
}
@Test
void testRegisterUser_EmailAlreadyExists() {
// Arrange
when(userRepository.findByEmail("test@example.com"))
.thenReturn(Optional.of(testUser));
// Act & Assert
assertThrows(
IllegalArgumentException.class,
() -> userService.registerUser(
"new_user",
"test@example.com",
"password"
)
);
}
@Test
void testLoginUser_Success() {
// Arrange
when(userRepository.findByEmail("test@example.com"))
.thenReturn(Optional.of(testUser));
when(passwordEncoder.matches("raw_password", "encoded_password"))
.thenReturn(true);
// Act
User result = userService.loginUser("test@example.com", "raw_password");
// Assert
assertNotNull(result);
assertEquals("test_user", result.getUsername());
}
@Test
void testLoginUser_InvalidPassword() {
// Arrange
when(userRepository.findByEmail("test@example.com"))
.thenReturn(Optional.of(testUser));
when(passwordEncoder.matches("wrong_password", "encoded_password"))
.thenReturn(false);
// Act & Assert
assertThrows(
AuthenticationException.class,
() -> userService.loginUser("test@example.com", "wrong_password")
);
}
}
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
describe('LoginForm', () => {
const mockOnSubmit = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('renders form elements correctly', () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
expect(screen.getByPlaceholderText('邮箱')).toBeInTheDocument();
expect(screen.getByPlaceholderText('密码')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '登录' })).toBeInTheDocument();
});
it('shows validation error for invalid email', async () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
const emailInput = screen.getByPlaceholderText('邮箱');
const submitButton = screen.getByRole('button', { name: '登录' });
fireEvent.change(emailInput, { target: { value: 'invalid-email' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText('邮箱格式不正确')).toBeInTheDocument();
});
});
it('submits form with valid data', async () => {
const user = userEvent.setup();
mockOnSubmit.mockResolvedValue(undefined);
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByPlaceholderText('邮箱'), 'test@example.com');
await user.type(screen.getByPlaceholderText('密码'), 'password123');
await user.click(screen.getByRole('button', { name: '登录' }));
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
expect(await screen.findByText('登录成功')).toBeInTheDocument();
});
it('handles submission error gracefully', async () => {
const user = userEvent.setup();
mockOnSubmit.mockRejectedValue(new Error('Login failed'));
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByPlaceholderText('邮箱'), 'test@example.com');
await user.type(screen.getByPlaceholderText('密码'), 'wrongpass');
await user.click(screen.getByRole('button', { name: '登录' }));
await waitFor(() => {
expect(screen.getByText('登录失败')).toBeInTheDocument();
});
});
it('disables submit button while loading', async () => {
const user = userEvent.setup();
mockOnSubmit.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1000)));
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByPlaceholderText('邮箱'), 'test@example.com');
await user.type(screen.getByPlaceholderText('密码'), 'password123');
await user.click(screen.getByRole('button', { name: '登录' }));
expect(screen.getByRole('button', { name: '登录' })).toBeDisabled();
});
});
@SpringBootTest
@Testcontainers
@AutoConfigureMockMvc
class UserIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
@Container
static RedisContainer redis = new RedisContainer("redis:7-alpine");
@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", redis::getMappedPort);
}
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void testUserRegistrationFlow() throws Exception {
// 1. 注册用户
String registrationJson = """
{
"username": "integration_test",
"email": "test@example.com",
"password": "SecurePass123!"
}
""";
MvcResult registerResult = mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(registrationJson))
.andExpect(status().isCreated())
.andReturn();
User registeredUser = objectMapper.readValue(
registerResult.getResponse().getContentAsString(),
User.class
);
assertNotNull(registeredUser.getId());
// 2. 用户登录
String loginJson = """
{
"email": "test@example.com",
"password": "SecurePass123!"
}
""";
MvcResult loginResult = mockMvc.perform(post("/api/v1/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(loginJson))
.andExpect(status().isOk())
.andReturn();
String token = JsonPath.read(loginResult.getResponse().getContentAsString(), "$.token");
assertNotNull(token);
// 3. 使用 Token 访问受保护资源
mockMvc.perform(get("/api/v1/users/" + registeredUser.getId())
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("integration_test"));
}
}
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.9-eclipse-temurin-17
command:
- cat
tty: true
resources:
requests:
memory: 2Gi
cpu: 1000m
limits:
memory: 4Gi
cpu: 2000m
- name: docker
image: docker:24-dind
securityContext:
privileged: true
resources:
requests:
memory: 1Gi
cpu: 500m
volumes:
- name: maven-cache
persistentVolumeClaim:
claimName: maven-cache-pvc
- name: docker-socket
hostPath:
path: /var/run/docker.sock
'''
}
}
environment {
REGISTRY = 'registry.example.com'
IMAGE_NAME = 'user-service'
KUBECONFIG = credentials('kubeconfig')
SONAR_HOST_URL = 'https://sonarqube.example.com'
SONAR_TOKEN = credentials('sonar-token')
}
triggers {
pollSCM('*/5 * * * *')
upstream(upstreamProjects: 'common-lib', threshold: hudson.model.Result.SUCCESS)
}
options {
timeout(time: 60, unit: 'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '30'))
timestamps()
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
env.BUILD_VERSION = "${env.BUILD_NUMBER}-${env.GIT_COMMIT_SHORT}"
}
}
}
stage('Code Quality') {
parallel {
stage('SonarQube Analysis') {
steps {
container('maven') {
sh '''
mvn sonar:sonar \
-Dsonar.projectKey=user-service \
-Dsonar.host.url=${SONAR_HOST_URL} \
-Dsonar.login=${SONAR_TOKEN} \
-Dsonar.qualitygate.wait=true
'''
}
}
}
stage('Security Scan') {
steps {
container('maven') {
sh '''
mvn org.owasp:dependency-check-maven:check \
-Dformat=HTML \
-DfailBuildOnCVSS=7
'''
}
}
}
}
}
stage('Build & Test') {
steps {
container('maven') {
sh '''
mvn clean package \
-DskipTests=false \
-Dmaven.test.failure.ignore=false
'''
}
}
post {
always {
junit '**/target/surefire-reports/*.xml'
publishCoverage adapters: [jacocoAdapter()], sourceFileResolver: sourceFiles('NEVER_STORE')
}
}
}
stage('Build Docker Image') {
steps {
container('docker') {
sh '''
docker build \
-t ${REGISTRY}/${IMAGE_NAME}:${BUILD_VERSION} \
-t ${REGISTRY}/${IMAGE_NAME}:latest \
--build-arg VERSION=${BUILD_VERSION} \
.
'''
}
}
}
stage('Push Image') {
steps {
container('docker') {
withCredentials([usernamePassword(credentialsId: 'docker-registry', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh '''
echo ${DOCKER_PASS} | docker login ${REGISTRY} -u ${DOCKER_USER} --password-stdin
docker push ${REGISTRY}/${IMAGE_NAME}:${BUILD_VERSION}
docker push ${REGISTRY}/${IMAGE_NAME}:latest
'''
}
}
}
}
stage('Deploy to K8S') {
when {
branch 'main'
}
steps {
container('maven') {
sh '''
envsubst < k8s/deployment.yaml | kubectl apply -f -
envsubst < k8s/service.yaml | kubectl apply -f -
kubectl rollout status deployment/user-service -n production --timeout=300s
'''
}
}
}
stage('Smoke Test') {
when {
branch 'main'
}
steps {
container('maven') {
sh '''
curl -f https://user-service.example.com/health || exit 1
curl -f https://user-service.example.com/api/v1/users/1 || exit 1
'''
}
}
}
stage('UI Automation Test') {
when {
branch 'main'
}
steps {
container('maven') {
sh '''
cd ui-tests
npm install
npm run test:e2e -- --baseUrl=https://user-service.example.com
'''
}
}
post {
always {
archiveArtifacts artifacts: 'ui-tests/playwright-report/**/*', allowEmptyArchive: true
}
}
}
}
post {
always {
cleanWs()
script {
if (currentBuild.result == 'SUCCESS') {
slackSend(color: 'good', message: "✅ 构建成功:${env.JOB_NAME} #${env.BUILD_NUMBER}")
} else if (currentBuild.result == 'FAILURE') {
slackSend(color: 'danger', message: "❌ 构建失败:${env.JOB_NAME} #${env.BUILD_NUMBER}")
}
}
}
failure {
emailext(
subject: "构建失败:${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: """请查看构建日志:${env.BUILD_URL}console""",
to: 'dev-team@example.com'
)
}
}
}
# 构建阶段 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app # 利用 Docker 层缓存优化依赖下载 COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests -B # 运行阶段 FROM eclipse-temurin:17-jre-alpine # 创建非 root 用户 RUN addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app # 从构建阶段复制产物 COPY --from=builder /app/target/*.jar app.jar # 设置 JVM 参数 ENV JAVA_OPTS="-Xms512m -Xmx1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200" USER appuser EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ CMD wget -qO- http://localhost:8080/actuator/health || exit 1 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: production
labels:
app: user-service
version: ${BUILD_VERSION}
spec:
replicas: 3
selector:
matchLabels:
app: user-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: user-service
version: ${BUILD_VERSION}
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: user-service
topologyKey: kubernetes.io/hostname
containers:
- name: user-service
image: registry.example.com/user-service:${BUILD_VERSION}
imagePullPolicy: Always
ports:
- containerPort: 8080
name: http
env:
- name: SPRING_PROFILES_ACTIVE
value: "production"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: db-host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
failureThreshold: 3
volumeMounts:
- name: logs
mountPath: /app/logs
volumes:
- name: logs
emptyDir: {}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: user-service-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: user-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 60
| 特性 | Playwright | Selenium |
|---|---|---|
| 浏览器支持 | Chromium, Firefox, WebKit | Chrome, Firefox, Safari, Edge, IE |
| 执行速度 | 快(直接浏览器协议) | 较慢(WebDriver 协议) |
| 自动等待 | ✅ 内置智能等待 | ❌ 需手动实现 |
| 网络拦截 | ✅ 原生支持 | ❌ 需额外工具 |
| 移动端测试 | ✅ 设备模拟 | ✅ 需真实设备/Appium |
| 学习曲线 | 平缓 | 陡峭 |
import { test, expect } from '@playwright/test';
test.describe('用户登录流程', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('成功登录并跳转 dashboard', async ({ page }) => {
// 填写登录表单
await page.fill('input[placeholder="邮箱"]', 'test@example.com');
await page.fill('input[placeholder="密码"]', 'SecurePass123!');
// 点击登录按钮
await page.click('button[type="submit"]');
// 等待跳转并验证 URL
await expect(page).toHaveURL('/dashboard');
// 验证欢迎信息
await expect(page.locator('text=欢迎回来')).toBeVisible();
});
test('显示邮箱格式错误提示', async ({ page }) => {
await page.fill('input[placeholder="邮箱"]', 'invalid-email');
await page.fill('input[placeholder="密码"]', 'password123');
await page.click('button[type="submit"]');
await expect(page.locator('text=邮箱格式不正确')).toBeVisible();
});
test('显示密码错误提示', async ({ page }) => {
await page.fill('input[placeholder="邮箱"]', 'test@example.com');
await page.fill('input[placeholder="密码"]', 'wrong-password');
await page.click('button[type="submit"]');
await expect(page.locator('text=登录失败')).toBeVisible();
});
test('记住登录状态', async ({ context }) => {
const page = await context.newPage();
await page.goto('/login');
await page.fill('input[placeholder="邮箱"]', 'test@example.com');
await page.fill('input[placeholder="密码"]', 'SecurePass123!');
await page.check('input[type="checkbox"]'); // 记住我
await page.click('button[type="submit"]');
// 关闭页面重新打开
await page.close();
const newPage = await context.newPage();
await newPage.goto('/');
// 验证仍然保持登录状态
await expect(newPage).toHaveURL('/dashboard');
});
test('移动端适配测试', async ({ browser }) => {
const mobilePage = await browser.newPage({
viewport: { width: 375, height: 667 } // iPhone SE
});
await mobilePage.goto('/login');
// 验证移动端布局
await expect(mobilePage.locator('.login-form')).toBeVisible();
await expect(mobilePage.locator('input[placeholder="邮箱"]')).toBeInViewport();
await mobilePage.close();
});
test('无障碍访问测试', async ({ page }) => {
// 验证所有表单字段都有 label
const inputs = page.locator('input');
const count = await inputs.count();
for (let i = 0; i < count; i++) {
const input = inputs.nth(i);
const ariaLabel = await input.getAttribute('aria-label');
const id = await input.getAttribute('id');
expect(ariaLabel || id).toBeTruthy();
}
// 验证键盘导航
await page.keyboard.press('Tab');
await expect(page.locator(':focus')).toBeVisible();
});
});
// 视觉回归测试
test('登录页面视觉回归', async ({ page }) => {
await page.goto('/login');
// 截图并与基准对比
await expect(page).toHaveScreenshot('login-page.png', {
maxDiffPixels: 100, // 允许的最大差异像素数
});
});
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { outputFolder: 'playwright-report' }],
['junit', { outputFile: 'results.xml' }],
['json', { outputFile: 'results.json' }]
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10000,
navigationTimeout: 30000,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
timeout: 120 * 1000,
},
});
# UI 自动化验收测试报告 ## 测试概览 - **测试执行时间**: 2026-03-13 10:30:00 - **总测试用例数**: 156 - **通过**: 152 (97.4%) - **失败**: 3 (1.9%) - **跳过**: 1 (0.6%) ## 浏览器兼容性 | 浏览器 | 通过 | 失败 | 状态 | |--------|------|------|------| | Chrome 122 | 156 | 0 | ✅ | | Firefox 123 | 154 | 2 | ⚠️ | | Safari 17 | 153 | 3 | ⚠️ | | Mobile Chrome | 155 | 1 | ✅ | | Mobile Safari | 154 | 2 | ⚠️ | ## 失败用例详情 ### 用例 1: 用户登录流程 - Safari 浏览器兼容性问题 - **错误信息**: TimeoutError: 元素未在 30s 内可见 - **影响范围**: Safari 17 及以上版本 - **优先级**: 高 - **建议修复**: 检查 Safari 特定的 CSS 兼容性问题 ### 用例 2: 视觉回归测试 - 移动端布局偏移 - **差异像素数**: 256px (阈值:100px) - **影响范围**: iPhone 14 Pro Max - **优先级**: 中 - **建议修复**: 调整响应式断点 ## 性能指标 - **平均页面加载时间**: 1.2s - **首次内容绘制 (FCP)**: 0.8s - **最大内容绘制 (LCP)**: 1.5s - **累积布局偏移 (CLS)**: 0.05 ## 无障碍合规性 - **WCAG 2.1 AA 级别**: ✅ 通过 - **颜色对比度**: ✅ 全部达标 - **键盘导航**: ✅ 功能完整 - **屏幕阅读器兼容性**: ✅ 测试通过 ## 结论与建议 ✅ **验收通过** - 系统满足上线标准,建议修复 3 个中低优先级问题后发布。
# AI 生成的 Pull Request 描述模板 ## 📋 PR 概述 **需求来源**: PRD-2026-03-USER-MGMT **关联 Issue**: #1234, #1235 **变更类型**: ✨ 新功能 ## 🔍 AI 自检报告 - [x] 代码符合团队规范 - [x] 单元测试覆盖率 ≥ 85% - [x] 无 SonarQube 严重问题 - [x] 安全扫描通过 - [x] 性能测试达标 ## 📝 变更详情 ### 新增功能 1. 用户注册 API(POST /api/v1/users) 2. 用户登录 API(POST /api/v1/auth/login) 3. 用户信息查询 API(GET /api/v1/users/:id) ### 技术实现 - 使用 Spring Security 实现 JWT 认证 - 密码采用 BCrypt 加密存储 - 集成 Redis 实现登录态缓存 ## 🧪 测试覆盖 ``` =============================== Coverage Summary =============================== Lines: 87.3% (234/268) Branches: 82.1% (87/106) Methods: 91.2% (62/68) ``` ## ⚠️ 需要人工审查的重点 1. **安全相关**: JWT Token 生成与验证逻辑(第 45-78 行) 2. **性能关键**: 用户查询缓存策略(第 112-145 行) 3. **业务复杂**: 密码强度校验规则(第 201-230 行) ## 📸 截图/录屏 [如有 UI 变更,附上截图] ## 🚀 部署注意事项 - 需要配置新的环境变量:JWT_SECRET - 数据库迁移脚本已准备:V20260313__add_user_table.sql - 建议灰度发布:先 10% 流量验证 --- *此 PR 由 AI Coding Agent 生成,经开发者 @张三 初审通过*
| 场景 | 触发条件 | 干预方式 |
|---|---|---|
| 需求模糊 | AI 置信度 < 70% | 产品经理补充说明 |
| 技术选型争议 | 多个方案评分接近 | 架构师决策 |
| 安全敏感代码 | 涉及认证、支付、隐私 | 安全专家审查 |
| 性能瓶颈 | 响应时间 > 阈值 200% | 性能工程师优化 |
| 测试失败 | 连续 3 次修复仍失败 | 开发人员介入 |
| 质量不达标 | 覆盖率 < 80% 或 Bug 数 > 阈值 | QA 经理判定是否放行 |
某头部电商平台需要重构用户中心系统,支持 5000 万日活用户的登录、个人信息管理、订单查询等功能。传统开发模式预计需要 3 个月,采用本系统后仅用 5 周完成。