diff --git a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql index e02f276..38f0150 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql @@ -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), diff --git a/urbanLifelineServ/apis/api-message/src/main/java/org/xyzh/api/message/MessageService.java b/urbanLifelineServ/apis/api-message/src/main/java/org/xyzh/api/message/MessageService.java deleted file mode 100644 index d18da9b..0000000 --- a/urbanLifelineServ/apis/api-message/src/main/java/org/xyzh/api/message/MessageService.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.xyzh.api.message; - -/** - * Message服务接口 - * 用于消息管理 - */ -public interface MessageService { - -} diff --git a/urbanLifelineServ/apis/api-message/src/main/java/org/xyzh/api/message/service/MessageService.java b/urbanLifelineServ/apis/api-message/src/main/java/org/xyzh/api/message/service/MessageService.java index 180128e..d692883 100644 --- a/urbanLifelineServ/apis/api-message/src/main/java/org/xyzh/api/message/service/MessageService.java +++ b/urbanLifelineServ/apis/api-message/src/main/java/org/xyzh/api/message/service/MessageService.java @@ -14,8 +14,20 @@ import org.xyzh.common.core.page.PageParam; * @since 2025-11-05 */ public interface MessageService { + + // ================ 发送邮件 ================== + ResultDomain sendSimpleEmail(String to, String subject, String content); + + ResultDomain sendHtmlEmail(String to, String subject, String content); + + ResultDomain sendEmailVerificationCode(String to, String code); + + // ================ 发送短信 ================== //================= 用户查看消息列表 ================= + ResultDomain sendPhoneVerificationCode(String phone, String code); + + /** * @description 获取我的消息列表 diff --git a/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/SysConfigService.java b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/SysConfigService.java index 8f12f8a..4ab9fa4 100644 --- a/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/SysConfigService.java +++ b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/SysConfigService.java @@ -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 插入系统配置 diff --git a/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/SysUserService.java b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/SysUserService.java index 7b8df7f..9a62273 100644 --- a/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/SysUserService.java +++ b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/SysUserService.java @@ -18,6 +18,10 @@ import org.xyzh.common.dto.sys.TbSysUserRoleDTO; public interface SysUserService { // ================= 用户基本信息管理 ================= + + ResultDomain registerUser(SysUserVO userVO); + + /** * @description 插入用户 * @param userVO 用户VO @@ -61,7 +65,7 @@ public interface SysUserService { * @author yslg * @since 2025-11-05 */ - ResultDomain getLoginUser(SysUserVO filter); + ResultDomain getLoginUser(TbSysUserDTO filter); /** * @description 根据条件查询用户列表 diff --git a/urbanLifelineServ/auth/pom.xml b/urbanLifelineServ/auth/pom.xml index cc2d05f..11688ec 100644 --- a/urbanLifelineServ/auth/pom.xml +++ b/urbanLifelineServ/auth/pom.xml @@ -23,10 +23,26 @@ org.xyzh.apis api-auth + + org.xyzh.apis + api-system + + + org.xyzh.apis + api-message + org.xyzh.common common-auth + + org.xyzh.common + common-core + + + org.xyzh.common + common-utils + \ No newline at end of file diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/controller/AuthController.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/controller/AuthController.java new file mode 100644 index 0000000..3da1a77 --- /dev/null +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/controller/AuthController.java @@ -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 登录结果 + * @author yslg + * @since 2025-09-28 + */ + @PostMapping("/login") + public ResultDomain login(@RequestBody LoginParam loginParam, HttpServletRequest request) { + return authService.login(loginParam, request); + } + + /** + * @description 用户退出登录 + * @param loginDomain 登录域对象 + * @return ResultDomain 退出结果 + * @author yslg + * @since 2025-09-28 + */ + @PostMapping("/logout") + public ResultDomain logout(HttpServletRequest request) { + return authService.logout(request); + } + + /** + * @description 获取验证码 + * @return ResultDomain 验证码 + * @author yslg + * @since 2025-09-28 + */ + @GetMapping("/captcha") + public ResultDomain getCaptcha() { + // TODO: 实现验证码生成逻辑 + // 生成验证码会话ID,用于验证时匹配 + String captchaId = IDUtils.generateID(); + String captchaData = captchaId + ":captcha-placeholder"; // 格式: ID:验证码内容 + + return ResultDomain.success("验证码获取成功", captchaData); + } + + /** + * @description 刷新令牌 + * @param token 原令牌 + * @return ResultDomain 新令牌 + * @author yslg + * @since 2025-09-28 + */ + @PostMapping("/refresh") + public ResultDomain refreshToken(@RequestHeader("Authorization") String token) { + // TODO: 实现令牌刷新逻辑 + // 为新令牌生成唯一ID + String newTokenId = IDUtils.generateID(); + String newToken = "new-token-" + newTokenId; // 临时占位符 + + return ResultDomain.success("令牌刷新成功", newToken); + } + + /** + * @description 健康检查 + * @return ResultDomain 健康状态 + * @author yslg + * @since 2025-09-28 + */ + @GetMapping("/health") + public ResultDomain health() { + return ResultDomain.success("认证服务运行正常", "OK"); + } + + /** + * @description 发送邮箱验证码 + * @param requestBody 包含email字段的请求体 + * @return ResultDomain 发送结果 + * @author yslg + * @since 2025-11-03 + */ + @PostMapping("/send-email-code") + public ResultDomain> sendEmailCode(@RequestBody Map 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 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 发送结果 + * @author yslg + * @since 2025-11-03 + */ + @PostMapping("/send-sms-code") + public ResultDomain> sendSmsCode(@RequestBody Map 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 data = Map.of( + "sessionId", sessionId, + "message", "验证码已发送" + ); + + logger.info("短信验证码已发送,手机号: {}, sessionId: {}", phone, sessionId); + return ResultDomain.success("验证码已发送", data); + } else { + return ResultDomain.failure("验证码发送失败,请稍后重试"); + } + } + + /** + * @description 用户注册 + * @param requestBody 注册参数 + * @return ResultDomain 注册结果 + * @author yslg + * @since 2025-11-03 + */ + @PostMapping("/register") + public ResultDomain register(@RequestBody Map 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 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 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()); + } + } +} diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/exception/AuthException.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/exception/AuthException.java new file mode 100644 index 0000000..ae544c1 --- /dev/null +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/exception/AuthException.java @@ -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); + } +} diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/service/impl/AuthServiceImpl.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/service/impl/AuthServiceImpl.java index fed4332..cd6876e 100644 --- a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/service/impl/AuthServiceImpl.java +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/service/impl/AuthServiceImpl.java @@ -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); diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/LoginStrategy.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/LoginStrategy.java new file mode 100644 index 0000000..10a3c16 --- /dev/null +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/LoginStrategy.java @@ -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; + } +} \ No newline at end of file diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/LoginStrategyFactory.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/LoginStrategyFactory.java new file mode 100644 index 0000000..c468068 --- /dev/null +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/LoginStrategyFactory.java @@ -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 strategies; + + public LoginStrategyFactory(List 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 登录类型集合 + * @author yslg + * @since 2025-12-05 + */ + public java.util.Set getSupportedLoginTypes() { + return strategies.keySet(); + } +} \ No newline at end of file diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/EmailLoginStrategy.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/EmailLoginStrategy.java new file mode 100644 index 0000000..260c338 --- /dev/null +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/EmailLoginStrategy.java @@ -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; + } +} \ No newline at end of file diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/PasswordLoginStrategy.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/PasswordLoginStrategy.java new file mode 100644 index 0000000..b16d8f9 --- /dev/null +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/PasswordLoginStrategy.java @@ -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); + } +} \ No newline at end of file diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/PhoneLoginStrategy.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/PhoneLoginStrategy.java new file mode 100644 index 0000000..775184b --- /dev/null +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/PhoneLoginStrategy.java @@ -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; + } +} \ No newline at end of file diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/UsernameLoginStrategy.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/UsernameLoginStrategy.java new file mode 100644 index 0000000..53ffd7e --- /dev/null +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/UsernameLoginStrategy.java @@ -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); + } +} \ No newline at end of file diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/WechatLoginStrategy.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/WechatLoginStrategy.java new file mode 100644 index 0000000..ad94d22 --- /dev/null +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/strategy/impl/WechatLoginStrategy.java @@ -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; + } +} diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/utils/CapcatUtils.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/utils/CapcatUtils.java new file mode 100644 index 0000000..75e5582 --- /dev/null +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/utils/CapcatUtils.java @@ -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)); + } +} diff --git a/urbanLifelineServ/auth/src/main/resources/application.yml b/urbanLifelineServ/auth/src/main/resources/application.yml index 4fb023c..80644dc 100644 --- a/urbanLifelineServ/auth/src/main/resources/application.yml +++ b/urbanLifelineServ/auth/src/main/resources/application.yml @@ -10,7 +10,9 @@ urban-lifeline: enabled: false # 认证服务自己不需要认证 whitelist: - /** # 认证服务的所有接口都放行 - +security: + aes: + secret-key: 1234567890qwer # ================== Spring ================== spring: application: diff --git a/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/config/AuthProperties.java b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/config/AuthProperties.java index d4d7a57..e20b84c 100644 --- a/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/config/AuthProperties.java +++ b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/config/AuthProperties.java @@ -14,7 +14,7 @@ import java.util.List; * @author yslg */ @Component -@ConfigurationProperties(prefix = "urban-lifeline.auth") +@ConfigurationProperties(prefix = "auth") public class AuthProperties { /** diff --git a/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/JwtTokenUtil.java b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/JwtTokenUtil.java index 7d3eb63..59aaa2a 100644 --- a/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/JwtTokenUtil.java +++ b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/JwtTokenUtil.java @@ -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() { diff --git a/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/exception/BaseException.java b/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/exception/BaseException.java new file mode 100644 index 0000000..e0a27bf --- /dev/null +++ b/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/exception/BaseException.java @@ -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(); + } + + +} diff --git a/urbanLifelineServ/common/common-utils/pom.xml b/urbanLifelineServ/common/common-utils/pom.xml index 8c855aa..669ac17 100644 --- a/urbanLifelineServ/common/common-utils/pom.xml +++ b/urbanLifelineServ/common/common-utils/pom.xml @@ -13,7 +13,6 @@ common-utils ${urban-lifeline.version} jar - 21 21 @@ -44,5 +43,22 @@ spring-boot-starter provided + + + org.springframework.boot + spring-boot-starter-mail + + + + + com.aliyun + dysmsapi20170525 + 4.2.0 + + + + com.alibaba.fastjson2 + fastjson2-extension-spring6 + \ No newline at end of file diff --git a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/config/FastJsonConfiguration.java b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/config/FastJsonConfiguration.java new file mode 100644 index 0000000..7d00be0 --- /dev/null +++ b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/config/FastJsonConfiguration.java @@ -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> 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); + } +} diff --git a/urbanLifelineServ/gateway/src/main/resources/application.yml b/urbanLifelineServ/gateway/src/main/resources/application.yml index 6239462..88c78a9 100644 --- a/urbanLifelineServ/gateway/src/main/resources/application.yml +++ b/urbanLifelineServ/gateway/src/main/resources/application.yml @@ -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: diff --git a/urbanLifelineServ/message/pom.xml b/urbanLifelineServ/message/pom.xml index 17934c8..4bce1e5 100644 --- a/urbanLifelineServ/message/pom.xml +++ b/urbanLifelineServ/message/pom.xml @@ -17,5 +17,48 @@ 21 21 + + + + org.xyzh.common + common-all + + + org.xyzh.apis + api-system + + + org.xyzh.apis + api-message + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-logging + + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.nacos + nacos-logback-adapter-12 + + + com.alibaba.nacos + logback-adapter + + + + \ No newline at end of file diff --git a/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/DynamicConfigLoader.java b/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/DynamicConfigLoader.java new file mode 100644 index 0000000..497f6a1 --- /dev/null +++ b/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/DynamicConfigLoader.java @@ -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); + } + } +} diff --git a/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/EmailConfigProperties.java b/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/EmailConfigProperties.java new file mode 100644 index 0000000..d0203e6 --- /dev/null +++ b/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/EmailConfigProperties.java @@ -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; + } +} diff --git a/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/MailSenderConfig.java b/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/MailSenderConfig.java new file mode 100644 index 0000000..e91fe54 --- /dev/null +++ b/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/MailSenderConfig.java @@ -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; + } +} diff --git a/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/SmsConfigProperties.java b/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/SmsConfigProperties.java new file mode 100644 index 0000000..b673088 --- /dev/null +++ b/urbanLifelineServ/message/src/main/java/org/xyzh/message/config/SmsConfigProperties.java @@ -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; + } +} diff --git a/urbanLifelineServ/message/src/main/java/org/xyzh/message/service/MessageServiceImpl.java b/urbanLifelineServ/message/src/main/java/org/xyzh/message/service/MessageServiceImpl.java new file mode 100644 index 0000000..d56c786 --- /dev/null +++ b/urbanLifelineServ/message/src/main/java/org/xyzh/message/service/MessageServiceImpl.java @@ -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 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 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 sendEmailVerificationCode(String to, String code) { + boolean flag = emailUtils.sendVerificationCode(to, code); + if (flag){ + return ResultDomain.success("发生成功"); + }else{ + return ResultDomain.failure("发送失败"); + } + } + + @Override + public ResultDomain sendPhoneVerificationCode(String phone, String code) { + boolean flag = smsUtils.sendVerificationCode(phone, code); + if (flag){ + return ResultDomain.success("发生成功"); + }else{ + return ResultDomain.failure("发送失败"); + } + } + + + + @Override + public ResultDomain createMessage(MessageVO messageVO) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultDomain deleteMessage(String messageId) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultDomain getMessageDetail(String messageId) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultDomain getMessageList(TbMessageDTO filter) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultDomain getMessagePage(TbMessageDTO filter, PageParam pageParam) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultDomain getMyMessageDetail(String messageId) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultDomain getMyMessageList(TbMessageDTO filter) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultDomain getMyMessagePage(TbMessageDTO filter, PageParam pageParam) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultDomain handleMessage(String messageId, String status) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultDomain sendMessage(MessageVO messageVO) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultDomain updateMessage(MessageVO messageVO) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultDomain withdrawMessage(String messageId) { + // TODO Auto-generated method stub + return null; + } + + + +} diff --git a/urbanLifelineServ/message/src/main/java/org/xyzh/message/utils/EmailUtils.java b/urbanLifelineServ/message/src/main/java/org/xyzh/message/utils/EmailUtils.java new file mode 100644 index 0000000..2a5da88 --- /dev/null +++ b/urbanLifelineServ/message/src/main/java/org/xyzh/message/utils/EmailUtils.java @@ -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 "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
" + + "
" + + "

红色思政学习平台

" + + "
" + + "
" + + "

尊敬的用户,您好!

" + + "

您正在进行邮箱验证,您的验证码为:

" + + "
" + + "
" + code + "
" + + "
" + + "
" + + "

• 验证码有效期为10分钟,请尽快完成验证

" + + "

• 如果这不是您的操作,请忽略此邮件

" + + "

• 为了保护您的账号安全,请勿将验证码告知他人

" + + "
" + + "
" + + "
" + + "

此邮件由系统自动发送,请勿回复

" + + "

Copyright © 红色思政智能体平台

" + + "
" + + "
" + + "" + + ""; + } + + /** + * 生成6位数字验证码 + * @return 验证码 + */ + public static String generateVerificationCode() { + return String.valueOf((int)((Math.random() * 9 + 1) * 100000)); + } +} + diff --git a/urbanLifelineServ/message/src/main/java/org/xyzh/message/utils/SmsUtils.java b/urbanLifelineServ/message/src/main/java/org/xyzh/message/utils/SmsUtils.java new file mode 100644 index 0000000..f0a4260 --- /dev/null +++ b/urbanLifelineServ/message/src/main/java/org/xyzh/message/utils/SmsUtils.java @@ -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); + } +} + diff --git a/urbanLifelineServ/message/src/main/resources/application.yml b/urbanLifelineServ/message/src/main/resources/application.yml index c8f7f9f..b9e427f 100644 --- a/urbanLifelineServ/message/src/main/resources/application.yml +++ b/urbanLifelineServ/message/src/main/resources/application.yml @@ -17,7 +17,10 @@ urban-lifeline: - /error - /actuator/health - /actuator/info - + +security: + aes: + secret-key: 1234567890qwer # ================== Spring ================== spring: application: diff --git a/urbanLifelineServ/pom.xml b/urbanLifelineServ/pom.xml index 5cee6ec..d47ed93 100644 --- a/urbanLifelineServ/pom.xml +++ b/urbanLifelineServ/pom.xml @@ -208,7 +208,11 @@ fastjson2 ${fastjson.version} - + + com.alibaba.fastjson2 + fastjson2-extension-spring6 + ${fastjson.version} + diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/config/TbSysConfigMapper.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/config/TbSysConfigMapper.java index dece7f8..956327c 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/config/TbSysConfigMapper.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/config/TbSysConfigMapper.java @@ -20,6 +20,15 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; @Mapper public interface TbSysConfigMapper extends BaseMapper { + /** + * @description 从key读取配置 + * @param + * @author yslg + * @since 2025-12-05 + */ + TbSysConfigDTO selectSysConfigByKey(@Param("configKey") String configKey); + + /** * @description 插入系统配置 * @param configDTO 系统配置DTO diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysConfigServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysConfigServiceImpl.java index 9b05876..5ace9c3 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysConfigServiceImpl.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysConfigServiceImpl.java @@ -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 insertConfig(TbSysConfigDTO configDTO) { if (configDTO == null) { diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java index 713f97d..126a9d6 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java @@ -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 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 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 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 users = userMapper.getUserByFilter(filter); + if (users != null && !users.isEmpty()) { + logger.warn("邮箱已存在: {}", userVO.getEmail()); + return true; + } + } + + return false; + } + @Override public ResultDomain insertUser(SysUserVO userVO) { if (userVO == null) { @@ -120,9 +231,12 @@ public class SysUserServiceImpl implements SysUserService { } @Override - public ResultDomain getLoginUser(SysUserVO filter) { + public ResultDomain getLoginUser(TbSysUserDTO filter) { // 登录查询语义与 getUser 相同(可根据用户名/手机号/邮箱查询) - return getUser(filter); + if(NonUtils.isNotNull(filter.getPhone())){ + filter.setPhone_hash(aesEncryptUtil.encrypt(filter.getPhone())); + } + return null; } @Override diff --git a/urbanLifelineServ/system/src/main/resources/mapper/config/TbSysConfigMapper.xml b/urbanLifelineServ/system/src/main/resources/mapper/config/TbSysConfigMapper.xml index 1d4b05b..f4b2b89 100644 --- a/urbanLifelineServ/system/src/main/resources/mapper/config/TbSysConfigMapper.xml +++ b/urbanLifelineServ/system/src/main/resources/mapper/config/TbSysConfigMapper.xml @@ -67,6 +67,15 @@ optsn, creator, updater, dept_path, remark, create_time, update_time, delete_time, deleted + + + + INSERT INTO config.tb_sys_config diff --git a/urbanLifelineWeb/.vscode/launch.json b/urbanLifelineWeb/.vscode/launch.json new file mode 100644 index 0000000..f3cac27 --- /dev/null +++ b/urbanLifelineWeb/.vscode/launch.json @@ -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 + } + } + ] +}