342 lines
9.2 KiB
Markdown
342 lines
9.2 KiB
Markdown
|
|
# Mosquito System Implementation Plan
|
|||
|
|
|
|||
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|||
|
|
|
|||
|
|
**Goal:** 统一鉴权与响应契约,补齐三端前端工程骨架,并让前后端可在同一契约下联调。
|
|||
|
|
|
|||
|
|
**Architecture:** 在后端引入 introspection 校验与缓存,统一 API 响应为 `ApiResponse`,并将鉴权策略按路由分层。前端三端共享组件库与 Design Tokens,使用一致的 API Client 与错误处理。
|
|||
|
|
|
|||
|
|
**Tech Stack:** Spring Boot 3, Java 17, Redis, Vite, Vue 3, TypeScript, Pinia, Vue Router, Tailwind CSS
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
> 注意:根据项目指令,本计划不包含 git commit 步骤。
|
|||
|
|
|
|||
|
|
### Task 1: 定义并落地 introspection 协议与缓存结构
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `src/main/java/com/mosquito/project/security/IntrospectionRequest.java`
|
|||
|
|
- Create: `src/main/java/com/mosquito/project/security/IntrospectionResponse.java`
|
|||
|
|
- Create: `src/main/java/com/mosquito/project/security/UserIntrospectionService.java`
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/config/AppConfig.java`
|
|||
|
|
- Modify: `src/main/resources/application.properties`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// src/test/java/com/mosquito/project/security/UserIntrospectionServiceTest.java
|
|||
|
|
@Test
|
|||
|
|
void shouldReturnInactive_whenTokenInvalid() {
|
|||
|
|
UserIntrospectionService service = buildServiceWithMockResponse(false);
|
|||
|
|
var result = service.introspect("bad-token");
|
|||
|
|
assertFalse(result.isActive());
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=UserIntrospectionServiceTest test`
|
|||
|
|
Expected: FAIL (class not found)
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public class IntrospectionResponse {
|
|||
|
|
private boolean active;
|
|||
|
|
private String userId;
|
|||
|
|
private String tenantId;
|
|||
|
|
private java.util.List<String> roles;
|
|||
|
|
private java.util.List<String> scopes;
|
|||
|
|
private long exp;
|
|||
|
|
private long iat;
|
|||
|
|
private String jti;
|
|||
|
|
// getters/setters
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=UserIntrospectionServiceTest test`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
### Task 2: 实现 API Key + 用户态双重鉴权拦截器
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `src/main/java/com/mosquito/project/web/UserAuthInterceptor.java`
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/config/WebMvcConfig.java`
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/web/ApiKeyAuthInterceptor.java`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// src/test/java/com/mosquito/project/web/UserAuthInterceptorTest.java
|
|||
|
|
@Test
|
|||
|
|
void shouldRejectRequest_whenMissingAuthorization() {
|
|||
|
|
var request = mockRequestWithoutAuth();
|
|||
|
|
var response = new MockHttpServletResponse();
|
|||
|
|
var result = interceptor.preHandle(request, response, new Object());
|
|||
|
|
assertFalse(result);
|
|||
|
|
assertEquals(401, response.getStatus());
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=UserAuthInterceptorTest test`
|
|||
|
|
Expected: FAIL (class not found)
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
|||
|
|
String token = request.getHeader("Authorization");
|
|||
|
|
if (token == null || !token.startsWith("Bearer ")) {
|
|||
|
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
// call UserIntrospectionService
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=UserAuthInterceptorTest test`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
### Task 3: 路由分层鉴权策略
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/config/WebMvcConfig.java`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// src/test/java/com/mosquito/project/config/WebMvcConfigTest.java
|
|||
|
|
@Test
|
|||
|
|
void shouldProtectMeEndpoints_withApiKeyAndUserAuth() {
|
|||
|
|
// verify interceptors order and path patterns
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=WebMvcConfigTest test`
|
|||
|
|
Expected: FAIL
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
registry.addInterceptor(apiKeyAuthInterceptor).addPathPatterns("/api/**");
|
|||
|
|
registry.addInterceptor(userAuthInterceptor).addPathPatterns("/api/v1/me/**", "/api/v1/activities/**", "/api/v1/api-keys/**", "/api/v1/share/**");
|
|||
|
|
registry.addInterceptor(apiKeyAuthInterceptor).excludePathPatterns("/r/**", "/actuator/**");
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=WebMvcConfigTest test`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
### Task 4: 统一 API 响应为 ApiResponse
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/controller/ActivityController.java`
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/controller/ApiKeyController.java`
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/controller/UserExperienceController.java`
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/controller/ShareTrackingController.java`
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/exception/GlobalExceptionHandler.java`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// src/test/java/com/mosquito/project/controller/ActivityControllerContractTest.java
|
|||
|
|
@Test
|
|||
|
|
void shouldReturnApiResponseEnvelope() throws Exception {
|
|||
|
|
mockMvc.perform(get("/api/v1/activities/1"))
|
|||
|
|
.andExpect(jsonPath("$.code").value(200))
|
|||
|
|
.andExpect(jsonPath("$.data").exists());
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=ActivityControllerContractTest test`
|
|||
|
|
Expected: FAIL
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
return ResponseEntity.ok(ApiResponse.success(activity));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=ActivityControllerContractTest test`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
### Task 5: 排行榜分页与元数据
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/controller/ActivityController.java`
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/service/ActivityService.java`
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/persistence/repository/ActivityRepository.java`
|
|||
|
|
- Modify: `src/test/java/com/mosquito/project/controller/ActivityStatsAndGraphControllerTest.java`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// add pagination meta assertion
|
|||
|
|
.andExpect(jsonPath("$.meta.pagination.total").value(3))
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=ActivityStatsAndGraphControllerTest test`
|
|||
|
|
Expected: FAIL
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
var data = list.subList(from, to);
|
|||
|
|
return ResponseEntity.ok(ApiResponse.paginated(data, page, size, list.size()));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=ActivityStatsAndGraphControllerTest test`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
### Task 6: 更新 Java SDK 与前端 API Client
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/sdk/ApiClient.java`
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/sdk/MosquitoClient.java`
|
|||
|
|
- Modify: `frontend/index.ts`
|
|||
|
|
- Modify: `frontend/components/MosquitoLeaderboard.vue`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// src/test/java/com/mosquito/project/sdk/ApiClientTest.java
|
|||
|
|
@Test
|
|||
|
|
void shouldUnwrapApiResponse() {
|
|||
|
|
// response: { code: 200, data: {...} }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=ApiClientTest test`
|
|||
|
|
Expected: FAIL
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// ApiClient: parse ApiResponse<T>, return data field
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=ApiClientTest test`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
### Task 7: H5 与管理端基础页面接通组件库
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `frontend/h5/src/views/ShareView.vue`
|
|||
|
|
- Create: `frontend/admin/src/views/ActivityListView.vue`
|
|||
|
|
- Modify: `frontend/h5/src/router/index.ts`
|
|||
|
|
- Modify: `frontend/admin/src/router/index.ts`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
// frontend/h5/src/tests/appRoutes.test.ts
|
|||
|
|
it('should render share page', () => {
|
|||
|
|
// mount router and assert route
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `npm --prefix "frontend/h5" run type-check`
|
|||
|
|
Expected: FAIL
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<MosquitoShareButton :activity-id="1" :user-id="1" />
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `npm --prefix "frontend/h5" run type-check`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
### Task 8: 更新 API 文档与对外契约
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `docs/api.md`
|
|||
|
|
- Modify: `README.md`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
# 手动校对:文档端点与控制器一致
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run verification**
|
|||
|
|
|
|||
|
|
Run: `rg "api/v1/me" "docs/api.md"`
|
|||
|
|
Expected: path consistent with controllers
|
|||
|
|
|
|||
|
|
**Step 3: Apply updates**
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
- 错误响应改为 ApiResponse
|
|||
|
|
- /api/v1/me/poster -> /api/v1/me/poster/image|html|config
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 9: 安全与配置校验
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/service/ApiKeyEncryptionService.java`
|
|||
|
|
- Modify: `src/main/resources/application-prod.yml`
|
|||
|
|
- Modify: `src/main/java/com/mosquito/project/config/CacheConfig.java`
|
|||
|
|
|
|||
|
|
**Step 1: Write the failing test**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Test
|
|||
|
|
void shouldFailStartup_whenEncryptionKeyDefault() {
|
|||
|
|
// assert illegal state
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Run test to verify it fails**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=ApiKeyEncryptionServiceTest test`
|
|||
|
|
Expected: FAIL
|
|||
|
|
|
|||
|
|
**Step 3: Write minimal implementation**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
if (isDefaultKey(encryptionKey)) {
|
|||
|
|
throw new IllegalStateException("Encryption key must be set in production");
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Run test to verify it passes**
|
|||
|
|
|
|||
|
|
Run: `mvn -Dtest=ApiKeyEncryptionServiceTest test`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
Plan complete and saved to `docs/plans/2026-01-26-mosquito-system-implementation-plan.md`. Two execution options:
|
|||
|
|
|
|||
|
|
1. Subagent-Driven (this session)
|
|||
|
|
2. Parallel Session (separate)
|
|||
|
|
|
|||
|
|
Which approach?
|