|
|
@@ -0,0 +1,340 @@
|
|
|
+package org.dromara.auth.controller;
|
|
|
+
|
|
|
+import cn.dev33.satoken.annotation.SaIgnore;
|
|
|
+import cn.dev33.satoken.stp.StpUtil;
|
|
|
+import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
|
|
+import cn.hutool.core.bean.BeanUtil;
|
|
|
+import cn.hutool.core.util.ObjectUtil;
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import jakarta.validation.constraints.NotNull;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.dubbo.config.annotation.DubboReference;
|
|
|
+import org.apache.seata.spring.annotation.GlobalTransactional;
|
|
|
+import org.dromara.auth.domain.SysUserWechat;
|
|
|
+import org.dromara.auth.domain.bo.SysUserWechatBo;
|
|
|
+import org.dromara.auth.domain.vo.SysUserWechatVo;
|
|
|
+import org.dromara.auth.service.MiniAuthService;
|
|
|
+import org.dromara.common.core.domain.R;
|
|
|
+import org.dromara.common.core.utils.ServletUtils;
|
|
|
+import org.dromara.common.satoken.utils.LoginHelper;
|
|
|
+import org.dromara.common.tenant.helper.TenantHelper;
|
|
|
+import org.dromara.resource.api.RemoteFileService;
|
|
|
+import org.dromara.resource.api.domain.RemoteFile;
|
|
|
+import org.dromara.system.api.model.LoginUser;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
+
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+
|
|
|
+/**
|
|
|
+ * mini端 控制器
|
|
|
+ *
|
|
|
+ * @description:
|
|
|
+ * @create: 2026-04-21
|
|
|
+ **/
|
|
|
+@Slf4j
|
|
|
+@RequiredArgsConstructor
|
|
|
+@RestController
|
|
|
+@RequestMapping("/wx")
|
|
|
+public class MiniTokenController {
|
|
|
+
|
|
|
+ private final MiniAuthService miniAuthService;
|
|
|
+ @DubboReference
|
|
|
+ private RemoteFileService remoteFileService;
|
|
|
+
|
|
|
+ @Value("${tr.wechat.secret}")
|
|
|
+ private String appSecret;
|
|
|
+
|
|
|
+ @Value("${tr.wechat.appid}")
|
|
|
+ private String appId;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * 小程序登录
|
|
|
+ * */
|
|
|
+ @SaIgnore
|
|
|
+ @PostMapping("/login")
|
|
|
+ public R<SysUserWechatVo> login(@RequestBody SysUserWechatBo wechatBo) {
|
|
|
+ log.info("微信用户登录:{}", wechatBo.getCode());
|
|
|
+
|
|
|
+ // 从请求头中获取 tenantId
|
|
|
+ String tenantId = ServletUtils.getHeader(Objects.requireNonNull(ServletUtils.getRequest()), "tenantId");
|
|
|
+ log.info("从请求头获取到 tenantId: {}", tenantId);
|
|
|
+
|
|
|
+ // 在动态租户上下文中执行登录逻辑
|
|
|
+ return TenantHelper.dynamic(tenantId, () -> {
|
|
|
+ SysUserWechat wechat = miniAuthService.wxLogin(wechatBo);
|
|
|
+ LoginUser loginUser = new LoginUser();
|
|
|
+ loginUser.setUserId(wechat.getUserId());
|
|
|
+ loginUser.setNickname(wechat.getNickname());
|
|
|
+ loginUser.setUserType("app_user");
|
|
|
+ loginUser.setOpenid(wechat.getOpenId());
|
|
|
+ loginUser.setTenantId(tenantId); // 设置租户ID到登录用户
|
|
|
+
|
|
|
+ // 配置登录参数
|
|
|
+ SaLoginParameter model = new SaLoginParameter();
|
|
|
+ model.setDeviceType("xcx"); // 小程序设备类型
|
|
|
+ model.setTimeout(30 * 24 * 60 * 60); // 30天过期时间
|
|
|
+ model.setActiveTimeout(7 * 24 * 60 * 60); // 7天活跃超时
|
|
|
+ model.setExtra(LoginHelper.CLIENT_KEY, "miniprogram_client_id_2025"); // 使用系统认可的APP端客户端ID
|
|
|
+
|
|
|
+ //生成token
|
|
|
+ LoginHelper.login(loginUser, model);
|
|
|
+ // 获取token信息
|
|
|
+ String accessToken = StpUtil.getTokenValue();
|
|
|
+ Long expireIn = StpUtil.getTokenTimeout();
|
|
|
+ SysUserWechatVo wechatVo = BeanUtil.copyProperties(wechat, SysUserWechatVo.class);
|
|
|
+ wechatVo.setOpenId(wechat.getOpenId());
|
|
|
+ wechatVo.setAccessToken(accessToken);
|
|
|
+ wechatVo.setExpireIn(expireIn);
|
|
|
+ return R.ok(wechatVo);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @SaIgnore
|
|
|
+ @PostMapping("/bindPhone")
|
|
|
+ @GlobalTransactional
|
|
|
+ public R<SysUserWechatVo> getUserPhone(@RequestBody Map<String, Object> phoneData) {
|
|
|
+ try {
|
|
|
+ String code = (String) phoneData.get("code");
|
|
|
+ Long userId = Long.valueOf(phoneData.get("userId").toString());
|
|
|
+
|
|
|
+ log.info("获取手机号请求 - userId: {}, code: {}", userId, code);
|
|
|
+
|
|
|
+ // 从请求头中获取 tenantId
|
|
|
+ String tenantId = ServletUtils.getHeader(Objects.requireNonNull(ServletUtils.getRequest()), "tenantId");
|
|
|
+ log.info("从请求头获取到 tenantId: {}", tenantId);
|
|
|
+
|
|
|
+ // 在动态租户上下文中执行绑定手机号逻辑
|
|
|
+ return TenantHelper.dynamic(tenantId, () -> {
|
|
|
+ // 1. 通过手机号专用code直接获取手机号(新方法)
|
|
|
+ String phoneNumber = getPhoneNumberByCode(code);
|
|
|
+ if (phoneNumber == null) {
|
|
|
+ return R.fail("获取手机号失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 验证手机号是否已被其他用户使用
|
|
|
+ if (isPhoneNumberExists(phoneNumber, userId)) {
|
|
|
+ return R.fail("该手机号已被其他用户绑定");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("解密得到手机号: {}", phoneNumber.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
|
|
|
+
|
|
|
+ // 3. 更新用户手机号
|
|
|
+ SysUserWechatVo user = miniAuthService.queryById(userId);
|
|
|
+ if (user == null) {
|
|
|
+ return R.fail("用户不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ user.setPhoneNumber(phoneNumber);
|
|
|
+ // 创建BO对象进行更新
|
|
|
+ SysUserWechatBo updateBo = new SysUserWechatBo();
|
|
|
+ updateBo.setUserId(userId);
|
|
|
+ updateBo.setPhoneNumber(phoneNumber);
|
|
|
+ miniAuthService.updateByBo(updateBo);
|
|
|
+
|
|
|
+ // 4. 重新生成token并返回用户信息
|
|
|
+ LoginUser loginUser = new LoginUser();
|
|
|
+ loginUser.setUserId(user.getUserId());
|
|
|
+ loginUser.setNickname(user.getNickname());
|
|
|
+ loginUser.setUserType("app_user");
|
|
|
+ loginUser.setOpenid(user.getOpenId());
|
|
|
+ loginUser.setTenantId(tenantId); // 设置租户ID到登录用户
|
|
|
+
|
|
|
+ // 配置登录参数
|
|
|
+ SaLoginParameter model = new SaLoginParameter();
|
|
|
+ model.setDeviceType("xcx");
|
|
|
+ model.setTimeout(30 * 24 * 60 * 60);
|
|
|
+ model.setActiveTimeout(7 * 24 * 60 * 60);
|
|
|
+ model.setExtra(LoginHelper.CLIENT_KEY, "miniprogram_client_id_2025");
|
|
|
+
|
|
|
+ // 生成token
|
|
|
+ LoginHelper.login(loginUser, model);
|
|
|
+
|
|
|
+ // 获取token信息
|
|
|
+ String accessToken = StpUtil.getTokenValue();
|
|
|
+ Long expireIn = StpUtil.getTokenTimeout();
|
|
|
+ SysUserWechatVo userVo = new SysUserWechatVo();
|
|
|
+ userVo.setOpenId(user.getOpenId());
|
|
|
+ userVo.setUserId(user.getUserId());
|
|
|
+ userVo.setStatus(user.getStatus());
|
|
|
+ userVo.setAccessToken(accessToken);
|
|
|
+ userVo.setExpireIn(expireIn);
|
|
|
+ userVo.setNickname(user.getNickname());
|
|
|
+ userVo.setAvatarUrl(user.getAvatarUrl());
|
|
|
+ userVo.setPhoneNumber(phoneNumber); // 返回手机号
|
|
|
+
|
|
|
+ return R.ok(userVo);
|
|
|
+ });
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取手机号失败", e);
|
|
|
+ return R.fail("获取手机号失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /*
|
|
|
+ * 获取手机号
|
|
|
+ * */
|
|
|
+ @PostMapping("/getPhoneNumber")
|
|
|
+ public Object getPhoneNumber(@RequestBody Map<String, String> data) {
|
|
|
+ return R.ok(getPhoneNumberByCode(data.get("code")));
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private String getPhoneNumberByCode(String code) {
|
|
|
+ try {
|
|
|
+ // 1. 获取 access_token
|
|
|
+ String accessToken = getAccessToken();
|
|
|
+ if (accessToken == null) {
|
|
|
+ log.error("access_token 获取失败");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 构建请求 URL
|
|
|
+ String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken;
|
|
|
+
|
|
|
+ // 3. 设置请求头为 JSON 格式(重点修复)
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
+ headers.setContentType(MediaType.APPLICATION_JSON);
|
|
|
+
|
|
|
+ // 4. 构建请求体(使用 JSONObject 或字符串)
|
|
|
+ JSONObject requestBody = new JSONObject();
|
|
|
+ requestBody.put("code", code);
|
|
|
+
|
|
|
+ // 5. 组装 HTTP 请求
|
|
|
+ HttpEntity<String> requestEntity = new HttpEntity<>(requestBody.toString(), headers);
|
|
|
+
|
|
|
+ // 6. 发送 POST 请求
|
|
|
+ RestTemplate restTemplate = new RestTemplate();
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class);
|
|
|
+
|
|
|
+ log.info("微信API响应: {}", response.getBody());
|
|
|
+
|
|
|
+ // 7. 解析响应
|
|
|
+ ObjectMapper mapper = new ObjectMapper();
|
|
|
+ JsonNode jsonNode = mapper.readTree(response.getBody());
|
|
|
+
|
|
|
+ // 检查错误码
|
|
|
+ if (jsonNode.has("errcode") && jsonNode.get("errcode").asInt() != 0) {
|
|
|
+ int errcode = jsonNode.get("errcode").asInt();
|
|
|
+ String errmsg = jsonNode.get("errmsg").asText();
|
|
|
+ log.error("微信API返回错误 - errcode: {}, errmsg: {}", errcode, errmsg);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取手机号
|
|
|
+ if (jsonNode.has("phone_info")) {
|
|
|
+ JsonNode phoneInfo = jsonNode.get("phone_info");
|
|
|
+ if (phoneInfo.has("phoneNumber")) {
|
|
|
+ String phoneNumber = phoneInfo.get("phoneNumber").asText();
|
|
|
+ log.info("成功获取手机号");
|
|
|
+ return phoneNumber;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ log.error("响应中没有找到手机号");
|
|
|
+ return null;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取手机号异常", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取小程序 access_token
|
|
|
+ */
|
|
|
+ private String getAccessToken() {
|
|
|
+ try {
|
|
|
+ String url = String.format(
|
|
|
+ "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
|
|
|
+ appId, appSecret
|
|
|
+ );
|
|
|
+
|
|
|
+ RestTemplate restTemplate = new RestTemplate();
|
|
|
+ ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
|
|
+
|
|
|
+ ObjectMapper mapper = new ObjectMapper();
|
|
|
+ JsonNode jsonNode = mapper.readTree(response.getBody());
|
|
|
+
|
|
|
+ if (jsonNode.has("access_token")) {
|
|
|
+ return jsonNode.get("access_token").asText();
|
|
|
+ }
|
|
|
+
|
|
|
+ log.error("获取access_token失败: {}", response.getBody());
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取access_token异常", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查手机号是否已被其他用户使用
|
|
|
+ */
|
|
|
+ private boolean isPhoneNumberExists(String phoneNumber, Long excludeUserId) {
|
|
|
+ try {
|
|
|
+ // 这里需要调用service层方法查询数据库
|
|
|
+ // 由于没有现成的方法,暂时返回false,建议后续完善
|
|
|
+ // WxVsUser existingUser = vsUserService.selectByPhoneNumber(phoneNumber);
|
|
|
+ // return existingUser != null && !existingUser.getUserId().equals(excludeUserId);
|
|
|
+
|
|
|
+ log.info("检查手机号是否存在: {} (排除用户: {})",
|
|
|
+ phoneNumber.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"), excludeUserId);
|
|
|
+ SysUserWechat existingUser = miniAuthService.queryByPhoneNumber(phoneNumber);
|
|
|
+ return existingUser != null && !existingUser.getUserId().equals(excludeUserId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("检查手机号重复时发生异常", e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小程序文件上传(无需权限)
|
|
|
+ *
|
|
|
+ * @param file 上传的文件
|
|
|
+ * @return 上传结果,包含文件URL和ossId
|
|
|
+ */
|
|
|
+ @SaIgnore
|
|
|
+ @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
|
|
+ public R<RemoteFile> upload(@RequestPart("file") MultipartFile file) {
|
|
|
+ try {
|
|
|
+ if (ObjectUtil.isNull(file)) {
|
|
|
+ return R.fail("上传文件不能为空");
|
|
|
+ }
|
|
|
+ log.info("小程序上传文件: {}, 大小: {} bytes", file.getOriginalFilename(), file.getSize());
|
|
|
+
|
|
|
+ // 通过Dubbo远程调用resource服务上传文件
|
|
|
+ RemoteFile remoteFile = remoteFileService.upload(
|
|
|
+ file.getName(),
|
|
|
+ file.getOriginalFilename(),
|
|
|
+ file.getContentType(),
|
|
|
+ file.getBytes()
|
|
|
+ );
|
|
|
+
|
|
|
+ log.info("文件上传成功: ossId={}, url={}", remoteFile.getOssId(), remoteFile.getUrl());
|
|
|
+ return R.ok(remoteFile);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("小程序文件上传失败", e);
|
|
|
+ return R.fail("文件上传失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @GetMapping("/userInfo/{userId}")
|
|
|
+ public R<SysUserWechatVo> getInfo(@NotNull(message = "主键不能为空")
|
|
|
+ @PathVariable("userId") Long id) {
|
|
|
+ return R.ok(miniAuthService.queryById(id));
|
|
|
+ }
|
|
|
+}
|