tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/config/AppWechatAuthProperties.java
New file @@ -0,0 +1,87 @@ package com.tcm.training.admin.app.auth.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * APP 微信登录配置。 */ @Component @ConfigurationProperties(prefix = "app.auth.wechat") public class AppWechatAuthProperties { private String appId = ""; private String appSecret = ""; private String userInfoUrl = "https://api.weixin.qq.com/sns/userinfo"; private String accessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token"; /** * 获取微信开放平台 AppId。 */ public String getAppId() { return appId; } /** * 设置微信开放平台 AppId。 */ public void setAppId(String appId) { this.appId = appId; } /** * 获取微信开放平台 AppSecret。 */ public String getAppSecret() { return appSecret; } /** * 设置微信开放平台 AppSecret。 */ public void setAppSecret(String appSecret) { this.appSecret = appSecret; } /** * 获取微信用户信息接口地址。 */ public String getUserInfoUrl() { return userInfoUrl; } /** * 设置微信用户信息接口地址。 */ public void setUserInfoUrl(String userInfoUrl) { this.userInfoUrl = userInfoUrl; } /** * 获取微信授权换票接口地址。 */ public String getAccessTokenUrl() { return accessTokenUrl; } /** * 设置微信授权换票接口地址。 */ public void setAccessTokenUrl(String accessTokenUrl) { this.accessTokenUrl = accessTokenUrl; } /** * 判断是否已配置服务端换票所需参数。 */ public boolean isServerAuthConfigured() { return hasText(appId) && hasText(appSecret); } /** * 判断字符串是否包含有效文本。 */ private boolean hasText(String value) { return value != null && !value.trim().isEmpty(); } } tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/controller/AppAuthController.java
@@ -5,6 +5,7 @@ import com.tcm.training.admin.app.auth.dto.AppPasswordResetRequest; import com.tcm.training.admin.app.auth.dto.AppPasswordResetVerifyRequest; import com.tcm.training.admin.app.auth.dto.AppSmsLoginRequest; import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest; import com.tcm.training.admin.app.auth.service.AppAuthService; import com.tcm.training.admin.app.auth.vo.AppLoginResponse; import com.tcm.training.admin.app.auth.vo.AppPasswordResetVerifyResponse; @@ -60,6 +61,14 @@ } /** * 使用微信授权信息完成 APP 登录。 */ @PostMapping("/wechat-login") public ApiResponse<AppLoginResponse> wechatLogin(@RequestBody AppWechatLoginRequest request) { return ApiResponse.ok(appAuthService.wechatLogin(request)); } /** * 发送 APP 忘记密码验证码。 */ @PostMapping("/password-reset/code") tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppWechatLoginRequest.java
New file @@ -0,0 +1,19 @@ package com.tcm.training.admin.app.auth.dto; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; /** * APP 微信登录请求。 */ @Getter @Setter @NoArgsConstructor public class AppWechatLoginRequest { private String code; private String accessToken; private String openid; private String unionid; } tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java
@@ -6,6 +6,7 @@ import com.tcm.training.admin.app.auth.dto.AppPasswordResetVerifyRequest; import com.tcm.training.admin.app.auth.dto.AppSmsCodeSendRequest; import com.tcm.training.admin.app.auth.dto.AppSmsLoginRequest; import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest; import com.tcm.training.admin.app.auth.vo.AppLoginResponse; import com.tcm.training.admin.app.auth.vo.AppLoginUserVO; import com.tcm.training.admin.app.auth.vo.AppPasswordResetVerifyResponse; @@ -17,12 +18,13 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; /** * APP 登录认证服务,负责短信登录、密码登录与忘记密码流程。 * APP 登录认证服务,负责短信登录、密码登录、微信登录与忘记密码流程。 */ @Service public class AppAuthService { @@ -36,6 +38,7 @@ private final AppSmsCodeService appSmsCodeService; private final BCryptPasswordEncoder passwordEncoder; private final AppAuthSessionService appAuthSessionService; private final AppWechatAuthClient appWechatAuthClient; private final StringRedisTemplate stringRedisTemplate; /** @@ -45,11 +48,13 @@ AppSmsCodeService appSmsCodeService, BCryptPasswordEncoder passwordEncoder, AppAuthSessionService appAuthSessionService, AppWechatAuthClient appWechatAuthClient, StringRedisTemplate stringRedisTemplate) { this.userMapper = userMapper; this.appSmsCodeService = appSmsCodeService; this.passwordEncoder = passwordEncoder; this.appAuthSessionService = appAuthSessionService; this.appWechatAuthClient = appWechatAuthClient; this.stringRedisTemplate = stringRedisTemplate; } @@ -91,6 +96,25 @@ if (!passwordEncoder.matches(password, user.getPassword())) { throw new IllegalArgumentException("账号或密码错误"); } return createLoginResponse(user); } /** * 使用微信授权结果完成 APP 登录,首次登录自动创建普通用户。 */ public AppLoginResponse wechatLogin(AppWechatLoginRequest request) { AppWechatUserProfile profile = appWechatAuthClient.resolveUserProfile(request); AppUser user = findUserByWechatOpenid(profile.getOpenid()); if (user == null) { user = buildWechatUser(profile); userMapper.insert(user); return createLoginResponse(user); } requireEnabledUser(user); // 登录时同步微信专用资料,并仅在本地昵称头像为空时补齐展示字段。 applyWechatProfile(user, profile, false); userMapper.updateById(user); return createLoginResponse(user); } @@ -145,7 +169,7 @@ loginUser.setId(user.getId()); loginUser.setNickname(resolveNickname(user)); loginUser.setPhone(user.getPhone()); loginUser.setAvatar(user.getAvatar()); loginUser.setAvatar(resolveAvatar(user)); loginUser.setType(user.getType()); loginUser.setStatus(user.getStatus()); loginUser.setClinicId(user.getClinicId()); @@ -172,10 +196,77 @@ */ private AppUser requireEnabledUserByPhone(String phone) { AppUser user = requireExistingUserByPhone(phone); requireEnabledUser(user); return user; } /** * 校验给定 APP 用户是否处于启用状态。 */ private void requireEnabledUser(AppUser user) { if (user.getStatus() == null || user.getStatus() != 1) { throw new IllegalArgumentException("账号已被禁用"); } } /** * 按微信 openid 查询本地 APP 用户。 */ private AppUser findUserByWechatOpenid(String openid) { return userMapper.selectOne(new LambdaQueryWrapper<AppUser>() .eq(AppUser::getWechatOpenid, openid) .eq(AppUser::getDeleted, 0) .last("limit 1")); } /** * 基于微信资料构造首次登录时的本地普通用户。 */ private AppUser buildWechatUser(AppWechatUserProfile profile) { AppUser user = new AppUser(); user.setType(0); user.setStatus(1); applyWechatProfile(user, profile, true); return user; } /** * 将微信资料回填到本地用户,必要时补齐昵称头像展示字段。 */ private void applyWechatProfile(AppUser user, AppWechatUserProfile profile, boolean syncLocalProfile) { String nickname = trimToNull(profile.getNickname()); String avatar = trimToNull(profile.getAvatar()); user.setWechatOpenid(profile.getOpenid()); user.setWechatNickname(nickname); user.setWechatAvatar(avatar); user.setWechatBoundAt(LocalDateTime.now()); if (syncLocalProfile || trimToNull(user.getNickname()) == null) { user.setNickname(nickname == null ? profile.getOpenid() : nickname); } if (syncLocalProfile || trimToNull(user.getAvatar()) == null) { user.setAvatar(avatar); } if (syncLocalProfile || user.getGender() == null) { user.setGender(resolveWechatGender(profile.getSex())); } } /** * 将微信性别值转换为本地用户性别枚举。 */ private Integer resolveWechatGender(Integer sex) { if (sex == null) { return null; } if (sex == 1) { return 1; } if (sex == 2) { return 0; } return null; } /** @@ -301,7 +392,25 @@ */ private String resolveNickname(AppUser user) { String nickname = trimToNull(user.getNickname()); return nickname == null ? user.getPhone() : nickname; if (nickname != null) { return nickname; } String wechatNickname = trimToNull(user.getWechatNickname()); if (wechatNickname != null) { return wechatNickname; } return user.getPhone(); } /** * 优先使用本地头像,缺失时回退到微信头像。 */ private String resolveAvatar(AppUser user) { String avatar = trimToNull(user.getAvatar()); if (avatar != null) { return avatar; } return trimToNull(user.getWechatAvatar()); } /** tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatAuthClient.java
New file @@ -0,0 +1,250 @@ package com.tcm.training.admin.app.auth.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.tcm.training.admin.app.auth.config.AppWechatAuthProperties; import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.web.client.RestOperations; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; /** * 微信登录接口客户端,负责换取授权信息并拉取微信头像昵称。 */ @Service public class AppWechatAuthClient { private static final Logger log = LoggerFactory.getLogger(AppWechatAuthClient.class); private final AppWechatAuthProperties properties; private final ObjectMapper objectMapper; private final RestOperations restOperations; /** * 使用默认 HTTP 客户端构造微信认证客户端。 */ public AppWechatAuthClient(AppWechatAuthProperties properties, ObjectMapper objectMapper) { this(properties, objectMapper, new RestTemplate()); } /** * 注入微信配置、JSON 解析器与 HTTP 客户端。 */ AppWechatAuthClient(AppWechatAuthProperties properties, ObjectMapper objectMapper, RestOperations restOperations) { this.properties = properties; this.objectMapper = objectMapper; this.restOperations = restOperations; } /** * 按当前请求中的授权信息换取微信用户资料。 */ public AppWechatUserProfile resolveUserProfile(AppWechatLoginRequest request) { if (request == null) { throw new IllegalArgumentException("微信授权信息不完整"); } String accessToken = trimToNull(request.getAccessToken()); String openid = trimToNull(request.getOpenid()); String unionid = trimToNull(request.getUnionid()); if (accessToken != null && openid != null) { AppWechatUserProfile profile = fetchUserInfo(accessToken, openid); if (profile.getUnionid() == null) { profile.setUnionid(unionid); } return profile; } String code = trimToNull(request.getCode()); if (code == null) { throw new IllegalArgumentException("微信授权信息不完整"); } if (!properties.isServerAuthConfigured()) { throw new IllegalArgumentException("微信登录暂未配置,请稍后重试"); } WechatAccessTokenSnapshot tokenSnapshot = exchangeCode(code); AppWechatUserProfile profile = fetchUserInfo(tokenSnapshot.getAccessToken(), tokenSnapshot.getOpenid()); if (profile.getUnionid() == null) { profile.setUnionid(tokenSnapshot.getUnionid()); } return profile; } /** * 使用临时 code 向微信换取 accessToken 与 openid。 */ private WechatAccessTokenSnapshot exchangeCode(String code) { String url = UriComponentsBuilder.fromHttpUrl(properties.getAccessTokenUrl()) .queryParam("appid", properties.getAppId()) .queryParam("secret", properties.getAppSecret()) .queryParam("code", code) .queryParam("grant_type", "authorization_code") .build(true) .toUriString(); JsonNode payload = getWechatPayload(url, "微信登录授权失败"); ensureWechatSuccess(payload, "微信登录授权失败"); String accessToken = trimToNull(readText(payload, "access_token")); String openid = trimToNull(readText(payload, "openid")); if (accessToken == null || openid == null) { throw new IllegalArgumentException("微信登录授权失败"); } WechatAccessTokenSnapshot snapshot = new WechatAccessTokenSnapshot(); snapshot.setAccessToken(accessToken); snapshot.setOpenid(openid); snapshot.setUnionid(trimToNull(readText(payload, "unionid"))); return snapshot; } /** * 使用微信 accessToken 与 openid 拉取昵称、头像和性别。 */ private AppWechatUserProfile fetchUserInfo(String accessToken, String openid) { String url = UriComponentsBuilder.fromHttpUrl(properties.getUserInfoUrl()) .queryParam("access_token", accessToken) .queryParam("openid", openid) .queryParam("lang", "zh_CN") .build(true) .toUriString(); JsonNode payload = getWechatPayload(url, "获取微信用户信息失败"); ensureWechatSuccess(payload, "获取微信用户信息失败"); AppWechatUserProfile profile = new AppWechatUserProfile(); profile.setOpenid(openid); profile.setUnionid(trimToNull(readText(payload, "unionid"))); profile.setNickname(trimToNull(readText(payload, "nickname"))); profile.setAvatar(trimToNull(readText(payload, "headimgurl"))); profile.setSex(readInteger(payload, "sex")); return profile; } /** * 发起微信接口调用并将响应解析为 JSON。 */ private JsonNode getWechatPayload(String url, String failureMessage) { try { String responseBody = restOperations.getForObject(url, String.class); if (responseBody == null || responseBody.trim().isEmpty()) { throw new IllegalArgumentException(failureMessage); } return objectMapper.readTree(responseBody); } catch (IllegalArgumentException ex) { throw ex; } catch (Exception ex) { log.error("调用微信接口失败: url={}", url, ex); throw new IllegalArgumentException(failureMessage); } } /** * 校验微信接口返回是否为成功态。 */ private void ensureWechatSuccess(JsonNode payload, String failureMessage) { Integer errcode = readInteger(payload, "errcode"); if (errcode == null || errcode == 0) { return; } String errmsg = trimToNull(readText(payload, "errmsg")); if (errmsg == null) { errmsg = failureMessage; } log.warn("微信接口返回异常: errcode={}, errmsg={}", errcode, errmsg); throw new IllegalArgumentException(errmsg); } /** * 读取字符串字段。 */ private String readText(JsonNode payload, String fieldName) { JsonNode valueNode = payload.get(fieldName); if (valueNode == null || valueNode.isNull()) { return null; } return valueNode.asText(); } /** * 读取整数字段。 */ private Integer readInteger(JsonNode payload, String fieldName) { String value = trimToNull(readText(payload, fieldName)); if (value == null) { return null; } try { return Integer.valueOf(value); } catch (NumberFormatException ex) { return null; } } /** * 将空白字符串归一化为 null。 */ private String trimToNull(String value) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } /** * 保存微信换票结果,避免在多个私有方法之间散落参数。 */ private static class WechatAccessTokenSnapshot { private String accessToken; private String openid; private String unionid; /** * 获取 accessToken。 */ public String getAccessToken() { return accessToken; } /** * 设置 accessToken。 */ public void setAccessToken(String accessToken) { this.accessToken = accessToken; } /** * 获取 openid。 */ public String getOpenid() { return openid; } /** * 设置 openid。 */ public void setOpenid(String openid) { this.openid = openid; } /** * 获取 unionid。 */ public String getUnionid() { return unionid; } /** * 设置 unionid。 */ public void setUnionid(String unionid) { this.unionid = unionid; } } } tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppWechatUserProfile.java
New file @@ -0,0 +1,20 @@ package com.tcm.training.admin.app.auth.service; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; /** * 微信登录链路解析出的用户资料。 */ @Getter @Setter @NoArgsConstructor public class AppWechatUserProfile { private String openid; private String unionid; private String nickname; private String avatar; private Integer sex; } tcm-training-admin-backend/src/main/resources/application.yml
@@ -72,6 +72,15 @@ debug-enabled: false # 开发环境调试验证码,默认留空,需在 application-local.yml 中显式开启 debug-code: "" wechat: # 微信开放平台 AppId,当前阶段可先留空,补齐后启用服务端换票模式 app-id: "" # 微信开放平台 AppSecret,当前阶段可先留空,补齐后启用服务端换票模式 app-secret: "" # 微信用户信息接口地址 user-info-url: https://api.weixin.qq.com/sns/userinfo # 微信授权换票接口地址 access-token-url: https://api.weixin.qq.com/sns/oauth2/access_token logging: level: tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java
@@ -2,11 +2,11 @@ import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.fasterxml.jackson.databind.ObjectMapper; import com.tcm.training.admin.app.auth.dto.AppPasswordLoginRequest; import com.tcm.training.admin.app.auth.dto.AppPasswordResetRequest; import com.tcm.training.admin.app.auth.dto.AppPasswordResetVerifyRequest; import com.tcm.training.admin.app.auth.dto.AppSmsCodeSendRequest; import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest; import com.tcm.training.admin.app.auth.vo.AppLoginResponse; import com.tcm.training.admin.app.auth.vo.AppPasswordResetVerifyResponse; import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse; @@ -31,10 +31,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -63,6 +65,9 @@ private AppAuthSessionService appAuthSessionService; @Mock private AppWechatAuthClient appWechatAuthClient; @Mock private StringRedisTemplate stringRedisTemplate; @Mock @@ -83,6 +88,7 @@ appSmsCodeService, passwordEncoder, appAuthSessionService, appWechatAuthClient, stringRedisTemplate ); } @@ -213,6 +219,82 @@ } /** * 微信已绑定用户登录时应更新微信资料并返回统一登录响应。 */ @Test void wechatLoginShouldReuseExistingUser() { AppWechatLoginRequest request = new AppWechatLoginRequest(); request.setAccessToken("access-token"); request.setOpenid("openid-1"); AppWechatUserProfile profile = new AppWechatUserProfile(); profile.setOpenid("openid-1"); profile.setNickname("微信昵称"); profile.setAvatar("https://cdn.example/avatar.png"); profile.setSex(2); AppUser user = buildEnabledUser("13800138000", "ENCODED"); user.setWechatOpenid("openid-1"); user.setNickname(null); user.setAvatar(null); SaTokenInfo tokenInfo = new SaTokenInfo(); tokenInfo.setTokenValue("wechat-token"); tokenInfo.setTokenTimeout(7200L); when(appWechatAuthClient.resolveUserProfile(request)).thenReturn(profile); when(userMapper.selectOne(any())).thenReturn(user); when(appAuthSessionService.getTokenInfo()).thenReturn(tokenInfo); AppLoginResponse response = appAuthService.wechatLogin(request); verify(userMapper).updateById(appUserCaptor.capture()); verify(userMapper, never()).insert(any()); assertEquals("openid-1", appUserCaptor.getValue().getWechatOpenid()); assertEquals("微信昵称", appUserCaptor.getValue().getNickname()); assertEquals("https://cdn.example/avatar.png", appUserCaptor.getValue().getAvatar()); assertEquals(Integer.valueOf(0), appUserCaptor.getValue().getGender()); assertEquals("微信昵称", response.getUser().getNickname()); assertEquals("https://cdn.example/avatar.png", response.getUser().getAvatar()); } /** * 微信首次登录时应自动创建普通用户并返回统一登录响应。 */ @Test void wechatLoginShouldCreateUserWhenOpenidNotExists() { AppWechatLoginRequest request = new AppWechatLoginRequest(); request.setAccessToken("access-token"); request.setOpenid("openid-new"); AppWechatUserProfile profile = new AppWechatUserProfile(); profile.setOpenid("openid-new"); profile.setNickname("新微信用户"); profile.setAvatar("https://cdn.example/new-avatar.png"); profile.setSex(1); SaTokenInfo tokenInfo = new SaTokenInfo(); tokenInfo.setTokenValue("new-token"); tokenInfo.setTokenTimeout(7200L); when(appWechatAuthClient.resolveUserProfile(request)).thenReturn(profile); when(userMapper.selectOne(any())).thenReturn(null); when(appAuthSessionService.getTokenInfo()).thenReturn(tokenInfo); AppLoginResponse response = appAuthService.wechatLogin(request); verify(userMapper).insert(appUserCaptor.capture()); verify(userMapper, never()).updateById(any()); assertNull(appUserCaptor.getValue().getPhone()); assertEquals("openid-new", appUserCaptor.getValue().getWechatOpenid()); assertEquals("新微信用户", appUserCaptor.getValue().getNickname()); assertEquals("https://cdn.example/new-avatar.png", appUserCaptor.getValue().getAvatar()); assertEquals(Integer.valueOf(1), appUserCaptor.getValue().getGender()); assertEquals(Integer.valueOf(0), appUserCaptor.getValue().getType()); assertEquals("new-token", response.getToken()); } /** * 统一构造启用中的 APP 用户测试桩。 */ private AppUser buildEnabledUser(String phone, String password) { traditionalApp/pages/login/login.vue
@@ -58,7 +58,7 @@ <script setup lang="ts"> import { onLoad } from '@dcloudio/uni-app' import { onUnmounted, ref } from 'vue' import { passwordLogin, sendSmsCode, smsLogin } from './service.js' import { passwordLogin, sendSmsCode, smsLogin, wechatLogin } from './service.js' const loginType = ref<'code' | 'password'>('code') const phone = ref('') @@ -93,6 +93,27 @@ msg?: string message?: string data: T } interface AppWechatLoginPayload { code?: string accessToken?: string openid?: string unionid?: string } interface WechatAuthResult { access_token?: string accessToken?: string openid?: string openId?: string unionid?: string unionId?: string } interface WechatLoginResult { code?: string authResult?: WechatAuthResult } /** @@ -150,25 +171,42 @@ } /** * 执行微信登录占位逻辑。 * 执行微信登录,优先将微信授权结果交给后端拉取昵称和头像。 */ const handleWechatLogin = () => { const handleWechatLogin = async () => { if (loading.value) { return } // #ifdef APP-PLUS uni.login({ provider: 'weixin', success: () => { uni.showToast({ title: '微信登录暂未开放', icon: 'none' }) }, fail: () => { uni.showToast({ title: '微信登录失败', icon: 'none' }) loading.value = true try { const loginRes = await requestWechatAuthorization() const payload = buildWechatLoginPayload(loginRes) if (!payload) { return } }) const res = await wechatLogin(payload) if (!isSuccessResponse(res)) { return } saveLoginSession(res.data) uni.showToast({ title: '登录成功', icon: 'success' }) setTimeout(() => { uni.switchTab({ url: '/pages/home/home' }) }, 300) } catch (error) { uni.showToast({ title: '微信登录失败', icon: 'none' }) } finally { loading.value = false } // #endif // #ifndef APP-PLUS @@ -177,6 +215,47 @@ icon: 'none' }) // #endif } /** * 拉起微信授权,获取后端换取用户资料所需的授权结果。 */ const requestWechatAuthorization = () => { return new Promise<WechatLoginResult>((resolve, reject) => { uni.login({ provider: 'weixin', success: (res) => { resolve(res as WechatLoginResult) }, fail: (error) => { reject(error) } }) }) } /** * 从 uni.login 返回结果中提取微信登录接口需要的参数。 */ const buildWechatLoginPayload = (loginRes: WechatLoginResult) => { const authResult = loginRes?.authResult || {} const payload: AppWechatLoginPayload = { code: normalizeOptionalText(loginRes?.code), accessToken: normalizeOptionalText(authResult.access_token || authResult.accessToken), openid: normalizeOptionalText(authResult.openid || authResult.openId), unionid: normalizeOptionalText(authResult.unionid || authResult.unionId) } // 当前阶段优先兼容 APP 端直接返回的 accessToken/openid,后续补齐配置后也可走 code 模式。 if ((payload.accessToken && payload.openid) || payload.code) { return payload } uni.showToast({ title: '微信授权信息缺失', icon: 'none' }) return null } /** @@ -330,6 +409,16 @@ } /** * 将可选字符串参数归一化为空串或有效文本。 */ const normalizeOptionalText = (value: unknown) => { if (typeof value !== 'string') { return '' } return value.trim() } /** * 判断接口响应是否为成功态。 */ const isSuccessResponse = (response: ApiResponse<unknown> | undefined) => { traditionalApp/pages/login/service.js
@@ -1,10 +1,10 @@ import request from '@/utils/request.js' /** * 微信一键登录接口占位。 * 使用微信授权结果完成 APP 登录。 */ export const login = (data) => { return request.post('/openIdByJsCode', data) export const wechatLogin = (data) => { return request.post('/api/app/auth/wechat-login', data) } /**