diff --git a/src/main/java/com/petstore/controller/UserController.java b/src/main/java/com/petstore/controller/UserController.java index 271e0da..521e593 100644 --- a/src/main/java/com/petstore/controller/UserController.java +++ b/src/main/java/com/petstore/controller/UserController.java @@ -2,6 +2,7 @@ package com.petstore.controller; import com.petstore.entity.User; import com.petstore.service.UserService; +import com.petstore.service.WechatMiniProgramService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -15,6 +16,7 @@ import java.util.Map; @CrossOrigin public class UserController { private final UserService userService; + private final WechatMiniProgramService wechatMiniProgramService; /** 老板注册店铺 */ @PostMapping("/register-boss") @@ -34,6 +36,22 @@ public class UserController { return userService.login(phone, code); } + /** + * 微信小程序:button open-type=getPhoneNumber 拿到的 phoneCode 换手机号并登录。 + */ + @PostMapping("/wx-phone-login") + public Map wxPhoneLogin(@RequestBody Map params) { + String phoneCode = params.get("phoneCode"); + if (phoneCode == null || phoneCode.isBlank()) { + return Map.of("code", 400, "message", "缺少手机号授权码"); + } + String phone = wechatMiniProgramService.getPhoneNumberByCode(phoneCode); + if (phone == null || phone.isBlank()) { + return Map.of("code", 400, "message", "获取手机号失败,请检查服务端微信小程序 AppID/AppSecret 配置,或使用验证码登录"); + } + return userService.loginByVerifiedPhone(phone); + } + /** 员工注册(邀请码方式) */ @PostMapping("/register-staff") public Map registerStaff(@RequestBody Map params) { diff --git a/src/main/java/com/petstore/service/UserService.java b/src/main/java/com/petstore/service/UserService.java index 538957c..95745da 100644 --- a/src/main/java/com/petstore/service/UserService.java +++ b/src/main/java/com/petstore/service/UserService.java @@ -55,9 +55,18 @@ public class UserService { if (!"123456".equals(code)) { return Map.of("code", 401, "message", "验证码错误"); } + return loginByVerifiedPhone(phone); + } + + /** + * 已通过短信或微信授权校验后的手机号登录(与验证码登录成功后的逻辑一致)。 + */ + public Map loginByVerifiedPhone(String phone) { + if (phone == null || !phone.matches("^1\\d{10}$")) { + return Map.of("code", 400, "message", "手机号格式不正确"); + } User user = userMapper.findByPhone(phone); if (user == null) { - // 自动注册为 C 端用户 (customer) user = new User(); user.setUsername(phone); user.setPhone(phone); diff --git a/src/main/java/com/petstore/service/WechatMiniProgramService.java b/src/main/java/com/petstore/service/WechatMiniProgramService.java new file mode 100644 index 0000000..20eb2f2 --- /dev/null +++ b/src/main/java/com/petstore/service/WechatMiniProgramService.java @@ -0,0 +1,133 @@ +package com.petstore.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.petstore.config.WechatConfig; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +/** + * 微信小程序:通过 getPhoneNumber 返回的 code 换取用户手机号。 + * 文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class WechatMiniProgramService { + private final WechatConfig wechatConfig; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final RestTemplate restTemplate = new RestTemplate(); + + private volatile String cachedAccessToken; + private volatile long tokenExpiresAtEpochMs; + + private boolean isConfigured() { + String id = wechatConfig.getAppid(); + String sec = wechatConfig.getAppsecret(); + return id != null && !id.isBlank() + && sec != null && !sec.isBlank() + && !"YOUR_APPID".equals(id) + && !"YOUR_APPSECRET".equals(sec); + } + + /** + * @param phoneCode 前端 button getPhoneNumber 回调中的 detail.code + * @return 11 位大陆手机号,失败返回 null + */ + public String getPhoneNumberByCode(String phoneCode) { + if (phoneCode == null || phoneCode.isBlank()) { + return null; + } + if (!isConfigured()) { + log.warn("微信 appid/appsecret 未配置,无法换取手机号"); + return null; + } + try { + String accessToken = getAccessToken(); + if (accessToken == null) { + return null; + } + String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity> entity = new HttpEntity<>(Map.of("code", phoneCode), headers); + String body = restTemplate.postForObject(url, entity, String.class); + if (body == null) { + return null; + } + JsonNode root = objectMapper.readTree(body); + int errcode = root.path("errcode").asInt(-1); + if (errcode != 0) { + log.warn("getuserphonenumber 失败: {}", body); + return null; + } + String raw = root.path("phone_info").path("phoneNumber").asText(""); + return normalizeMainlandPhone(raw); + } catch (Exception e) { + log.warn("换取手机号异常", e); + return null; + } + } + + private String getAccessToken() { + long now = System.currentTimeMillis(); + if (cachedAccessToken != null && now < tokenExpiresAtEpochMs - 60_000) { + return cachedAccessToken; + } + synchronized (this) { + now = System.currentTimeMillis(); + if (cachedAccessToken != null && now < tokenExpiresAtEpochMs - 60_000) { + return cachedAccessToken; + } + String url = String.format( + "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", + wechatConfig.getAppid(), + wechatConfig.getAppsecret()); + try { + String body = restTemplate.getForObject(url, String.class); + if (body == null) { + return null; + } + JsonNode root = objectMapper.readTree(body); + if (root.hasNonNull("errcode") && root.path("errcode").asInt() != 0) { + log.warn("获取 access_token 失败: {}", body); + return null; + } + String token = root.path("access_token").asText(null); + int expiresIn = root.path("expires_in").asInt(7200); + if (token == null || token.isBlank()) { + return null; + } + cachedAccessToken = token; + tokenExpiresAtEpochMs = System.currentTimeMillis() + expiresIn * 1000L; + return cachedAccessToken; + } catch (Exception e) { + log.warn("获取 access_token 异常", e); + return null; + } + } + } + + /** 微信可能返回带区号的号码,统一为 11 位 */ + private String normalizeMainlandPhone(String raw) { + if (raw == null || raw.isBlank()) { + return null; + } + String digits = raw.replaceAll("\\D", ""); + if (digits.startsWith("86") && digits.length() == 13) { + digits = digits.substring(2); + } + if (digits.length() == 11) { + return digits; + } + log.warn("无法解析为 11 位手机号: {}", raw); + return null; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5ef7cee..ba735ee 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -35,8 +35,8 @@ logging: level: com.petstore: debug -# 微信登录配置(需替换为实际值) +# 微信小程序(与 manifest / 微信后台一致) wechat: - appid: YOUR_APPID - appsecret: YOUR_APPSECRET + appid: wx8ca2dfa89af72edf + appsecret: 7afb49f3a31fe9b5a083c7c40be45c5b redirect_uri: http://localhost:8080/api/wechat/callback