diff --git a/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityController.java b/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityController.java index 8f35852..54efcc1 100644 --- a/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityController.java +++ b/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityController.java @@ -25,7 +25,7 @@ public class ActivityController { @GetMapping public ApiResponse getActivities( @AuthenticationPrincipal UserPrincipal principal) { - String userId = principal.id(); + String userId = principal == null ? null : principal.id(); return ApiResponse.ok(activityService.getActivities(userId)); } diff --git a/backend/src/main/java/com/danyang/match/platform/shared/config/SecurityConfig.java b/backend/src/main/java/com/danyang/match/platform/shared/config/SecurityConfig.java index 2f3b1e7..5d5779e 100644 --- a/backend/src/main/java/com/danyang/match/platform/shared/config/SecurityConfig.java +++ b/backend/src/main/java/com/danyang/match/platform/shared/config/SecurityConfig.java @@ -42,6 +42,7 @@ public class SecurityConfig { .requestMatchers("/auth/wechat/login", "/auth/refresh", "/auth/operator/login").permitAll() .requestMatchers("/health/**").permitAll() .requestMatchers("/version").permitAll() + .requestMatchers("/app/config").permitAll() .requestMatchers("/activities").permitAll() .requestMatchers("/auth/**").authenticated() // 操作台接口需要认证 diff --git a/backend/src/test/java/com/danyang/match/platform/security/PublicMiniappApiTest.java b/backend/src/test/java/com/danyang/match/platform/security/PublicMiniappApiTest.java new file mode 100644 index 0000000..5baa111 --- /dev/null +++ b/backend/src/test/java/com/danyang/match/platform/security/PublicMiniappApiTest.java @@ -0,0 +1,43 @@ +package com.danyang.match.platform.security; + +import com.danyang.match.platform.activity.service.ActivityService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class PublicMiniappApiTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ActivityService activityService; + + @Test + void appConfigIsAvailableWithoutLogin() throws Exception { + mockMvc.perform(get("/api/v1/app/config").contextPath("/api/v1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + void activityListIsAvailableWithoutLogin() throws Exception { + when(activityService.getActivities(null)).thenReturn(List.of()); + + mockMvc.perform(get("/api/v1/activities").contextPath("/api/v1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } +} diff --git a/miniapp/stores/authStore.js b/miniapp/stores/authStore.js index 547e002..b659a12 100644 --- a/miniapp/stores/authStore.js +++ b/miniapp/stores/authStore.js @@ -35,13 +35,18 @@ class AuthStore extends BaseStore { /** 冷启动从 storage 还原登录态 */ hydrate() { const snapshot = BaseStore.hydrate(STORAGE_KEYS.IS_LOGGED_IN); + if (!api.getToken()) { + try { wx.removeStorageSync(STORAGE_KEYS.IS_LOGGED_IN); } catch (e) {} + this.setState({ isLoggedIn: false, userId: null, phoneBound: false }); + return; + } if (snapshot && typeof snapshot === 'object') { this.setState({ isLoggedIn: !!snapshot.isLoggedIn, userId: snapshot.userId || null, phoneBound: !!snapshot.phoneBound, }); - } else if (api.getToken()) { + } else { // 兼容旧版仅存了 token 的场景 this.setState({ isLoggedIn: true }); } diff --git a/ui-tests/miniapp-auth-contracts.mjs b/ui-tests/miniapp-auth-contracts.mjs new file mode 100644 index 0000000..681651c --- /dev/null +++ b/ui-tests/miniapp-auth-contracts.mjs @@ -0,0 +1,22 @@ +import assert from 'node:assert/strict' +import { createRequire } from 'node:module' + +const removed = [] +globalThis.wx = { + getStorageSync(key) { + if (key === 'isLoggedIn') return { isLoggedIn: true, userId: 'stale-user' } + return '' + }, + removeStorageSync(key) { + removed.push(key) + }, +} + +const require = createRequire(import.meta.url) +const authStore = require('../miniapp/stores/authStore.js') +authStore.hydrate() + +assert.equal(authStore.state.isLoggedIn, false, '没有 access token 时不得恢复登录态') +assert.ok(removed.includes('isLoggedIn'), '应清理失真的登录快照') + +console.log('Miniapp auth contracts passed')