## 🏗️ Architecture & Design - Add ADR collection (15 Architecture Decision Records) - Comprehensive system design documentation - Implementation roadmap and project planning ## 📋 Requirements & Documentation - Detailed functional requirements - API specifications (OpenAPI & AsyncAPI) - Postman collections and optimization reports ## 🗄️ Database & Configuration - Database migration scripts (CDC, core schema) - Resilience4j configuration for resilience patterns - Alert manager and Prometheus monitoring rules ## 🎯 Project Templates - Admin frontend template (Arco Pro + Vue 3) - Big screen data visualization template (ECharts) - Enterprise backend template (Jeecg Boot + Spring Boot) ## 🧪 Testing & Quality - K6 performance testing scenarios - Load testing benchmark scripts - Quality assurance framework setup ## 📦 Infrastructure & Deployment - Complete infrastructure configurations - Deployment strategies and CI/CD pipelines - Monitoring and observability setup This commit establishes the complete foundation for the MVP Sprint 01 development cycle with all necessary architectural decisions, templates, and documentation ready for team collaboration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
开发指南与实施方案
概述
基于系统架构设计和开源技术栈,制定光明科学城科技成果转化平台的开发实施方案,包括开发规范、代码结构、实现指南和最佳实践。
1. 开发环境搭建
1.1 本地开发环境
基础环境要求
# Java开发环境
Java: OpenJDK 17 LTS
Maven: 3.9.0+
IDE: IntelliJ IDEA 2023.3+ / Eclipse 2023-12
# 前端开发环境
Node.js: 18.18.0 LTS
npm: 9.8.0+ / yarn: 1.22.0+
IDE: VS Code / WebStorm
# Python开发环境 (AI服务)
Python: 3.11+
pip: 23.0+
IDE: PyCharm / VS Code
# 容器环境
Docker: 24.0+
Docker Compose: 2.20+
Kubernetes: 1.28+ (可选)
基础设施服务
# docker-compose.yml - 开发环境
version: '3.8'
services:
# 达梦数据库 (开发环境可用 PostgreSQL 替代)
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: gmkx_dev
POSTGRES_USER: gmkx
POSTGRES_PASSWORD: gmkx123
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./sql/init:/docker-entrypoint-initdb.d
# Redis 缓存
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
ports:
- "6379:6379"
volumes:
- redis_data:/data
# RabbitMQ 消息队列
rabbitmq:
image: rabbitmq:3.12-management-alpine
environment:
RABBITMQ_DEFAULT_USER: gmkx
RABBITMQ_DEFAULT_PASS: gmkx123
ports:
- "5672:5672"
- "15672:15672"
volumes:
- rabbitmq_data:/var/lib/rabbitmq
# OpenSearch 搜索引擎
opensearch:
image: opensearchproject/opensearch:2.11.0
environment:
- discovery.type=single-node
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=Admin123!
ports:
- "9200:9200"
- "9600:9600"
volumes:
- opensearch_data:/usr/share/opensearch/data
# Milvus 向量数据库
milvus:
image: milvusdb/milvus:v2.3.2
command: ["milvus", "run", "standalone"]
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
ports:
- "19530:19530"
depends_on:
- etcd
- minio
# MinIO 对象存储
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/data
# Nacos 配置中心
nacos:
image: nacos/nacos-server:v2.3.0
environment:
MODE: standalone
SPRING_DATASOURCE_PLATFORM: mysql
ports:
- "8848:8848"
- "9848:9848"
volumes:
postgres_data:
redis_data:
rabbitmq_data:
opensearch_data:
minio_data:
1.2 开发工具配置
IDE 配置模板
<!-- IntelliJ IDEA - .idea/codeStyles/Project.xml -->
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="java" withSubpackages="true" static="false"/>
<package name="javax" withSubpackages="true" static="false"/>
<emptyLine/>
<package name="org.springframework" withSubpackages="true" static="false"/>
<package name="org" withSubpackages="true" static="false"/>
<package name="com" withSubpackages="true" static="false"/>
<emptyLine/>
<package name="" withSubpackages="true" static="false"/>
<emptyLine/>
<package name="" withSubpackages="true" static="true"/>
</value>
</option>
</JavaCodeStyleSettings>
</code_scheme>
</component>
VS Code 配置
// .vscode/settings.json
{
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.detectIndentation": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"typescript.preferences.importModuleSpecifier": "relative",
"vue.codeActions.enabled": true,
"vue.complete.casing.tags": "kebab",
"vue.complete.casing.props": "camel"
}
// .vscode/extensions.json
{
"recommendations": [
"vue.volar",
"vue.vscode-typescript-vue-plugin",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"ms-python.python",
"ms-vscode.vscode-typescript-next"
]
}
2. 项目结构与规范
2.1 后端项目结构 (Java)
gmkx-platform/
├── gmkx-common/ # 公共模块
│ ├── gmkx-common-core/ # 核心工具类
│ ├── gmkx-common-security/ # 安全组件
│ ├── gmkx-common-cache/ # 缓存组件
│ ├── gmkx-common-database/ # 数据库组件
│ ├── gmkx-common-mq/ # 消息队列组件
│ └── gmkx-common-web/ # Web组件
├── gmkx-gateway/ # API网关
├── gmkx-auth/ # 认证服务
├── gmkx-services/ # 业务服务
│ ├── gmkx-user-service/ # 用户服务
│ ├── gmkx-achievement-service/ # 成果服务
│ ├── gmkx-demand-service/ # 需求服务
│ ├── gmkx-broker-service/ # 经纪服务
│ ├── gmkx-activity-service/ # 活动服务
│ ├── gmkx-file-service/ # 文件服务
│ └── gmkx-notification-service/ # 通知服务
├── gmkx-ai/ # AI服务 (Python)
└── gmkx-deployment/ # 部署配置
├── docker/
├── kubernetes/
└── helm/
单个服务内部结构
gmkx-user-service/
├── src/main/java/com/gmkx/user/
│ ├── UserServiceApplication.java # 启动类
│ ├── config/ # 配置类
│ │ ├── DatabaseConfig.java
│ │ ├── RedisConfig.java
│ │ └── SecurityConfig.java
│ ├── controller/ # 控制器层
│ │ ├── UserController.java
│ │ ├── OrganizationController.java
│ │ └── ExpertController.java
│ ├── service/ # 服务层
│ │ ├── UserService.java
│ │ ├── impl/
│ │ │ └── UserServiceImpl.java
│ │ └── external/ # 外部服务调用
│ │ └── AuthServiceClient.java
│ ├── repository/ # 数据访问层
│ │ ├── UserRepository.java
│ │ └── mapper/
│ │ └── UserMapper.java
│ ├── domain/ # 领域层
│ │ ├── entity/ # 实体类
│ │ │ ├── User.java
│ │ │ └── Organization.java
│ │ ├── aggregate/ # 聚合根
│ │ │ └── UserAggregate.java
│ │ ├── valueobject/ # 值对象
│ │ │ ├── UserId.java
│ │ │ └── ContactInfo.java
│ │ └── event/ # 领域事件
│ │ ├── UserRegisteredEvent.java
│ │ └── UserActivatedEvent.java
│ ├── dto/ # 数据传输对象
│ │ ├── request/
│ │ │ ├── CreateUserRequest.java
│ │ │ └── UpdateUserRequest.java
│ │ └── response/
│ │ ├── UserResponse.java
│ │ └── UserDetailResponse.java
│ ├── infrastructure/ # 基础设施层
│ │ ├── messaging/ # 消息处理
│ │ │ ├── UserEventPublisher.java
│ │ │ └── UserEventListener.java
│ │ ├── cache/ # 缓存实现
│ │ │ └── UserCacheService.java
│ │ └── external/ # 外部集成
│ │ └── EmailServiceImpl.java
│ └── common/ # 通用组件
│ ├── exception/
│ │ └── UserNotFoundException.java
│ ├── enums/
│ │ └── UserStatus.java
│ └── constants/
│ └── UserConstants.java
├── src/main/resources/
│ ├── application.yml # 应用配置
│ ├── application-dev.yml # 开发环境配置
│ ├── application-prod.yml # 生产环境配置
│ ├── bootstrap.yml # 启动配置
│ ├── mapper/ # MyBatis映射文件
│ │ └── UserMapper.xml
│ └── db/migration/ # 数据库迁移
│ ├── V1.0.0__Create_user_tables.sql
│ └── V1.0.1__Add_user_indexes.sql
├── src/test/java/ # 测试代码
└── pom.xml # Maven配置
2.2 前端项目结构 (Vue 3)
gmkx-web/
├── public/ # 静态资源
│ ├── index.html
│ ├── favicon.ico
│ └── assets/
├── src/
│ ├── main.ts # 应用入口
│ ├── App.vue # 根组件
│ ├── components/ # 通用组件
│ │ ├── common/ # 基础组件
│ │ │ ├── GButton.vue
│ │ │ ├── GModal.vue
│ │ │ ├── GTable.vue
│ │ │ └── GForm.vue
│ │ ├── business/ # 业务组件
│ │ │ ├── AchievementCard.vue
│ │ │ ├── DemandCard.vue
│ │ │ └── UserProfile.vue
│ │ └── layout/ # 布局组件
│ │ ├── AppHeader.vue
│ │ ├── AppSidebar.vue
│ │ └── AppFooter.vue
│ ├── views/ # 页面组件
│ │ ├── Home.vue
│ │ ├── achievement/
│ │ │ ├── AchievementList.vue
│ │ │ ├── AchievementDetail.vue
│ │ │ └── AchievementForm.vue
│ │ ├── demand/
│ │ │ ├── DemandList.vue
│ │ │ ├── DemandDetail.vue
│ │ │ └── DemandForm.vue
│ │ └── user/
│ │ ├── UserCenter.vue
│ │ └── UserSettings.vue
│ ├── router/ # 路由配置
│ │ ├── index.ts
│ │ └── modules/
│ │ ├── achievement.ts
│ │ ├── demand.ts
│ │ └── user.ts
│ ├── store/ # 状态管理
│ │ ├── index.ts
│ │ └── modules/
│ │ ├── user.ts
│ │ ├── achievement.ts
│ │ └── app.ts
│ ├── api/ # API接口
│ │ ├── index.ts
│ │ ├── types.ts
│ │ └── modules/
│ │ ├── user.ts
│ │ ├── achievement.ts
│ │ └── demand.ts
│ ├── utils/ # 工具函数
│ │ ├── request.ts # HTTP请求封装
│ │ ├── auth.ts # 认证工具
│ │ ├── storage.ts # 存储工具
│ │ ├── validation.ts # 表单验证
│ │ └── format.ts # 格式化工具
│ ├── composables/ # 组合式函数
│ │ ├── useAuth.ts
│ │ ├── useTable.ts
│ │ ├── useForm.ts
│ │ └── useWebSocket.ts
│ ├── styles/ # 样式文件
│ │ ├── index.scss # 全局样式
│ │ ├── variables.scss # 样式变量
│ │ ├── mixins.scss # 样式混入
│ │ └── components/ # 组件样式
│ ├── assets/ # 资源文件
│ │ ├── images/
│ │ ├── icons/
│ │ └── fonts/
│ ├── locales/ # 国际化
│ │ ├── index.ts
│ │ ├── zh-CN.ts
│ │ └── en-US.ts
│ └── types/ # TypeScript类型
│ ├── global.d.ts
│ ├── api.d.ts
│ └── components.d.ts
├── tests/ # 测试文件
│ ├── unit/
│ └── e2e/
├── .env # 环境变量
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── vite.config.ts # Vite配置
├── tsconfig.json # TypeScript配置
├── package.json # 项目配置
└── README.md # 项目说明
2.3 AI服务项目结构 (Python)
gmkx-ai/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI应用入口
│ ├── core/ # 核心配置
│ │ ├── __init__.py
│ │ ├── config.py # 应用配置
│ │ ├── security.py # 安全配置
│ │ └── logging.py # 日志配置
│ ├── api/ # API路由
│ │ ├── __init__.py
│ │ ├── deps.py # 依赖注入
│ │ └── v1/
│ │ ├── __init__.py
│ │ ├── match.py # 匹配接口
│ │ ├── recommend.py # 推荐接口
│ │ └── vectorize.py # 向量化接口
│ ├── services/ # 业务服务
│ │ ├── __init__.py
│ │ ├── match_service.py # 匹配服务
│ │ ├── recommend_service.py # 推荐服务
│ │ ├── feature_service.py # 特征服务
│ │ └── model_service.py # 模型服务
│ ├── models/ # 算法模型
│ │ ├── __init__.py
│ │ ├── matching/ # 匹配算法
│ │ │ ├── semantic_matcher.py
│ │ │ ├── keyword_matcher.py
│ │ │ └── hybrid_matcher.py
│ │ ├── recommendation/ # 推荐算法
│ │ │ ├── collaborative_filter.py
│ │ │ ├── content_filter.py
│ │ │ └── deep_recommender.py
│ │ └── nlp/ # NLP模型
│ │ ├── text_encoder.py
│ │ ├── keyword_extractor.py
│ │ └── classifier.py
│ ├── schemas/ # 数据模式
│ │ ├── __init__.py
│ │ ├── match.py
│ │ ├── recommend.py
│ │ └── common.py
│ ├── db/ # 数据库
│ │ ├── __init__.py
│ │ ├── session.py # 数据库会话
│ │ ├── models.py # 数据模型
│ │ └── repositories/ # 数据仓库
│ │ ├── __init__.py
│ │ ├── achievement_repo.py
│ │ └── demand_repo.py
│ ├── utils/ # 工具函数
│ │ ├── __init__.py
│ │ ├── vector_utils.py # 向量工具
│ │ ├── text_utils.py # 文本工具
│ │ └── cache_utils.py # 缓存工具
│ └── tests/ # 测试代码
│ ├── __init__.py
│ ├── test_match.py
│ └── test_recommend.py
├── requirements/ # 依赖管理
│ ├── base.txt # 基础依赖
│ ├── dev.txt # 开发依赖
│ └── prod.txt # 生产依赖
├── scripts/ # 脚本文件
│ ├── start.sh # 启动脚本
│ ├── train_model.py # 模型训练
│ └── data_import.py # 数据导入
├── configs/ # 配置文件
│ ├── development.yml
│ ├── production.yml
│ └── model_configs/
├── Dockerfile # Docker构建
├── docker-compose.yml # 容器编排
└── README.md # 项目说明
3. 编码规范与标准
3.1 Java 编码规范
3.1.1 命名规范
// 类名:大驼峰命名法
public class UserService { }
public class AchievementManagementController { }
// 方法名:小驼峰命名法
public void createUser() { }
public List<Achievement> searchAchievements() { }
// 变量名:小驼峰命名法
private String userName;
private Long achievementId;
// 常量名:全大写+下划线
public static final String DEFAULT_CHARSET = "UTF-8";
public static final int MAX_RETRY_TIMES = 3;
// 包名:全小写
package com.gmkx.user.service;
package com.gmkx.achievement.controller;
3.1.2 代码结构规范
/**
* 用户服务实现类
*
* @author gmkx-team
* @since 1.0.0
*/
@Service
@Transactional(rollbackFor = Exception.class)
@Slf4j
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserEventPublisher eventPublisher;
private final RedisTemplate<String, Object> redisTemplate;
/**
* 构造函数注入(推荐方式)
*/
public UserServiceImpl(UserRepository userRepository,
UserEventPublisher eventPublisher,
RedisTemplate<String, Object> redisTemplate) {
this.userRepository = userRepository;
this.eventPublisher = eventPublisher;
this.redisTemplate = redisTemplate;
}
/**
* 创建用户
*
* @param request 创建用户请求
* @return 用户响应
* @throws UserExistsException 用户已存在异常
*/
@Override
public UserResponse createUser(CreateUserRequest request) {
// 1. 参数校验
validateCreateRequest(request);
// 2. 业务逻辑处理
User user = buildUserFromRequest(request);
user.setStatus(UserStatus.PENDING);
user.setCreatedTime(LocalDateTime.now());
// 3. 数据持久化
User savedUser = userRepository.save(user);
// 4. 发布领域事件
UserRegisteredEvent event = UserRegisteredEvent.builder()
.userId(savedUser.getId())
.tenantId(savedUser.getTenantId())
.userType(savedUser.getUserType())
.timestamp(LocalDateTime.now())
.build();
eventPublisher.publish(event);
// 5. 返回结果
return UserResponse.fromEntity(savedUser);
}
/**
* 私有方法:校验创建请求
*/
private void validateCreateRequest(CreateUserRequest request) {
Assert.notNull(request, "创建用户请求不能为空");
Assert.hasText(request.getUsername(), "用户名不能为空");
Assert.hasText(request.getEmail(), "邮箱不能为空");
// 检查用户名是否已存在
if (userRepository.existsByUsername(request.getUsername())) {
throw new UserExistsException("用户名已存在: " + request.getUsername());
}
}
/**
* 私有方法:构建用户实体
*/
private User buildUserFromRequest(CreateUserRequest request) {
return User.builder()
.username(request.getUsername())
.email(request.getEmail())
.phone(request.getPhone())
.userType(request.getUserType())
.tenantId(TenantContext.getCurrentTenantId())
.build();
}
}
3.1.3 异常处理规范
// 自定义业务异常
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BusinessException extends RuntimeException {
private final String errorCode;
private final Object[] args;
public BusinessException(String errorCode, String message, Object... args) {
super(message);
this.errorCode = errorCode;
this.args = args;
}
}
// 全局异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常处理
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
ErrorResponse response = ErrorResponse.builder()
.code(e.getErrorCode())
.message(e.getMessage())
.timestamp(LocalDateTime.now())
.path(getCurrentRequestPath())
.build();
return ResponseEntity.badRequest().body(response);
}
/**
* 参数验证异常处理
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException e) {
List<String> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse response = ErrorResponse.builder()
.code("VALIDATION_ERROR")
.message("参数验证失败")
.details(errors)
.timestamp(LocalDateTime.now())
.path(getCurrentRequestPath())
.build();
return ResponseEntity.badRequest().body(response);
}
/**
* 系统异常处理
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleSystemException(Exception e) {
log.error("系统异常", e);
ErrorResponse response = ErrorResponse.builder()
.code("SYSTEM_ERROR")
.message("系统内部错误")
.timestamp(LocalDateTime.now())
.path(getCurrentRequestPath())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
3.2 Vue/TypeScript 编码规范
3.2.1 组件规范
<!-- AchievementCard.vue -->
<template>
<div class="achievement-card">
<!-- 成果标题 -->
<h3 class="achievement-card__title">
{{ achievement.title }}
</h3>
<!-- 成果摘要 -->
<p class="achievement-card__summary">
{{ achievement.summary }}
</p>
<!-- 操作按钮 -->
<div class="achievement-card__actions">
<GButton
type="primary"
size="small"
@click="handleViewDetail"
>
查看详情
</GButton>
<GButton
type="default"
size="small"
@click="handleAddToFavorite"
>
收藏
</GButton>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { Achievement } from '@/api/types'
import { GButton } from '@/components/common'
// Props 定义
interface Props {
achievement: Achievement
showActions?: boolean
}
const props = withDefaults(defineProps<Props>(), {
showActions: true
})
// Emits 定义
interface Emits {
viewDetail: [id: string]
addToFavorite: [id: string]
}
const emit = defineEmits<Emits>()
// 计算属性
const truncatedSummary = computed(() => {
const maxLength = 100
if (props.achievement.summary.length <= maxLength) {
return props.achievement.summary
}
return props.achievement.summary.substring(0, maxLength) + '...'
})
// 事件处理
const handleViewDetail = () => {
emit('viewDetail', props.achievement.id)
}
const handleAddToFavorite = () => {
emit('addToFavorite', props.achievement.id)
}
</script>
<style scoped lang="scss">
.achievement-card {
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fff;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
&__title {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #333;
line-height: 1.4;
}
&__summary {
margin: 0 0 16px 0;
color: #666;
line-height: 1.5;
}
&__actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
}
</style>
3.2.2 Composables 规范
// composables/useTable.ts
import { ref, reactive, computed, type Ref } from 'vue'
import type { TableColumn, TablePagination, TableFilter } from '@/types'
export interface UseTableOptions<T = any> {
columns: TableColumn[]
fetchData: (params: any) => Promise<{ data: T[]; total: number }>
defaultFilter?: Record<string, any>
defaultSort?: { field: string; order: 'asc' | 'desc' }
pageSize?: number
}
export function useTable<T = any>(options: UseTableOptions<T>) {
// 响应式状态
const loading = ref(false)
const data = ref<T[]>([])
const total = ref(0)
const error = ref<string | null>(null)
// 分页状态
const pagination = reactive<TablePagination>({
current: 1,
pageSize: options.pageSize || 20,
total: 0
})
// 过滤状态
const filters = reactive<TableFilter>(options.defaultFilter || {})
// 排序状态
const sorter = reactive({
field: options.defaultSort?.field || '',
order: options.defaultSort?.order || 'desc'
})
// 计算属性
const hasData = computed(() => data.value.length > 0)
const isEmpty = computed(() => !loading.value && data.value.length === 0)
// 获取数据
const fetchData = async (resetPage = false) => {
if (resetPage) {
pagination.current = 1
}
loading.value = true
error.value = null
try {
const params = {
page: pagination.current,
size: pagination.pageSize,
...filters,
sort: sorter.field ? `${sorter.field},${sorter.order}` : undefined
}
const result = await options.fetchData(params)
data.value = result.data
total.value = result.total
pagination.total = result.total
} catch (err) {
error.value = err instanceof Error ? err.message : '数据加载失败'
console.error('Table data fetch error:', err)
} finally {
loading.value = false
}
}
// 分页处理
const handlePageChange = (page: number) => {
pagination.current = page
fetchData()
}
const handlePageSizeChange = (size: number) => {
pagination.pageSize = size
pagination.current = 1
fetchData()
}
// 过滤处理
const handleFilter = (newFilters: Record<string, any>) => {
Object.assign(filters, newFilters)
fetchData(true)
}
const resetFilter = () => {
Object.keys(filters).forEach(key => {
delete filters[key]
})
fetchData(true)
}
// 排序处理
const handleSort = (field: string, order: 'asc' | 'desc') => {
sorter.field = field
sorter.order = order
fetchData(true)
}
// 刷新数据
const refresh = () => {
fetchData()
}
// 重置到初始状态
const reset = () => {
pagination.current = 1
Object.keys(filters).forEach(key => {
delete filters[key]
})
sorter.field = options.defaultSort?.field || ''
sorter.order = options.defaultSort?.order || 'desc'
fetchData()
}
return {
// 状态
loading: readonly(loading),
data: readonly(data),
total: readonly(total),
error: readonly(error),
pagination: readonly(pagination),
filters: readonly(filters),
sorter: readonly(sorter),
// 计算属性
hasData,
isEmpty,
// 方法
fetchData,
handlePageChange,
handlePageSizeChange,
handleFilter,
resetFilter,
handleSort,
refresh,
reset
}
}
3.2.3 API 接口规范
// api/modules/achievement.ts
import request from '@/utils/request'
import type {
Achievement,
CreateAchievementRequest,
UpdateAchievementRequest,
AchievementListQuery,
SearchAchievementQuery,
PageResult,
ApiResponse
} from '@/api/types'
/**
* 成果管理 API
*/
export class AchievementApi {
/**
* 获取成果列表
*/
static async getList(query: AchievementListQuery): Promise<PageResult<Achievement>> {
const response = await request.get<ApiResponse<PageResult<Achievement>>>('/api/v1/achievements', {
params: query
})
return response.data
}
/**
* 获取成果详情
*/
static async getById(id: string): Promise<Achievement> {
const response = await request.get<ApiResponse<Achievement>>(`/api/v1/achievements/${id}`)
return response.data
}
/**
* 创建成果
*/
static async create(data: CreateAchievementRequest): Promise<Achievement> {
const response = await request.post<ApiResponse<Achievement>>('/api/v1/achievements', data)
return response.data
}
/**
* 更新成果
*/
static async update(id: string, data: UpdateAchievementRequest): Promise<Achievement> {
const response = await request.put<ApiResponse<Achievement>>(`/api/v1/achievements/${id}`, data)
return response.data
}
/**
* 删除成果
*/
static async delete(id: string): Promise<void> {
await request.delete(`/api/v1/achievements/${id}`)
}
/**
* 搜索成果
*/
static async search(query: SearchAchievementQuery): Promise<PageResult<Achievement>> {
const response = await request.post<ApiResponse<PageResult<Achievement>>>('/api/v1/achievements/search', query)
return response.data
}
/**
* 收藏成果
*/
static async favorite(id: string): Promise<void> {
await request.post(`/api/v1/achievements/${id}/favorite`)
}
/**
* 取消收藏
*/
static async unfavorite(id: string): Promise<void> {
await request.delete(`/api/v1/achievements/${id}/favorite`)
}
}
// 导出便捷方法
export const {
getList: getAchievementList,
getById: getAchievementById,
create: createAchievement,
update: updateAchievement,
delete: deleteAchievement,
search: searchAchievements,
favorite: favoriteAchievement,
unfavorite: unfavoriteAchievement
} = AchievementApi
3.3 Python 编码规范
3.3.1 项目结构规范
# app/services/match_service.py
"""
智能匹配服务
提供需求与成果的智能匹配功能,包括语义匹配、关键词匹配和混合匹配算法。
"""
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from loguru import logger
from app.models.matching.hybrid_matcher import HybridMatcher
from app.schemas.match import MatchRequest, MatchResult
from app.db.repositories.achievement_repo import AchievementRepository
from app.db.repositories.demand_repo import DemandRepository
from app.utils.cache_utils import cached
from app.core.config import settings
@dataclass
class MatchConfig:
"""匹配配置"""
max_results: int = 20
similarity_threshold: float = 0.3
keyword_weight: float = 0.4
semantic_weight: float = 0.6
class MatchService:
"""智能匹配服务"""
def __init__(
self,
achievement_repo: AchievementRepository,
demand_repo: DemandRepository
):
self.achievement_repo = achievement_repo
self.demand_repo = demand_repo
self.matcher = HybridMatcher()
self.config = MatchConfig()
async def match_demand_to_achievements(
self,
demand_id: str,
config: Optional[MatchConfig] = None
) -> List[MatchResult]:
"""
为需求匹配相关成果
Args:
demand_id: 需求ID
config: 匹配配置
Returns:
匹配结果列表
Raises:
ValueError: 需求不存在
RuntimeError: 匹配过程出错
"""
# 使用传入的配置或默认配置
match_config = config or self.config
try:
# 获取需求信息
demand = await self.demand_repo.get_by_id(demand_id)
if not demand:
raise ValueError(f"需求不存在: {demand_id}")
logger.info(f"开始为需求 {demand_id} 匹配成果")
# 执行匹配
matches = await self._execute_matching(demand, match_config)
# 过滤和排序
filtered_matches = self._filter_and_sort_matches(matches, match_config)
logger.info(f"需求 {demand_id} 匹配完成,找到 {len(filtered_matches)} 个匹配项")
return filtered_matches
except Exception as e:
logger.error(f"匹配过程出错: {e}")
raise RuntimeError(f"匹配失败: {str(e)}") from e
async def _execute_matching(
self,
demand: Dict[str, Any],
config: MatchConfig
) -> List[MatchResult]:
"""执行匹配算法"""
# 获取候选成果
candidates = await self.achievement_repo.get_published_achievements(
tenant_id=demand.get('tenant_id'),
limit=config.max_results * 3 # 获取更多候选项
)
if not candidates:
return []
# 执行混合匹配
matches = await self.matcher.match(
demand=demand,
candidates=candidates,
weights={
'keyword': config.keyword_weight,
'semantic': config.semantic_weight
}
)
return matches
def _filter_and_sort_matches(
self,
matches: List[MatchResult],
config: MatchConfig
) -> List[MatchResult]:
"""过滤和排序匹配结果"""
# 过滤低分匹配
filtered = [
match for match in matches
if match.score >= config.similarity_threshold
]
# 按分数排序
filtered.sort(key=lambda x: x.score, reverse=True)
# 限制结果数量
return filtered[:config.max_results]
@cached(ttl=300) # 缓存5分钟
async def get_hot_matches(self, limit: int = 10) -> List[Dict[str, Any]]:
"""获取热门匹配"""
hot_demands = await self.demand_repo.get_hot_demands(limit=limit)
hot_matches = []
for demand in hot_demands:
matches = await self.match_demand_to_achievements(
demand_id=demand['id'],
config=MatchConfig(max_results=3) # 每个需求只返回3个匹配
)
if matches:
hot_matches.append({
'demand': demand,
'matches': matches
})
return hot_matches
# 依赖注入工厂
def create_match_service() -> MatchService:
"""创建匹配服务实例"""
achievement_repo = AchievementRepository()
demand_repo = DemandRepository()
return MatchService(achievement_repo, demand_repo)
3.3.2 API 接口规范
# app/api/v1/match.py
"""
匹配相关API接口
"""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from fastapi.security import HTTPBearer
from loguru import logger
from app.schemas.match import (
MatchRequest,
MatchResponse,
BatchMatchRequest,
MatchHistoryResponse
)
from app.services.match_service import MatchService, create_match_service
from app.core.deps import get_current_user, get_tenant_id
from app.core.response import ApiResponse, success_response, error_response
from app.utils.rate_limit import rate_limit
router = APIRouter(prefix="/match", tags=["智能匹配"])
security = HTTPBearer()
@router.post("/demand/{demand_id}", response_model=ApiResponse[List[MatchResponse]])
@rate_limit(max_calls=10, time_window=60) # 限制每分钟10次调用
async def match_demand(
demand_id: str,
background_tasks: BackgroundTasks,
current_user: dict = Depends(get_current_user),
tenant_id: str = Depends(get_tenant_id),
match_service: MatchService = Depends(create_match_service)
) -> ApiResponse[List[MatchResponse]]:
"""
为指定需求匹配相关成果
Args:
demand_id: 需求ID
background_tasks: 后台任务
current_user: 当前用户
tenant_id: 租户ID
match_service: 匹配服务
Returns:
匹配结果列表
Raises:
HTTPException: 需求不存在或匹配失败
"""
try:
logger.info(f"用户 {current_user['id']} 请求匹配需求 {demand_id}")
# 执行匹配
matches = await match_service.match_demand_to_achievements(demand_id)
# 转换为响应格式
response_data = [
MatchResponse(
achievement_id=match.achievement_id,
score=match.score,
reasons=match.reasons,
similarity=match.similarity
)
for match in matches
]
# 异步记录匹配历史
background_tasks.add_task(
_record_match_history,
demand_id=demand_id,
user_id=current_user['id'],
matches=matches
)
return success_response(
data=response_data,
message=f"匹配成功,找到 {len(response_data)} 个相关成果"
)
except ValueError as e:
logger.warning(f"匹配请求参数错误: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"匹配请求处理失败: {e}")
raise HTTPException(status_code=500, detail="匹配服务暂时不可用")
@router.post("/batch", response_model=ApiResponse[Dict[str, List[MatchResponse]]])
async def batch_match(
request: BatchMatchRequest,
current_user: dict = Depends(get_current_user),
match_service: MatchService = Depends(create_match_service)
) -> ApiResponse[Dict[str, List[MatchResponse]]]:
"""
批量匹配多个需求
Args:
request: 批量匹配请求
current_user: 当前用户
match_service: 匹配服务
Returns:
批量匹配结果
"""
if len(request.demand_ids) > 50:
raise HTTPException(status_code=400, detail="批量匹配需求数量不能超过50个")
try:
results = {}
for demand_id in request.demand_ids:
matches = await match_service.match_demand_to_achievements(demand_id)
results[demand_id] = [
MatchResponse(
achievement_id=match.achievement_id,
score=match.score,
reasons=match.reasons,
similarity=match.similarity
)
for match in matches
]
return success_response(
data=results,
message=f"批量匹配完成,处理了 {len(request.demand_ids)} 个需求"
)
except Exception as e:
logger.error(f"批量匹配失败: {e}")
raise HTTPException(status_code=500, detail="批量匹配失败")
@router.get("/hot", response_model=ApiResponse[List[Dict[str, Any]]])
async def get_hot_matches(
limit: int = 10,
match_service: MatchService = Depends(create_match_service)
) -> ApiResponse[List[Dict[str, Any]]]:
"""
获取热门匹配
Args:
limit: 限制数量
match_service: 匹配服务
Returns:
热门匹配列表
"""
try:
hot_matches = await match_service.get_hot_matches(limit=limit)
return success_response(
data=hot_matches,
message="获取热门匹配成功"
)
except Exception as e:
logger.error(f"获取热门匹配失败: {e}")
raise HTTPException(status_code=500, detail="获取热门匹配失败")
async def _record_match_history(
demand_id: str,
user_id: str,
matches: List[MatchResult]
) -> None:
"""记录匹配历史(后台任务)"""
try:
# 这里实现匹配历史记录逻辑
logger.info(f"记录匹配历史: 需求={demand_id}, 用户={user_id}, 匹配数={len(matches)}")
except Exception as e:
logger.error(f"记录匹配历史失败: {e}")
4. 开发流程与规范
4.1 Git 工作流
4.1.1 分支策略
# 主分支
main # 生产环境分支,只允许合并,不允许直接提交
develop # 开发环境分支,集成分支
# 功能分支
feature/user-management # 用户管理功能
feature/achievement-search # 成果搜索功能
feature/ai-matching # AI匹配功能
# 发布分支
release/v1.0.0 # 发布分支,用于发布准备
# 修复分支
hotfix/fix-login-bug # 紧急修复分支
# 个人开发分支
dev/zhangsan/user-api # 个人开发分支
4.1.2 提交规范
# 提交消息格式
<type>(<scope>): <subject>
<body>
<footer>
# 类型(type)
feat: 新功能
fix: 修复bug
docs: 文档变更
style: 代码格式变更(不影响功能)
refactor: 重构(既不是新功能也不是修复bug)
perf: 性能优化
test: 测试相关
chore: 构建过程或辅助工具变更
# 示例
feat(user): 添加用户注册功能
- 实现用户注册接口
- 添加邮箱验证功能
- 集成短信验证服务
Closes #123
fix(achievement): 修复成果搜索分页问题
成果搜索时分页参数计算错误,导致结果重复
Fixes #456
4.1.3 代码审查规范
# Pull Request 模板
## 变更说明
简要描述本次变更的内容和目的
## 变更类型
- [ ] 新功能
- [ ] Bug修复
- [ ] 重构
- [ ] 文档更新
- [ ] 其他
## 测试
- [ ] 单元测试已通过
- [ ] 集成测试已通过
- [ ] 手工测试已完成
## 检查清单
- [ ] 代码遵循项目编码规范
- [ ] 已添加必要的注释
- [ ] 已更新相关文档
- [ ] 无安全漏洞
- [ ] 性能影响可接受
## 相关Issue
Closes #issue_number
## 截图(如适用)
4.2 开发工作流
4.2.1 功能开发流程
# 1. 创建功能分支
git checkout develop
git pull origin develop
git checkout -b feature/achievement-search
# 2. 开发功能
# ... 编码实现 ...
# 3. 提交代码
git add .
git commit -m "feat(achievement): 实现成果搜索功能"
# 4. 推送分支
git push origin feature/achievement-search
# 5. 创建 Pull Request
# 在 GitLab/GitHub 上创建 MR/PR
# 6. 代码审查
# 团队成员进行代码审查
# 7. 合并代码
# 审查通过后合并到 develop 分支
# 8. 删除功能分支
git branch -d feature/achievement-search
git push origin --delete feature/achievement-search
4.2.2 发布流程
# 1. 创建发布分支
git checkout develop
git pull origin develop
git checkout -b release/v1.0.0
# 2. 准备发布
# - 更新版本号
# - 更新 CHANGELOG
# - 最后的测试和修复
# 3. 合并到主分支
git checkout main
git merge release/v1.0.0
git tag v1.0.0
git push origin main --tags
# 4. 合并回开发分支
git checkout develop
git merge release/v1.0.0
git push origin develop
# 5. 删除发布分支
git branch -d release/v1.0.0
git push origin --delete release/v1.0.0
5. 质量保证
5.1 代码质量工具
5.1.1 Java 质量工具配置
<!-- pom.xml - 代码质量插件 -->
<build>
<plugins>
<!-- SonarQube 代码质量分析 -->
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.1.2184</version>
</plugin>
<!-- SpotBugs 静态代码分析 -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.7.3.0</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<xmlOutput>true</xmlOutput>
</configuration>
</plugin>
<!-- Checkstyle 代码风格检查 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<failOnViolation>true</failOnViolation>
</configuration>
</plugin>
<!-- JaCoCo 代码覆盖率 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.10</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
5.1.2 前端质量工具配置
// package.json - 代码质量工具
{
"scripts": {
"lint": "eslint src --ext .vue,.js,.ts --fix",
"lint:style": "stylelint src/**/*.{vue,scss,css} --fix",
"type-check": "vue-tsc --noEmit",
"test": "vitest",
"test:coverage": "vitest --coverage",
"build": "vue-tsc --noEmit && vite build"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"eslint": "^8.45.0",
"eslint-plugin-vue": "^9.15.0",
"stylelint": "^15.10.0",
"stylelint-config-standard-scss": "^10.0.0",
"vitest": "^0.34.0",
"@vitest/coverage-v8": "^0.34.0"
}
}
// .eslintrc.js
module.exports = {
root: true,
env: {
node: true,
browser: true,
es2022: true
},
extends: [
'eslint:recommended',
'@vue/typescript/recommended',
'plugin:vue/vue3-recommended'
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
// Vue 规则
'vue/multi-word-component-names': 'off',
'vue/component-definition-name-casing': ['error', 'PascalCase'],
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
'vue/define-emits-declaration': ['error', 'type-based'],
'vue/define-props-declaration': ['error', 'type-based'],
// TypeScript 规则
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
// 通用规则
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'prefer-const': 'error',
'no-var': 'error'
}
}
5.2 自动化检查
5.2.1 pre-commit 钩子
#!/bin/sh
# .git/hooks/pre-commit
echo "运行 pre-commit 检查..."
# Java 项目检查
if [ -f "pom.xml" ]; then
echo "检查 Java 代码..."
# 代码格式检查
mvn checkstyle:check
if [ $? -ne 0 ]; then
echo "Checkstyle 检查失败,请修复代码风格问题"
exit 1
fi
# 静态代码分析
mvn spotbugs:check
if [ $? -ne 0 ]; then
echo "SpotBugs 检查失败,请修复代码问题"
exit 1
fi
# 运行测试
mvn test
if [ $? -ne 0 ]; then
echo "测试失败,请修复测试问题"
exit 1
fi
fi
# 前端项目检查
if [ -f "package.json" ]; then
echo "检查前端代码..."
# ESLint 检查
npm run lint
if [ $? -ne 0 ]; then
echo "ESLint 检查失败,请修复代码风格问题"
exit 1
fi
# 类型检查
npm run type-check
if [ $? -ne 0 ]; then
echo "TypeScript 类型检查失败"
exit 1
fi
# 运行测试
npm run test
if [ $? -ne 0 ]; then
echo "测试失败,请修复测试问题"
exit 1
fi
fi
echo "所有检查通过 ✅"
exit 0
5.2.2 CI/CD 流水线
# .gitlab-ci.yml
stages:
- code-quality
- test
- build
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
cache:
paths:
- .m2/repository/
- node_modules/
# 代码质量检查阶段
code-quality:
stage: code-quality
image: openjdk:17-jdk-slim
script:
- ./mvnw $MAVEN_CLI_OPTS checkstyle:check
- ./mvnw $MAVEN_CLI_OPTS spotbugs:check
- ./mvnw $MAVEN_CLI_OPTS org.sonarsource.scanner.maven:sonar-maven-plugin:sonar
artifacts:
reports:
junit:
- target/surefire-reports/TEST-*.xml
- target/failsafe-reports/TEST-*.xml
paths:
- target/site/
only:
- merge_requests
- develop
- main
# 后端测试
backend-test:
stage: test
image: openjdk:17-jdk-slim
services:
- postgres:15-alpine
- redis:7-alpine
variables:
POSTGRES_DB: gmkx_test
POSTGRES_USER: gmkx
POSTGRES_PASSWORD: gmkx123
SPRING_PROFILES_ACTIVE: test
script:
- ./mvnw $MAVEN_CLI_OPTS test
- ./mvnw $MAVEN_CLI_OPTS jacoco:report
coverage: '/Total.*?([0-9]{1,3})%/'
artifacts:
reports:
junit: target/surefire-reports/TEST-*.xml
coverage_report:
coverage_format: jacoco
path: target/site/jacoco/jacoco.xml
# 前端测试
frontend-test:
stage: test
image: node:18-alpine
script:
- cd gmkx-web
- npm ci
- npm run lint
- npm run type-check
- npm run test:coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: gmkx-web/coverage/cobertura-coverage.xml
# 构建阶段
build:
stage: build
image: docker:24-dind
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- develop
- main
6. 开发最佳实践
6.1 性能优化指南
6.1.1 后端性能优化
// 数据库查询优化
@Repository
public class UserRepositoryImpl implements UserRepository {
// 使用分页查询避免大结果集
@Override
public Page<User> findUsers(UserQuery query, Pageable pageable) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 只查询必要字段
wrapper.select("id", "username", "email", "status", "created_time");
// 添加索引字段查询条件
if (StringUtils.hasText(query.getStatus())) {
wrapper.eq("status", query.getStatus());
}
if (StringUtils.hasText(query.getTenantId())) {
wrapper.eq("tenant_id", query.getTenantId());
}
return userMapper.selectPage(new Page<>(pageable.getPageNumber(), pageable.getPageSize()), wrapper);
}
// 批量操作优化
@Override
@Transactional
public void batchUpdateUserStatus(List<String> userIds, UserStatus status) {
// 使用批量更新而不是循环单个更新
if (userIds.size() <= 1000) {
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.in("id", userIds);
wrapper.set("status", status);
wrapper.set("updated_time", LocalDateTime.now());
userMapper.update(null, wrapper);
} else {
// 大批量数据分批处理
List<List<String>> batches = Lists.partition(userIds, 1000);
for (List<String> batch : batches) {
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.in("id", batch);
wrapper.set("status", status);
wrapper.set("updated_time", LocalDateTime.now());
userMapper.update(null, wrapper);
}
}
}
}
// 缓存优化
@Service
public class AchievementServiceImpl implements AchievementService {
// 多级缓存
@Cacheable(value = "achievement", key = "#id", condition = "#id != null")
@Override
public Achievement getById(String id) {
return achievementRepository.findById(id).orElse(null);
}
// 缓存预热
@EventListener
@Async
public void warmUpCache(ApplicationReadyEvent event) {
List<String> hotAchievementIds = getHotAchievementIds();
hotAchievementIds.parallelStream().forEach(this::getById);
}
// 缓存失效
@CacheEvict(value = "achievement", key = "#result.id")
@Override
public Achievement update(String id, UpdateAchievementRequest request) {
Achievement achievement = getById(id);
if (achievement == null) {
throw new AchievementNotFoundException(id);
}
BeanUtils.copyProperties(request, achievement);
achievement.setUpdatedTime(LocalDateTime.now());
return achievementRepository.save(achievement);
}
}
6.1.2 前端性能优化
// 组件懒加载
const AchievementDetail = defineAsyncComponent(() => import('@/views/achievement/AchievementDetail.vue'))
const DemandList = defineAsyncComponent(() => import('@/views/demand/DemandList.vue'))
// 路由懒加载
const routes = [
{
path: '/achievement',
component: () => import('@/views/achievement/AchievementLayout.vue'),
children: [
{
path: 'list',
component: () => import('@/views/achievement/AchievementList.vue')
},
{
path: 'detail/:id',
component: () => import('@/views/achievement/AchievementDetail.vue')
}
]
}
]
// 虚拟滚动优化大列表
<template>
<VirtualList
:items="achievements"
:item-height="120"
:container-height="600"
v-slot="{ item, index }"
>
<AchievementCard :achievement="item" :key="item.id" />
</VirtualList>
</template>
// 图片懒加载和优化
<template>
<img
v-lazy="achievement.thumbnail"
:alt="achievement.title"
loading="lazy"
:style="{ aspectRatio: '16/9' }"
/>
</template>
// API 请求优化
import { debounce } from 'lodash-es'
export const useSearchAchievements = () => {
const query = ref('')
const results = ref<Achievement[]>([])
const loading = ref(false)
// 防抖搜索
const debouncedSearch = debounce(async (searchQuery: string) => {
if (!searchQuery.trim()) {
results.value = []
return
}
loading.value = true
try {
const response = await searchAchievements({ query: searchQuery })
results.value = response.data
} finally {
loading.value = false
}
}, 300)
watch(query, debouncedSearch)
return {
query,
results: readonly(results),
loading: readonly(loading)
}
}
6.2 安全开发指南
6.2.1 输入验证
// 参数验证
@PostMapping("/users")
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
// Spring Validation 自动验证
UserResponse response = userService.createUser(request);
return ResponseEntity.ok(response);
}
// 自定义验证器
@Component
public class UserValidator {
public void validateCreateRequest(CreateUserRequest request) {
// 用户名格式验证
if (!isValidUsername(request.getUsername())) {
throw new ValidationException("用户名格式不正确");
}
// 邮箱格式验证
if (!isValidEmail(request.getEmail())) {
throw new ValidationException("邮箱格式不正确");
}
// 密码强度验证
if (!isStrongPassword(request.getPassword())) {
throw new ValidationException("密码强度不足");
}
}
private boolean isValidUsername(String username) {
return username != null &&
username.matches("^[a-zA-Z0-9_]{3,20}$");
}
private boolean isValidEmail(String email) {
return email != null &&
email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
}
private boolean isStrongPassword(String password) {
return password != null &&
password.length() >= 8 &&
password.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]");
}
}
6.2.2 SQL 注入防护
// 使用 MyBatis-Plus 参数化查询
@Repository
public class AchievementRepositoryImpl implements AchievementRepository {
// 正确方式:使用参数化查询
@Override
public List<Achievement> searchByKeyword(String keyword, String techField) {
QueryWrapper<Achievement> wrapper = new QueryWrapper<>();
if (StringUtils.hasText(keyword)) {
// 使用 like 查询,MyBatis-Plus 自动处理参数化
wrapper.like("title", keyword)
.or()
.like("summary", keyword);
}
if (StringUtils.hasText(techField)) {
wrapper.eq("tech_field", techField);
}
return achievementMapper.selectList(wrapper);
}
// 错误方式:字符串拼接(永远不要这样做)
// public List<Achievement> searchByKeywordUnsafe(String keyword) {
// String sql = "SELECT * FROM t_achievement WHERE title LIKE '%" + keyword + "%'";
// return jdbcTemplate.query(sql, achievementRowMapper);
// }
}
6.2.3 XSS 防护
// 前端 XSS 防护
import DOMPurify from 'dompurify'
// 清理用户输入
export const sanitizeInput = (input: string): string => {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
})
}
// 安全的 HTML 渲染
<template>
<div v-html="sanitizedContent"></div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { sanitizeInput } from '@/utils/security'
interface Props {
content: string
}
const props = defineProps<Props>()
const sanitizedContent = computed(() => {
return sanitizeInput(props.content)
})
</script>
6.3 监控与日志
6.3.1 结构化日志
// 使用结构化日志
@Slf4j
@RestController
public class AchievementController {
@PostMapping("/achievements")
public ResponseEntity<AchievementResponse> createAchievement(
@RequestBody CreateAchievementRequest request,
HttpServletRequest httpRequest) {
String requestId = UUID.randomUUID().toString();
String userId = getCurrentUserId();
String tenantId = getCurrentTenantId();
// 结构化日志
log.info("创建成果请求开始: requestId={}, userId={}, tenantId={}, title={}",
requestId, userId, tenantId, request.getTitle());
try {
AchievementResponse response = achievementService.createAchievement(request);
log.info("创建成果请求成功: requestId={}, achievementId={}, duration={}ms",
requestId, response.getId(), getDuration(startTime));
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("创建成果请求失败: requestId={}, error={}",
requestId, e.getMessage(), e);
throw e;
}
}
}
// 自定义日志注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BusinessLog {
String operation();
String description() default "";
}
@Aspect
@Component
@Slf4j
public class BusinessLogAspect {
@Around("@annotation(businessLog)")
public Object logBusinessOperation(ProceedingJoinPoint joinPoint, BusinessLog businessLog) throws Throwable {
String operationId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
try {
log.info("业务操作开始: operationId={}, operation={}, description={}",
operationId, businessLog.operation(), businessLog.description());
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
log.info("业务操作成功: operationId={}, duration={}ms", operationId, duration);
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
log.error("业务操作失败: operationId={}, duration={}ms, error={}",
operationId, duration, e.getMessage(), e);
throw e;
}
}
}
这个开发指南提供了完整的开发环境搭建、项目结构、编码规范、开发流程和最佳实践,为团队提供了详细的实施指导。