Browse Source

1.牧户端小程序登录和绑定

2.兽医端小程序登录和绑定
3.兽医PC端手机号加密码登录
master
ma-zhongxu 22 hours ago
parent
commit
c87db7bae2
  1. 6
      chenhai-admin/pom.xml
  2. 302
      chenhai-admin/src/main/java/com/chenhai/web/controller/auth/MultiAuthController.java
  3. 15
      chenhai-admin/src/main/resources/application.yml
  4. 12
      chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUser.java
  5. 99
      chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUserAuth.java
  6. 23
      chenhai-common/src/main/java/com/chenhai/common/core/domain/model/LoginUser.java
  7. 90
      chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneDecryptRequest.java
  8. 33
      chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneLoginBody.java
  9. 100
      chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindRequest.java
  10. 64
      chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindResult.java
  11. 25
      chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatLoginBody.java
  12. 66
      chenhai-common/src/main/java/com/chenhai/common/utils/WechatDecryptUtil.java
  13. 6
      chenhai-framework/pom.xml
  14. 80
      chenhai-framework/src/main/java/com/chenhai/framework/config/AuthenticationProviderConfig.java
  15. 51
      chenhai-framework/src/main/java/com/chenhai/framework/config/SecurityConfig.java
  16. 29
      chenhai-framework/src/main/java/com/chenhai/framework/security/exception/WechatNeedBindException.java
  17. 90
      chenhai-framework/src/main/java/com/chenhai/framework/security/provider/PhoneAuthenticationProvider.java
  18. 142
      chenhai-framework/src/main/java/com/chenhai/framework/security/provider/WechatAuthenticationProvider.java
  19. 69
      chenhai-framework/src/main/java/com/chenhai/framework/security/token/PhoneAuthenticationToken.java
  20. 62
      chenhai-framework/src/main/java/com/chenhai/framework/security/token/WechatAuthenticationToken.java
  21. 17
      chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsService.java
  22. 27
      chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsServiceImpl.java
  23. 63
      chenhai-framework/src/main/java/com/chenhai/framework/web/service/TokenService.java
  24. 38
      chenhai-system/pom.xml
  25. 57
      chenhai-system/src/main/java/com/chenhai/muhu/domain/model/MuhuWxLoginBody.java
  26. 36
      chenhai-system/src/main/java/com/chenhai/muhu/service/IUserAuthService.java
  27. 39
      chenhai-system/src/main/java/com/chenhai/muhu/service/WechatService.java
  28. 79
      chenhai-system/src/main/java/com/chenhai/muhu/service/impl/UserAuthServiceImpl.java
  29. 76
      chenhai-system/src/main/java/com/chenhai/muhu/service/impl/WechatServiceImpl.java
  30. 56
      chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserAuthMapper.java
  31. 8
      chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserMapper.java
  32. 8
      chenhai-system/src/main/java/com/chenhai/system/service/ISysUserService.java
  33. 5
      chenhai-system/src/main/java/com/chenhai/system/service/impl/SysUserServiceImpl.java
  34. 97
      chenhai-system/src/main/resources/mapper/system/SysUserAuthMapper.xml
  35. 7
      chenhai-system/src/main/resources/mapper/system/SysUserMapper.xml

6
chenhai-admin/pom.xml

@ -54,6 +54,12 @@
<artifactId>chenhai-generator</artifactId> <artifactId>chenhai-generator</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies> </dependencies>
<build> <build>

302
chenhai-admin/src/main/java/com/chenhai/web/controller/auth/MultiAuthController.java

@ -0,0 +1,302 @@
package com.chenhai.web.controller.auth;
import com.alibaba.fastjson2.JSONObject;
import com.chenhai.common.constant.Constants;
import com.chenhai.common.core.domain.AjaxResult;
import com.chenhai.common.core.domain.entity.SysUser;
import com.chenhai.common.core.domain.model.*;
import com.chenhai.common.core.redis.RedisCache;
import com.chenhai.common.utils.WechatDecryptUtil;
import com.chenhai.framework.security.exception.WechatNeedBindException;
import com.chenhai.framework.security.token.PhoneAuthenticationToken;
import com.chenhai.framework.security.token.WechatAuthenticationToken;
import com.chenhai.framework.web.service.SysLoginService;
import com.chenhai.framework.web.service.SysPermissionService;
import com.chenhai.framework.web.service.SysSmsService;
import com.chenhai.framework.web.service.TokenService;
import com.chenhai.muhu.service.IUserAuthService;
import com.chenhai.muhu.service.WechatService;
import com.chenhai.system.service.ISysConfigService;
import com.chenhai.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 多端认证控制器 - 统一处理牧户和兽医
*/
@RestController
@RequestMapping("/auth")
public class MultiAuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenService tokenService;
@Autowired
private SysSmsService smsService;
@Autowired
private IUserAuthService userAuthService;
@Autowired
private WechatService wechatService;
@Autowired
private SysPermissionService permissionService;
@Autowired
private RedisCache redisCache;
@Autowired
private ISysUserService userService;
@Autowired
private ISysConfigService sysConfigService;
@Autowired
private SysLoginService loginService;
/**
* 微信小程序登录统一入口
*/
@PostMapping("/wechat/login")
public AjaxResult wechatLogin(@Valid @RequestBody WechatLoginBody loginBody) {
// 验证参数
if (loginBody.getCode() == null || loginBody.getClientType() == null) {
return AjaxResult.error("参数错误");
}
// 验证clientType
if (!"herdsman-app".equals(loginBody.getClientType()) && !"vet-app".equals(loginBody.getClientType())) {
return AjaxResult.error("不支持的客户端类型");
}
// 创建认证token
WechatAuthenticationToken authToken = new WechatAuthenticationToken(
loginBody.getCode(),
loginBody.getClientType()
);
try {
// 认证
Authentication authentication = authenticationManager.authenticate(authToken);
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
loginService.recordLoginInfo(loginUser.getUserId());
// 生成token
String token = tokenService.createToken(loginUser);
// 返回结果
AjaxResult ajax = AjaxResult.success();
ajax.put(Constants.TOKEN, token);
return ajax;
} catch (WechatNeedBindException e) {
// 需要绑定手机号
Map<String, Object> data = new HashMap<>();
data.put("needBind", true);
data.put("userType", e.getUserType()); // "herdsman" "vet"
data.put("clientType", e.getClientType()); // "herdsman-app" "vet-app"
data.put("openid", e.getOpenid());
data.put("tempCode", e.getTempCode());
data.put("authType", e.getAuthType());
data.put("message", "请绑定手机号");
return AjaxResult.success(data);
}
}
/**
* 微信绑定手机号统一绑定接口
* 牧户和兽医使用同一套逻辑
*/
@PostMapping("/wechat/bind")
@Transactional
public AjaxResult wechatBind(@Valid @RequestBody WechatBindRequest request) {
try {
// 1. 基本参数验证
if (request.getEncryptedData() == null || request.getIv() == null) {
return AjaxResult.error("请通过微信授权获取手机号");
}
// 2. 从Redis获取sessionKey
String sessionKey = redisCache.getCacheObject("wechat:session:" + request.getTempCode());
if (sessionKey == null) {
return AjaxResult.error("绑定会话已过期,请重新登录");
}
// 3. 解密手机号
String phone = WechatDecryptUtil.decryptPhone(
request.getEncryptedData(),
request.getIv(),
sessionKey
);
if (phone == null || phone.trim().isEmpty()) {
return AjaxResult.error("获取手机号失败");
}
// 4. 查询手机号是否已注册
SysUser existingUser = userService.selectUserByPhone(phone);
// 5. 用户不存在创建新用户
if (existingUser == null) {
// 根据userType创建对应类型的用户
String userTypeCode = "herdsman".equals(request.getUserType()) ? "02" : "01";
existingUser = createUser(
phone,
userTypeCode,
request.getNickName(),
request.getAvatarUrl(),
request.getClientType()
);
} else {
// 6. 用户已存在检查用户类型是否匹配
String existingUserType = existingUser.getUserType();
String expectedUserType = "herdsman".equals(request.getUserType()) ? "02" : "01";
if (!existingUserType.equals(expectedUserType)) {
String existingTypeName = "01".equals(existingUserType) ? "兽医" : "牧户";
String expectedTypeName = "01".equals(expectedUserType) ? "兽医" : "牧户";
return AjaxResult.error("该手机号已注册为" + existingTypeName + ",请使用" + expectedTypeName + "小程序");
}
// 7. 用户类型匹配更新用户信息昵称头像等
if (request.getNickName() != null) {
existingUser.setNickName(request.getNickName());
}
if (request.getAvatarUrl() != null) {
existingUser.setAvatar(request.getAvatarUrl());
}
userService.updateUser(existingUser);
}
// 8. 绑定微信openid
userAuthService.bindAuth(
existingUser.getUserId(),
request.getAuthType(),
request.getOpenid(),
sessionKey
);
// 9. 创建登录用户
LoginUser loginUser = new LoginUser(
existingUser.getUserId(),
existingUser.getDeptId(),
existingUser,
permissionService.getMenuPermission(existingUser),
request.getClientType()
);
loginService.recordLoginInfo(loginUser.getUserId());
// 10. 生成token
String token = tokenService.createToken(loginUser);
// 11. 清理Redis临时数据
redisCache.deleteObject("wechat:session:" + request.getTempCode());
AjaxResult ajax = AjaxResult.success("绑定成功");
ajax.put(Constants.TOKEN, token);
return ajax;
} catch (Exception e) {
return AjaxResult.error("绑定失败: " + e.getMessage());
}
}
/**
* 手机号+密码登录PC端使用
* 牧户和兽医都可用
*/
@PostMapping("/phone/login")
public AjaxResult phoneLogin(@Valid @RequestBody PhoneLoginBody loginBody) {
// 验证参数
if (loginBody.getPhone() == null || loginBody.getPassword() == null) {
return AjaxResult.error("参数错误");
}
// 根据请求来源确定clientType
String clientType = loginBody.getClientType(); // 前端可传 "vet-pc" "herdsman-pc"
if (clientType == null) {
clientType = "vet-pc"; // 默认兽医PC端
}
// 创建认证token
PhoneAuthenticationToken authToken = new PhoneAuthenticationToken(
loginBody.getPhone(),
loginBody.getPassword(),
clientType
);
// 认证
Authentication authentication = authenticationManager.authenticate(authToken);
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
loginService.recordLoginInfo(loginUser.getUserId());
// 生成token
String token = tokenService.createToken(loginUser);
// 返回结果
AjaxResult ajax = AjaxResult.success();
ajax.put(Constants.TOKEN, token);
return ajax;
}
/**
* 创建用户牧户或兽医
*/
private SysUser createUser(String phone, String userType,
String nickName, String avatarUrl, String clientType) {
SysUser user = new SysUser();
// 生成用户名前缀 + 手机后4位 + 4位随机数
String usernamePrefix = "01".equals(userType) ? "vet_" : "muhu_";
String phoneSuffix = phone.length() > 4 ? phone.substring(phone.length() - 4) : phone;
String randomSuffix = String.format("%04d", (int)(Math.random() * 10000)); // 4位随机数
String username = usernamePrefix + phoneSuffix + "_" + randomSuffix;
user.setUserName(username);
// 设置昵称统一为"用户" + 去掉前缀的username部分
String displayName = phoneSuffix + "_" + randomSuffix; // 去掉前缀的部分
user.setNickName(nickName != null ? nickName : ("用户" + displayName));
user.setUserType(userType); // "01":兽医, "02":牧户
user.setEmail("");
user.setPhonenumber(phone);
user.setSex("0");
user.setAvatar(avatarUrl != null ? avatarUrl : "");
// 设置默认密码微信登录用不到但PC端登录需要
String defaultPassword = sysConfigService.selectConfigByKey("sys.user.initPassword");
user.setPassword(com.chenhai.common.utils.SecurityUtils.encryptPassword(defaultPassword));
user.setStatus("0"); // 正常状态
user.setDelFlag("0");
user.setCreateTime(new Date());
// 如果是兽医设置初始审核状态
// if ("01".equals(userType)) {
// user.setVetStatus("0"); // 0:未提交资质, 1:审核中, 2:已认证, 3:审核不通过
// }
userService.insertUser(user);
// 重新查询获取完整用户信息
return userService.selectUserByUserName(username);
}
}

15
chenhai-admin/src/main/resources/application.yml

@ -94,9 +94,22 @@ token:
# 令牌自定义标识 # 令牌自定义标识
header: Authorization header: Authorization
# 令牌密钥 # 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
# secret: abcdefghijklmnopqrstuvwxyz
secret: e8f5b8c9d2a1f3e5c7b9d1a3f5c7e9b1d3a5f7c9e1b3d5a7f9c1e3b5d7a9f1c3e5b7d9a1f3c5e7b9d1a3f5c7e9b1d3a5f7c9e1b3d5a7f9c1e3b5d7a9f1c3e5b7d9a1f3c5
# 令牌有效期(默认30分钟) # 令牌有效期(默认30分钟)
expireTime: 30 expireTime: 30
# 牧户30天(30*24*3600秒)
# application.yml
# 微信小程序配置
wx:
muhu:
app-id: wxb5becc8d6d8123a6
app-secret: 74f4211d3985aa782ff9148aa00f824e
vet:
app-id: ${WX_MINI_APPID:your_app_id}
app-secret: ${WX_MINI_SECRET:your_app_secret}
# MyBatis配置 # MyBatis配置
mybatis: mybatis:

12
chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUser.java

@ -38,6 +38,10 @@ public class SysUser extends BaseEntity
@Excel(name = "用户名称") @Excel(name = "用户名称")
private String nickName; private String nickName;
/** 用户类型 */
@Excel(name = "用户类型", readConverterExp = "00=系统用户,01=注册用户")
private String userType;
/** 用户邮箱 */ /** 用户邮箱 */
@Excel(name = "用户邮箱") @Excel(name = "用户邮箱")
private String email; private String email;
@ -145,6 +149,14 @@ public class SysUser extends BaseEntity
this.nickName = nickName; this.nickName = nickName;
} }
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
@Xss(message = "用户账号不能包含脚本字符") @Xss(message = "用户账号不能包含脚本字符")
@NotBlank(message = "用户账号不能为空") @NotBlank(message = "用户账号不能为空")
@Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符")

99
chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUserAuth.java

@ -0,0 +1,99 @@
package com.chenhai.common.core.domain.entity;
import com.chenhai.common.annotation.Excel;
import com.chenhai.common.core.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
/**
* 用户认证方式对象 sys_user_auth
*/
public class SysUserAuth extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 认证ID */
private Long authId;
/** 用户ID */
private Long userId;
/** 认证类型: admin/phone/wechat_muhu/wechat_vet */
@Excel(name = "认证类型")
private String authType;
/** 认证标识: 用户名/手机号/微信openid */
@Excel(name = "认证标识")
private String authKey;
/** 认证密钥: 密码/微信session_key */
private String authSecret;
/** 状态(0正常 1停用) */
@Excel(name = "状态", readConverterExp = "0=正常,1=停用")
private String status;
public Long getAuthId() {
return authId;
}
public void setAuthId(Long authId) {
this.authId = authId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getAuthType() {
return authType;
}
public void setAuthType(String authType) {
this.authType = authType;
}
public String getAuthKey() {
return authKey;
}
public void setAuthKey(String authKey) {
this.authKey = authKey;
}
public String getAuthSecret() {
return authSecret;
}
public void setAuthSecret(String authSecret) {
this.authSecret = authSecret;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("authId", getAuthId())
.append("userId", getUserId())
.append("authType", getAuthType())
.append("authKey", getAuthKey())
.append("authSecret", getAuthSecret())
.append("status", getStatus())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.toString();
}
}

23
chenhai-common/src/main/java/com/chenhai/common/core/domain/model/LoginUser.java

@ -71,6 +71,12 @@ public class LoginUser implements UserDetails
*/ */
private SysUser user; private SysUser user;
/**
* 客户端类型: admin/vet-pc/vet-app/herdsman-app
*/
private String clientType;
public LoginUser() public LoginUser()
{ {
} }
@ -89,6 +95,15 @@ public class LoginUser implements UserDetails
this.permissions = permissions; this.permissions = permissions;
} }
// 修改构造函数支持clientType
public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions, String clientType) {
this.userId = userId;
this.deptId = deptId;
this.user = user;
this.permissions = permissions;
this.clientType = clientType;
}
public Long getUserId() public Long getUserId()
{ {
return userId; return userId;
@ -258,6 +273,14 @@ public class LoginUser implements UserDetails
this.user = user; this.user = user;
} }
public String getClientType() {
return clientType;
}
public void setClientType(String clientType) {
this.clientType = clientType;
}
@Override @Override
public Collection<? extends GrantedAuthority> getAuthorities() public Collection<? extends GrantedAuthority> getAuthorities()
{ {

90
chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneDecryptRequest.java

@ -0,0 +1,90 @@
// PhoneDecryptRequest.java
package com.chenhai.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
public class PhoneDecryptRequest {
@NotBlank private String openid;
@NotBlank private String tempCode;
@NotBlank private String encryptedData;
@NotBlank private String iv;
@NotBlank private String authType;
@NotBlank private String userType; // "herdsman"
// 用户基本信息可选
private String nickName;
private String avatarUrl;
private Integer gender;
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getTempCode() {
return tempCode;
}
public void setTempCode(String tempCode) {
this.tempCode = tempCode;
}
public String getEncryptedData() {
return encryptedData;
}
public void setEncryptedData(String encryptedData) {
this.encryptedData = encryptedData;
}
public String getIv() {
return iv;
}
public void setIv(String iv) {
this.iv = iv;
}
public String getAuthType() {
return authType;
}
public void setAuthType(String authType) {
this.authType = authType;
}
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
}

33
chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneLoginBody.java

@ -0,0 +1,33 @@
package com.chenhai.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
public class PhoneLoginBody {
@NotBlank private String phone;
@NotBlank private String password;
private String clientType; // 可选前端可以指定 "vet-pc" "herdsman-pc"
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getClientType() {
return clientType;
}
public void setClientType(String clientType) {
this.clientType = clientType;
}
}

100
chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindRequest.java

@ -0,0 +1,100 @@
package com.chenhai.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
public class WechatBindRequest {
@NotBlank private String openid;
@NotBlank private String tempCode;
@NotBlank private String authType; // "wechat_muhu" "wechat_vet"
@NotBlank private String userType; // "herdsman" "vet"
@NotBlank private String clientType; // "herdsman-app" "vet-app"
// 微信加密的手机号数据
@NotBlank private String encryptedData;
@NotBlank private String iv;
// 用户基本信息
private String nickName;
private String avatarUrl;
private Integer gender;
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getTempCode() {
return tempCode;
}
public void setTempCode(String tempCode) {
this.tempCode = tempCode;
}
public String getAuthType() {
return authType;
}
public void setAuthType(String authType) {
this.authType = authType;
}
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
public String getClientType() {
return clientType;
}
public void setClientType(String clientType) {
this.clientType = clientType;
}
public String getEncryptedData() {
return encryptedData;
}
public void setEncryptedData(String encryptedData) {
this.encryptedData = encryptedData;
}
public String getIv() {
return iv;
}
public void setIv(String iv) {
this.iv = iv;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
}

64
chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindResult.java

@ -0,0 +1,64 @@
package com.chenhai.common.core.domain.model;
/**
* 微信绑定结果
*/
public class WechatBindResult {
private boolean success;
private String message;
private String token;
private boolean needBind; // 是否需要绑定
public WechatBindResult() {
}
public WechatBindResult(boolean success, String message) {
this.success = success;
this.message = message;
}
public WechatBindResult(boolean success, String message, String token) {
this.success = success;
this.message = message;
this.token = token;
}
// public WechatBindResult(boolean needBind, String message) {
// this.needBind = needBind;
// this.message = message;
// }
// Getters and Setters
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public boolean isNeedBind() {
return needBind;
}
public void setNeedBind(boolean needBind) {
this.needBind = needBind;
}
}

25
chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatLoginBody.java

@ -0,0 +1,25 @@
package com.chenhai.common.core.domain.model;
/**
* 微信登录请求体
*/
public class WechatLoginBody {
private String code;
private String clientType; // herdsman-app / vet-app
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getClientType() {
return clientType;
}
public void setClientType(String clientType) {
this.clientType = clientType;
}
}

66
chenhai-common/src/main/java/com/chenhai/common/utils/WechatDecryptUtil.java

@ -0,0 +1,66 @@
package com.chenhai.common.utils;
import com.alibaba.fastjson2.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* 微信解密工具类
*/
public class WechatDecryptUtil {
/**
* 解密微信加密数据手机号
*/
public static String decryptPhone(String encryptedData, String iv, String sessionKey) {
try {
byte[] dataByte = Base64.getDecoder().decode(encryptedData);
byte[] keyByte = Base64.getDecoder().decode(sessionKey);
byte[] ivByte = Base64.getDecoder().decode(iv);
// AES-128-CBC 解密
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivByte);
cipher.init(Cipher.DECRYPT_MODE, spec, ivSpec);
byte[] resultByte = cipher.doFinal(dataByte);
String result = new String(resultByte, StandardCharsets.UTF_8);
// 解析JSON获取手机号
JSONObject jsonObject = JSONObject.parseObject(result);
return jsonObject.getString("phoneNumber");
} catch (Exception e) {
throw new RuntimeException("微信手机号解密失败", e);
}
}
/**
* 解密用户信息
*/
public static JSONObject decryptUserInfo(String encryptedData, String iv, String sessionKey) {
try {
byte[] dataByte = Base64.getDecoder().decode(encryptedData);
byte[] keyByte = Base64.getDecoder().decode(sessionKey);
byte[] ivByte = Base64.getDecoder().decode(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivByte);
cipher.init(Cipher.DECRYPT_MODE, spec, ivSpec);
byte[] resultByte = cipher.doFinal(dataByte);
String result = new String(resultByte, StandardCharsets.UTF_8);
return JSONObject.parseObject(result);
} catch (Exception e) {
throw new RuntimeException("微信用户信息解密失败", e);
}
}
}

6
chenhai-framework/pom.xml

@ -59,6 +59,12 @@
<artifactId>chenhai-system</artifactId> <artifactId>chenhai-system</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies> </dependencies>
</project> </project>

80
chenhai-framework/src/main/java/com/chenhai/framework/config/AuthenticationProviderConfig.java

@ -0,0 +1,80 @@
package com.chenhai.framework.config;
import com.chenhai.common.core.redis.RedisCache;
import com.chenhai.framework.security.provider.PhoneAuthenticationProvider;
import com.chenhai.framework.security.provider.WechatAuthenticationProvider;
import com.chenhai.framework.web.service.UserDetailsServiceImpl;
import com.chenhai.framework.web.service.SysPermissionService;
import com.chenhai.framework.web.service.SysPasswordService;
import com.chenhai.muhu.service.IUserAuthService;
import com.chenhai.muhu.service.WechatService;
import com.chenhai.system.service.ISysConfigService;
import com.chenhai.system.service.ISysUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* 认证提供者配置类
*/
@Configuration
public class AuthenticationProviderConfig {
/**
* 默认的用户名密码认证提供者用于管理后台
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider(
UserDetailsServiceImpl userDetailsService,
BCryptPasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
provider.setHideUserNotFoundExceptions(false);
return provider;
}
/**
* 手机号认证提供者用于兽医PC端
*/
@Bean
public PhoneAuthenticationProvider phoneAuthenticationProvider(
IUserAuthService userAuthService,
ISysUserService userService,
SysPermissionService permissionService,
SysPasswordService passwordService,
BCryptPasswordEncoder passwordEncoder) {
return new PhoneAuthenticationProvider(
userAuthService,
userService,
permissionService,
passwordService,
passwordEncoder
);
}
/**
* 微信认证提供者用于小程序
*/
@Bean
public WechatAuthenticationProvider wechatAuthenticationProvider(
WechatService wechatService,
IUserAuthService userAuthService,
ISysUserService userService,
SysPermissionService permissionService,
ISysConfigService sysConfigService,
RedisCache redisCache) {
return new WechatAuthenticationProvider(
wechatService,
userAuthService,
userService,
permissionService,
sysConfigService,
redisCache
);
}
}

51
chenhai-framework/src/main/java/com/chenhai/framework/config/SecurityConfig.java

@ -1,10 +1,17 @@
package com.chenhai.framework.config; package com.chenhai.framework.config;
import com.chenhai.framework.security.provider.PhoneAuthenticationProvider;
import com.chenhai.framework.security.provider.WechatAuthenticationProvider;
import com.chenhai.framework.web.service.UserDetailsServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@ -19,11 +26,16 @@ import com.chenhai.framework.security.filter.JwtAuthenticationTokenFilter;
import com.chenhai.framework.security.handle.AuthenticationEntryPointImpl; import com.chenhai.framework.security.handle.AuthenticationEntryPointImpl;
import com.chenhai.framework.security.handle.LogoutSuccessHandlerImpl; import com.chenhai.framework.security.handle.LogoutSuccessHandlerImpl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** /**
* spring security配置 * spring security配置
* *
* @author ruoyi * @author ruoyi
*/ */
@Slf4j
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) @EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Configuration @Configuration
public class SecurityConfig public class SecurityConfig
@ -58,13 +70,45 @@ public class SecurityConfig
@Autowired @Autowired
private PermitAllUrlProperties permitAllUrl; private PermitAllUrlProperties permitAllUrl;
@Autowired
private UserDetailsServiceImpl userDetailsService;
/** /**
* 身份验证实现 * 身份验证实现
*/ */
// @Bean
// public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception
// {
// return authenticationConfiguration.getAuthenticationManager();
// }
@Bean @Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception
{
return authenticationConfiguration.getAuthenticationManager();
public AuthenticationManager authenticationManager(
DaoAuthenticationProvider daoAuthenticationProvider,
PhoneAuthenticationProvider phoneAuthenticationProvider,
WechatAuthenticationProvider wechatAuthenticationProvider) {
// 组合所有Provider
List<AuthenticationProvider> providers = new ArrayList<>();
providers.add(daoAuthenticationProvider); // 管理端用户名密码
// 添加手机号认证
if (phoneAuthenticationProvider != null) {
providers.add(phoneAuthenticationProvider);
log.info("PhoneAuthenticationProvider loaded successfully");
} else {
log.warn("PhoneAuthenticationProvider is not available");
}
// 添加微信认证
if (wechatAuthenticationProvider != null) {
providers.add(wechatAuthenticationProvider);
log.info("WechatAuthenticationProvider loaded successfully");
} else {
log.warn("WechatAuthenticationProvider is not available");
}
return new ProviderManager(providers);
} }
/** /**
@ -101,6 +145,7 @@ public class SecurityConfig
permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll()); permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll());
// 对于登录login 注册register 验证码captchaImage 允许匿名访问 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
requests.requestMatchers("/login", "/register", "/captchaImage").permitAll() requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
.requestMatchers("/auth/**").permitAll()
// 静态资源可匿名访问 // 静态资源可匿名访问
.requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll() .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll()
.requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll() .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll()

29
chenhai-framework/src/main/java/com/chenhai/framework/security/exception/WechatNeedBindException.java

@ -0,0 +1,29 @@
package com.chenhai.framework.security.exception;
import org.springframework.security.core.AuthenticationException;
public class WechatNeedBindException extends AuthenticationException {
private final String openid;
private final String tempCode;
private final String authType;
private final String userType; // "herdsman" "vet"
private final String clientType; // "herdsman-app" "vet-app"
public WechatNeedBindException(String msg, String openid, String tempCode,
String authType, String userType, String clientType) {
super(msg);
this.openid = openid;
this.tempCode = tempCode;
this.authType = authType;
this.userType = userType;
this.clientType = clientType;
}
// Getters
public String getOpenid() { return openid; }
public String getTempCode() { return tempCode; }
public String getAuthType() { return authType; }
public String getUserType() { return userType; }
public String getClientType() { return clientType; }
}

90
chenhai-framework/src/main/java/com/chenhai/framework/security/provider/PhoneAuthenticationProvider.java

@ -0,0 +1,90 @@
package com.chenhai.framework.security.provider;
import com.chenhai.common.core.domain.entity.SysUser;
import com.chenhai.common.core.domain.model.LoginUser;
import com.chenhai.common.exception.ServiceException;
import com.chenhai.common.utils.MessageUtils;
import com.chenhai.framework.security.token.PhoneAuthenticationToken;
import com.chenhai.framework.web.service.SysPasswordService;
import com.chenhai.framework.web.service.SysPermissionService;
import com.chenhai.muhu.service.IUserAuthService;
import com.chenhai.system.service.ISysUserService;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* 手机号认证Provider
*/
public class PhoneAuthenticationProvider implements AuthenticationProvider {
private final IUserAuthService userAuthService;
private final ISysUserService userService;
private final SysPermissionService permissionService;
private final SysPasswordService passwordService;
private final BCryptPasswordEncoder passwordEncoder;
public PhoneAuthenticationProvider(IUserAuthService userAuthService,
ISysUserService userService,
SysPermissionService permissionService,
SysPasswordService passwordService,
BCryptPasswordEncoder passwordEncoder) {
this.userAuthService = userAuthService;
this.userService = userService;
this.permissionService = permissionService;
this.passwordService = passwordService;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
PhoneAuthenticationToken authToken = (PhoneAuthenticationToken) authentication;
String phone = authToken.getPhone();
String password = authToken.getPassword();
String clientType = authToken.getClientType();
// 1. 验证参数
if (phone == null || password == null) {
throw new BadCredentialsException("手机号或密码不能为空");
}
// 2. 根据手机号查找用户
SysUser user = userAuthService.findUserByAuth("phone", phone);
if (user == null) {
throw new BadCredentialsException("手机号未注册");
}
// 3. 验证用户状态
if ("1".equals(user.getStatus())) {
throw new ServiceException(MessageUtils.message("user.blocked"));
}
if ("2".equals(user.getDelFlag())) {
throw new ServiceException(MessageUtils.message("user.password.delete"));
}
// 4. 验证密码
if (!passwordEncoder.matches(password, user.getPassword())) {
// 密码错误计数逻辑复用原有的
throw new BadCredentialsException("密码错误");
}
// 5. 创建LoginUser
LoginUser loginUser = new LoginUser(
user.getUserId(),
user.getDeptId(),
user,
permissionService.getMenuPermission(user),
clientType
);
return new PhoneAuthenticationToken(loginUser, loginUser.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return PhoneAuthenticationToken.class.isAssignableFrom(authentication);
}
}

142
chenhai-framework/src/main/java/com/chenhai/framework/security/provider/WechatAuthenticationProvider.java

@ -0,0 +1,142 @@
package com.chenhai.framework.security.provider;
import com.alibaba.fastjson2.JSONObject;
import com.chenhai.common.core.domain.entity.SysUser;
import com.chenhai.common.core.domain.model.LoginUser;
import com.chenhai.common.core.redis.RedisCache;
import com.chenhai.framework.security.exception.WechatNeedBindException;
import com.chenhai.framework.security.token.WechatAuthenticationToken;
import com.chenhai.framework.web.service.SysPermissionService;
import com.chenhai.system.service.ISysConfigService;
import com.chenhai.system.service.ISysUserService;
import com.chenhai.muhu.service.IUserAuthService;
import com.chenhai.muhu.service.WechatService;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.DigestUtils;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* 微信认证Provider - 统一处理牧户和兽医
*/
public class WechatAuthenticationProvider implements AuthenticationProvider {
private final WechatService wechatService;
private final IUserAuthService userAuthService;
private final ISysUserService userService;
private final SysPermissionService permissionService;
private final ISysConfigService sysConfigService;
private final RedisCache redisCache;
public WechatAuthenticationProvider(WechatService wechatService,
IUserAuthService userAuthService,
ISysUserService userService,
SysPermissionService permissionService,
ISysConfigService sysConfigService,
RedisCache redisCache) {
this.wechatService = wechatService;
this.userAuthService = userAuthService;
this.userService = userService;
this.permissionService = permissionService;
this.sysConfigService = sysConfigService;
this.redisCache = redisCache;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
WechatAuthenticationToken authToken = (WechatAuthenticationToken) authentication;
String code = authToken.getCode();
String clientType = authToken.getClientType();
// 1. 调用微信API获取openid
JSONObject wechatResult = wechatService.code2session(code, clientType);
String openid = wechatResult.getString("openid");
String sessionKey = wechatResult.getString("session_key");
if (openid == null) {
throw new BadCredentialsException("微信登录失败:未获取到openid");
}
// 2. 确定认证类型根据clientType
String authType = getAuthTypeByClientType(clientType);
// 3. 查询用户是否已绑定微信
SysUser user = userAuthService.findUserByAuth(authType, openid);
// 4. 已绑定用户直接登录
if (user != null) {
return createSuccessAuthentication(user, clientType);
}
// 5. 未绑定需要绑定手机号
String tempCode = generateTempCode(openid, authType);
// 存储sessionKey到Redis5分钟过期
redisCache.setCacheObject(
"wechat:session:" + tempCode,
sessionKey,
5, TimeUnit.MINUTES
);
// 6. 根据客户端类型确定用户类型
String userType = getUserTypeByClientType(clientType);
// 7. 抛出需要绑定的异常
throw new WechatNeedBindException(
"需要绑定手机号",
openid,
tempCode,
authType,
userType, // "herdsman" "vet"
clientType // "herdsman-app" "vet-app"
);
}
@Override
public boolean supports(Class<?> authentication) {
return WechatAuthenticationToken.class.isAssignableFrom(authentication);
}
private String getAuthTypeByClientType(String clientType) {
switch (clientType) {
case "herdsman-app":
return "wechat_muhu";
case "vet-app":
return "wechat_vet";
default:
throw new IllegalArgumentException("不支持的客户端类型: " + clientType);
}
}
private String getUserTypeByClientType(String clientType) {
switch (clientType) {
case "herdsman-app":
return "herdsman";
case "vet-app":
return "vet";
default:
throw new IllegalArgumentException("不支持的客户端类型: " + clientType);
}
}
private Authentication createSuccessAuthentication(SysUser user, String clientType) {
LoginUser loginUser = new LoginUser(
user.getUserId(),
user.getDeptId(),
user,
permissionService.getMenuPermission(user),
clientType
);
return new WechatAuthenticationToken(loginUser, loginUser.getAuthorities());
}
private String generateTempCode(String openid, String authType) {
String raw = openid + authType + System.currentTimeMillis() + Math.random();
return DigestUtils.md5DigestAsHex(raw.getBytes()).substring(0, 16);
}
}

69
chenhai-framework/src/main/java/com/chenhai/framework/security/token/PhoneAuthenticationToken.java

@ -0,0 +1,69 @@
package com.chenhai.framework.security.token;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* 手机号认证Token
*/
public class PhoneAuthenticationToken extends AbstractAuthenticationToken {
private final String phone;
private final String password;
private final String clientType;
private Object principal;
public PhoneAuthenticationToken(String phone, String password, String clientType) {
super(null);
this.phone = phone;
this.password = password;
this.clientType = clientType;
this.setAuthenticated(false);
}
public PhoneAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.phone = null;
this.password = null;
this.clientType = null;
super.setAuthenticated(true);
}
public String getPhone() {
return phone;
}
public String getPassword() {
return password;
}
public String getClientType() {
return clientType;
}
@Override
public Object getCredentials() {
return password;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}

62
chenhai-framework/src/main/java/com/chenhai/framework/security/token/WechatAuthenticationToken.java

@ -0,0 +1,62 @@
package com.chenhai.framework.security.token;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* 微信认证Token
*/
public class WechatAuthenticationToken extends AbstractAuthenticationToken {
private final String code;
private final String clientType;
private Object principal;
public WechatAuthenticationToken(String code, String clientType) {
super(null);
this.code = code;
this.clientType = clientType;
this.setAuthenticated(false);
}
public WechatAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.code = null;
this.clientType = null;
super.setAuthenticated(true);
}
public String getCode() {
return code;
}
public String getClientType() {
return clientType;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}

17
chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsService.java

@ -0,0 +1,17 @@
package com.chenhai.framework.web.service;
/**
* 短信服务接口
*/
public interface SysSmsService {
/**
* 发送短信验证码
*/
boolean sendSmsCode(String phone);
/**
* 验证短信验证码
*/
boolean verifySmsCode(String phone, String code);
}

27
chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsServiceImpl.java

@ -0,0 +1,27 @@
package com.chenhai.framework.web.service;
import com.chenhai.framework.web.service.SysSmsService;
import org.springframework.stereotype.Service;
/**
* 短信服务实现临时实现用于绕过依赖检查
*/
@Service
public class SysSmsServiceImpl implements SysSmsService {
@Override
public boolean sendSmsCode(String phone) {
// 临时实现直接返回true
// TODO: 后续实现真正的短信发送逻辑
System.out.println("发送短信验证码到: " + phone + " (模拟)");
return true;
}
@Override
public boolean verifySmsCode(String phone, String code) {
// 临时实现验证码固定为"123456"
// TODO: 后续实现真正的短信验证逻辑
System.out.println("验证短信验证码: phone=" + phone + ", code=" + code);
return "123456".equals(code);
}
}

63
chenhai-framework/src/main/java/com/chenhai/framework/web/service/TokenService.java

@ -145,13 +145,25 @@ public class TokenService
* *
* @param loginUser 登录信息 * @param loginUser 登录信息
*/ */
public void refreshToken(LoginUser loginUser)
{
// public void refreshToken(LoginUser loginUser)
// {
// loginUser.setLoginTime(System.currentTimeMillis());
// loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// // 根据uuid将loginUser缓存
// String userKey = getTokenKey(loginUser.getToken());
// redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
// }
// 2. 修改refreshToken方法支持多端
public void refreshToken(LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据客户端类型设置不同的过期时间
int customExpireTime = getExpireTimeByClientType(loginUser.getClientType());
loginUser.setExpireTime(loginUser.getLoginTime() + customExpireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存 // 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken()); String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
redisCache.setCacheObject(userKey, loginUser, customExpireTime, TimeUnit.MINUTES);
} }
/** /**
@ -229,4 +241,47 @@ public class TokenService
{ {
return CacheConstants.LOGIN_TOKEN_KEY + uuid; return CacheConstants.LOGIN_TOKEN_KEY + uuid;
} }
/**
* 根据客户端类型获取token过期时间分钟
*/
private int getExpireTimeByClientType(String clientType) {
if (clientType == null) {
return expireTime; // 默认30分钟
}
switch(clientType) {
case "herdsman-app":
// 从配置读取牧户token时长默认30天
return 43200; // 30天分钟
case "vet-app":
return 10080; // 7天
case "vet-pc":
return 480; // 8小时
case "admin":
default:
return expireTime; // 30分钟
}
}
// 修改createToken方法使用客户端类型决定过期时间
// public String createToken(LoginUser loginUser) {
// String token = IdUtils.fastUUID();
// loginUser.setToken(token);
// setUserAgent(loginUser);
//
// // 根据客户端类型设置不同的过期时间
// int customExpireTime = getExpireTimeByClientType(loginUser.getClientType());
// loginUser.setLoginTime(System.currentTimeMillis());
// loginUser.setExpireTime(loginUser.getLoginTime() + customExpireTime * MILLIS_MINUTE);
//
// // 根据uuid将loginUser缓存
// String userKey = getTokenKey(loginUser.getToken());
// redisCache.setCacheObject(userKey, loginUser, customExpireTime, TimeUnit.MINUTES);
//
// Map<String, Object> claims = new HashMap<>();
// claims.put(Constants.LOGIN_USER_KEY, token);
// claims.put(Constants.JWT_USERNAME, loginUser.getUsername());
// return createToken(claims);
// }
} }

38
chenhai-system/pom.xml

@ -23,6 +23,44 @@
<artifactId>chenhai-common</artifactId> <artifactId>chenhai-common</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- hutool工具包(HTTP请求和JSON处理) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version> <!-- 使用最新稳定版 -->
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Jackson(JSON处理,Spring Boot已包含,确认版本) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

57
chenhai-system/src/main/java/com/chenhai/muhu/domain/model/MuhuWxLoginBody.java

@ -0,0 +1,57 @@
package com.chenhai.muhu.domain.model;
import jakarta.validation.constraints.NotBlank;
/**
* 牧户微信登录请求体
*/
public class MuhuWxLoginBody {
@NotBlank(message = "微信code不能为空")
private String code; // 微信登录code
private String encryptedData; // 加密数据
private String iv; // 加密向量
private String nickName; // 昵称可选
private String avatarUrl; // 头像可选
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getEncryptedData() {
return encryptedData;
}
public void setEncryptedData(String encryptedData) {
this.encryptedData = encryptedData;
}
public String getIv() {
return iv;
}
public void setIv(String iv) {
this.iv = iv;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
}

36
chenhai-system/src/main/java/com/chenhai/muhu/service/IUserAuthService.java

@ -0,0 +1,36 @@
package com.chenhai.muhu.service;
import com.chenhai.common.core.domain.entity.SysUser;
import com.chenhai.common.core.domain.entity.SysUserAuth;
import java.util.List;
/**
* 用户认证方式服务接口
*/
public interface IUserAuthService {
/**
* 根据认证类型和标识查找用户
*/
SysUser findUserByAuth(String authType, String authKey);
/**
* 绑定新的认证方式
*/
int bindAuth(Long userId, String authType, String authKey, String authSecret);
/**
* 解绑认证方式
*/
int unbindAuth(Long userId, String authType);
/**
* 获取用户的所有认证方式
*/
List<SysUserAuth> getUserAuths(Long userId);
/**
* 检查认证方式是否存在
*/
boolean checkAuthExists(String authType, String authKey);
}

39
chenhai-system/src/main/java/com/chenhai/muhu/service/WechatService.java

@ -0,0 +1,39 @@
package com.chenhai.muhu.service;
import com.alibaba.fastjson2.JSONObject;
/**
* 微信服务接口
*/
public interface WechatService {
/**
* 小程序登录获取openid和session_key
*/
JSONObject code2session(String code, String clientType);
/**
* 获取微信小程序配置
*/
MiniProgramConfig getMiniProgramConfig(String clientType);
/**
* 微信小程序配置类
*/
class MiniProgramConfig {
private String appId;
private String appSecret;
public MiniProgramConfig(String appId, String appSecret) {
this.appId = appId;
this.appSecret = appSecret;
}
public String getAppId() {
return appId;
}
public String getAppSecret() {
return appSecret;
}
}
}

79
chenhai-system/src/main/java/com/chenhai/muhu/service/impl/UserAuthServiceImpl.java

@ -0,0 +1,79 @@
package com.chenhai.muhu.service.impl;
import com.chenhai.common.core.domain.entity.SysUser;
import com.chenhai.common.core.domain.entity.SysUserAuth;
import com.chenhai.muhu.service.IUserAuthService;
import com.chenhai.system.mapper.SysUserAuthMapper;
import com.chenhai.system.mapper.SysUserMapper;
import com.chenhai.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
/**
* 用户认证方式服务实现
*/
@Service
public class UserAuthServiceImpl implements IUserAuthService {
@Autowired
private SysUserAuthMapper sysUserAuthMapper;
@Autowired
private SysUserMapper sysUserMapper;
@Override
public SysUser findUserByAuth(String authType, String authKey) {
SysUserAuth auth = sysUserAuthMapper.selectByAuthTypeAndKey(authType, authKey);
if (auth == null) {
return null;
}
return sysUserMapper.selectUserById(auth.getUserId());
}
@Override
@Transactional
public int bindAuth(Long userId, String authType, String authKey, String authSecret) {
// 检查是否已存在
SysUserAuth existAuth = sysUserAuthMapper.selectByAuthTypeAndKey(authType, authKey);
if (existAuth != null) {
throw new RuntimeException("该认证方式已被其他用户绑定");
}
SysUserAuth auth = new SysUserAuth();
auth.setUserId(userId);
auth.setAuthType(authType);
auth.setAuthKey(authKey);
auth.setAuthSecret(authSecret);
auth.setStatus("0");
auth.setCreateTime(new Date());
return sysUserAuthMapper.insertSysUserAuth(auth);
}
@Override
@Transactional
public int unbindAuth(Long userId, String authType) {
List<SysUserAuth> auths = sysUserAuthMapper.selectByUserIdAndType(userId, authType);
if (auths.isEmpty()) {
return 0;
}
Long[] authIds = auths.stream().map(SysUserAuth::getAuthId).toArray(Long[]::new);
return sysUserAuthMapper.deleteSysUserAuthByAuthIds(authIds);
}
@Override
public List<SysUserAuth> getUserAuths(Long userId) {
return sysUserAuthMapper.selectByUserId(userId);
}
@Override
public boolean checkAuthExists(String authType, String authKey) {
SysUserAuth auth = sysUserAuthMapper.selectByAuthTypeAndKey(authType, authKey);
return auth != null;
}
}

76
chenhai-system/src/main/java/com/chenhai/muhu/service/impl/WechatServiceImpl.java

@ -0,0 +1,76 @@
package com.chenhai.muhu.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.chenhai.common.utils.StringUtils;
import com.chenhai.common.utils.http.HttpUtils;
import com.chenhai.muhu.service.WechatService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* 微信服务实现
*/
@Service
public class WechatServiceImpl implements WechatService {
private static final Logger log = LoggerFactory.getLogger(WechatServiceImpl.class);
// 微信API地址
private static final String WECHAT_API_URL = "https://api.weixin.qq.com/sns/jscode2session";
@Value("${wx.muhu.app-id:}")
private String muhuAppId;
@Value("${wx.muhu.app-secret:}")
private String muhuAppSecret;
@Value("${wx.vet.app-id:}")
private String vetAppId;
@Value("${wx.vet.app-secret:}")
private String vetAppSecret;
@Override
public JSONObject code2session(String code, String clientType) {
MiniProgramConfig config = getMiniProgramConfig(clientType);
if (config == null) {
throw new RuntimeException("未找到微信小程序配置");
}
String url = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
WECHAT_API_URL, config.getAppId(), config.getAppSecret(), code);
try {
String response = HttpUtils.sendGet(url);
log.info("微信API响应: {}", response);
JSONObject result = JSONObject.parseObject(response);
if (result.containsKey("errcode")) {
throw new RuntimeException("微信API调用失败: " + result.getString("errmsg"));
}
return result;
} catch (Exception e) {
log.error("调用微信API失败", e);
throw new RuntimeException("微信登录失败: " + e.getMessage());
}
}
@Override
public MiniProgramConfig getMiniProgramConfig(String clientType) {
if ("herdsman-app".equals(clientType)) {
if (StringUtils.isEmpty(muhuAppId) || StringUtils.isEmpty(muhuAppSecret)) {
throw new RuntimeException("牧户小程序配置未设置");
}
return new MiniProgramConfig(muhuAppId, muhuAppSecret);
} else if ("vet-app".equals(clientType)) {
if (StringUtils.isEmpty(vetAppId) || StringUtils.isEmpty(vetAppSecret)) {
throw new RuntimeException("兽医小程序配置未设置");
}
return new MiniProgramConfig(vetAppId, vetAppSecret);
}
throw new RuntimeException("不支持的客户端类型: " + clientType);
}
}

56
chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserAuthMapper.java

@ -0,0 +1,56 @@
package com.chenhai.system.mapper;
import com.chenhai.common.core.domain.entity.SysUserAuth;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户认证方式Mapper接口
*/
public interface SysUserAuthMapper {
/**
* 查询用户认证方式
*/
SysUserAuth selectSysUserAuthByAuthId(Long authId);
/**
* 根据认证类型和标识查询
*/
SysUserAuth selectByAuthTypeAndKey(@Param("authType") String authType, @Param("authKey") String authKey);
/**
* 查询用户的所有认证方式
*/
List<SysUserAuth> selectByUserId(Long userId);
/**
* 查询用户特定类型的认证方式
*/
List<SysUserAuth> selectByUserIdAndType(@Param("userId") Long userId, @Param("authType") String authType);
/**
* 新增用户认证方式
*/
int insertSysUserAuth(SysUserAuth sysUserAuth);
/**
* 修改用户认证方式
*/
int updateSysUserAuth(SysUserAuth sysUserAuth);
/**
* 删除用户认证方式
*/
int deleteSysUserAuthByAuthId(Long authId);
/**
* 批量删除用户认证方式
*/
int deleteSysUserAuthByAuthIds(Long[] authIds);
/**
* 删除用户的所有认证方式
*/
int deleteByUserId(Long userId);
}

8
chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserMapper.java

@ -52,6 +52,14 @@ public interface SysUserMapper
*/ */
public SysUser selectUserById(Long userId); public SysUser selectUserById(Long userId);
/**
* 通过手机号查询用户
*
* @param phone 手机号
* @return 用户对象信息
*/
public SysUser selectUserByPhone(String phone);
/** /**
* 新增用户信息 * 新增用户信息
* *

8
chenhai-system/src/main/java/com/chenhai/system/service/ISysUserService.java

@ -67,6 +67,14 @@ public interface ISysUserService
*/ */
public String selectUserPostGroup(String userName); public String selectUserPostGroup(String userName);
/**
* 通过手机号码查询用户
*
* @param phone 手机号码
* @return 结果
*/
public SysUser selectUserByPhone(String phone);
/** /**
* 校验用户名称是否唯一 * 校验用户名称是否唯一
* *

5
chenhai-system/src/main/java/com/chenhai/system/service/impl/SysUserServiceImpl.java

@ -163,6 +163,11 @@ public class SysUserServiceImpl implements ISysUserService
return list.stream().map(SysPost::getPostName).collect(Collectors.joining(",")); return list.stream().map(SysPost::getPostName).collect(Collectors.joining(","));
} }
@Override
public SysUser selectUserByPhone(String phone) {
return userMapper.selectUserByPhone(phone);
}
/** /**
* 校验用户名称是否唯一 * 校验用户名称是否唯一
* *

97
chenhai-system/src/main/resources/mapper/system/SysUserAuthMapper.xml

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.chenhai.system.mapper.SysUserAuthMapper">
<resultMap type="SysUserAuth" id="SysUserAuthResult">
<id property="authId" column="auth_id"/>
<result property="userId" column="user_id"/>
<result property="authType" column="auth_type"/>
<result property="authKey" column="auth_key"/>
<result property="authSecret" column="auth_secret"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="selectSysUserAuthVo">
select auth_id, user_id, auth_type, auth_key, auth_secret, status, create_time, update_time
from sys_user_auth
</sql>
<select id="selectSysUserAuthByAuthId" parameterType="Long" resultMap="SysUserAuthResult">
<include refid="selectSysUserAuthVo"/>
where auth_id = #{authId}
</select>
<select id="selectByAuthTypeAndKey" resultMap="SysUserAuthResult">
<include refid="selectSysUserAuthVo"/>
where auth_type = #{authType} and auth_key = #{authKey}
limit 1
</select>
<select id="selectByUserId" parameterType="Long" resultMap="SysUserAuthResult">
<include refid="selectSysUserAuthVo"/>
where user_id = #{userId}
order by create_time desc
</select>
<select id="selectByUserIdAndType" resultMap="SysUserAuthResult">
<include refid="selectSysUserAuthVo"/>
where user_id = #{userId} and auth_type = #{authType}
order by create_time desc
</select>
<insert id="insertSysUserAuth" parameterType="SysUserAuth" useGeneratedKeys="true" keyProperty="authId">
insert into sys_user_auth
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userId != null">user_id,</if>
<if test="authType != null and authType != ''">auth_type,</if>
<if test="authKey != null and authKey != ''">auth_key,</if>
<if test="authSecret != null and authSecret != ''">auth_secret,</if>
<if test="status != null">status,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userId != null">#{userId},</if>
<if test="authType != null and authType != ''">#{authType},</if>
<if test="authKey != null and authKey != ''">#{authKey},</if>
<if test="authSecret != null and authSecret != ''">#{authSecret},</if>
<if test="status != null">#{status},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
</trim>
</insert>
<update id="updateSysUserAuth" parameterType="SysUserAuth">
update sys_user_auth
<set>
<if test="userId != null">user_id = #{userId},</if>
<if test="authType != null and authType != ''">auth_type = #{authType},</if>
<if test="authKey != null and authKey != ''">auth_key = #{authKey},</if>
<if test="authSecret != null and authSecret != ''">auth_secret = #{authSecret},</if>
<if test="status != null">status = #{status},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
</set>
where auth_id = #{authId}
</update>
<delete id="deleteSysUserAuthByAuthId" parameterType="Long">
delete from sys_user_auth where auth_id = #{authId}
</delete>
<delete id="deleteSysUserAuthByAuthIds" parameterType="Long">
delete from sys_user_auth where auth_id in
<foreach collection="array" item="authId" open="(" separator="," close=")">
#{authId}
</foreach>
</delete>
<delete id="deleteByUserId" parameterType="Long">
delete from sys_user_auth where user_id = #{userId}
</delete>
</mapper>

7
chenhai-system/src/main/resources/mapper/system/SysUserMapper.xml

@ -131,6 +131,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where u.user_id = #{userId} where u.user_id = #{userId}
</select> </select>
<select id="selectUserByPhone" parameterType="String" resultMap="SysUserResult">
<include refid="selectUserVo"/>
where u.phonenumber = #{phone}
</select>
<select id="checkUserNameUnique" parameterType="String" resultMap="SysUserResult"> <select id="checkUserNameUnique" parameterType="String" resultMap="SysUserResult">
select user_id, user_name from sys_user where user_name = #{userName} and del_flag = '0' limit 1 select user_id, user_name from sys_user where user_name = #{userName} and del_flag = '0' limit 1
</select> </select>
@ -149,6 +154,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="deptId != null and deptId != 0">dept_id,</if> <if test="deptId != null and deptId != 0">dept_id,</if>
<if test="userName != null and userName != ''">user_name,</if> <if test="userName != null and userName != ''">user_name,</if>
<if test="nickName != null and nickName != ''">nick_name,</if> <if test="nickName != null and nickName != ''">nick_name,</if>
<if test="userType != null and userType != ''">user_type,</if>
<if test="email != null and email != ''">email,</if> <if test="email != null and email != ''">email,</if>
<if test="avatar != null and avatar != ''">avatar,</if> <if test="avatar != null and avatar != ''">avatar,</if>
<if test="phonenumber != null and phonenumber != ''">phonenumber,</if> <if test="phonenumber != null and phonenumber != ''">phonenumber,</if>
@ -164,6 +170,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="deptId != null and deptId != ''">#{deptId},</if> <if test="deptId != null and deptId != ''">#{deptId},</if>
<if test="userName != null and userName != ''">#{userName},</if> <if test="userName != null and userName != ''">#{userName},</if>
<if test="nickName != null and nickName != ''">#{nickName},</if> <if test="nickName != null and nickName != ''">#{nickName},</if>
<if test="userType != null and userType != ''">#{userType},</if>
<if test="email != null and email != ''">#{email},</if> <if test="email != null and email != ''">#{email},</if>
<if test="avatar != null and avatar != ''">#{avatar},</if> <if test="avatar != null and avatar != ''">#{avatar},</if>
<if test="phonenumber != null and phonenumber != ''">#{phonenumber},</if> <if test="phonenumber != null and phonenumber != ''">#{phonenumber},</if>

Loading…
Cancel
Save