Compare commits
2 Commits
3c04019af4
...
6e3c49d02d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e3c49d02d | ||
|
|
3c7c8d1f73 |
@ -39,6 +39,16 @@ public class AppointmentController {
|
||||
return Map.of("code", 200, "data", appointments);
|
||||
}
|
||||
|
||||
/** 预约详情 */
|
||||
@GetMapping("/detail")
|
||||
public Map<String, Object> detail(@RequestParam Long id) {
|
||||
Appointment appointment = appointmentService.getById(id);
|
||||
if (appointment != null) {
|
||||
return Map.of("code", 200, "data", appointment);
|
||||
}
|
||||
return Map.of("code", 404, "message", "预约不存在");
|
||||
}
|
||||
|
||||
/** 创建预约 */
|
||||
@PostMapping("/create")
|
||||
public Map<String, Object> create(@RequestBody Map<String, Object> params) {
|
||||
|
||||
@ -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<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")
|
||||
public Map<String, Object> registerStaff(@RequestBody Map<String, String> params) {
|
||||
|
||||
@ -33,6 +33,10 @@ public class AppointmentService {
|
||||
return appointmentMapper.findByStoreIdAndStatus(storeId, status);
|
||||
}
|
||||
|
||||
public Appointment getById(Long id) {
|
||||
return appointmentMapper.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
public Appointment create(Appointment appointment) {
|
||||
appointment.setCreateTime(LocalDateTime.now());
|
||||
appointment.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
@ -55,9 +55,18 @@ public class UserService {
|
||||
if (!"123456".equals(code)) {
|
||||
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);
|
||||
if (user == null) {
|
||||
// 自动注册为 C 端用户 (customer)
|
||||
user = new User();
|
||||
user.setUsername(phone);
|
||||
user.setPhone(phone);
|
||||
|
||||
133
src/main/java/com/petstore/service/WechatMiniProgramService.java
Normal file
133
src/main/java/com/petstore/service/WechatMiniProgramService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user