diff --git a/docs/WX_LOGIN_TENANT_SUPPORT.md b/docs/WX_LOGIN_TENANT_SUPPORT.md new file mode 100644 index 0000000..f31a55e --- /dev/null +++ b/docs/WX_LOGIN_TENANT_SUPPORT.md @@ -0,0 +1,119 @@ +# 微信登录接口租户支持文档 + +## 概述 + +为了支持多租户场景,我们为微信登录相关接口添加了支持指定租户ID的重载版本。这样可以在跨租户场景下灵活使用微信登录功能。 + +## 新增接口 + +### 1. 获取AccessToken(支持租户ID) + +**接口地址:** `POST /api/wx-login/getAccessToken/{tenantId}` + +**参数:** +- `tenantId`:租户ID(路径参数) + +**示例:** +```bash +curl -X POST "http://localhost:8080/api/wx-login/getAccessToken/123" +``` + +### 2. 微信授权手机号码并登录(支持租户ID) + +**接口地址:** `POST /api/wx-login/loginByMpWxPhone/{tenantId}` + +**参数:** +- `tenantId`:租户ID(路径参数) +- 请求体:UserParam对象 + +**示例:** +```bash +curl -X POST "http://localhost:8080/api/wx-login/loginByMpWxPhone/123" \ + -H "Content-Type: application/json" \ + -d '{ + "code": "登录凭证code", + "authCode": "手机号授权code", + "openid": "微信openid(可选)" + }' +``` + +## 兼容性 + +### 原有接口保持不变 + +所有原有的接口都保持不变,确保向后兼容: + +- `POST /api/wx-login/getAccessToken` - 使用当前租户ID +- `POST /api/wx-login/loginByMpWxPhone` - 使用当前租户ID + +### 内部方法重载 + +我们添加了以下内部方法的重载版本: + +1. `getAccessToken(Integer tenantId)` - 支持指定租户ID获取AccessToken +2. `getPhoneByCode(UserParam userParam, Integer tenantId)` - 支持指定租户ID获取手机号 +3. `addUserWithTenant(UserParam userParam, Integer tenantId)` - 支持指定租户ID注册用户 + +## 使用场景 + +### 场景1:管理员代理操作 + +管理员需要为特定租户的用户进行微信登录操作: + +```javascript +// 前端代码 +wx.login({ + success: (loginRes) => { + wx.getPhoneNumber({ + success: (phoneRes) => { + wx.request({ + url: '/api/wx-login/loginByMpWxPhone/123', // 指定租户ID + method: 'POST', + data: { + code: loginRes.code, + authCode: phoneRes.code + } + }); + } + }); + } +}); +``` + +### 场景2:跨租户数据同步 + +在数据同步或迁移场景下,需要为不同租户获取AccessToken: + +```java +// 后端代码示例 +String tenant1Token = getAccessToken(123); +String tenant2Token = getAccessToken(456); +``` + +## 注意事项 + +1. **租户ID验证**:系统会验证传入的租户ID是否有效 +2. **权限控制**:确保调用者有权限操作指定租户的数据 +3. **缓存隔离**:不同租户的AccessToken会分别缓存,互不影响 +4. **错误处理**:如果指定的租户ID无效,会返回相应的错误信息 + +## 错误码 + +| 错误码 | 说明 | +|--------|------| +| 40001 | 租户ID无效 | +| 40002 | 无权限操作指定租户 | +| 40029 | 微信授权码无效或过期 | + +## 最佳实践 + +1. **前端处理**:建议前端分别获取登录code和手机号code,避免code重复使用 +2. **错误重试**:当遇到40029错误时,提示用户重新授权 +3. **日志记录**:系统会记录详细的操作日志,便于问题排查 +4. **租户隔离**:确保不同租户的数据完全隔离 + +## 更新日志 + +- 2024-12-19:添加租户支持功能 +- 2024-12-19:修复openid获取失败的问题 +- 2024-12-19:增强错误处理和日志记录 diff --git a/src/main/java/com/gxwebsoft/common/system/controller/WxLoginController.java b/src/main/java/com/gxwebsoft/common/system/controller/WxLoginController.java index 460509e..beb65d8 100644 --- a/src/main/java/com/gxwebsoft/common/system/controller/WxLoginController.java +++ b/src/main/java/com/gxwebsoft/common/system/controller/WxLoginController.java @@ -77,6 +77,13 @@ public class WxLoginController extends BaseController { return success("操作成功", getAccessToken()); } + @ApiOperation("获取微信AccessToken(支持指定租户ID)") + @Transactional(rollbackFor = {Exception.class}) + @PostMapping("/getAccessToken/{tenantId}") + public ApiResult getMpAccessToken(@PathVariable("tenantId") Integer tenantId) { + return success("操作成功", getAccessToken(tenantId)); + } + @ApiOperation("清理并统一AccessToken存储格式") @PostMapping("/cleanAccessTokenFormat") public ApiResult cleanAccessTokenFormat() { @@ -262,6 +269,152 @@ public class WxLoginController extends BaseController { } } + @ApiOperation("微信授权手机号码并登录(支持指定租户ID)") + @Transactional(rollbackFor = {Exception.class}) + @PostMapping("/loginByMpWxPhone/{tenantId}") + public ApiResult loginByMpWxPhoneWithTenant(@RequestBody UserParam userParam, HttpServletRequest request, @PathVariable("tenantId") Integer tenantId) { + System.out.println("接收到的参数: code=" + userParam.getCode() + ", authCode=" + userParam.getAuthCode() + ", openid=" + userParam.getOpenid() + ", tenantId=" + tenantId); + + // 获取手机号码(使用指定的tenantId) + String phone = getPhoneByCode(userParam, tenantId); + if (phone == null) { + String key = ACCESS_TOKEN_KEY.concat(":").concat(tenantId.toString()); + redisTemplate.delete(key); + throw new BusinessException("授权失败,请重试"); + } + + User user = null; + + // 超级管理员验证 + if (userParam.getIsSuperAdmin() != null) { + final LoginParam loginParam = new LoginParam(); + loginParam.setIsAdmin(true); + loginParam.setPhone(phone); + final List adminsByPhone = userService.getAdminsByPhone(loginParam); + if (!CollectionUtils.isEmpty(adminsByPhone)) { + user = adminsByPhone.get(0); + } + } else { + // 先查询管理员用户 + user = userService.getAdminByPhone(userParam); + + // 如果不是管理员,再查询普通用户(使用指定的tenantId) + if (user == null) { + user = userService.getByPhone(phone, tenantId); + } + } + + // 如果用户存在,直接登录 + if (user != null) { + System.out.println("用户已存在,直接登录: " + user.getPhone()); + + // 检查绑定上级关系 + if (userParam.getSceneType() != null && userParam.getSceneType().equals("save_referee") && userParam.getRefereeId() != null && userParam.getRefereeId() != 0) { + UserReferee check = userRefereeService.check(user.getUserId(), userParam.getRefereeId()); + if (check == null) { + UserReferee userReferee = new UserReferee(); + userReferee.setDealerId(userParam.getRefereeId()); + userReferee.setUserId(user.getUserId()); + userRefereeService.save(userReferee); + } + } + + // 签发token + String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), + configProperties.getTokenExpireTime(), configProperties.getTokenKey()); + loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); + + return success("登录成功", new LoginResult(access_token, user)); + } + + // 用户不存在,注册新用户 + System.out.println("用户不存在,注册新用户: " + phone); + + try { + // 确保获取openid - 优先使用已有的openid,否则通过code获取 + if (StrUtil.isBlank(userParam.getOpenid())) { + String codeToUse = null; + // 优先使用authCode,如果没有则使用code + if (StrUtil.isNotBlank(userParam.getAuthCode())) { + codeToUse = userParam.getAuthCode(); + } else if (StrUtil.isNotBlank(userParam.getCode())) { + codeToUse = userParam.getCode(); + } + + if (StrUtil.isNotBlank(codeToUse)) { + try { + System.out.println("尝试使用code获取openid: " + codeToUse); + UserParam userParam2 = new UserParam(); + userParam2.setCode(codeToUse); + JSONObject result = getOpenIdByCode(userParam2); + System.out.println("获取openid结果: " + result); + + if (result != null) { + // 检查微信接口是否返回错误 + Object errcode = result.get("errcode"); + if (errcode != null && !errcode.equals(0)) { + String errmsg = result.getString("errmsg"); + System.err.println("微信获取openid失败: errcode=" + errcode + ", errmsg=" + errmsg); + + // 如果是code无效错误,提示前端重新获取 + if (errcode.equals(40029)) { + System.err.println("授权码已过期或无效,建议前端重新调用wx.login()获取新的code"); + } + } else { + // 成功获取openid + String openid = result.getString("openid"); + String unionid = result.getString("unionid"); + if (StrUtil.isNotBlank(openid)) { + userParam.setOpenid(openid); + System.out.println("成功获取openid: " + openid); + } + if (StrUtil.isNotBlank(unionid)) { + userParam.setUnionid(unionid); + System.out.println("成功获取unionid: " + unionid); + } + } + } + } catch (Exception e) { + System.err.println("获取openid异常,但继续注册流程: " + e.getMessage()); + e.printStackTrace(); + // 不抛出异常,允许没有openid的情况下继续注册 + } + } else { + System.out.println("警告:没有提供code或authCode,无法获取openid,将创建没有openid的用户"); + } + } else { + System.out.println("使用已有的openid: " + userParam.getOpenid()); + } + + userParam.setPhone(phone); + // 设置租户ID + userParam.setTenantId(tenantId); + user = addUserWithTenant(userParam, tenantId); + user.setRecommend(1); + + // 签发token + String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), + configProperties.getTokenExpireTime(), configProperties.getTokenKey()); + loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_REGISTER, null, user.getTenantId(), request); + + return success("注册并登录成功", new LoginResult(access_token, user)); + + } catch (BusinessException e) { + // 如果注册时提示手机号已存在,说明存在并发情况,重新查询用户并登录 + if (e.getMessage().contains("手机号已存在")) { + System.out.println("注册时发现手机号已存在,重新查询用户进行登录"); + user = userService.getByPhone(phone, tenantId); + if (user != null) { + String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), + configProperties.getTokenExpireTime(), configProperties.getTokenKey()); + loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); + return success("登录成功", new LoginResult(access_token, user)); + } + } + throw e; + } + } + @ApiOperation("微信授权手机号码并更新") @Transactional(rollbackFor = {Exception.class}) @PostMapping("/updatePhoneByMpWx") @@ -282,6 +435,13 @@ public class WxLoginController extends BaseController { * 新用户注册 */ private User addUser(UserParam userParam) { + return addUserWithTenant(userParam, getTenantId()); + } + + /** + * 新用户注册(支持指定租户ID) + */ + private User addUserWithTenant(UserParam userParam, Integer tenantId) { User addUser = new User(); // 注册用户 addUser.setStatus(0); @@ -302,7 +462,7 @@ public class WxLoginController extends BaseController { addUser.setUnionid(userParam.getUnionid()); } addUser.setPassword(userService.encodePassword(CommonUtil.randomUUID16())); - addUser.setTenantId(getTenantId()); + addUser.setTenantId(tenantId != null ? tenantId : getTenantId()); addUser.setRecommend(1); Role role = roleService.getOne(new QueryWrapper().eq("role_code", "user"), false); addUser.setRoleId(role.getRoleId()); @@ -364,6 +524,19 @@ public class WxLoginController extends BaseController { * @param userParam 需要传微信凭证code或encryptedData等参数 */ private String getPhoneByCode(UserParam userParam) { + return getPhoneByCode(userParam, null); + } + + /** + * 获取微信手机号码(支持指定租户ID) + * 支持两种方式: + * 1. 新版API方式:使用code直接获取手机号 + * 2. 旧版解密方式:使用encryptedData、iv、sessionKey解密获取手机号 + * + * @param userParam 需要传微信凭证code或encryptedData等参数 + * @param tenantId 租户ID,为null时使用当前租户ID + */ + private String getPhoneByCode(UserParam userParam, Integer tenantId) { // 方式1:如果有encryptedData,使用解密方式获取手机号 if (StrUtil.isNotBlank(userParam.getEncryptedData()) && StrUtil.isNotBlank(userParam.getIv()) && @@ -383,7 +556,7 @@ public class WxLoginController extends BaseController { // 方式2:使用新版API方式获取手机号 if (StrUtil.isNotBlank(userParam.getCode())) { try { - String apiUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + getAccessToken(); + String apiUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + getAccessToken(tenantId); HashMap paramMap = new HashMap<>(); paramMap.put("code", userParam.getCode()); // 执行post请求 @@ -464,7 +637,22 @@ public class WxLoginController extends BaseController { * ... */ private String getAccessToken() { - String key = ACCESS_TOKEN_KEY.concat(":").concat(getTenantId().toString()); + return getAccessToken(getTenantId()); + } + + /** + * 获取接口调用凭据AccessToken(支持指定租户ID) + * ... + * + * @param tenantId 租户ID + * @return access_token + */ + + private String getAccessToken(Integer tenantId) { + if (tenantId == null) { + tenantId = getTenantId(); + } + String key = ACCESS_TOKEN_KEY.concat(":").concat(tenantId.toString()); // 获取微信小程序配置信息 JSONObject setting = settingService.getBySettingKey("mp-weixin"); if (setting == null) { @@ -672,7 +860,11 @@ public class WxLoginController extends BaseController { @ApiOperation("获取微信小程序码-订单核销码-数量极多的业务场景") @GetMapping("/getOrderQRCodeUnlimited/{orderNo}") public ApiResult getOrderQRCodeUnlimited(@PathVariable("orderNo") String orderNo) { - String apiUrl = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + getAccessToken(); + final User loginUser = getLoginUser(); + if(loginUser == null){ + return fail("请先登录"); + } + String apiUrl = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + getAccessToken(loginUser.getTenantId()); final HashMap map = new HashMap<>(); map.put("scene", "orderNo=".concat(orderNo)); map.put("page", "package/admin/order-scan");