Spring Boot 3.5.7 对接微信小程序获取手机号完整实现

核心流程

  1. 前端:调用微信小程序wx.getPhoneNumber获取手机号专属code
  2. 后端:
    • 从Redis缓存获取微信access_token(缓存有效期7100秒,避免7200秒过期)
    • 缓存未命中时,调用微信接口获取新access_token并写入Redis
    • 用access_token + 手机号code调用微信接口获取用户手机号
  3. 技术栈:Spring Boot 3.5.7、JDK 17、Redis、Spring Data Redis、WebClient(Spring3推荐HTTP客户端)

一、项目依赖(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.7</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>wechat-mini-phone</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Data Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- 连接池(Redis必需) -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- Lombok 简化代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

二、配置文件(application.yml)

server:
  port: 8080

# 微信小程序配置
wechat:
  mini:
    app-id: 你的微信小程序APPID
    app-secret: 你的微信小程序APPSECRET
    # access_token缓存key
    access-token-key: wechat:mini:program:access_token
    # 缓存有效期(秒):微信官方7200秒,这里设置7100秒防止过期
    token-expire: 7100

# Redis配置
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password:  # 无密码留空
      database: 0
      # 连接池配置
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0
          max-wait: -1ms

三、核心代码实现

1. 统一返回结果封装(Result.java)

package com.example.wechatmini.entity;

import lombok.Data;

/**
 * 统一接口返回结果
 */
@Data
public class Result<T> {
    private int code;
    private String msg;
    private T data;

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMsg("操作成功");
        result.setData(data);
        return result;
    }

    public static <T> Result<T> error(String msg) {
        Result<T> result = new Result<>();
        result.setCode(500);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }
}

2. 微信自定义异常(WechatException.java)

package com.example.wechatmini.exception;

/**
 * 微信接口调用异常
 */
public class WechatException extends RuntimeException {
    public WechatException(String message) {
        super(message);
    }
}

3. Redis配置类(RedisConfig.java)

解决JDK17下Redis序列化乱码问题

package com.example.wechatmini.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // String序列化器
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        // JSON序列化器
        Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
        jsonSerializer.setObjectMapper(om);

        // key和hashKey使用String序列化
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        // value和hashValue使用JSON序列化
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

4. 微信常量/配置类(WechatMiniProperties.java)

读取yml中的微信配置

package com.example.wechatmini.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 微信小程序配置参数
 */
@Data
@Component
@ConfigurationProperties(prefix = "wechat.mini")
public class WechatMiniProperties {
    /**
     * 小程序APPID
     */
    private String appId;

    /**
     * 小程序APPSECRET
     */
    private String appSecret;

    /**
     * access_token缓存key
     */
    private String accessTokenKey;

    /**
     * token缓存有效期(秒)
     */
    private Long tokenExpire;

    // 微信接口地址常量
    public static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
    public static final String GET_PHONE_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s";
}

5. AccessToken缓存服务(WechatTokenService.java)

核心:从Redis获取/刷新access_token,防止并发重复请求微信接口

package com.example.wechatmini.service;

import com.example.wechatmini.config.WechatMiniProperties;
import com.example.wechatmini.exception.WechatException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 微信access_token获取与缓存服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatTokenService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final WechatMiniProperties wechatProperties;
    private final WebClient webClient;

    /**
     * 获取有效的access_token(优先从缓存取)
     * 加synchronized防止并发重复请求微信接口
     */
    public synchronized String getAccessToken() {
        // 1. 从Redis缓存获取
        String token = (String) redisTemplate.opsForValue().get(wechatProperties.getAccessTokenKey());
        if (token != null && !token.isEmpty()) {
            log.info("从Redis缓存获取access_token: {}", token);
            return token;
        }

        // 2. 缓存无数据,调用微信接口获取新token
        return refreshAccessToken();
    }

    /**
     * 调用微信接口刷新access_token并写入Redis
     */
    private String refreshAccessToken() {
        String url = String.format(
                WechatMiniProperties.ACCESS_TOKEN_URL,
                wechatProperties.getAppId(),
                wechatProperties.getAppSecret()
        );

        try {
            // 调用微信接口
            Map<String, Object> response = webClient.get()
                    .uri(url)
                    .retrieve()
                    .bodyToMono(Map.class)
                    .block();

            // 校验返回结果
            if (response == null || response.containsKey("errcode")) {
                String errMsg = response == null ? "接口调用失败" : response.get("errmsg").toString();
                log.error("获取微信access_token失败: {}", errMsg);
                throw new WechatException("获取access_token失败:" + errMsg);
            }

            String accessToken = response.get("access_token").toString();
            // 3. 存入Redis,设置过期时间
            redisTemplate.opsForValue().set(
                    wechatProperties.getAccessTokenKey(),
                    accessToken,
                    wechatProperties.getTokenExpire(),
                    TimeUnit.SECONDS
            );
            log.info("调用微信接口获取access_token成功,已缓存到Redis: {}", accessToken);
            return accessToken;

        } catch (Exception e) {
            log.error("刷新access_token异常", e);
            throw new WechatException("刷新access_token异常:" + e.getMessage());
        }
    }
}

6. 小程序获取手机号服务(WechatMiniPhoneService.java)

package com.example.wechatmini.service;

import com.example.wechatmini.config.WechatMiniProperties;
import com.example.wechatmini.exception.WechatException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

/**
 * 微信小程序获取手机号服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatMiniPhoneService {

    private final WechatTokenService wechatTokenService;
    private final WebClient webClient;

    /**
     * 获取用户手机号
     * @param code 前端wx.getPhoneNumber返回的手机号专属code
     * @return 手机号
     */
    public String getUserPhone(String code) {
        // 1. 获取有效的access_token
        String accessToken = wechatTokenService.getAccessToken();

        // 2. 拼接微信获取手机号接口
        String url = String.format(WechatMiniProperties.GET_PHONE_URL, accessToken);

        // 3. 构造请求参数
        Map<String, String> requestParam = new HashMap<>();
        requestParam.put("code", code);

        try {
            // 4. 调用微信接口
            Map<String, Object> response = webClient.post()
                    .uri(url)
                    .bodyValue(requestParam)
                    .retrieve()
                    .bodyToMono(Map.class)
                    .block();

            // 5. 校验返回结果
            if (response == null || !"0".equals(response.get("errcode").toString())) {
                String errMsg = response == null ? "接口调用失败" : response.get("errmsg").toString();
                log.error("获取手机号失败: {}", errMsg);
                throw new WechatException("获取手机号失败:" + errMsg);
            }

            // 6. 解析手机号
            Map<String, Object> phoneInfo = (Map<String, Object>) response.get("phone_info");
            String phoneNumber = phoneInfo.get("phoneNumber").toString();
            log.info("获取用户手机号成功: {}", phoneNumber);
            return phoneNumber;

        } catch (Exception e) {
            log.error("获取手机号异常", e);
            throw new WechatException("获取手机号异常:" + e.getMessage());
        }
    }
}

7. WebClient配置(WebClientConfig.java)

Spring Boot3推荐使用WebClient替代RestTemplate

package com.example.wechatmini.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        return WebClient.create();
    }
}

8. 控制器(WechatMiniController.java)

对外提供接口,供前端调用

package com.example.wechatmini.controller;

import com.example.wechatmini.entity.Result;
import com.example.wechatmini.service.WechatMiniPhoneService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 微信小程序接口
 */
@RestController
@RequestMapping("/wechat/mini")
@RequiredArgsConstructor
public class WechatMiniController {

    private final WechatMiniPhoneService phoneService;

    /**
     * 获取用户手机号
     * @param code 前端传入的手机号code
     * @return 手机号
     */
    @PostMapping("/getPhone")
    public Result<String> getPhoneNumber(@RequestParam String code) {
        String phone = phoneService.getUserPhone(code);
        return Result.success(phone);
    }
}

9. 全局异常处理器(GlobalExceptionHandler.java)

package com.example.wechatmini.exception;

import com.example.wechatmini.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(WechatException.class)
    public Result<String> handleWechatException(WechatException e) {
        log.error("微信业务异常: {}", e.getMessage());
        return Result.error(e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result<String> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error("系统繁忙,请稍后再试");
    }
}

四、前端配合代码(微信小程序)

// 页面wxml
<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">获取手机号</button>

// 页面js
getPhoneNumber(e) {
  // 获取手机号code
  const code = e.detail.code;
  if (!code) {
    wx.showToast({ title: '授权失败', icon: 'error' });
    return;
  }

  // 调用后端接口
  wx.request({
    url: 'http://localhost:8080/wechat/mini/getPhone',
    method: 'POST',
    data: { code: code },
    success: (res) => {
      if (res.data.code === 200) {
        console.log('用户手机号:', res.data.data);
        wx.showToast({ title: '获取成功' });
      } else {
        wx.showToast({ title: res.data.msg, icon: 'error' });
      }
    }
  });
}

五、关键说明

  1. access_token缓存

    • 缓存到Redis,有效期7100秒(微信官方7200秒,预留100秒缓冲)
    • 加synchronized防止并发场景下多次请求微信接口
    • 缓存key:wechat:mini:program:access_token
  2. 接口权限

    • 小程序必须完成企业认证才能调用获取手机号接口
    • code是一次性有效的,前端每次获取都要重新调用wx.getPhoneNumber
  3. 环境要求

    • JDK 17+
    • Redis 5.0+
    • Spring Boot 3.5.7
    • 小程序APPID/APPSECRET必须正确
  4. 测试方式

    • 启动Redis
    • 填写yml中的小程序app-id和app-secret
    • 启动Spring Boot项目
    • 用微信开发者工具测试前端授权获取手机号

总结

  1. 实现了Redis缓存access_token,避免频繁调用微信接口
  2. 完整对接微信小程序获取手机号全流程
  3. 代码遵循Spring Boot3最佳实践,使用WebClient、全局异常、统一返回值
  4. 支持高并发场景,防止重复刷新token
  5. 前端代码可直接复用,后端可直接部署运行
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐