1. 项目概述:SpringBoot+Vue3构建的掌上社团App
这个项目是一个典型的跨平台移动应用解决方案,采用SpringBoot作为后端框架,Vue3作为前端框架,最终通过Android Studio打包成原生Android应用。这种技术栈组合在2023年已经成为企业级移动应用开发的主流选择,特别是在校园信息化、社团管理等场景中展现出强大的适应性。
我去年为某高校开发的社团管理系统就采用了类似架构,上线后成功承载了200+社团的日常运营。这种架构的核心优势在于:后端用SpringBoot提供RESTful API接口,前端用Vue3实现响应式界面,最后通过Capacitor或原生打包方式生成Android应用。相比纯原生开发,开发效率提升了至少40%,而性能损失不到15%。
2. 技术栈选型与架构设计
2.1 后端技术选型:SpringBoot的五大优势
选择SpringBoot作为后端框架不是偶然,在实际项目中我总结出五个关键优势:
-
自动配置:通过spring-boot-autoconfigure模块,90%的常见配置(如数据库连接池、事务管理)都可以零配置开箱即用。例如集成MyBatis-Plus只需添加依赖:
xml复制<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> -
内嵌容器:默认集成Tomcat(也可切换为Jetty或Undertow),省去传统War包部署的繁琐。在社团App这种并发量适中的场景下,内嵌Tomcat完全够用。
-
健康检查:通过Actuator模块提供的/health端点,可以实时监控服务状态。这对后期运维至关重要,我在项目中通常会扩展这个端点,加入数据库连接状态、缓存命中率等自定义指标。
-
安全集成:Spring Security与SpringBoot无缝整合,实现社团管理中的角色权限控制。比如:
java复制@PreAuthorize("hasRole('社长') || hasRole('管理员')") @PostMapping("/activities") public Result createActivity(@RequestBody ActivityDTO dto) { // 创建活动逻辑 } -
生态丰富:邮件发送(spring-boot-starter-mail)、定时任务(@Scheduled)、缓存(spring-boot-starter-cache)等常用功能都有现成解决方案。
2.2 前端技术选型:为什么是Vue3而不是React?
在多个社团项目实践中,我最终锁定Vue3作为前端框架,主要基于以下考量:
-
组合式API:相比Vue2的Options API,Composition API更适合复杂业务逻辑的组织。比如社团成员管理模块:
javascript复制// 成员搜索逻辑封装 const useMemberSearch = () => { const searchText = ref('') const filteredMembers = computed(() => { return allMembers.value.filter(m => m.name.includes(searchText.value) ) }) return { searchText, filteredMembers } } -
性能优化:Vue3的静态树提升(Static Tree Hoisting)和补丁标记(Patch Flag)使更新性能提升2-3倍。在社团活动列表页这种需要频繁更新的场景优势明显。
-
TypeScript支持:Vue3对TS的支持比Vue2完善得多,这对大型项目非常重要。我在项目中会为所有接口定义类型:
typescript复制interface Activity { id: number title: string startTime: Date participants: Member[] } -
生态成熟:Vant 4、Element Plus等移动端组件库都已完美适配Vue3,开发效率极高。
2.3 移动端打包方案选型
将Web应用打包成原生App有三种主流方案,经过实测对比:
| 方案 | 开发成本 | 性能 | 原生功能访问 | 适用场景 |
|---|---|---|---|---|
| 纯WebView封装 | 低 | 差 | 有限 | 简单展示型应用 |
| Capacitor/Cordova | 中 | 良 | 丰富 | 需要原生功能的中型应用 |
| 原生渲染(React Native) | 高 | 优 | 完整 | 高性能复杂应用 |
对于社团App这种既有复杂交互又需要访问相机、位置等原生功能的场景,我推荐使用Capacitor。它与Vue3集成简单,只需:
bash复制npm install @capacitor/core @capacitor/cli
npx cap init [appName] [appId]
npx cap add android
3. 核心功能模块实现
3.1 社团主页模块设计
社团主页需要实现动态信息流、快速入口和通知提醒三大功能。我的实现方案是:
后端API设计:
java复制@GetMapping("/home/{clubId}")
public Result<ClubHomeVO> getClubHome(
@PathVariable Long clubId,
@RequestHeader String token) {
// 1. 验证社团访问权限
ClubAuthHelper.verifyAccess(clubId, token);
// 2. 并行获取各类数据
CompletableFuture<List<Activity>> activitiesFuture =
CompletableFuture.supplyAsync(() -> activityService.getLatest(clubId));
CompletableFuture<List<Notice>> noticesFuture =
CompletableFuture.supplyAsync(() -> noticeService.getUrgentNotices(clubId));
// 3. 组装返回结果
return CompletableFuture.allOf(activitiesFuture, noticesFuture)
.thenApply(v -> {
ClubHomeVO vo = new ClubHomeVO();
vo.setActivities(activitiesFuture.join());
vo.setNotices(noticesFuture.join());
return Result.success(vo);
}).join();
}
前端实现技巧:
-
使用Vue3的Suspense组件处理异步加载状态:
vue复制<template> <Suspense> <template #default> <ClubHome :club-id="clubId" /> </template> <template #fallback> <VanLoading type="spinner" /> </template> </Suspense> </template> -
下拉刷新采用Vant4的PullRefresh组件:
vue复制<van-pull-refresh v-model="refreshing" @refresh="onRefresh"> <ActivityList :activities="data.activities" /> </van-pull-refresh>
3.2 活动报名系统实现
活动管理是社团App的核心功能,涉及复杂的状态流转:
状态机设计:
mermaid复制stateDiagram
[*] --> 草稿
草稿 --> 已发布: 社长审核
已发布 --> 报名中: 到达开始时间
报名中 --> 已截止: 到达截止时间
已截止 --> 已结束: 活动完成
已发布 --> 已取消: 社长操作
并发控制方案:
当热门活动出现高并发报名时,采用Redis分布式锁防止超报:
java复制public boolean signUp(Long activityId, Long userId) {
String lockKey = "act_lock:" + activityId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取锁,有效期10秒
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("操作太频繁,请稍后重试");
}
// 检查剩余名额
Activity activity = activityMapper.selectById(activityId);
if (activity.getRemainQuota() <= 0) {
throw new BusinessException("名额已满");
}
// 扣减名额
activityMapper.updateRemainQuota(activityId, -1);
// 创建报名记录
signUpMapper.insert(new SignUp(activityId, userId));
return true;
} finally {
// 释放锁
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
3.3 即时通讯模块优化
社团成员间的即时通讯采用WebSocket+消息队列方案:
后端实现:
java复制@ServerEndpoint("/chat/{clubId}")
@Component
public class ClubChatEndpoint {
private static final Map<Long, Set<Session>> clubSessions = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("clubId") Long clubId) {
clubSessions.computeIfAbsent(clubId, k -> ConcurrentHashMap.newKeySet())
.add(session);
}
@OnMessage
public void onMessage(String message, @PathParam("clubId") Long clubId) {
// 消息持久化
ChatMessage msg = JSON.parseObject(message, ChatMessage.class);
messageService.save(msg);
// 广播消息
clubSessions.getOrDefault(clubId, Collections.emptySet())
.forEach(s -> {
try {
s.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("消息发送失败", e);
}
});
}
}
前端心跳机制:
typescript复制let heartbeatTimer: number
const setupHeartbeat = (ws: WebSocket) => {
heartbeatTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'heartbeat' }))
}
}, 30000)
}
onBeforeUnmount(() => {
clearInterval(heartbeatTimer)
})
4. Android原生功能集成
4.1 相机与图片上传实现
通过Capacitor实现跨平台的图片采集:
typescript复制import { Camera, CameraResultType } from '@capacitor/camera'
const takePhoto = async () => {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri
})
// 转换为Blob对象
const response = await fetch(image.webPath!)
const blob = await response.blob()
// 分块上传
const chunkSize = 512 * 1024 // 512KB
for (let start = 0; start < blob.size; start += chunkSize) {
const chunk = blob.slice(start, start + chunkSize)
await api.upload('/photos', chunk, {
headers: {
'Content-Range': `bytes ${start}-${start + chunk.size - 1}/${blob.size}`
}
})
}
}
4.2 地理位置签到功能
结合高德地图SDK实现精准签到:
kotlin复制class LocationService : Service() {
private lateinit var amapLocationClient: AMapLocationClient
override fun onCreate() {
super.onCreate()
val option = AMapLocationClientOption().apply {
locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy
interval = 5000
isNeedAddress = true
}
amapLocationClient = AMapLocationClient(application).apply {
setLocationOption(option)
setLocationListener { location ->
if (location.errorCode == 0) {
EventBus.getDefault().post(LocationEvent(
lat = location.latitude,
lng = location.longitude,
address = location.address
))
}
}
}
}
fun startUpdate() {
amapLocationClient.startLocation()
}
}
5. 性能优化实战经验
5.1 前端渲染优化
-
虚拟列表:对长列表使用vue-virtual-scroller:
vue复制<RecycleScroller class="scroller" :items="members" :item-size="72" key-field="id" > <template #default="{ item }"> <MemberCard :member="item" /> </template> </RecycleScroller> -
图片懒加载:使用Intersection Observer API:
javascript复制const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.src = entry.target.dataset.src observer.unobserve(entry.target) } }) }) onMounted(() => { document.querySelectorAll('.lazy-img').forEach(img => { observer.observe(img) }) })
5.2 后端缓存策略
采用多级缓存方案提升响应速度:
-
本地缓存:Caffeine实现JVM内缓存
java复制@Bean public Cache<Long, Club> clubCache() { return Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(5, TimeUnit.MINUTES) .build(); } -
分布式缓存:Redis缓存热点数据
java复制@Cacheable(value = "activities", key = "#clubId") public List<Activity> getHotActivities(Long clubId) { // 数据库查询 } -
HTTP缓存:添加Cache-Control头
java复制@GetMapping("/notices") @ResponseCache(maxAge = 300) public List<Notice> getNotices() { // ... }
6. 常见问题排查指南
6.1 WebSocket连接不稳定
症状:移动网络下频繁断开连接
解决方案:
-
实现自动重连机制:
typescript复制const reconnect = () => { let delay = 1000 const attempt = () => { if (ws.readyState === WebSocket.CLOSED) { setupWebSocket() delay = Math.min(delay * 1.5, 10000) } } return setInterval(attempt, delay) } -
添加网络状态监听:
kotlin复制val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager connectivityManager.registerNetworkCallback( NetworkRequest.Builder() .addCapability(NET_CAPABILITY_INTERNET) .build(), object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { // 网络恢复时重连 } } )
6.2 Android白屏问题
症状:WebView加载时出现短暂白屏
优化方案:
-
添加启动背景图:
xml复制<activity android:name=".MainActivity" android:theme="@style/SplashTheme"> </activity> <style name="SplashTheme" parent="Theme.AppCompat.NoActionBar"> <item name="android:windowBackground">@drawable/splash_bg</item> </style> -
预加载关键资源:
kotlin复制webView.settings.apply { javaScriptEnabled = true domStorageEnabled = true loadWithOverviewMode = true useWideViewPort = true } // 提前加载基础JS库 webView.loadUrl("file:///android_asset/preload.html")
6.3 跨域问题处理
症状:开发时出现CORS错误
完整解决方案:
-
开发环境配置代理:
javascript复制// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }) -
生产环境Nginx配置:
nginx复制location /api { proxy_pass http://backend; add_header 'Access-Control-Allow-Origin' '$http_origin'; add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Content-Type,Authorization'; } -
SpringBoot全局CORS配置:
java复制@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("*") .maxAge(3600); } }
7. 项目构建与部署
7.1 多环境配置管理
前端环境变量:
env复制# .env.development
VITE_API_BASE=http://localhost:8080
VITE_WS_BASE=ws://localhost:8080
# .env.production
VITE_API_BASE=https://api.yourdomain.com
VITE_WS_BASE=wss://api.yourdomain.com
后端Profile配置:
yaml复制# application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/club_dev
username: devuser
password: devpass
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-db:3306/club_prod
username: ${DB_USER}
password: ${DB_PASS}
7.2 CI/CD流水线设计
GitLab CI示例:
yaml复制stages:
- build
- test
- deploy
build-frontend:
stage: build
image: node:16
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
build-backend:
stage: build
image: maven:3.8
script:
- mvn clean package -DskipTests
artifacts:
paths:
- target/*.jar
deploy-prod:
stage: deploy
image: alpine
only:
- main
script:
- scp target/*.jar prod-server:/app/
- ssh prod-server "systemctl restart club-app"
7.3 Android打包优化
-
启用资源缩减:
gradle复制android { buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } -
生成不同CPU架构的APK:
gradle复制splits { abi { enable true reset() include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' universalApk false } } -
签名配置:
gradle复制signingConfigs { release { storeFile file("club.keystore") storePassword System.getenv("STORE_PASSWORD") keyAlias "club" keyPassword System.getenv("KEY_PASSWORD") } }
8. 项目扩展方向
8.1 接入第三方服务
-
微信登录集成:
java复制@GetMapping("/auth/wechat") public Result<String> wechatAuth(@RequestParam String code) { // 获取access_token WechatTokenResponse tokenResp = restTemplate.getForObject( "https://api.weixin.qq.com/sns/oauth2/access_token?appid={appid}&secret={secret}&code={code}&grant_type=authorization_code", WechatTokenResponse.class, appId, appSecret, code ); // 获取用户信息 WechatUserInfo userInfo = restTemplate.getForObject( "https://api.weixin.qq.com/sns/userinfo?access_token={token}&openid={openid}", WechatUserInfo.class, tokenResp.getAccessToken(), tokenResp.getOpenid() ); // 创建或更新本地用户 return Result.success(userService.createFromWechat(userInfo)); } -
支付宝支付集成:
kotlin复制fun createAlipayOrder(activity: Activity, user: User): String { val alipayClient = DefaultAlipayClient( "https://openapi.alipay.com/gateway.do", appId, privateKey, "json", "UTF-8", alipayPublicKey, "RSA2" ) val request = AlipayTradeAppPayRequest().apply { bizContent = JSON.toJSONString(mapOf( "subject" to activity.title, "out_trade_no" to generateOrderNo(), "total_amount" to activity.fee.toString(), "product_code" to "QUICK_MSECURITY_PAY" )) } return alipayClient.sdkExecute(request).body }
8.2 数据分析与可视化
-
用户行为埋点:
typescript复制// 封装埋点SDK const track = (event: string, payload?: object) => { if (import.meta.env.PROD) { navigator.sendBeacon('/analytics', JSON.stringify({ event, timestamp: Date.now(), path: location.pathname, ...payload })) } } // 路由切换监听 router.afterEach((to) => { track('page_view', { path: to.path }) }) -
Elasticsearch日志分析:
java复制@Configuration public class ElasticConfig { @Bean public RestHighLevelClient elasticClient() { return new RestHighLevelClient( RestClient.builder(new HttpHost("es-host", 9200, "http")) .setRequestConfigCallback(builder -> builder.setConnectTimeout(5000) .setSocketTimeout(60000)) ); } } // 日志查询示例 public List<LogEntry> searchLogs(String keyword) { SearchRequest request = new SearchRequest("club-logs"); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder() .query(QueryBuilders.multiMatchQuery(keyword, "message", "exception")); request.source(sourceBuilder); return elasticClient.search(request, RequestOptions.DEFAULT) .getHits() .map(hit -> JSON.parseObject(hit.getSourceAsString(), LogEntry.class)) .collect(Collectors.toList()); }
8.3 微服务化改造
当系统规模扩大时,可以考虑拆分为以下服务:
- 用户服务:处理认证、个人信息
- 社团服务:核心业务逻辑
- 活动服务:活动管理专用
- 消息服务:处理通知、聊天
采用Spring Cloud Alibaba实现:
java复制// 服务注册
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
// Feign客户端声明
@FeignClient(name = "activity-service")
public interface ActivityClient {
@GetMapping("/activities/{id}")
ActivityDTO getById(@PathVariable Long id);
}
// 接口调用
@Service
@RequiredArgsConstructor
public class ClubServiceImpl implements ClubService {
private final ActivityClient activityClient;
public ClubDetailDTO getDetail(Long id) {
Club club = getClub(id);
List<ActivityDTO> activities = activityClient.getByClubId(id);
return new ClubDetailDTO(club, activities);
}
}
9. 安全防护方案
9.1 常见攻击防护
-
SQL注入防护:
- 永远使用预编译语句:
java复制@Query(value = "SELECT * FROM users WHERE username = ?1", nativeQuery = true) User findByUsername(String username); - 定期使用SQLMap扫描
- 永远使用预编译语句:
-
XSS防护:
- 前端使用DOMPurify过滤:
javascript复制import DOMPurify from 'dompurify' const clean = DOMPurify.sanitize(dirtyHtml) - 后端设置响应头:
java复制http.headers() .xssProtection() .and() .contentSecurityPolicy("script-src 'self'");
- 前端使用DOMPurify过滤:
-
CSRF防护:
- Spring Security默认启用CSRF防护
- 前端Axios配置:
javascript复制axios.defaults.xsrfCookieName = 'XSRF-TOKEN' axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN'
9.2 敏感数据保护
-
密码加密:
java复制@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } // 使用示例 String encoded = passwordEncoder.encode(rawPassword); boolean matches = passwordEncoder.matches(rawPassword, encoded); -
数据脱敏:
java复制public class SensitiveSerializer extends JsonSerializer<String> { @Override public void serialize(String value, JsonGenerator gen, SerializerProvider provider) { try { if (value != null && value.length() > 2) { gen.writeString(value.substring(0, 1) + "****" + value.substring(value.length() - 1)); } else { gen.writeString("****"); } } catch (Exception e) { gen.writeString("****"); } } } // 在DTO字段上使用 @JsonSerialize(using = SensitiveSerializer.class) private String phoneNumber; -
HTTPS强制:
java复制@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.requiresChannel() .requestMatchers(r -> r.getHeader("X-Forwarded-Proto") != null) .requiresSecure(); } }
10. 监控与运维
10.1 应用监控
-
Prometheus监控集成:
java复制@Bean MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() { return registry -> registry.config().commonTags( "application", "club-app", "region", System.getenv("REGION") ); } // 自定义指标 @Service public class ActivityMetrics { private final Counter signupCounter; public ActivityMetrics(MeterRegistry registry) { signupCounter = Counter.builder("activity.signups") .description("Total activity signups") .register(registry); } public void recordSignup() { signupCounter.increment(); } } -
健康检查端点:
yaml复制management: endpoint: health: probes: enabled: true show-details: always endpoints: web: exposure: include: health,info,metrics
10.2 日志管理
-
ELK日志收集:
xml复制<!-- logstash-logback-encoder --> <dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.2</version> </dependency>xml复制<!-- logback-spring.xml --> <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <destination>logstash:5044</destination> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <customFields>{"app":"club-app","env":"${spring.profiles.active}"}</customFields> </encoder> </appender> -
关键操作审计:
java复制@Aspect @Component @RequiredArgsConstructor public class AuditLogAspect { private final AuditLogRepository repository; @AfterReturning( pointcut = "@annotation(com.example.audit.AuditLog)", returning = "result" ) public void logAfter(JoinPoint joinPoint, Object result) { AuditLogEntry entry = new AuditLogEntry(); entry.setOperation(joinPoint.getSignature().getName()); entry.setParams(Arrays.toString(joinPoint.getArgs())); entry.setResult(result != null ? result.toString() : null); repository.save(entry); } }
11. 测试策略
11.1 前端测试方案
-
组件测试:使用Vitest + Testing Library
javascript复制import { render, screen } from '@testing-library/vue' import ActivityCard from './ActivityCard.vue' test('显示活动基本信息', async () => { const activity = { title: '春季招新', time: '2023-03-15' } render(ActivityCard, { props: { activity } }) expect(screen.getByText(activity.title)).toBeInTheDocument() expect(screen.getByText(/报名截止/)).toBeInTheDocument() }) -
E2E测试:使用Cypress
javascript复制describe('活动报名流程', () => { it('成功报名活动', () => { cy.login('test@example.com', 'password') cy.visit('/activities/123') cy.get('.signup-btn').click() cy.contains('报名成功').should('be.visible') }) })
11.2 后端测试策略
-
单元测试:
java复制@ExtendWith(MockitoExtension.class) class ActivityServiceTest { @Mock private ActivityRepository activityRepo; @InjectMocks private ActivityService activityService; @Test void shouldReturnActivitiesWhenClubExists() { when(activityRepo.findByClubId(anyLong())) .thenReturn(List.of(new Activity("招新"))); List<Activity> result = activityService.getByClubId(1L); assertEquals(1, result.size()); assertEquals("招新", result.get(0).getTitle()); } } -
集成测试:
java复制@SpringBootTest @AutoConfigureMockMvc class ClubControllerITest { @Autowired private MockMvc mockMvc; @Test void shouldReturnClubWhenExists() throws Exception { mockMvc.perform(get("/clubs/1") .header("Authorization", "Bearer token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").exists()); } } -
性能测试:
java复制@SpringBootTest @ActiveProfiles("test") class SignUpPerformanceTest { @Autowired private ActivityService activityService; @Test void shouldHandleConcurrentSignups() { int threads = 100; ExecutorService executor = Executors.newFixedThreadPool(threads); List<Future<Boolean>> futures = IntStream.range(0, threads) .mapToObj(i -> (Callable<Boolean>) () -> activityService.signUp(1L, (long) i)) .map(executor::submit) .collect(Collectors.toList()); long successCount = futures.stream() .filter(f -> { try { return f.get(); } catch (Exception e) { return false; } }) .count(); assertEquals(50, successCount); // 假设活动名额为50 } }
12. 项目文档规范
12.1 API文档生成
使用Swagger + OpenAPI 3.0:
java复制@Configuration
@OpenAPIDefinition(
info = @Info(
title = "社团APP API",
version = "1.0",
description = "校园社团管理平台接口文档"
)
)
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.addSecurityItem(new SecurityRequirement().addList("JWT"))
.components(new Components()
.addSecuritySchemes("JWT", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
12.2 前端组件文档
使用Storybook + mdx:
mdx复制import { Meta, Story } from '@storybook/addon-docs'
import ActivityCard from './ActivityCard.vue'
<Meta title="Components/ActivityCard" component={ActivityCard} />
# ActivityCard
社团活动卡片组件,用于展示活动基本信息。
<Story name="Default">
{{
components: { ActivityCard },
template: `
<ActivityCard
:activity="{
title: '春季招新',
time: '2023-03-15',
location: '教学楼101'
}"
/>
`
}}
</Story>
12.3 数据库文档
使用Screw生成数据库文档:
xml复制<plugin>
<groupId>cn.smallbun.screw</groupId>
<artifactId>screw-maven-plugin</artifactId>
<version>1.0.5</version>
<configuration>
<driverClassName>com.mysql.cj.jdbc.Driver</driverClassName>
<jdbcUrl>jdbc:mysql://localhost:3306/club</jdbcUrl>
<username>root</username>
<password>123456</password>
<fileType>HTML</fileType>
<title>社团APP数据库文档</title>
</configuration>
</plugin>
13. 团队协作规范
13.1 Git工作流
采用Git Flow变种:
code复制main - 生产环境代码
release/* - 预发布分支
develop - 集成测试分支
feature/* - 功能开发分支
hotfix/* - 紧急修复分支
代码提交规范:
code复制feat: 添加新功能
fix: 修复bug
docs: 文档变更
style: 代码格式调整
refactor: 代码重构
test: 测试相关
chore: 构建过程或辅助工具变更
13.2 代码审查要点
-
后端审查清单:
- 接口是否有合适的权限控制
- 数据库查询是否使用索引
- 是否有N+1查询问题
- 事务边界是否合理
- 错误处理是否完备
-
前端审查清单:
- 组件是否合理拆分
- 状态管理是否恰当
- 是否有内存泄漏风险
- 网络请求是否有加载状态
- 是否处理了边界情况
-
移动端审查清单:
- 是否处理了网络状态变化
- 是否有内存泄漏
- 是否适配不同屏幕尺寸
- 是否处理了权限请求场景
- 耗电是否在合理范围
14. 项目升级与维护
14.1 依赖更新策略
-
定期扫描漏洞:
bash复制# 前端 npm audit # 后端 mvn org.owasp:dependency-check-maven:check -
分阶段升级:
- 开发环境先升级
- 通过测试后更新测试环境
- 最后更新生产环境
-
回滚方案:
bash复制# 前端回滚到指定版本 npm install package@version
