服务启动

This commit is contained in:
2025-12-05 18:24:21 +08:00
parent a8233ceb72
commit 133209691e
39 changed files with 2526 additions and 30 deletions

View File

@@ -28,10 +28,23 @@ INSERT INTO config.tb_sys_config (
('CFG-0303', 'cfg_storage_base', 'storage.basePath', '存储路径', '/data/urban-lifeline', 'String', 'input', '本地存储基路径', NULL, NULL, 'storage', 'mod_file', 30, 0, '当 backend=local', 'system', NULL, NULL, now(), NULL, NULL, false),
-- 通知(邮件/SMS
('CFG-0401', 'cfg_mail_host', 'mail.smtp.host', 'SMTP主机', '', 'String', 'input', 'SMTP主机', NULL, NULL, 'notify', 'mod_message', 10, 1, '留空为未配置', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0402', 'cfg_mail_port', 'mail.smtp.port', 'SMTP端口', '465', 'INTEGER', 'input', 'SMTP端口', NULL, NULL, 'notify', 'mod_message', 20, 1, 'SSL常用465', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0403', 'cfg_mail_from', 'mail.from', '发件人邮箱', '', 'String', 'input', '发件人邮箱', NULL, NULL, 'notify', 'mod_message', 30, 1, '如 no-reply@x.com', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0411', 'cfg_sms_provider', 'sms.provider', '短信服务商', '', 'String', 'select', '短信服务商', NULL, '["aliyun", "tencent"]'::json, 'notify', 'mod_message', 40, 1, '如 aliyun/tencent', 'system', NULL, NULL, now(), NULL, NULL, false),
-- 邮件配置
('CFG-0401', 'cfg_mail_host', 'email.host', 'SMTP服务器地址', 'smtp.qq.com', 'String', 'input', 'SMTP服务器地址', NULL, NULL, 'notify', 'mod_message', 10, 1, '邮件发送服务器地址', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0402', 'cfg_mail_port', 'email.port', 'SMTP端口', '587', 'INTEGER', 'input', 'SMTP服务器端口', NULL, NULL, 'notify', 'mod_message', 20, 1, '常用25/465/587', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0403', 'cfg_mail_username', 'email.username', '发件人邮箱', '3223905473@qq.com', 'String', 'input', '发件人邮箱地址', NULL, NULL, 'notify', 'mod_message', 30, 1, '用于发送邮件的邮箱账号', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0404', 'cfg_mail_password', 'email.password', '邮箱授权码', 'xmdmxvtjumxocicc', 'String', 'password', '邮箱授权码/密码', NULL, NULL, 'notify', 'mod_message', 40, 1, '邮箱的授权码或密码', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0405', 'cfg_mail_fromname', 'email.fromName', '发件人名称', 'urban-lifeline平台', 'String', 'input', '发件人显示名称', NULL, NULL, 'notify', 'mod_message', 50, 1, '邮件中显示的发件人名称', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0406', 'cfg_mail_ssl', 'email.ssl.enable', '启用SSL', 'true', 'BOOLEAN', 'switch', '是否启用SSL', NULL, NULL, 'notify', 'mod_message', 60, 1, 'SSL加密连接', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0407', 'cfg_mail_timeout', 'email.timeout', '连接超时时间', '30000', 'INTEGER', 'input', '连接超时时间(毫秒)', NULL, NULL, 'notify', 'mod_message', 70, 1, 'SMTP连接超时时间5000-60000', 'system', NULL, NULL, now(), NULL, NULL, false),
-- 短信配置
('CFG-0411', 'cfg_sms_provider', 'sms.provider', '短信服务商', 'aliyun', 'String', 'select', '短信服务提供商', NULL, '["aliyun", "tencent"]'::json, 'notify', 'mod_message', 80, 1, '短信服务提供商类型', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0412', 'cfg_sms_keyid', 'sms.accessKeyId', 'AccessKey ID', 'LTAI5t68do3qVXx5Rufugt3X', 'String', 'input', '短信服务AccessKey ID', NULL, NULL, 'notify', 'mod_message', 90, 1, '云服务商的AccessKey ID', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0413', 'cfg_sms_secret', 'sms.accessKeySecret', 'AccessKey Secret', '2vD9ToIff49Vph4JQXsn0Cy8nXQfzA', 'String', 'password', '短信服务AccessKey Secret', NULL, NULL, 'notify', 'mod_message', 100, 1, '云服务商的AccessKey Secret', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0414', 'cfg_sms_sign', 'sms.signName', '短信签名', 'urban-lifeline', 'String', 'input', '短信签名', NULL, NULL, 'notify', 'mod_message', 110, 1, '发送短信使用的签名', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0415', 'cfg_sms_tpl_login', 'sms.templateCode.login', '登录验证码模板', 'SMS_491985030', 'String', 'input', '登录验证码模板编码', NULL, NULL, 'notify', 'mod_message', 120, 1, '登录验证码短信模板', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0416', 'cfg_sms_tpl_register', 'sms.templateCode.register','注册验证码模板', 'SMS_491985030', 'String', 'input', '注册验证码模板编码', NULL, NULL, 'notify', 'mod_message', 130, 1, '注册验证码短信模板', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0417', 'cfg_sms_timeout', 'sms.timeout', '请求超时时间', '30000', 'INTEGER', 'input', '请求超时时间(毫秒)', NULL, NULL, 'notify', 'mod_message', 140, 1, 'API请求超时时间5000-60000', 'system', NULL, NULL, now(), NULL, NULL, false),
-- 日志与审计
('CFG-0501', 'cfg_log_level', 'log.level', '日志级别', 'INFO', 'String', 'select', '系统日志级别', NULL, '["DEBUG", "INFO", "WARN", "ERROR"]'::json, 'log', 'mod_system', 10, 0, 'DEBUG/INFO/WARN/ERROR', 'system', NULL, NULL, now(), NULL, NULL, false),

View File

@@ -1,9 +0,0 @@
package org.xyzh.api.message;
/**
* Message服务接口
* 用于消息管理
*/
public interface MessageService {
}

View File

@@ -15,7 +15,19 @@ import org.xyzh.common.core.page.PageParam;
*/
public interface MessageService {
// ================ 发送邮件 ==================
ResultDomain<String> sendSimpleEmail(String to, String subject, String content);
ResultDomain<String> sendHtmlEmail(String to, String subject, String content);
ResultDomain<String> sendEmailVerificationCode(String to, String code);
// ================ 发送短信 ==================
//================= 用户查看消息列表 =================
ResultDomain<String> sendPhoneVerificationCode(String phone, String code);
/**
* @description 获取我的消息列表

View File

@@ -13,6 +13,43 @@ import org.xyzh.common.dto.sys.TbSysConfigDTO;
* @since 2025-11-05
*/
public interface SysConfigService {
// ================== 读取配置 ========================
/**
* 获取字符串类型配置
* @param key 配置键
* @return 配置值
*/
String getStringConfig(String key);
/**
* 获取整数类型配置
* @param key 配置键
* @return 配置值如果不存在或解析失败返回null
*/
Integer getIntConfig(String key);
/**
* 获取布尔类型配置
* @param key 配置键
* @return 配置值如果不存在或解析失败返回null
*/
Boolean getBooleanConfig(String key);
/**
* 获取浮点数类型配置
* @param key 配置键
* @return 配置值如果不存在或解析失败返回null
*/
Double getDoubleConfig(String key);
/**
* 获取长整数类型配置
* @param key 配置键
* @return 配置值如果不存在或解析失败返回null
*/
Long getLongConfig(String key);
// =====================================================
/**
* @description 插入系统配置

View File

@@ -18,6 +18,10 @@ import org.xyzh.common.dto.sys.TbSysUserRoleDTO;
public interface SysUserService {
// ================= 用户基本信息管理 =================
ResultDomain<TbSysUserDTO> registerUser(SysUserVO userVO);
/**
* @description 插入用户
* @param userVO 用户VO
@@ -61,7 +65,7 @@ public interface SysUserService {
* @author yslg
* @since 2025-11-05
*/
ResultDomain<SysUserVO> getLoginUser(SysUserVO filter);
ResultDomain<TbSysUserDTO> getLoginUser(TbSysUserDTO filter);
/**
* @description 根据条件查询用户列表

View File

@@ -23,10 +23,26 @@
<groupId>org.xyzh.apis</groupId>
<artifactId>api-auth</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.apis</groupId>
<artifactId>api-system</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.apis</groupId>
<artifactId>api-message</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-auth</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-core</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-utils</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,433 @@
package org.xyzh.auth.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import org.xyzh.api.auth.service.AuthService;
import org.xyzh.api.message.service.MessageService;
import org.xyzh.api.system.service.SysUserService;
import org.xyzh.api.system.vo.SysUserVO;
import org.xyzh.auth.utils.CapcatUtils;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.sys.TbSysUserDTO;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.common.utils.validation.method.PhoneValidateMethod;
import org.xyzh.common.redis.service.RedisService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
import java.util.Map;
/**
* @description AuthController.java文件描述 认证控制器
* @filename AuthController.java
* @author yslg
* @copyright xyzh
* @since 2025-09-28
*/
@RestController
@RequestMapping("/auth")
public class AuthController {
private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
@Autowired
private AuthService authService;
@DubboReference(version = "1.0.0", group = "system", timeout = 5000, check = false)
private SysUserService userService;
@DubboReference(version = "1.0.0", group = "message", timeout = 5000, check = false)
private MessageService messageService;
@Autowired
private RedisService redisService;
@Autowired
private PasswordEncoder passwordEncoder;
/**
* @description 用户登录
* @param loginParam 登录参数
* @return ResultDomain<LoginDomain> 登录结果
* @author yslg
* @since 2025-09-28
*/
@PostMapping("/login")
public ResultDomain<LoginDomain> login(@RequestBody LoginParam loginParam, HttpServletRequest request) {
return authService.login(loginParam, request);
}
/**
* @description 用户退出登录
* @param loginDomain 登录域对象
* @return ResultDomain<String> 退出结果
* @author yslg
* @since 2025-09-28
*/
@PostMapping("/logout")
public ResultDomain<LoginDomain> logout(HttpServletRequest request) {
return authService.logout(request);
}
/**
* @description 获取验证码
* @return ResultDomain<String> 验证码
* @author yslg
* @since 2025-09-28
*/
@GetMapping("/captcha")
public ResultDomain<String> getCaptcha() {
// TODO: 实现验证码生成逻辑
// 生成验证码会话ID用于验证时匹配
String captchaId = IDUtils.generateID();
String captchaData = captchaId + ":captcha-placeholder"; // 格式: ID:验证码内容
return ResultDomain.success("验证码获取成功", captchaData);
}
/**
* @description 刷新令牌
* @param token 原令牌
* @return ResultDomain<String> 新令牌
* @author yslg
* @since 2025-09-28
*/
@PostMapping("/refresh")
public ResultDomain<String> refreshToken(@RequestHeader("Authorization") String token) {
// TODO: 实现令牌刷新逻辑
// 为新令牌生成唯一ID
String newTokenId = IDUtils.generateID();
String newToken = "new-token-" + newTokenId; // 临时占位符
return ResultDomain.success("令牌刷新成功", newToken);
}
/**
* @description 健康检查
* @return ResultDomain<String> 健康状态
* @author yslg
* @since 2025-09-28
*/
@GetMapping("/health")
public ResultDomain<String> health() {
return ResultDomain.success("认证服务运行正常", "OK");
}
/**
* @description 发送邮箱验证码
* @param requestBody 包含email字段的请求体
* @return ResultDomain<Boolean> 发送结果
* @author yslg
* @since 2025-11-03
*/
@PostMapping("/send-email-code")
public ResultDomain<Map<String, String>> sendEmailCode(@RequestBody Map<String, String> requestBody) {
String email = requestBody.get("email");
// 验证邮箱格式
if (email == null || email.trim().isEmpty()) {
return ResultDomain.failure("邮箱不能为空");
}
String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
if (!email.matches(emailRegex)) {
return ResultDomain.failure("邮箱格式不正确");
}
// 检查是否频繁发送60秒内只能发送一次
String rateLimitKey = "email:code:ratelimit:" + email;
if (redisService.hasKey(rateLimitKey)) {
return ResultDomain.failure("验证码已发送,请勿重复发送");
}
// 生成会话ID用于绑定验证码和用户
String sessionId = IDUtils.generateID();
// 生成6位数字验证码
String code = CapcatUtils.generateVerificationCode();
// 发送邮件
boolean success = messageService.sendEmailVerificationCode(email, code).getSuccess();
if (success) {
// 将验证码存储到Redis绑定sessionId有效期5分钟
String codeKey = "email:code:" + sessionId;
String codeValue = email + ":" + code; // 格式:邮箱:验证码
redisService.set(codeKey, codeValue, 5, TimeUnit.MINUTES);
// 设置5分钟的发送频率限制
redisService.set(rateLimitKey, "1", 5, TimeUnit.MINUTES);
// 返回sessionId给前端
Map<String, String> data = Map.of(
"sessionId", sessionId,
"message", "验证码已发送到邮箱"
);
logger.info("邮箱验证码已发送,邮箱: {}, sessionId: {}", email, sessionId);
return ResultDomain.success("验证码已发送到邮箱", data);
} else {
return ResultDomain.failure("验证码发送失败,请稍后重试");
}
}
/**
* @description 发送手机验证码
* @param requestBody 包含phone字段的请求体
* @return ResultDomain<Boolean> 发送结果
* @author yslg
* @since 2025-11-03
*/
@PostMapping("/send-sms-code")
public ResultDomain<Map<String, String>> sendSmsCode(@RequestBody Map<String, String> requestBody) {
String phone = requestBody.get("phone");
// 验证手机号格式
if (phone == null || phone.trim().isEmpty()) {
return ResultDomain.failure("手机号不能为空");
}
PhoneValidateMethod validateMethod = new PhoneValidateMethod();
if (!validateMethod.validate(phone)) {
return ResultDomain.failure("手机号格式不正确");
}
// 检查是否频繁发送60秒内只能发送一次
String rateLimitKey = "sms:code:ratelimit:" + phone;
if (redisService.hasKey(rateLimitKey)) {
return ResultDomain.failure("验证码已发送,请勿重复发送");
}
// 生成会话ID用于绑定验证码和用户
String sessionId = IDUtils.generateID();
// 生成6位数字验证码
String code = CapcatUtils.generateVerificationCode();
// 发送短信
boolean success = messageService.sendPhoneVerificationCode(phone, code).getSuccess();
if (success) {
// 将验证码存储到Redis绑定sessionId有效期5分钟
String codeKey = "sms:code:" + sessionId;
String codeValue = phone + ":" + code; // 格式:手机号:验证码
redisService.set(codeKey, codeValue, 5, TimeUnit.MINUTES);
// 设置5分钟的发送频率限制
redisService.set(rateLimitKey, "1", 5, TimeUnit.MINUTES);
// 返回sessionId给前端
Map<String, String> data = Map.of(
"sessionId", sessionId,
"message", "验证码已发送"
);
logger.info("短信验证码已发送,手机号: {}, sessionId: {}", phone, sessionId);
return ResultDomain.success("验证码已发送", data);
} else {
return ResultDomain.failure("验证码发送失败,请稍后重试");
}
}
/**
* @description 用户注册
* @param requestBody 注册参数
* @return ResultDomain<LoginDomain> 注册结果
* @author yslg
* @since 2025-11-03
*/
@PostMapping("/register")
public ResultDomain<LoginDomain> register(@RequestBody Map<String, Object> requestBody, HttpServletRequest request) {
try {
// 获取注册参数
String registerType = (String) requestBody.get("registerType");
String username = (String) requestBody.get("username");
String phone = (String) requestBody.get("phone");
String email = (String) requestBody.get("email");
String password = (String) requestBody.get("password");
String confirmPassword = (String) requestBody.get("confirmPassword");
String smsCode = (String) requestBody.get("smsCode");
String emailCode = (String) requestBody.get("emailCode");
String smsSessionId = (String) requestBody.get("smsSessionId");
String emailSessionId = (String) requestBody.get("emailSessionId");
String studentId = (String) requestBody.get("studentId");
// 1. 参数验证
if (password == null || password.trim().isEmpty()) {
return ResultDomain.failure("密码不能为空");
}
if (password.length() < 6) {
return ResultDomain.failure("密码至少6个字符");
}
if (!password.equals(confirmPassword)) {
return ResultDomain.failure("两次输入的密码不一致");
}
// 2. 根据注册类型进行不同的验证
SysUserVO user = new SysUserVO();
switch (registerType) {
case "username":
// 用户名注册
if (username == null || username.trim().isEmpty()) {
return ResultDomain.failure("用户名不能为空");
}
if (username.length() < 3 || username.length() > 20) {
return ResultDomain.failure("用户名长度为3-20个字符");
}
if (!username.matches("^[a-zA-Z0-9_]+$")) {
return ResultDomain.failure("用户名只能包含字母、数字和下划线");
}
user.setUsername(username);
logger.info("用户名注册: {}", username);
break;
case "phone":
// 手机号注册
if (phone == null || phone.trim().isEmpty()) {
return ResultDomain.failure("手机号不能为空");
}
if (!phone.matches("^1[3-9]\\d{9}$")) {
return ResultDomain.failure("手机号格式不正确");
}
if (smsCode == null || smsCode.trim().isEmpty()) {
return ResultDomain.failure("请输入手机验证码");
}
if (smsSessionId == null || smsSessionId.trim().isEmpty()) {
return ResultDomain.failure("会话已失效,请重新获取验证码");
}
// 通过sessionId验证手机验证码
String smsCodeKey = "sms:code:" + smsSessionId;
String storedSmsValue = (String) redisService.get(smsCodeKey);
if (storedSmsValue == null) {
return ResultDomain.failure("验证码已过期,请重新获取");
}
// 解析存储的值:手机号:验证码
String[] smsParts = storedSmsValue.split(":");
if (smsParts.length != 2) {
return ResultDomain.failure("验证码数据异常");
}
String storedPhone = smsParts[0];
String storedSmsCode = smsParts[1];
// 验证手机号和验证码是否匹配
if (!storedPhone.equals(phone)) {
logger.warn("手机号注册验证失败,提交手机号: {}, 验证码绑定手机号: {}", phone, storedPhone);
return ResultDomain.failure("手机号与验证码不匹配");
}
if (!storedSmsCode.equals(smsCode)) {
return ResultDomain.failure("验证码错误");
}
// 验证码使用后删除
redisService.delete(smsCodeKey);
user.setPhone(phone);
user.setUsername(phone); // 使用手机号作为用户名
logger.info("手机号注册: {}, sessionId: {}", phone, smsSessionId);
break;
case "email":
// 邮箱注册
if (email == null || email.trim().isEmpty()) {
return ResultDomain.failure("邮箱不能为空");
}
if (!email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) {
return ResultDomain.failure("邮箱格式不正确");
}
if (emailCode == null || emailCode.trim().isEmpty()) {
return ResultDomain.failure("请输入邮箱验证码");
}
if (emailSessionId == null || emailSessionId.trim().isEmpty()) {
return ResultDomain.failure("会话已失效,请重新获取验证码");
}
// 通过sessionId验证邮箱验证码
String emailCodeKey = "email:code:" + emailSessionId;
String storedEmailValue = (String) redisService.get(emailCodeKey);
if (storedEmailValue == null) {
return ResultDomain.failure("验证码已过期,请重新获取");
}
// 解析存储的值:邮箱:验证码
String[] emailParts = storedEmailValue.split(":");
if (emailParts.length != 2) {
return ResultDomain.failure("验证码数据异常");
}
String storedEmail = emailParts[0];
String storedEmailCode = emailParts[1];
// 验证邮箱和验证码是否匹配
if (!storedEmail.equals(email)) {
logger.warn("邮箱注册验证失败,提交邮箱: {}, 验证码绑定邮箱: {}", email, storedEmail);
return ResultDomain.failure("邮箱与验证码不匹配");
}
if (!storedEmailCode.equals(emailCode)) {
return ResultDomain.failure("验证码错误");
}
// 验证码使用后删除
redisService.delete(emailCodeKey);
user.setEmail(email);
user.setUsername(email.split("@")[0]); // 使用邮箱前缀作为用户名
logger.info("邮箱注册: {}, sessionId: {}", email, emailSessionId);
break;
default:
return ResultDomain.failure("未知的注册类型");
}
// 3. 设置密码明文Service层会加密
user.setPassword(password);
// 4. 设置用户状态为正常
user.setStatus("0");
// 5. 调用UserService注册用户Service层会加密密码
ResultDomain<TbSysUserDTO> registerResult = userService.registerUser(user);
if (!registerResult.getSuccess()) {
return ResultDomain.failure(registerResult.getMessage());
}
logger.info("用户注册成功: {}", user.getUsername());
// 6. 注册成功后自动登录
LoginParam loginParam = new LoginParam();
loginParam.setUsername(user.getUsername());
loginParam.setPassword(password);
loginParam.setLoginType("password");
if (phone != null && !phone.trim().isEmpty()) {
loginParam.setPhone(phone);
}
if (email != null && !email.trim().isEmpty()) {
loginParam.setEmail(email);
}
ResultDomain<LoginDomain> loginResult = authService.login(loginParam, request);
if (loginResult.getSuccess()) {
return ResultDomain.success("注册成功", loginResult.getData());
} else {
// 注册成功但登录失败,返回注册成功信息
return ResultDomain.success("注册成功,请登录", loginResult.getData());
}
} catch (Exception e) {
logger.error("用户注册失败", e);
return ResultDomain.failure("注册失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,9 @@
package org.xyzh.auth.exception;
import org.xyzh.common.core.exception.BaseException;
public class AuthException extends BaseException {
public AuthException(Integer code, String description, String message){
super(code, description, message);
}
}

View File

@@ -4,7 +4,7 @@ import org.xyzh.api.auth.service.AuthService;
import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.core.domain.ResultDomain;
import org.apache.dubbo.config.annotation.DubboService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,6 +17,12 @@ import jakarta.servlet.http.HttpServletRequest;
* @copyright yslg
* @since 2025-11-09
*/
@DubboService(
version = "1.0.0",
group = "auth",
timeout = 3000,
retries = 0
)
public class AuthServiceImpl implements AuthService{
private static final Logger logger = LoggerFactory.getLogger(AuthServiceImpl.class);

View File

@@ -0,0 +1,62 @@
package org.xyzh.auth.strategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.sys.TbSysUserDTO;
/**
* @description LoginStrategy.java文件描述 登录策略接口
* @filename LoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-12-05
*/
public interface LoginStrategy {
/**
* @description 支持的登录类型
* @return String 登录类型
* @author yslg
* @since 2025-12-05
*/
String getLoginType();
/**
* @description 验证登录参数
* @param loginParam 登录参数
* @return boolean 是否有效
* @author yslg
* @since 2025-12-05
*/
boolean validate(LoginParam loginParam);
/**
* @description 根据登录参数查找用户
* @param loginParam 登录参数
* @return TbSysUser 用户对象
* @author yslg
* @since 2025-12-05
*/
TbSysUserDTO findUser(LoginParam loginParam);
/**
* @description 验证凭据(密码或验证码)
* @param inputCredential 输入凭据(密码或验证码)
* @param storedCredential 存储凭据(密码或验证码)
* @return boolean 是否匹配
* @author yslg
* @since 2025-12-05
*/
boolean verifyCredential(String inputCredential, String storedCredential);
/**
* @description 验证验证码从Redis获取并验证SessionID
* @param loginParam 登录参数
* @return boolean 是否验证成功
* @author yslg
* @since 2025-12-05
*/
default boolean verifyCaptchaWithSession(LoginParam loginParam) {
// 默认实现:不支持验证码登录
return false;
}
}

View File

@@ -0,0 +1,52 @@
package org.xyzh.auth.strategy;
import org.springframework.stereotype.Component;
import org.xyzh.auth.exception.AuthException;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @description LoginStrategyFactory.java文件描述 登录策略工厂
* @filename LoginStrategyFactory.java
* @author yslg
* @copyright xyzh
* @since 2025-12-05
*/
@Component
public class LoginStrategyFactory {
private final Map<String, LoginStrategy> strategies;
public LoginStrategyFactory(List<LoginStrategy> loginStrategies) {
this.strategies = loginStrategies.stream()
.collect(Collectors.toMap(LoginStrategy::getLoginType, Function.identity()));
}
/**
* @description 获取登录策略
* @param loginType 登录类型
* @return LoginStrategy 登录策略
* @author yslg
* @since 2025-12-05
*/
public LoginStrategy getStrategy(String loginType) {
LoginStrategy strategy = strategies.get(loginType);
if (strategy == null) {
throw new AuthException(1,"UNSUPPORTED_LOGIN_TYPE", "不支持的登录类型: " + loginType);
}
return strategy;
}
/**
* @description 获取所有支持的登录类型
* @return Set<String> 登录类型集合
* @author yslg
* @since 2025-12-05
*/
public java.util.Set<String> getSupportedLoginTypes() {
return strategies.keySet();
}
}

View File

@@ -0,0 +1,104 @@
package org.xyzh.auth.strategy.impl;
import org.springframework.stereotype.Component;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.sys.TbSysUserDTO;
import org.xyzh.api.system.service.SysUserService;
import org.xyzh.api.system.vo.SysUserVO;
import org.xyzh.common.redis.service.RedisService;
/**
* @description EmailLoginStrategy.java文件描述 邮箱登录策略
* @filename EmailLoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-12-05
*/
@Component
public class EmailLoginStrategy implements LoginStrategy {
@Autowired
private SysUserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public String getLoginType() {
return "email";
}
@Override
public boolean validate(LoginParam loginParam) {
if (loginParam.getEmail() == null || loginParam.getEmail().trim().isEmpty()) {
return false;
}
// 密码登录或验证码登录都可以
return (loginParam.getPassword() != null && !loginParam.getPassword().trim().isEmpty())
|| (loginParam.getCaptcha() != null && !loginParam.getCaptcha().trim().isEmpty());
}
@Override
public TbSysUserDTO findUser(LoginParam loginParam) {
TbSysUserDTO filter = new TbSysUserDTO();
filter.setEmail(loginParam.getEmail());
TbSysUserDTO user = userService.getLoginUser(filter).getData();
if(user == null) {
return null;
}
return user;
}
@Autowired
private RedisService redisService;
@Override
public boolean verifyCredential(String inputCredential, String storedCredential) {
// 密码验证
return passwordEncoder.matches(inputCredential, storedCredential);
}
@Override
public boolean verifyCaptchaWithSession(LoginParam loginParam) {
String captchaId = loginParam.getCaptchaId();
String inputCaptcha = loginParam.getCaptcha();
String email = loginParam.getEmail();
// 验证参数
if (captchaId == null || captchaId.trim().isEmpty()) {
return false;
}
if (inputCaptcha == null || inputCaptcha.trim().isEmpty()) {
return false;
}
// 从Redis获取验证码
String codeKey = "email:code:" + captchaId;
String storedValue = (String) redisService.get(codeKey);
if (storedValue == null) {
return false;
}
// 解析存储的值:邮箱:验证码
String[] parts = storedValue.split(":");
if (parts.length != 2) {
return false;
}
String storedEmail = parts[0];
String storedCaptcha = parts[1];
// 验证邮箱和验证码是否匹配
if (!storedEmail.equals(email) || !storedCaptcha.equals(inputCaptcha)) {
return false;
}
// 验证码使用后删除
redisService.delete(codeKey);
return true;
}
}

View File

@@ -0,0 +1,81 @@
package org.xyzh.auth.strategy.impl;
import org.springframework.stereotype.Component;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.sys.TbSysUserDTO;
import org.xyzh.common.utils.NonUtils;
import org.xyzh.common.utils.validation.method.EmailValidateMethod;
import org.xyzh.common.utils.validation.method.PhoneValidateMethod;
import org.xyzh.api.system.service.SysUserService;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @description PasswordLoginStrategy.java文件描述 密码登录策略
* @filename PasswordLoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-12-05
*/
@Component
public class PasswordLoginStrategy implements LoginStrategy {
private static final Logger logger = LoggerFactory.getLogger(PasswordLoginStrategy.class);
@Autowired
private SysUserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public String getLoginType() {
return "password";
}
@Override
public boolean validate(LoginParam loginParam) {
if (NonUtils.isEmpty(loginParam.getPassword())) {
return false;
}
if (NonUtils.isEmpty(loginParam.getUsername()) && NonUtils.isEmpty(loginParam.getEmail()) && NonUtils.isEmpty(loginParam.getPhone())) {
return false;
}
return true;
}
@Override
public TbSysUserDTO findUser(LoginParam loginParam) {
TbSysUserDTO filter = new TbSysUserDTO();
EmailValidateMethod emailValidateMethod = new EmailValidateMethod();
PhoneValidateMethod phoneValidateMethod = new PhoneValidateMethod();
if(emailValidateMethod.validate(loginParam.getUsername())){
filter.setEmail(loginParam.getUsername());
}else if (phoneValidateMethod.validate(loginParam.getUsername())){
filter.setPhone(loginParam.getUsername());
}else{
filter.setUsername(loginParam.getUsername());
}
// 【优化】删除无用的密码编码SQL查询不使用password字段
// 密码验证在 verifyCredential() 方法中进行
TbSysUserDTO user = userService.getLoginUser(filter).getData();
if(user == null) {
return null;
}
return user;
}
@Override
public boolean verifyCredential(String inputCredential, String storedCredential) {
// 使用BCrypt的matches方法验证密码内部会自动处理salt
return passwordEncoder.matches(inputCredential, storedCredential);
}
}

View File

@@ -0,0 +1,103 @@
package org.xyzh.auth.strategy.impl;
import org.springframework.stereotype.Component;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.sys.TbSysUserDTO;
import org.xyzh.api.system.service.SysUserService;
import org.xyzh.common.redis.service.RedisService;
/**
* @description PhoneLoginStrategy.java文件描述 手机号登录策略
* @filename PhoneLoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-12-05
*/
@Component
public class PhoneLoginStrategy implements LoginStrategy {
@Autowired
private SysUserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public String getLoginType() {
return "phone";
}
@Override
public boolean validate(LoginParam loginParam) {
if (loginParam.getPhone() == null || loginParam.getPhone().trim().isEmpty()) {
return false;
}
// 密码登录或验证码登录都可以
return (loginParam.getPassword() != null && !loginParam.getPassword().trim().isEmpty())
|| (loginParam.getCaptcha() != null && !loginParam.getCaptcha().trim().isEmpty());
}
@Override
public TbSysUserDTO findUser(LoginParam loginParam) {
TbSysUserDTO filter = new TbSysUserDTO();
filter.setPhone(loginParam.getPhone());
TbSysUserDTO user = userService.getLoginUser(filter).getData();
if(user == null) {
return null;
}
return user;
}
@Autowired
private RedisService redisService;
@Override
public boolean verifyCredential(String inputCredential, String storedCredential) {
// 密码验证
return passwordEncoder.matches(inputCredential, storedCredential);
}
@Override
public boolean verifyCaptchaWithSession(LoginParam loginParam) {
String captchaId = loginParam.getCaptchaId();
String inputCaptcha = loginParam.getCaptcha();
String phone = loginParam.getPhone();
// 验证参数
if (captchaId == null || captchaId.trim().isEmpty()) {
return false;
}
if (inputCaptcha == null || inputCaptcha.trim().isEmpty()) {
return false;
}
// 从Redis获取验证码
String codeKey = "sms:code:" + captchaId;
String storedValue = (String) redisService.get(codeKey);
if (storedValue == null) {
return false;
}
// 解析存储的值:手机号:验证码
String[] parts = storedValue.split(":");
if (parts.length != 2) {
return false;
}
String storedPhone = parts[0];
String storedCaptcha = parts[1];
// 验证手机号和验证码是否匹配
if (!storedPhone.equals(phone) || !storedCaptcha.equals(inputCaptcha)) {
return false;
}
// 验证码使用后删除
redisService.delete(codeKey);
return true;
}
}

View File

@@ -0,0 +1,53 @@
package org.xyzh.auth.strategy.impl;
import org.springframework.stereotype.Component;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.sys.TbSysUserDTO;
import org.xyzh.api.system.service.SysUserService;
/**
* @description UsernameLoginStrategy.java文件描述 用户名登录策略
* @filename UsernameLoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-12-05
*/
@Component
public class UsernameLoginStrategy implements LoginStrategy {
@Autowired
private SysUserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public String getLoginType() {
return "username";
}
@Override
public boolean validate(LoginParam loginParam) {
return loginParam.getUsername() != null && !loginParam.getUsername().trim().isEmpty()
&& loginParam.getPassword() != null && !loginParam.getPassword().trim().isEmpty();
}
@Override
public TbSysUserDTO findUser(LoginParam loginParam) {
TbSysUserDTO filter = new TbSysUserDTO();
filter.setUsername(loginParam.getUsername());
TbSysUserDTO user = userService.getLoginUser(filter).getData();
if(user == null) {
return null;
}
return user;
}
@Override
public boolean verifyCredential(String inputCredential, String storedCredential) {
return passwordEncoder.matches(inputCredential, storedCredential);
}
}

View File

@@ -0,0 +1,51 @@
package org.xyzh.auth.strategy.impl;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.auth.strategy.LoginStrategy;
import org.xyzh.common.core.domain.LoginParam;
import org.xyzh.common.dto.sys.TbSysUserDTO;
import org.xyzh.api.system.service.SysUserService;
/**
* @description WechatLoginStrategy.java文件描述 微信登录策略
* @filename WechatLoginStrategy.java
* @author yslg
* @copyright xyzh
* @since 2025-12-05
*/
@Component
public class WechatLoginStrategy implements LoginStrategy {
@Autowired
private SysUserService userService;
@Override
public String getLoginType() {
return "wechat";
}
@Override
public boolean validate(LoginParam loginParam) {
return loginParam.getWechatId() != null && !loginParam.getWechatId().trim().isEmpty();
}
@Override
public TbSysUserDTO findUser(LoginParam loginParam) {
TbSysUserDTO filter = new TbSysUserDTO();
filter.setWechatId(loginParam.getWechatId());
TbSysUserDTO user = userService.getLoginUser(filter).getData();
if(user == null) {
return null;
}
return user;
}
@Override
public boolean verifyCredential(String inputCredential, String storedCredential) {
// 微信登录通常不需要密码验证,通过微信授权码验证
// 这里可以添加微信授权验证逻辑
return true;
}
}

View File

@@ -0,0 +1,11 @@
package org.xyzh.auth.utils;
public class CapcatUtils {
/**
* 生成6位数字验证码
* @return 验证码
*/
public static String generateVerificationCode() {
return String.valueOf((int)((Math.random() * 9 + 1) * 100000));
}
}

View File

@@ -10,7 +10,9 @@ urban-lifeline:
enabled: false # 认证服务自己不需要认证
whitelist:
- /** # 认证服务的所有接口都放行
security:
aes:
secret-key: 1234567890qwer
# ================== Spring ==================
spring:
application:

View File

@@ -14,7 +14,7 @@ import java.util.List;
* @author yslg
*/
@Component
@ConfigurationProperties(prefix = "urban-lifeline.auth")
@ConfigurationProperties(prefix = "auth")
public class AuthProperties {
/**

View File

@@ -23,10 +23,10 @@ import java.util.Map;
@Component
public class JwtTokenUtil {
@Value("${urban-lifeline.auth.jwt-secret:schoolNewsDefaultSecretKeyForJWT2025}")
@Value("${auth.jwt-secret:urbanLifelineDefaultSecretKeyForJWT2025}")
private String secret;
@Value("${urban-lifeline.auth.jwt-expiration:86400}")
@Value("${auth.jwt-expiration:86400}")
private Long expiration;
private SecretKey getSigningKey() {

View File

@@ -0,0 +1,27 @@
package org.xyzh.common.core.exception;
public class BaseException extends RuntimeException{
private Integer code;
private String description;
public BaseException(Integer code, String description, String message) {
super(message);
this.code = code;
this.description = description;
}
public Integer getCode(){
return this.code;
}
public String getDescription(){
return this.description;
}
public String getMessage(){
return super.getMessage();
}
}

View File

@@ -13,7 +13,6 @@
<artifactId>common-utils</artifactId>
<version>${urban-lifeline.version}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
@@ -44,5 +43,22 @@
<artifactId>spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
<!-- Spring Mail for Email -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- Aliyun SMS Service -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>4.2.0</version>
</dependency>
<!-- FastJson2 Spring6 支持 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension-spring6</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,50 @@
package org.xyzh.common.utils.config;
import com.alibaba.fastjson2.support.config.FastJsonConfig;
import com.alibaba.fastjson2.support.spring6.http.converter.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
/**
* @description FastJson配置类 - 统一处理日期时间格式序列化
* @filename FastJsonConfig.java
* @author yslg
* @copyright xyzh
* @since 2025-11-28
*/
@Configuration
public class FastJsonConfiguration implements WebMvcConfigurer {
/**
* 配置FastJson消息转换器
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
// FastJson配置
FastJsonConfig config = new FastJsonConfig();
// 设置日期格式
config.setDateFormat("yyyy-MM-dd HH:mm:ss");
// 设置字符集
converter.setDefaultCharset(StandardCharsets.UTF_8);
// 设置支持的MediaType
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
// 应用配置
converter.setFastJsonConfig(config);
// 添加到转换器列表(添加到最前面,优先使用)
converters.add(0, converter);
}
}

View File

@@ -159,11 +159,10 @@ auth:
token-header: Authorization
token-prefix: "Bearer "
# 认证接口白名单login/logout/captcha/refresh
auth-paths:
- /auth/login
- /auth/logout
- /auth/captcha
- /auth/refresh
login-path: /urban-lifeline/auth/login
logout-path: /urban-lifeline/auth/logout
captcha-path: /urban-lifeline/auth/captcha
refresh-path: /urban-lifeline/auth/refresh
# 通用白名单Swagger、健康检查等
whitelist:
- /actuator/**
@@ -174,7 +173,9 @@ auth:
- /doc.html
- /favicon.ico
- /error
security:
aes:
secret-key: 1234567890qwer
# Actuator 监控端点
management:
endpoints:

View File

@@ -17,5 +17,48 @@
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- 项目内部模块 -->
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-all</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.apis</groupId>
<artifactId>api-system</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.apis</groupId>
<artifactId>api-message</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-logback-adapter-12</artifactId>
</exclusion>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>logback-adapter</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,156 @@
package org.xyzh.message.config;
import org.apache.dubbo.config.annotation.DubboReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.util.StringUtils;
import org.xyzh.api.system.service.SysConfigService;
import java.util.Properties;
/**
* @description 动态配置加载器 - 从数据库加载邮件和短信配置
* @filename DynamicConfigLoader.java
* @author yslg
* @copyright xyzh
* @since 2025-12-05
*/
@Configuration
@Order(100) // 确保在其他组件初始化之后再加载配置
public class DynamicConfigLoader implements ApplicationRunner {
private static final Logger logger = LoggerFactory.getLogger(DynamicConfigLoader.class);
@DubboReference(version = "1.0.0", group = "system", timeout = 5000, check = false)
private SysConfigService sysConfigService;
@Autowired(required = false)
private JavaMailSenderImpl mailSender;
@Autowired(required = false)
private EmailConfigProperties emailConfigProperties;
@Autowired(required = false)
private SmsConfigProperties smsConfigProperties;
@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("=== 开始加载动态配置 ===");
try {
// 加载邮件配置
if (emailConfigProperties != null) {
loadEmailConfig();
} else {
logger.warn("EmailConfigProperties未注入跳过邮件配置加载");
}
// 加载短信配置
if (smsConfigProperties != null) {
loadSmsConfig();
} else {
logger.warn("SmsConfigProperties未注入跳过短信配置加载");
}
logger.info("=== 动态配置加载完成 ===");
} catch (Exception e) {
logger.error("动态配置加载失败", e);
}
}
/**
* 加载邮件配置
*/
private void loadEmailConfig() {
try {
if (sysConfigService == null) {
logger.warn("SysConfigService未注入无法加载邮件配置");
return;
}
String host = sysConfigService.getStringConfig("email.host");
String port = sysConfigService.getStringConfig("email.port");
String username = sysConfigService.getStringConfig("email.username");
String password = sysConfigService.getStringConfig("email.password");
String fromName = sysConfigService.getStringConfig("email.fromName");
String sslEnable = sysConfigService.getStringConfig("email.ssl.enable");
String timeout = sysConfigService.getStringConfig("email.timeout");
// 更新配置属性
emailConfigProperties.setHost(host);
emailConfigProperties.setPort(StringUtils.hasText(port) ? Integer.valueOf(port) : 587);
emailConfigProperties.setUsername(username);
emailConfigProperties.setPassword(password);
emailConfigProperties.setFromName(fromName);
emailConfigProperties.setSslEnable("true".equalsIgnoreCase(sslEnable));
emailConfigProperties.setTimeout(StringUtils.hasText(timeout) ? Integer.valueOf(timeout) : 30000);
// 如果邮箱配置完整则配置JavaMailSender
if (mailSender != null && StringUtils.hasText(host) && StringUtils.hasText(username) && StringUtils.hasText(password)) {
mailSender.setHost(host);
mailSender.setPort(emailConfigProperties.getPort());
mailSender.setUsername(username);
mailSender.setPassword(password);
// 设置邮件属性
Properties props = mailSender.getJavaMailProperties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", emailConfigProperties.getSslEnable() ? "true" : "false");
props.put("mail.smtp.starttls.required", emailConfigProperties.getSslEnable() ? "true" : "false");
props.put("mail.smtp.timeout", emailConfigProperties.getTimeout());
props.put("mail.smtp.connectiontimeout", emailConfigProperties.getTimeout());
logger.info("邮件配置加载成功: host={}, port={}, username={}", host, emailConfigProperties.getPort(), username);
} else {
logger.warn("邮件配置不完整,将使用默认配置或模拟模式");
}
} catch (Exception e) {
logger.error("加载邮件配置失败", e);
}
}
/**
* 加载短信配置
*/
private void loadSmsConfig() {
try {
if (sysConfigService == null) {
logger.warn("SysConfigService未注入无法加载短信配置");
return;
}
String provider = sysConfigService.getStringConfig("sms.provider");
String accessKeyId = sysConfigService.getStringConfig("sms.accessKeyId");
String accessKeySecret = sysConfigService.getStringConfig("sms.accessKeySecret");
String signName = sysConfigService.getStringConfig("sms.signName");
String templateCodeLogin = sysConfigService.getStringConfig("sms.templateCode.login");
String templateCodeRegister = sysConfigService.getStringConfig("sms.templateCode.register");
String timeout = sysConfigService.getStringConfig("sms.timeout");
// 更新配置属性
smsConfigProperties.setProvider(StringUtils.hasText(provider) ? provider : "aliyun");
smsConfigProperties.setAccessKeyId(accessKeyId);
smsConfigProperties.setAccessKeySecret(accessKeySecret);
smsConfigProperties.setSignName(StringUtils.hasText(signName) ? signName : "校园新闻");
smsConfigProperties.setTemplateCodeLogin(templateCodeLogin);
smsConfigProperties.setTemplateCodeRegister(templateCodeRegister);
smsConfigProperties.setTimeout(StringUtils.hasText(timeout) ? Integer.valueOf(timeout) : 30000);
if (StringUtils.hasText(accessKeyId) && StringUtils.hasText(accessKeySecret)) {
logger.info("短信配置加载成功: provider={}, signName={}", smsConfigProperties.getProvider(), smsConfigProperties.getSignName());
} else {
logger.warn("短信配置不完整,将使用模拟模式");
}
} catch (Exception e) {
logger.error("加载短信配置失败", e);
}
}
}

View File

@@ -0,0 +1,91 @@
package org.xyzh.message.config;
import org.springframework.stereotype.Component;
/**
* @description 邮件配置属性
* @filename EmailConfigProperties.java
* @author yslg
* @copyright xyzh
* @since 2025-11-26
*/
@Component
public class EmailConfigProperties {
/** SMTP服务器地址 */
private String host;
/** SMTP端口 */
private Integer port;
/** 发件人邮箱 */
private String username;
/** 邮箱授权码 */
private String password;
/** 发件人名称 */
private String fromName;
/** 是否启用SSL */
private Boolean sslEnable;
/** 连接超时时间(毫秒) */
private Integer timeout;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFromName() {
return fromName;
}
public void setFromName(String fromName) {
this.fromName = fromName;
}
public Boolean getSslEnable() {
return sslEnable;
}
public void setSslEnable(Boolean sslEnable) {
this.sslEnable = sslEnable;
}
public Integer getTimeout() {
return timeout;
}
public void setTimeout(Integer timeout) {
this.timeout = timeout;
}
}

View File

@@ -0,0 +1,44 @@
package org.xyzh.message.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import java.util.Properties;
/**
* @description 邮件发送器配置 - 手动创建JavaMailSender bean
* @filename MailSenderConfig.java
* @author yslg
* @copyright xyzh
* @since 2025-12-05
*/
@Configuration
public class MailSenderConfig {
/**
* 创建JavaMailSender bean
* 初始值为默认配置实际配置将在DynamicConfigLoader中从数据库加载
*/
@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
// 设置默认配置(防止未配置时报错)
mailSender.setHost("smtp.example.com");
mailSender.setPort(587);
mailSender.setUsername("default");
mailSender.setPassword("default");
// 设置邮件属性
Properties props = mailSender.getJavaMailProperties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.starttls.required", "false");
props.put("mail.smtp.timeout", "30000");
props.put("mail.smtp.connectiontimeout", "30000");
return mailSender;
}
}

View File

@@ -0,0 +1,91 @@
package org.xyzh.message.config;
import org.springframework.stereotype.Component;
/**
* @description 短信配置属性
* @filename SmsConfigProperties.java
* @author yslg
* @copyright xyzh
* @since 2025-11-26
*/
@Component
public class SmsConfigProperties {
/** 短信服务商 */
private String provider;
/** AccessKey ID */
private String accessKeyId;
/** AccessKey Secret */
private String accessKeySecret;
/** 短信签名 */
private String signName;
/** 登录验证码模板 */
private String templateCodeLogin;
/** 注册验证码模板 */
private String templateCodeRegister;
/** 请求超时时间(毫秒) */
private Integer timeout;
public String getProvider() {
return provider;
}
public void setProvider(String provider) {
this.provider = provider;
}
public String getAccessKeyId() {
return accessKeyId;
}
public void setAccessKeyId(String accessKeyId) {
this.accessKeyId = accessKeyId;
}
public String getAccessKeySecret() {
return accessKeySecret;
}
public void setAccessKeySecret(String accessKeySecret) {
this.accessKeySecret = accessKeySecret;
}
public String getSignName() {
return signName;
}
public void setSignName(String signName) {
this.signName = signName;
}
public String getTemplateCodeLogin() {
return templateCodeLogin;
}
public void setTemplateCodeLogin(String templateCodeLogin) {
this.templateCodeLogin = templateCodeLogin;
}
public String getTemplateCodeRegister() {
return templateCodeRegister;
}
public void setTemplateCodeRegister(String templateCodeRegister) {
this.templateCodeRegister = templateCodeRegister;
}
public Integer getTimeout() {
return timeout;
}
public void setTimeout(Integer timeout) {
this.timeout = timeout;
}
}

View File

@@ -0,0 +1,148 @@
package org.xyzh.message.service;
import org.apache.dubbo.config.annotation.DubboService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.api.message.dto.TbMessageDTO;
import org.xyzh.api.message.service.MessageService;
import org.xyzh.api.message.vo.MessageVO;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.message.config.DynamicConfigLoader;
import org.xyzh.message.utils.EmailUtils;
import org.xyzh.message.utils.SmsUtils;
@DubboService(
version = "1.0.0",
group = "message",
timeout = 3000,
retries = 0
)
public class MessageServiceImpl implements MessageService{
private static final Logger logger = LoggerFactory.getLogger(MessageServiceImpl.class);
@Autowired
private EmailUtils emailUtils;
@Autowired
private SmsUtils smsUtils;
@Override
public ResultDomain<String> sendSimpleEmail(String to, String subject, String content) {
boolean flag = emailUtils.sendSimpleEmail(to, subject, content);
if (flag){
return ResultDomain.success("发生成功");
}else{
return ResultDomain.failure("发送失败");
}
}
@Override
public ResultDomain<String> sendHtmlEmail(String to, String subject, String content) {
boolean flag = emailUtils.sendHtmlEmail(to, subject, content);
if (flag){
return ResultDomain.success("发生成功");
}else{
return ResultDomain.failure("发送失败");
}
}
@Override
public ResultDomain<String> sendEmailVerificationCode(String to, String code) {
boolean flag = emailUtils.sendVerificationCode(to, code);
if (flag){
return ResultDomain.success("发生成功");
}else{
return ResultDomain.failure("发送失败");
}
}
@Override
public ResultDomain<String> sendPhoneVerificationCode(String phone, String code) {
boolean flag = smsUtils.sendVerificationCode(phone, code);
if (flag){
return ResultDomain.success("发生成功");
}else{
return ResultDomain.failure("发送失败");
}
}
@Override
public ResultDomain<TbMessageDTO> createMessage(MessageVO messageVO) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<Boolean> deleteMessage(String messageId) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<MessageVO> getMessageDetail(String messageId) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbMessageDTO> getMessageList(TbMessageDTO filter) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbMessageDTO> getMessagePage(TbMessageDTO filter, PageParam pageParam) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbMessageDTO> getMyMessageDetail(String messageId) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbMessageDTO> getMyMessageList(TbMessageDTO filter) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbMessageDTO> getMyMessagePage(TbMessageDTO filter, PageParam pageParam) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbMessageDTO> handleMessage(String messageId, String status) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<MessageVO> sendMessage(MessageVO messageVO) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbMessageDTO> updateMessage(MessageVO messageVO) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbMessageDTO> withdrawMessage(String messageId) {
// TODO Auto-generated method stub
return null;
}
}

View File

@@ -0,0 +1,156 @@
package org.xyzh.message.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.xyzh.message.config.EmailConfigProperties;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
/**
* @description 邮件发送工具类
* @filename EmailUtils.java
* @author yslg
* @copyright xyzh
* @since 2025-11-03
*/
@Component
@ConditionalOnBean(JavaMailSender.class)
public class EmailUtils {
private static final Logger logger = LoggerFactory.getLogger(EmailUtils.class);
@Autowired
private JavaMailSender mailSender;
@Autowired
private EmailConfigProperties emailConfigProperties;
/**
* 发送简单文本邮件
* @param to 收件人邮箱
* @param subject 邮件主题
* @param content 邮件内容
* @return 是否发送成功
*/
public boolean sendSimpleEmail(String to, String subject, String content) {
try {
SimpleMailMessage message = new SimpleMailMessage();
String from = emailConfigProperties.getUsername();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
logger.info("简单邮件发送成功,收件人: {}", to);
return true;
} catch (Exception e) {
logger.error("简单邮件发送失败,收件人: {}, 错误: {}", to, e.getMessage(), e);
return false;
}
}
/**
* 发送HTML格式邮件
* @param to 收件人邮箱
* @param subject 邮件主题
* @param content HTML格式的邮件内容
* @return 是否发送成功
*/
public boolean sendHtmlEmail(String to, String subject, String content) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
String from = emailConfigProperties.getUsername();
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true); // true表示HTML格式
mailSender.send(message);
logger.info("HTML邮件发送成功收件人: {}", to);
return true;
} catch (MessagingException e) {
logger.error("HTML邮件发送失败收件人: {}, 错误: {}", to, e.getMessage(), e);
return false;
}
}
/**
* 发送验证码邮件
* @param to 收件人邮箱
* @param code 验证码
* @return 是否发送成功
*/
public boolean sendVerificationCode(String to, String code) {
String subject = "【红色思政学习平台】邮箱验证码";
String content = buildVerificationCodeHtml(code);
return sendHtmlEmail(to, subject, content);
}
/**
* 构建验证码邮件的HTML内容
* @param code 验证码
* @return HTML内容
*/
private String buildVerificationCodeHtml(String code) {
return "<!DOCTYPE html>" +
"<html>" +
"<head>" +
"<meta charset=\"UTF-8\">" +
"<style>" +
"body { font-family: 'Microsoft YaHei', Arial, sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px; }" +
".container { max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; }" +
".header { background: linear-gradient(135deg, #C62828 0%, #E53935 100%); padding: 30px; text-align: center; }" +
".header h1 { color: #ffffff; margin: 0; font-size: 24px; }" +
".content { padding: 40px 30px; }" +
".content p { color: #333333; line-height: 1.8; margin: 10px 0; }" +
".code-box { background-color: #f8f9fa; border-left: 4px solid #C62828; padding: 20px; margin: 20px 0; text-align: center; }" +
".code { font-size: 32px; font-weight: bold; color: #C62828; letter-spacing: 5px; font-family: 'Courier New', monospace; }" +
".tips { color: #666666; font-size: 14px; margin-top: 20px; line-height: 1.6; }" +
".footer { background-color: #f8f9fa; padding: 20px; text-align: center; color: #999999; font-size: 12px; }" +
"</style>" +
"</head>" +
"<body>" +
"<div class=\"container\">" +
"<div class=\"header\">" +
"<h1>红色思政学习平台</h1>" +
"</div>" +
"<div class=\"content\">" +
"<p>尊敬的用户,您好!</p>" +
"<p>您正在进行邮箱验证,您的验证码为:</p>" +
"<div class=\"code-box\">" +
"<div class=\"code\">" + code + "</div>" +
"</div>" +
"<div class=\"tips\">" +
"<p>• 验证码有效期为10分钟请尽快完成验证</p>" +
"<p>• 如果这不是您的操作,请忽略此邮件</p>" +
"<p>• 为了保护您的账号安全,请勿将验证码告知他人</p>" +
"</div>" +
"</div>" +
"<div class=\"footer\">" +
"<p>此邮件由系统自动发送,请勿回复</p>" +
"<p>Copyright © 红色思政智能体平台</p>" +
"</div>" +
"</div>" +
"</body>" +
"</html>";
}
/**
* 生成6位数字验证码
* @return 验证码
*/
public static String generateVerificationCode() {
return String.valueOf((int)((Math.random() * 9 + 1) * 100000));
}
}

View File

@@ -0,0 +1,260 @@
package org.xyzh.message.utils;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.xyzh.message.config.SmsConfigProperties;
/**
* @description 短信发送工具类 - 支持多种短信服务商
* @filename SmsUtils.java
* @author yslg
* @copyright xyzh
* @since 2025-11-03
*/
@Component
@ConditionalOnBean(SmsConfigProperties.class)
public class SmsUtils {
private static final Logger logger = LoggerFactory.getLogger(SmsUtils.class);
@Autowired
private SmsConfigProperties smsConfigProperties;
/**
* 发送短信验证码
* @param phone 手机号
* @param code 验证码
* @return 是否发送成功
*/
public boolean sendVerificationCode(String phone, String code) {
// 如果未启用短信服务,使用模拟模式
String accessKeyId = smsConfigProperties.getAccessKeyId();
String accessKeySecret = smsConfigProperties.getAccessKeySecret();
if (!StringUtils.hasText(accessKeyId) || !StringUtils.hasText(accessKeySecret)) {
logger.warn("短信服务未配置或未启用,使用模拟模式");
logger.info("【模拟发送】短信验证码,手机号: {}, 验证码: {}", phone, code);
return true;
}
// 根据配置的服务商选择发送方式
String provider = smsConfigProperties.getProvider();
if (provider == null) provider = "aliyun";
switch (provider.toLowerCase()) {
case "aliyun":
return sendByAliyun(phone, code, smsConfigProperties.getTemplateCodeLogin());
case "tencent":
logger.warn("腾讯云短信服务暂未实现,使用模拟模式");
logger.info("【模拟发送】短信验证码,手机号: {}, 验证码: {}", phone, code);
return true;
default:
logger.error("未知的短信服务商: {}", provider);
return false;
}
}
/**
* 使用阿里云发送短信验证码
* @param phone 手机号
* @param code 验证码
* @param templateCode 短信模板CODE
* @return 是否发送成功
*/
private boolean sendByAliyun(String phone, String code, String templateCode) {
try {
Client client = createAliyunClient();
SendSmsRequest request = new SendSmsRequest()
.setPhoneNumbers(phone)
.setSignName(smsConfigProperties.getSignName())
.setTemplateCode(templateCode)
.setTemplateParam("{\"code\":\"" + code + "\"}");
SendSmsResponse response = client.sendSms(request);
if ("OK".equals(response.getBody().getCode())) {
logger.info("阿里云短信发送成功,手机号: {}, BizId: {}", phone, response.getBody().getBizId());
return true;
} else {
logger.error("阿里云短信发送失败,手机号: {}, Code: {}, Message: {}",
phone, response.getBody().getCode(), response.getBody().getMessage());
return false;
}
} catch (Exception e) {
logger.error("阿里云短信发送异常,手机号: {}, 错误: {}", phone, e.getMessage(), e);
return false;
}
}
/**
* 创建阿里云短信客户端
* @return 短信客户端
*/
private Client createAliyunClient() throws Exception {
Config config = new Config()
.setAccessKeyId(smsConfigProperties.getAccessKeyId())
.setAccessKeySecret(smsConfigProperties.getAccessKeySecret())
.setEndpoint("dysmsapi.aliyuncs.com");
return new Client(config);
}
/**
* 发送通用短信(支持自定义模板)
* @param phone 手机号
* @param templateCode 模板CODE
* @param templateParam 模板参数JSON格式
* @return 是否发送成功
*/
public boolean sendSms(String phone, String templateCode, String templateParam) {
// 如果未启用短信服务,使用模拟模式
String accessKeyId = smsConfigProperties.getAccessKeyId();
String accessKeySecret = smsConfigProperties.getAccessKeySecret();
if (!StringUtils.hasText(accessKeyId) || !StringUtils.hasText(accessKeySecret)) {
logger.warn("短信服务未配置或未启用,使用模拟模式");
logger.info("【模拟发送】短信,手机号: {}, 模板: {}, 参数: {}", phone, templateCode, templateParam);
return true;
}
// 根据配置的服务商选择发送方式
String provider = smsConfigProperties.getProvider();
if (provider == null) provider = "aliyun";
switch (provider.toLowerCase()) {
case "aliyun":
return sendSmsAliyun(phone, templateCode, templateParam);
case "tencent":
logger.warn("腾讯云短信服务暂未实现,使用模拟模式");
logger.info("【模拟发送】短信,手机号: {}, 模板: {}, 参数: {}", phone, templateCode, templateParam);
return true;
default:
logger.error("未知的短信服务商: {}", provider);
return false;
}
}
/**
* 使用阿里云发送通用短信
*/
private boolean sendSmsAliyun(String phone, String templateCode, String templateParam) {
try {
Client client = createAliyunClient();
SendSmsRequest request = new SendSmsRequest()
.setPhoneNumbers(phone)
.setSignName(smsConfigProperties.getSignName())
.setTemplateCode(templateCode)
.setTemplateParam(templateParam);
SendSmsResponse response = client.sendSms(request);
if ("OK".equals(response.getBody().getCode())) {
logger.info("阿里云短信发送成功,手机号: {}, BizId: {}", phone, response.getBody().getBizId());
return true;
} else {
logger.error("阿里云短信发送失败,手机号: {}, Code: {}, Message: {}",
phone, response.getBody().getCode(), response.getBody().getMessage());
return false;
}
} catch (Exception e) {
logger.error("阿里云短信发送异常,手机号: {}, 错误: {}", phone, e.getMessage(), e);
return false;
}
}
/**
* 批量发送短信
* @param phones 手机号列表,用逗号分隔
* @param templateCode 模板CODE
* @param templateParam 模板参数JSON格式
* @return 是否发送成功
*/
public boolean sendBatchSms(String phones, String templateCode, String templateParam) {
// 如果未启用短信服务,使用模拟模式
String accessKeyId = smsConfigProperties.getAccessKeyId();
String accessKeySecret = smsConfigProperties.getAccessKeySecret();
if (!StringUtils.hasText(accessKeyId) || !StringUtils.hasText(accessKeySecret)) {
logger.warn("短信服务未配置或未启用,使用模拟模式");
logger.info("【模拟发送】批量短信,手机号: {}, 模板: {}, 参数: {}", phones, templateCode, templateParam);
return true;
}
// 根据配置的服务商选择发送方式
String provider = smsConfigProperties.getProvider();
if (provider == null) provider = "aliyun";
switch (provider.toLowerCase()) {
case "aliyun":
return sendBatchSmsAliyun(phones, templateCode, templateParam);
case "tencent":
logger.warn("腾讯云短信服务暂未实现,使用模拟模式");
logger.info("【模拟发送】批量短信,手机号: {}, 模板: {}, 参数: {}", phones, templateCode, templateParam);
return true;
default:
logger.error("未知的短信服务商: {}", provider);
return false;
}
}
/**
* 使用阿里云批量发送短信
*/
private boolean sendBatchSmsAliyun(String phones, String templateCode, String templateParam) {
try {
Client client = createAliyunClient();
SendSmsRequest request = new SendSmsRequest()
.setPhoneNumbers(phones)
.setSignName(smsConfigProperties.getSignName())
.setTemplateCode(templateCode)
.setTemplateParam(templateParam);
SendSmsResponse response = client.sendSms(request);
if ("OK".equals(response.getBody().getCode())) {
logger.info("阿里云批量短信发送成功,手机号: {}, BizId: {}", phones, response.getBody().getBizId());
return true;
} else {
logger.error("阿里云批量短信发送失败,手机号: {}, Code: {}, Message: {}",
phones, response.getBody().getCode(), response.getBody().getMessage());
return false;
}
} catch (Exception e) {
logger.error("阿里云批量短信发送异常,手机号: {}, 错误: {}", phones, e.getMessage(), e);
return false;
}
}
/**
* 生成6位数字验证码
* @return 验证码
*/
public static String generateVerificationCode() {
return String.valueOf((int)((Math.random() * 9 + 1) * 100000));
}
/**
* 验证手机号格式
* @param phone 手机号
* @return 是否有效
*/
public static boolean isValidPhone(String phone) {
if (phone == null || phone.trim().isEmpty()) {
return false;
}
// 中国大陆手机号验证
String regex = "^1[3-9]\\d{9}$";
return phone.matches(regex);
}
}

View File

@@ -18,6 +18,9 @@ urban-lifeline:
- /actuator/health
- /actuator/info
security:
aes:
secret-key: 1234567890qwer
# ================== Spring ==================
spring:
application:

View File

@@ -208,7 +208,11 @@
<artifactId>fastjson2</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension-spring6</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- urban-lifeline 系统 -->

View File

@@ -20,6 +20,15 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
@Mapper
public interface TbSysConfigMapper extends BaseMapper<TbSysConfigDTO> {
/**
* @description 从key读取配置
* @param
* @author yslg
* @since 2025-12-05
*/
TbSysConfigDTO selectSysConfigByKey(@Param("configKey") String configKey);
/**
* @description 插入系统配置
* @param configDTO 系统配置DTO

View File

@@ -41,6 +41,190 @@ public class SysConfigServiceImpl implements SysConfigService {
@Resource
private TbSysConfigMapper configMapper;
/**
* 根据key查询配置
*/
private TbSysConfigDTO getConfigByKey(String key) {
if (key == null || key.isEmpty()) {
return null;
}
return configMapper.selectSysConfigByKey(key);
}
public Object getSysConfig(String key) {
try {
TbSysConfigDTO config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
String configType = config.getConfigType();
String configValue = config.getValue();
if (configValue == null || configValue.isEmpty()) {
return null;
}
// 根据config_type返回对应的类型
if (configType == null || "string".equalsIgnoreCase(configType)) {
return configValue;
} else if ("number".equalsIgnoreCase(configType) || "integer".equalsIgnoreCase(configType)) {
try {
// 尝试解析为Integer如果失败则解析为Long
return Integer.parseInt(configValue);
} catch (NumberFormatException e) {
try {
return Long.parseLong(configValue);
} catch (NumberFormatException ex) {
logger.error("配置项 {} 的值无法转换为数字: {}", key, configValue);
return configValue;
}
}
} else if ("boolean".equalsIgnoreCase(configType)) {
String value = configValue.toLowerCase().trim();
if ("true".equals(value) || "1".equals(value) || "yes".equals(value) || "on".equals(value)) {
return true;
} else if ("false".equals(value) || "0".equals(value) || "no".equals(value) || "off".equals(value)) {
return false;
} else {
logger.warn("配置项 {} 的值无法识别为Boolean: {}", key, value);
return configValue;
}
} else if ("double".equalsIgnoreCase(configType) || "float".equalsIgnoreCase(configType)) {
try {
return Double.parseDouble(configValue);
} catch (NumberFormatException e) {
logger.error("配置项 {} 的值无法转换为Double: {}", key, configValue);
return configValue;
}
} else {
// 未知类型,直接返回字符串
return configValue;
}
} catch (Exception e) {
logger.error("获取配置失败: {}", key, e);
return null;
}
}
@Override
public String getStringConfig(String key) {
try {
TbSysConfigDTO config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
return config.getValue();
} catch (Exception e) {
logger.error("获取字符串配置失败: {}", key, e);
return null;
}
}
@Override
public Integer getIntConfig(String key) {
try {
TbSysConfigDTO config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
String value = config.getValue();
if (value == null || value.isEmpty()) {
return null;
}
return Integer.parseInt(value);
} catch (NumberFormatException e) {
logger.error("配置项 {} 的值无法转换为Integer: {}", key, e.getMessage());
return null;
} catch (Exception e) {
logger.error("获取Integer配置失败: {}", key, e);
return null;
}
}
@Override
public Boolean getBooleanConfig(String key) {
try {
TbSysConfigDTO config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
String value = config.getValue();
if (value == null || value.isEmpty()) {
return null;
}
// 支持多种布尔值表示true/false, 1/0, yes/no, on/off
value = value.toLowerCase().trim();
if ("true".equals(value) || "1".equals(value) || "yes".equals(value) || "on".equals(value)) {
return true;
} else if ("false".equals(value) || "0".equals(value) || "no".equals(value) || "off".equals(value)) {
return false;
} else {
logger.warn("配置项 {} 的值无法识别为Boolean: {}", key, value);
return null;
}
} catch (Exception e) {
logger.error("获取Boolean配置失败: {}", key, e);
return null;
}
}
@Override
public Double getDoubleConfig(String key) {
try {
TbSysConfigDTO config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
String value = config.getValue();
if (value == null || value.isEmpty()) {
return null;
}
return Double.parseDouble(value);
} catch (NumberFormatException e) {
logger.error("配置项 {} 的值无法转换为Double: {}", key, e.getMessage());
return null;
} catch (Exception e) {
logger.error("获取Double配置失败: {}", key, e);
return null;
}
}
@Override
public Long getLongConfig(String key) {
try {
TbSysConfigDTO config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
String value = config.getValue();
if (value == null || value.isEmpty()) {
return null;
}
return Long.parseLong(value);
} catch (NumberFormatException e) {
logger.error("配置项 {} 的值无法转换为Long: {}", key, e.getMessage());
return null;
} catch (Exception e) {
logger.error("获取Long配置失败: {}", key, e);
return null;
}
}
@Override
public ResultDomain<TbSysConfigDTO> insertConfig(TbSysConfigDTO configDTO) {
if (configDTO == null) {

View File

@@ -19,10 +19,15 @@ import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.common.utils.NonUtils;
import org.xyzh.common.utils.StringUtils;
import org.xyzh.common.utils.crypto.AesEncryptUtil;
import org.xyzh.system.mapper.user.TbSysUserMapper;
import org.xyzh.system.mapper.user.TbSysUserInfoMapper;
import org.xyzh.system.mapper.user.TbSysUserRoleMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
/**
* @description 用户服务实现类
@@ -52,6 +57,112 @@ public class SysUserServiceImpl implements SysUserService {
@Resource
private TbSysUserRoleMapper userRoleMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AesEncryptUtil aesEncryptUtil;
@Transactional
@Override
public ResultDomain<TbSysUserDTO> registerUser(SysUserVO userVO) {
try {
logger.info("开始注册用户:{}", userVO.getUsername());
// 检查用户是否已存在
if (checkUserExists(userVO)) {
return ResultDomain.failure("用户已存在");
}
// 转换为 DTO
TbSysUserDTO dto = SysUserVO.toDTO(userVO);
// 设置用户基本信息
Date now = new Date();
if (StringUtils.isBlank(dto.getUserId())) {
dto.setUserId(IDUtils.generateID());
}
dto.setCreateTime(now);
dto.setDeleted(false);
// 加密密码
if (StringUtils.isNotBlank(dto.getPassword())) {
dto.setPassword(passwordEncoder.encode(dto.getPassword()));
}
// 插入用户主表
int rows = userMapper.insertUser(dto);
if (rows <= 0) {
logger.warn("插入用户失败, username={}", dto.getUsername());
return ResultDomain.failure("插入用户失败");
}
// 创建用户信息表
TbSysUserInfoDTO userInfo = new TbSysUserInfoDTO();
userInfo.setUserId(dto.getUserId());
userInfo.setCreateTime(now);
userInfo.setAvatar("default");
userInfoMapper.insertUserInfo(userInfo);
// 分配默认角色role_guest
TbSysUserRoleDTO userRole = new TbSysUserRoleDTO();
userRole.setUserId(dto.getUserId());
userRole.setRoleId("role_guest");
userRole.setCreateTime(now);
userRoleMapper.insertUserRole(userRole);
logger.info("注册用户成功, userId={}, username={}", dto.getUserId(), dto.getUsername());
return ResultDomain.success("注册用户成功", dto);
} catch (Exception e) {
logger.error("注册用户失败:{}", userVO.getUsername(), e);
return ResultDomain.failure("注册用户失败:" + e.getMessage());
}
}
/**
* 检查用户是否已存在
* @param userVO 用户信息
* @return true-存在false-不存在
*/
private boolean checkUserExists(SysUserVO userVO) {
TbSysUserDTO filter = new TbSysUserDTO();
// 检查用户名是否存在
if (StringUtils.isNotBlank(userVO.getUsername())) {
filter.setUsername(userVO.getUsername());
List<SysUserVO> users = userMapper.getUserByFilter(filter);
if (users != null && !users.isEmpty()) {
logger.warn("用户名已存在: {}", userVO.getUsername());
return true;
}
}
// 检查手机号是否存在
if (StringUtils.isNotBlank(userVO.getPhone())) {
filter = new TbSysUserDTO();
filter.setPhone(userVO.getPhone());
List<SysUserVO> users = userMapper.getUserByFilter(filter);
if (users != null && !users.isEmpty()) {
logger.warn("手机号已存在: {}", userVO.getPhone());
return true;
}
}
// 检查邮箱是否存在
if (StringUtils.isNotBlank(userVO.getEmail())) {
filter = new TbSysUserDTO();
filter.setEmail(userVO.getEmail());
List<SysUserVO> users = userMapper.getUserByFilter(filter);
if (users != null && !users.isEmpty()) {
logger.warn("邮箱已存在: {}", userVO.getEmail());
return true;
}
}
return false;
}
@Override
public ResultDomain<TbSysUserDTO> insertUser(SysUserVO userVO) {
if (userVO == null) {
@@ -120,9 +231,12 @@ public class SysUserServiceImpl implements SysUserService {
}
@Override
public ResultDomain<SysUserVO> getLoginUser(SysUserVO filter) {
public ResultDomain<TbSysUserDTO> getLoginUser(TbSysUserDTO filter) {
// 登录查询语义与 getUser 相同(可根据用户名/手机号/邮箱查询)
return getUser(filter);
if(NonUtils.isNotNull(filter.getPhone())){
filter.setPhone_hash(aesEncryptUtil.encrypt(filter.getPhone()));
}
return null;
}
@Override

View File

@@ -67,6 +67,15 @@
optsn, creator, updater, dept_path, remark, create_time, update_time, delete_time, deleted
</sql>
<!-- selectSysConfigByKey -->
<select id="selectSysConfigByKey">
SELECT <include refid="Base_Column_List" />
FROM config.tb_sys_config
WHERE config_key = #{configKey}
AND deleted = 0
</select>
<!-- 插入系统配置 -->
<insert id="insertConfig" parameterType="TbSysConfigDTO">
INSERT INTO config.tb_sys_config

50
urbanLifelineWeb/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,50 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Chrome (Shared)",
"type": "chrome",
"request": "launch",
"url": "http://localhost:5000",
"webRoot": "${workspaceFolder}/packages/shared",
"runtimeExecutable": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"sourceMaps": true,
"sourceMapPathOverrides": {
"/@/*": "${workspaceFolder}/packages/shared/*",
"/src/*": "${workspaceFolder}/packages/shared/src/*"
},
"userDataDir": "${workspaceFolder}/.vscode/chrome-debug-profile",
"trace": true
},
{
"name": "启动 Shared 开发服务器",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
"cwd": "${workspaceFolder}/packages/shared",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "new",
"showReuseMessage": true,
"clear": false
}
}
],
"compounds": [
{
"name": "启动 Shared 并打开 Chrome",
"configurations": [
"启动 Shared 开发服务器",
"Launch Chrome (Shared)"
],
"stopAll": true,
"presentation": {
"hidden": false,
"group": "",
"order": 1
}
}
]
}