若依后端框架代码评审文档
1. 选择的代码文件和测试依据
1.选择的代码文件
-
SysProfileController:
- 路径:
src/com.ruoyi.web.controller.system
- 路径:
-
BaseController:
- 路径:
src/com.ruoyi.web.controller.common
- 路径:
-
CaptchaController:
- 路径:
src/com.ruoyi.common.core.controller
- 路径:
2.测试依据
依据一般后端框架的 Java 和 Spring Boot 的代码规范进行测试。
2. 测试点
2.1 SysProfileController
源码:
package com.ruoyi.web.controller.system;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysUserService;
/**
* 个人信息 业务处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/user/profile")
public class SysProfileController extends BaseController
{
@Autowired
private ISysUserService userService;
@Autowired
private TokenService tokenService;
/**
* 个人信息
*/
@GetMapping
public AjaxResult profile()
{
LoginUser loginUser = getLoginUser();
SysUser user = loginUser.getUser();
AjaxResult ajax = AjaxResult.success(user);
ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
return ajax;
}
/**
* 修改用户
*/
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult updateProfile(@RequestBody SysUser user)
{
if (StringUtils.isNotEmpty(user.getPhonenumber())
&& UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user)))
{
return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
}
if (StringUtils.isNotEmpty(user.getEmail())
&& UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user)))
{
return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
LoginUser loginUser = getLoginUser();
SysUser sysUser = loginUser.getUser();
user.setUserId(sysUser.getUserId());
user.setPassword(null);
if (userService.updateUserProfile(user) > 0)
{
// 更新缓存用户信息
sysUser.setNickName(user.getNickName());
sysUser.setPhonenumber(user.getPhonenumber());
sysUser.setEmail(user.getEmail());
sysUser.setSex(user.getSex());
tokenService.setLoginUser(loginUser);
return AjaxResult.success();
}
return AjaxResult.error("修改个人信息异常,请联系管理员");
}
/**
* 重置密码
*/
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping("/updatePwd")
public AjaxResult updatePwd(String oldPassword, String newPassword)
{
LoginUser loginUser = getLoginUser();
String userName = loginUser.getUsername();
String password = loginUser.getPassword();
if (!SecurityUtils.matchesPassword(oldPassword, password))
{
return AjaxResult.error("修改密码失败,旧密码错误");
}
if (SecurityUtils.matchesPassword(newPassword, password))
{
return AjaxResult.error("新密码不能与旧密码相同");
}
if (userService.resetUserPwd(userName, SecurityUtils.encryptPassword(newPassword)) > 0)
{
// 更新缓存用户密码
loginUser.getUser().setPassword(SecurityUtils.encryptPassword(newPassword));
tokenService.setLoginUser(loginUser);
return AjaxResult.success();
}
return AjaxResult.error("修改密码异常,请联系管理员");
}
/**
* 头像上传
*/
@Log(title = "用户头像", businessType = BusinessType.UPDATE)
@PostMapping("/avatar")
public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws IOException
{
if (!file.isEmpty())
{
LoginUser loginUser = getLoginUser();
String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file);
if (userService.updateUserAvatar(loginUser.getUsername(), avatar))
{
AjaxResult ajax = AjaxResult.success();
ajax.put("imgUrl", avatar);
// 更新缓存用户头像
loginUser.getUser().setAvatar(avatar);
tokenService.setLoginUser(loginUser);
return ajax;
}
}
return AjaxResult.error("上传图片异常,请联系管理员");
}
}
这是一个通用的基础控制器类,用于处理 Web 层的通用数据操作。以下是一些观察和建议:
-
日志记录:
- 使用了SLF4J进行日志记录,这是个好实践。
-
日期转换:
- 通过
initBinder
方法实现了将前端传递的日期字符串自动转换为Date
类型,这是一个很好的功能。
- 通过
-
分页处理:
- 使用了PageHelper库来处理分页,这是典型的MyBatis分页处理方式。确保
TableSupport
和PageDomain
类的实现是正确的。
- 使用了PageHelper库来处理分页,这是典型的MyBatis分页处理方式。确保
-
结果封装:
AjaxResult
类用于封装响应数据,这有助于在控制器中一致地处理返回结果。
-
安全性:
- 使用
SecurityUtils
来获取登录用户的信息,这是常见的安全实践。
- 使用
-
异常处理:
- 缺少全局异常处理,可以考虑添加一个全局异常处理器,以便在发生异常时提供友好的错误信息。
-
业务逻辑方法:
- 一些通用的业务逻辑方法,如获取登录用户信息、跳转页面等,有助于避免在每个控制器中都重复编写相似的代码。
-
重定向方法:
redirect
方法用于生成重定向路径,这是一个很好的抽象。
2.2 BaseController
源码:
package com.ruoyi.common.core.controller;
import java.beans.PropertyEditorSupport;
import java.util.Date;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.sql.SqlUtil;
/**
* web层通用数据处理
*
* @author ruoyi
*/
public class BaseController
{
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 将前台传递过来的日期格式的字符串,自动转化为Date类型
*/
@InitBinder
public void initBinder(WebDataBinder binder)
{
// Date 类型转换
binder.registerCustomEditor(Date.class, new PropertyEditorSupport()
{
@Override
public void setAsText(String text)
{
setValue(DateUtils.parseDate(text));
}
});
}
/**
* 设置请求分页数据
*/
protected void startPage()
{
PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize();
if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize))
{
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
Boolean reasonable = pageDomain.getReasonable();
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
}
}
/**
* 设置请求排序数据
*/
protected void startOrderBy()
{
PageDomain pageDomain = TableSupport.buildPageRequest();
if (StringUtils.isNotEmpty(pageDomain.getOrderBy()))
{
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
PageHelper.orderBy(orderBy);
}
}
/**
* 响应请求分页数据
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
protected TableDataInfo getDataTable(List<?> list)
{
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(HttpStatus.SUCCESS);
rspData.setMsg("查询成功");
rspData.setRows(list);
rspData.setTotal(new PageInfo(list).getTotal());
return rspData;
}
/**
* 返回成功
*/
public AjaxResult success()
{
return AjaxResult.success();
}
/**
* 返回失败消息
*/
public AjaxResult error()
{
return AjaxResult.error();
}
/**
* 返回成功消息
*/
public AjaxResult success(String message)
{
return AjaxResult.success(message);
}
/**
* 返回失败消息
*/
public AjaxResult error(String message)
{
return AjaxResult.error(message);
}
/**
* 响应返回结果
*
* @param rows 影响行数
* @return 操作结果
*/
protected AjaxResult toAjax(int rows)
{
return rows > 0 ? AjaxResult.success() : AjaxResult.error();
}
/**
* 响应返回结果
*
* @param result 结果
* @return 操作结果
*/
protected AjaxResult toAjax(boolean result)
{
return result ? success() : error();
}
/**
* 页面跳转
*/
public String redirect(String url)
{
return StringUtils.format("redirect:{}", url);
}
/**
* 获取用户缓存信息
*/
public LoginUser getLoginUser()
{
return SecurityUtils.getLoginUser();
}
/**
* 获取登录用户id
*/
public Long getUserId()
{
return getLoginUser().getUserId();
}
/**
* 获取登录部门id
*/
public Long getDeptId()
{
return getLoginUser().getDeptId();
}
/**
* 获取登录用户名
*/
public String getUsername()
{
return getLoginUser().getUsername();
}
}
以下是对代码的一些建议:
-
代码结构:
- 代码结构良好,符合Spring MVC控制器的典型结构。
-
注解使用:
- 使用
@RestController
,@RequestMapping
,@Autowired
等注解是Spring Boot控制器的典型使用方式。
- 使用
-
错误处理:
- 使用
AjaxResult
类返回错误消息。建议为常见的错误场景创建一个实用方法或全局异常处理程序,以保持一致的错误处理。
- 使用
-
密码处理:
- 使用
SecurityUtils.matchesPassword
对密码进行哈希和检查,这增强了安全性。
- 使用
-
文件上传:
- 文件上传处理看起来是合适的。在处理之前检查文件是否不为空。
-
安全性:
- 确保方法具有足够的安全性。可能根据应用程序的安全性要求添加适当的安全性注解(
@PreAuthorize
等)。
- 确保方法具有足够的安全性。可能根据应用程序的安全性要求添加适当的安全性注解(
-
注释:
- 考虑添加注释以解释复杂的部分或业务逻辑。
-
一致性:
- 确保在命名约定上保持一致。例如,
userName
和user.getNickName()
的使用可能要保持一致。
- 确保在命名约定上保持一致。例如,
-
异常处理:
- 对上传头像时的
IOException
进行了通用捕获。对于调试目的,记录异常详细信息可能会有帮助。
- 对上传头像时的
-
RESTful约定:
- 确保遵循RESTful约定。例如,如果适用,考虑使用
@PatchMapping
而不是@PutMapping
进行部分更新。
- 确保遵循RESTful约定。例如,如果适用,考虑使用
-
硬编码字符串:
- 谨慎使用硬编码的字符串,如"/system/user/profile/avatar"。最好将这些字符串定义为常量。
-
个人资料图像更新:
- 最好仅在数据库操作成功时更新缓存。目前,即使头像更新失败,缓存也会被更新。
2.3 CaptchaController
源码:
package com.ruoyi.web.controller.common;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.google.code.kaptcha.Producer;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.sign.Base64;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.system.service.ISysConfigService;
/**
* 验证码操作处理
*
* @author ruoyi
*/
@RestController
public class CaptchaController
{
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;
@Autowired
private RedisCache redisCache;
// 验证码类型
@Value("${ruoyi.captchaType}")
private String captchaType;
@Autowired
private ISysConfigService configService;
/**
* 生成验证码
*/
@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException
{
AjaxResult ajax = AjaxResult.success();
boolean captchaOnOff = configService.selectCaptchaOnOff();
ajax.put("captchaOnOff", captchaOnOff);
// if (!captchaOnOff)
// {
// return ajax;
// }
if (!captchaOnOff)
{
return ajax;
}
// 保存验证码信息
String uuid = IdUtils.simpleUUID();
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
if ("math".equals(captchaType))
{
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
}
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try
{
ImageIO.write(image, "jpg", os);
}
catch (IOException e)
{
return AjaxResult.error(e.getMessage());
}
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
}
这是一个处理验证码操作的控制器类,整体上看起来符合一般的 Java 和 Spring Boot 的代码规范。以下是一些建议:
-
注释:
- 添加一些注释,特别是对于非常规或者复杂的逻辑,这将有助于其他开发者更容易理解你的代码。
-
日志记录:
- 在代码中缺少日志记录。在关键的地方(如异常捕获、业务逻辑)添加日志将有助于调试和跟踪问题。
-
注入方式:
- 注入使用了
@Resource
和@Autowired
两种方式,这两者可以混合使用,但通常会使用一种保持一致性。在整个类中可以选择使用一种方式。
- 注入使用了
-
Magic Values:
- 在代码中有一些 “math” 和 “char” 的字符串,最好将其定义为常量,这样可以提高代码的可维护性。
-
异常处理:
- 在生成验证码的过程中,可能会抛出
IOException
异常,建议在方法签名上声明可能抛出的异常,并在发生异常时返回适当的错误信息。
- 在生成验证码的过程中,可能会抛出
-
代码逻辑:
- 在生成验证码时,通过
Base64.encode(os.toByteArray())
将验证码转换为 Base64 编码,这是合理的做法,但确保客户端能够正确地处理 Base64 图片。
- 在生成验证码时,通过
-
配置项:
captchaType
从配置文件中获取,确保这个配置项在配置文件中有明确的说明。
-
一致性:
- 在整个项目中保持一致性非常重要。确保在命名、风格和用法上保持一致,这有助于提高代码的可读性和维护性。
总体来说,这个控制器类看起来清晰且符合一般的最佳实践。确保在您的团队或项目中遵循一致的规范。
3. 修改意见
3.1 SysProfileController
- 异常处理:
- 缺少全局异常处理,可以考虑添加一个全局异常处理器,以便在发生异常时提供友好的错误信息。
3.2 BaseController
-
错误处理:
- 使用
AjaxResult
类返回错误消息。建议为常见的错误场景创建一个实用方法或全局异常处理程序,以保持一致的错误处理。
- 使用
-
注释:
- 考虑添加注释以解释复杂的部分或业务逻辑。
-
一致性:
- 确保在命名约定上保持一致。例如,
userName
和user.getNickName()
的使用可能要保持一致。
- 确保在命名约定上保持一致。例如,
-
异常处理:
- 对上传头像时的
IOException
进行了通用捕获。对于调试目的,记录异常详细信息可能会有帮助。
- 对上传头像时的
-
RESTful约定:
- 确保遵循RESTful约定。例如,如果适用,考虑使用
@PatchMapping
而不是@PutMapping
进行部分更新。
- 确保遵循RESTful约定。例如,如果适用,考虑使用
-
硬编码字符串:
- 谨慎使用硬编码的字符串,如"/system/user/profile/avatar"。最好将这些字符串定义为常量。
-
个人资料图像更新:
- 最好仅在数据库操作成功时更新缓存。目前,即使头像更新失败,缓存也会被更新。
3.3 CaptchaController
-
注释:
- 添加一些注释,特别是对于非常规或者复杂的逻辑,这将有助于其他开发者更容易理解你的代码。
-
日志记录:
- 在代码中缺少日志记录。在关键的地方(如异常捕获、业务逻辑)添加日志将有助于调试和跟踪问题。
-
注入方式:
- 注入使用了
@Resource
和@Autowired
两种方式,这两者可以混合使用,但通常会使用一种保持一致性。在整个类中可以选择使用一种方式。
- 注入使用了
-
异常处理:
- 在生成验证码的过程中,可能会抛出
IOException
异常,建议在方法签名上声明可能抛出的异常,并在发生异常时返回适当的错误信息。
- 在生成验证码的过程中,可能会抛出
-
代码逻辑:
- 在生成验证码时,通过
Base64.encode(os.toByteArray())
将验证码转换为 Base64 编码,这是合理的做法,但确保客户端能够正确地处理 Base64 图片。
- 在生成验证码时,通过
4. 总结
总体来说,代码质量良好,但建议根据上述测试点和修改意见进行相应的改进,以提高系统的稳定性、性能和安全性。