feat: 微信小程序用户登录服务及配置

This commit is contained in:
MaDaLei 2026-04-14 16:56:02 +08:00
parent 3c7c8d1f73
commit 6e3c49d02d
4 changed files with 164 additions and 4 deletions

View File

@ -2,6 +2,7 @@ package com.petstore.controller;
import com.petstore.entity.User; import com.petstore.entity.User;
import com.petstore.service.UserService; import com.petstore.service.UserService;
import com.petstore.service.WechatMiniProgramService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -15,6 +16,7 @@ import java.util.Map;
@CrossOrigin @CrossOrigin
public class UserController { public class UserController {
private final UserService userService; private final UserService userService;
private final WechatMiniProgramService wechatMiniProgramService;
/** 老板注册店铺 */ /** 老板注册店铺 */
@PostMapping("/register-boss") @PostMapping("/register-boss")
@ -34,6 +36,22 @@ public class UserController {
return userService.login(phone, code); return userService.login(phone, code);
} }
/**
* 微信小程序button open-type=getPhoneNumber 拿到的 phoneCode 换手机号并登录
*/
@PostMapping("/wx-phone-login")
public Map<String, Object> wxPhoneLogin(@RequestBody Map<String, String> 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") @PostMapping("/register-staff")
public Map<String, Object> registerStaff(@RequestBody Map<String, String> params) { public Map<String, Object> registerStaff(@RequestBody Map<String, String> params) {

View File

@ -55,9 +55,18 @@ public class UserService {
if (!"123456".equals(code)) { if (!"123456".equals(code)) {
return Map.of("code", 401, "message", "验证码错误"); return Map.of("code", 401, "message", "验证码错误");
} }
return loginByVerifiedPhone(phone);
}
/**
* 已通过短信或微信授权校验后的手机号登录与验证码登录成功后的逻辑一致
*/
public Map<String, Object> loginByVerifiedPhone(String phone) {
if (phone == null || !phone.matches("^1\\d{10}$")) {
return Map.of("code", 400, "message", "手机号格式不正确");
}
User user = userMapper.findByPhone(phone); User user = userMapper.findByPhone(phone);
if (user == null) { if (user == null) {
// 自动注册为 C 端用户 (customer)
user = new User(); user = new User();
user.setUsername(phone); user.setUsername(phone);
user.setPhone(phone); user.setPhone(phone);

View File

@ -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<Map<String, String>> 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;
}
}

View File

@ -35,8 +35,8 @@ logging:
level: level:
com.petstore: debug com.petstore: debug
# 微信登录配置(需替换为实际值 # 微信小程序(与 manifest / 微信后台一致
wechat: wechat:
appid: YOUR_APPID appid: wx8ca2dfa89af72edf
appsecret: YOUR_APPSECRET appsecret: 7afb49f3a31fe9b5a083c7c40be45c5b
redirect_uri: http://localhost:8080/api/wechat/callback redirect_uri: http://localhost:8080/api/wechat/callback