diff --git a/backend/src/main/java/com/yfd/platform/system/controller/SmsVerifyCodeController.java b/backend/src/main/java/com/yfd/platform/system/controller/SmsVerifyCodeController.java new file mode 100644 index 0000000..65023e1 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/system/controller/SmsVerifyCodeController.java @@ -0,0 +1,178 @@ +package com.yfd.platform.system.controller; + +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.SmsVerifyCode; +import com.yfd.platform.system.domain.SysUser; +import com.yfd.platform.system.service.ISmsVerifyCodeService; +import com.yfd.platform.system.service.IUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.sql.Timestamp; +import java.util.Date; + +/** + *

+ * 短信验证码控制器 + *

+ */ +@RestController +@RequestMapping("/sms") +@Tag(name = "短信验证码管理") +public class SmsVerifyCodeController { + + @Resource + private ISmsVerifyCodeService smsVerifyCodeService; + + @Resource + private IUserService userService; + + @Value("${rsa.private_key}") + private String privateKey; + + /** + * 发送验证码 + */ + @PostMapping("/sendCode") + @Operation(summary = "发送验证码") + public ResponseResult sendVerifyCode(@RequestParam String phone, + @RequestParam Integer type) { + if (phone == null || phone.isEmpty()) { + return ResponseResult.error("手机号不能为空"); + } + if (type == null || (type != SmsVerifyCode.TYPE_REGISTER && type != SmsVerifyCode.TYPE_FIND_PASSWORD)) { + return ResponseResult.error("类型错误:1-注册 2-找回密码"); + } + + if (type.equals(SmsVerifyCode.TYPE_REGISTER)) { + SysUser existUser = userService.getUserByPhone(phone); + if (existUser != null) { + return ResponseResult.error("该手机号已注册"); + } + } + + if (type.equals(SmsVerifyCode.TYPE_FIND_PASSWORD)) { + SysUser existUser = userService.getUserByPhone(phone); + if (existUser == null) { + return ResponseResult.error("该手机号未注册"); + } + } + + String code = smsVerifyCodeService.sendVerifyCode(phone, type); + if (code == null) { + return ResponseResult.error("验证码发送失败,请稍后重试"); + } + + return ResponseResult.success(); + } + + /** + * 注册用户 + */ + @PostMapping("/register") + @Operation(summary = "注册用户") + public ResponseResult register(@RequestBody SysUser user, + @RequestParam String code) { + if (user.getPhone() == null || user.getPhone().isEmpty()) { + return ResponseResult.error("手机号不能为空"); + } + if (user.getUsername() == null || user.getUsername().isEmpty()) { + return ResponseResult.error("用户名不能为空"); + } + if (user.getPassword() == null || user.getPassword().isEmpty()) { + return ResponseResult.error("密码不能为空"); + } + if (code == null || code.isEmpty()) { + return ResponseResult.error("验证码不能为空"); + } + + boolean verified = smsVerifyCodeService.verifyCode(user.getPhone(), code, SmsVerifyCode.TYPE_REGISTER); + if (!verified) { + return ResponseResult.error("验证码错误或已过期"); + } + + SysUser existUser = userService.getUserByPhone(user.getPhone()); + if (existUser != null) { + return ResponseResult.error("该手机号已注册"); + } + + try { + com.yfd.platform.utils.RsaUtils.decryptByPrivateKey(privateKey, user.getPassword()); + } catch (Exception e) { + return ResponseResult.error("密码解密失败"); + } + + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + user.setPassword(passwordEncoder.encode(user.getPassword())); + user.setRegStatus(0); + user.setRegTime(new Date()); + user.setStatus(1); + user.setUsertype(1); + + boolean success = userService.save(user); + if (success) { + return ResponseResult.success(); + } else { + return ResponseResult.error("注册失败"); + } + } + + /** + * 找回密码 + */ + @PostMapping("/resetPassword") + @Operation(summary = "找回密码") + public ResponseResult resetPassword(@RequestParam String phone, + @RequestParam String code, + @RequestParam String password) { + if (phone == null || phone.isEmpty()) { + return ResponseResult.error("手机号不能为空"); + } + if (code == null || code.isEmpty()) { + return ResponseResult.error("验证码不能为空"); + } + if (password == null || password.isEmpty()) { + return ResponseResult.error("新密码不能为空"); + } + + boolean verified = smsVerifyCodeService.verifyCode(phone, code, SmsVerifyCode.TYPE_FIND_PASSWORD); + if (!verified) { + return ResponseResult.error("验证码错误或已过期"); + } + + SysUser existUser = userService.getUserByPhone(phone); + if (existUser == null) { + return ResponseResult.error("该手机号未注册"); + } + + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + String encryptedPassword = passwordEncoder.encode(password); + + boolean success = userService.updatePasswordByPhone(phone, encryptedPassword); + if (success) { + return ResponseResult.success(); + } else { + return ResponseResult.error("密码重置失败"); + } + } + + /** + * 验证验证码是否有效 + */ + @GetMapping("/verifyCode") + @Operation(summary = "验证验证码") + public ResponseResult verifyCode(@RequestParam String phone, + @RequestParam String code, + @RequestParam Integer type) { + boolean valid = smsVerifyCodeService.verifyCode(phone, code, type); + if (valid) { + return ResponseResult.success(); + } else { + return ResponseResult.error("验证码错误或已过期"); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/system/controller/UserController.java b/backend/src/main/java/com/yfd/platform/system/controller/UserController.java index 914b8f7..6c9d287 100644 --- a/backend/src/main/java/com/yfd/platform/system/controller/UserController.java +++ b/backend/src/main/java/com/yfd/platform/system/controller/UserController.java @@ -200,4 +200,32 @@ public class UserController { boolean ok = userService.uploadAvatar(id, multipartFile); return ResponseResult.success(); } + + @Log(module = "系统用户", value = "审核用户注册") + @PostMapping("/auditUser") + @Operation(summary = "审核用户注册") + @ResponseBody + public ResponseResult auditUser(@RequestParam String userId, + @RequestParam Integer auditStatus) { + if (userId == null || userId.isEmpty()) { + return ResponseResult.error("用户ID不能为空"); + } + if (auditStatus == null || (auditStatus != 1 && auditStatus != 2)) { + return ResponseResult.error("审核状态错误:1-通过 2-驳回"); + } + boolean ok = userService.auditUser(userId, auditStatus); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error("审核失败"); + } + } + + @GetMapping("/queryPendingAuditUsers") + @Operation(summary = "查询待审核用户列表") + @ResponseBody + public ResponseResult queryPendingAuditUsers(Page page) { + Page result = userService.queryPendingAuditUsers(page); + return ResponseResult.successData(result); + } } diff --git a/backend/src/main/java/com/yfd/platform/system/domain/SmsVerifyCode.java b/backend/src/main/java/com/yfd/platform/system/domain/SmsVerifyCode.java new file mode 100644 index 0000000..2cc1351 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/system/domain/SmsVerifyCode.java @@ -0,0 +1,65 @@ +package com.yfd.platform.system.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.Date; + +/** + *

+ * 短信验证码表 + *

+ */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("SMS_VERIFY_CODE") +public class SmsVerifyCode implements Serializable { + + private static final long serialVersionUID = 1L; + + public static final Integer TYPE_REGISTER = 1; + public static final Integer TYPE_FIND_PASSWORD = 2; + + public static final Integer STATUS_UNUSED = 0; + public static final Integer STATUS_USED = 1; + + /** + * 主键,默认SYS_GUID() + */ + @TableId(type = IdType.INPUT) + private String id; + + /** + * 手机号 + */ + private String phone; + + /** + * 验证码 + */ + private String code; + + /** + * 1-注册 2-找回密码 + */ + private Integer type; + + /** + * 过期时间 + */ + private Date expireTime; + + /** + * 创建时间,默认当前时间 + */ + private Date createTime; + + /** + * 0-未使用 1-已使用 + */ + private Integer status; +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/system/domain/SysUser.java b/backend/src/main/java/com/yfd/platform/system/domain/SysUser.java index 57c5b85..ed9b24f 100644 --- a/backend/src/main/java/com/yfd/platform/system/domain/SysUser.java +++ b/backend/src/main/java/com/yfd/platform/system/domain/SysUser.java @@ -6,6 +6,7 @@ import lombok.EqualsAndHashCode; import java.io.Serializable; import java.sql.Timestamp; +import java.util.Date; import java.util.List; /** @@ -115,6 +116,31 @@ public class SysUser implements Serializable { */ private String custom3; + /** + * 真实姓名(注册必填) + */ + private String realName; + + /** + * 注册状态:0-待审核 1-已通过 2-已驳回 + */ + private Integer regStatus; + + /** + * 审核人ID + */ + private String auditUser; + + /** + * 审核时间 + */ + private Date auditTime; + + /** + * 注册申请时间 + */ + private Date regTime; + @TableField(exist = false) List roles; } diff --git a/backend/src/main/java/com/yfd/platform/system/mapper/SmsVerifyCodeMapper.java b/backend/src/main/java/com/yfd/platform/system/mapper/SmsVerifyCodeMapper.java new file mode 100644 index 0000000..714ab94 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/system/mapper/SmsVerifyCodeMapper.java @@ -0,0 +1,37 @@ +package com.yfd.platform.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.system.domain.SmsVerifyCode; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + *

+ * 短信验证码表 Mapper 接口 + *

+ */ +public interface SmsVerifyCodeMapper extends BaseMapper { + + /** + * 根据手机号和类型查询最新的有效验证码 + */ + SmsVerifyCode selectLatestByPhoneAndType(@Param("phone") String phone, @Param("type") Integer type); + + /** + * 标记验证码为已使用 + */ + @Update("UPDATE SMS_VERIFY_CODE SET STATUS = 1 WHERE ID = #{id}") + int markAsUsed(@Param("id") String id); + + /** + * 删除指定手机号和类型的过期验证码 + */ + int deleteExpiredByPhoneAndType(@Param("phone") String phone, @Param("type") Integer type); + + /** + * 查询指定手机号和类型的未使用验证码 + */ + List selectUnusedByPhoneAndType(@Param("phone") String phone, @Param("type") Integer type); +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/system/service/ISmsVerifyCodeService.java b/backend/src/main/java/com/yfd/platform/system/service/ISmsVerifyCodeService.java new file mode 100644 index 0000000..a8ca1b8 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/system/service/ISmsVerifyCodeService.java @@ -0,0 +1,40 @@ +package com.yfd.platform.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.yfd.platform.system.domain.SmsVerifyCode; + +/** + *

+ * 短信验证码表 服务类 + *

+ */ +public interface ISmsVerifyCodeService extends IService { + + /** + * 发送验证码 + * @param phone 手机号 + * @param type 1-注册 2-找回密码 + * @return 验证码 + */ + String sendVerifyCode(String phone, Integer type); + + /** + * 验证验证码 + * @param phone 手机号 + * @param code 验证码 + * @param type 类型 1-注册 2-找回密码 + * @return 是否验证通过 + */ + boolean verifyCode(String phone, String code, Integer type); + + /** + * 标记验证码已使用 + * @param id 验证码ID + */ + void markAsUsed(String id); + + /** + * 生成6位数字验证码 + */ + String generateCode(); +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/system/service/IUserService.java b/backend/src/main/java/com/yfd/platform/system/service/IUserService.java index e1ff507..f86da52 100644 --- a/backend/src/main/java/com/yfd/platform/system/service/IUserService.java +++ b/backend/src/main/java/com/yfd/platform/system/service/IUserService.java @@ -138,6 +138,38 @@ public interface IUserService extends IService { ************************************/ boolean deleteUserByIds(String ids); + /*********************************** + * 用途说明:根据手机号查询用户 + * 参数说明 + *phone 手机号 + * 返回值说明: 用户对象 + ************************************/ + SysUser getUserByPhone(String phone); + /*********************************** + * 用途说明:根据手机号修改密码 + * 参数说明 + *phone 手机号 + * encryptedPassword 加密后的密码 + * 返回值说明: 是否修改成功 + ************************************/ + boolean updatePasswordByPhone(String phone, String encryptedPassword); + + /*********************************** + * 用途说明:审核用户注册 + * 参数说明 + *userId 用户id + * auditStatus 审核状态:1-通过 2-驳回 + * 返回值说明: 是否审核成功 + ************************************/ + boolean auditUser(String userId, Integer auditStatus); + + /*********************************** + * 用途说明:查询待审核用户列表 + * 参数说明 + *page 分页参数 + * 返回值说明: 待审核用户分页列表 + ************************************/ + Page queryPendingAuditUsers(Page page); } diff --git a/backend/src/main/java/com/yfd/platform/system/service/impl/SmsVerifyCodeServiceImpl.java b/backend/src/main/java/com/yfd/platform/system/service/impl/SmsVerifyCodeServiceImpl.java new file mode 100644 index 0000000..2b939eb --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/system/service/impl/SmsVerifyCodeServiceImpl.java @@ -0,0 +1,99 @@ +package com.yfd.platform.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yfd.platform.system.domain.SmsVerifyCode; +import com.yfd.platform.system.mapper.SmsVerifyCodeMapper; +import com.yfd.platform.system.service.ISmsVerifyCodeService; +import com.yfd.platform.utils.SmsSender; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.Random; + +/** + *

+ * 短信验证码表 服务实现类 + *

+ */ +@Service +public class SmsVerifyCodeServiceImpl extends ServiceImpl implements ISmsVerifyCodeService { + + private static final int CODE_VALID_MINUTES = 5; + private static final Random RANDOM = new Random(); + + @Resource + private SmsSender smsSender; + + @Override + @Transactional(rollbackFor = Exception.class) + public String sendVerifyCode(String phone, Integer type) { + String code = generateCode(); + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SmsVerifyCode::getPhone, phone) + .eq(SmsVerifyCode::getType, type) + .eq(SmsVerifyCode::getStatus, SmsVerifyCode.STATUS_UNUSED); + this.remove(queryWrapper); + + SmsVerifyCode verifyCode = new SmsVerifyCode(); + verifyCode.setId(java.util.UUID.randomUUID().toString().replace("-", "")); + verifyCode.setPhone(phone); + verifyCode.setCode(code); + verifyCode.setType(type); + verifyCode.setStatus(SmsVerifyCode.STATUS_UNUSED); + verifyCode.setCreateTime(new Date()); + + Date expireTime = new Date(System.currentTimeMillis() + CODE_VALID_MINUTES * 60 * 1000L); + verifyCode.setExpireTime(expireTime); + + this.save(verifyCode); + + String content = "您的验证码为:" + code + "," + CODE_VALID_MINUTES + "分钟内有效,请勿泄露给他人。"; + boolean sent = smsSender.send(phone, content); + + if (!sent) { + return null; + } + + return code; + } + + @Override + public boolean verifyCode(String phone, String code, Integer type) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SmsVerifyCode::getPhone, phone) + .eq(SmsVerifyCode::getCode, code) + .eq(SmsVerifyCode::getType, type) + .eq(SmsVerifyCode::getStatus, SmsVerifyCode.STATUS_UNUSED) + .gt(SmsVerifyCode::getExpireTime, new Date()); + + SmsVerifyCode verifyCode = this.getOne(queryWrapper); + if (verifyCode == null) { + return false; + } + + verifyCode.setStatus(SmsVerifyCode.STATUS_USED); + this.updateById(verifyCode); + + return true; + } + + @Override + public void markAsUsed(String id) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SmsVerifyCode::getId, id); + SmsVerifyCode verifyCode = this.getOne(queryWrapper); + if (verifyCode != null) { + verifyCode.setStatus(SmsVerifyCode.STATUS_USED); + this.updateById(verifyCode); + } + } + + @Override + public String generateCode() { + return String.format("%06d", RANDOM.nextInt(1000000)); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/system/service/impl/UserServiceImpl.java b/backend/src/main/java/com/yfd/platform/system/service/impl/UserServiceImpl.java index 7e483f1..537a57f 100644 --- a/backend/src/main/java/com/yfd/platform/system/service/impl/UserServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/system/service/impl/UserServiceImpl.java @@ -561,6 +561,49 @@ public class UserServiceImpl extends ServiceImpl impleme } } + @Override + public SysUser getUserByPhone(String phone) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysUser::getPhone, phone); + return this.getOne(queryWrapper); + } + + @Override + public boolean updatePasswordByPhone(String phone, String encryptedPassword) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.eq("phone", phone) + .set("password", encryptedPassword) + .set("pwdresettime", new Timestamp(System.currentTimeMillis())) + .set("lastmodifydate", new Timestamp(System.currentTimeMillis())) + .set("lastmodifier", getUsername()); + return this.update(updateWrapper); + } + + @Override + public boolean auditUser(String userId, Integer auditStatus) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.eq("id", userId) + .set("reg_status", auditStatus) + .set("audit_user", getUsername()) + .set("audit_time", new Timestamp(System.currentTimeMillis())) + .set("lastmodifydate", new Timestamp(System.currentTimeMillis())) + .set("lastmodifier", getUsername()); + if (auditStatus == 1) { + updateWrapper.set("status", 1); + } else if (auditStatus == 2) { + updateWrapper.set("status", 0); + } + return this.update(updateWrapper); + } + + @Override + public Page queryPendingAuditUsers(Page page) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysUser::getRegStatus, 0); + queryWrapper.orderByDesc(SysUser::getRegTime); + return this.page(page, queryWrapper); + } + /*********************************** * 用途说明:比较登录名称是否有重复 * 参数说明 diff --git a/backend/src/main/java/com/yfd/platform/utils/DefaultSmsSender.java b/backend/src/main/java/com/yfd/platform/utils/DefaultSmsSender.java new file mode 100644 index 0000000..506429c --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/utils/DefaultSmsSender.java @@ -0,0 +1,21 @@ +package com.yfd.platform.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +/** + * 默认短信发送实现(模拟实现) + * 实际使用时替换为真实的短信网关 + */ +@Slf4j +@Component +@ConditionalOnProperty(name = "sms.enabled", havingValue = "false", matchIfMissing = true) +public class DefaultSmsSender implements SmsSender { + + @Override + public boolean send(String phone, String content) { + log.info("【模拟短信发送】手机号: {}, 内容: {}", phone, content); + return true; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/utils/SmsSender.java b/backend/src/main/java/com/yfd/platform/utils/SmsSender.java new file mode 100644 index 0000000..1b1b3bc --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/utils/SmsSender.java @@ -0,0 +1,15 @@ +package com.yfd.platform.utils; + +/** + * 短信发送接口 + */ +public interface SmsSender { + + /** + * 发送短信 + * @param phone 手机号 + * @param content 短信内容 + * @return 是否发送成功 + */ + boolean send(String phone, String content); +} \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 8f42778..547f405 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: profiles: - active: devtw + active: prod jasypt: encryptor: