JP3-3-MyClub后台后端(二)

发布于:2025-08-02 ⋅ 阅读:(17) ⋅ 点赞:(0)

Java道经 - 项目 - MyClub - 后台后端(二)


传送门:JP3-1-MyClub项目简介
传送门:JP3-2-MyClub公共服务
传送门:JP3-3-MyClub后台后端(一)
传送门:JP3-3-MyClub后台后端(二)

S03. UMS用户模块

E01. 开发部门接口

心法:部门记录需要关联房间记录,所以需要事先对实体类进行改造。

改造如下:

package com.joezhou.entity;

/** @author 周航宇 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept implements Serializable {

	...
    
    /** 每条部门记录对应 1 条房间记录 */
    private Room room;
}

1. 基础增删改查

  1. 开发 DTO 实体类:

负责(添加)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */  
@Schema(description = "部门添加DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptInsertDTO implements Serializable {

    @Schema(description = "部门名称")
    @NotEmpty(message = "部门名称不能为空")
    @Pattern(regexp = MC.Regex.TITLE_RE, message = MC.Regex.TITLE_RE_MSG)
    private String title;

    @Schema(description = "部门描述")
    @Pattern(regexp = MC.Regex.INFO_RE, message = MC.Regex.INFO_RE_MSG)
    private String info;

    @Schema(description = "房间外键")
    @NotNull(message = "房间外键不能为空")
    private Long fkRoomId;
}

负责(修改)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@Schema(description = "部门修改DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptUpdateDTO implements Serializable {

    @Schema(description = "主键")
    @NotNull(message = "主键不能为空")
    private Long id;
    
    @Schema(description = "部门名称")
    @Pattern(regexp = MC.Regex.TITLE_RE, message = MC.Regex.TITLE_RE_MSG)
    private String title;
    
    @Schema(description = "部门描述")
    @Pattern(regexp = MC.Regex.INFO_RE, message = MC.Regex.INFO_RE_MSG)
    private String info;
    
    @Schema(description = "房间外键")
    private Long fkRoomId;
}

负责(分页)业务的实体类

package com.joezhou.dto;

/**  @author 周航宇 */
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Schema(description = "按条件分页搜索部门DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptPageDTO extends PageDTO {
    @Schema(description = "部门名称")
    private String title;
}

负责(全查)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptVO implements Serializable {
    /** 主键 */
    private Long id;
    /** 部门名称 */
    private String title;
}
  1. 开发数据层代码:
package com.joezhou.mapper;

/** @author 周航宇 */
@Repository
public interface DeptMapper {

    @Insert("""
            insert into ums_dept (title, info, fk_room_id, version, deleted, created, updated)
            values (#{title}, #{info}, #{fkRoomId}, #{version}, #{deleted}, #{created}, #{updated})
            """)
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(Dept dept);

    @Results(id = "deptResultMap", value = {
            @Result(property = "id", column = "id", id = true),
            @Result(property = "fkRoomId", column = "fk_room_id"),
            @Result(property = "room", column = "fk_room_id",
                    one = @One(select = "com.joezhou.mapper.RoomMapper.select")),
    })
    @Select("""
            select * from ums_dept t
            where t.id = #{param1} and t.deleted = 0
            """)
    Dept select(Long id);

	@ResultMap("deptResultMap")
    @Select("""
            <script>
            select * from ums_dept t
            <where>
				<if test='title != null'> title like concat('%', #{title}, '%') and </if>
                t.deleted = 0
            </where>
            </script>
            """)
    List<Dept> list(DeptPageDTO dto);

    @Update("""
            <script>
            update ums_dept
            <set>
            <if test='title != null'> title = #{title}, </if>
            <if test='info != null'> info = #{info}, </if>
            <if test='fkRoomId != null'> fk_room_id = #{fkRoomId}, </if>
            <if test='deleted != null'> deleted = #{deleted}, </if>
            <if test='created != null'> created = #{created}, </if>
            <if test='updated != null'> updated = #{updated}, </if>
            version = version + 1
            </set>
            where id = #{id} and deleted = 0 and version = #{version}
            </script>
            """)
    int update(Dept dept);

    @Update("""
            update ums_dept set deleted = 1, updated = current_timestamp
            where id = #{param1}
            """)
    int delete(Long id);

    @Update("""
            <script>
            update ums_dept set deleted = 1, updated = current_timestamp
            where id in
            <foreach collection='list' item='e' open='(' close=')' separator=','>
                ${e}
            </foreach>
            </script>
            """)
    int deleteBatch(List<Long> ids);
}
  1. 开发业务层代码:
package com.joezhou.service;

/** @author 周航宇 */
public interface DeptService {
    int insert(DeptInsertDTO dto);
    Dept select(Long id);
    List<DeptVO> list();
    PageInfo<Dept> page(DeptPageDTO dto);
    int update(DeptUpdateDTO dto);
    int delete(Long id);
    int deleteBatch(List<Long> ids);
}
package com.joezhou.service.impl;

/** @author 周航宇 */
@Service
@CacheConfig(cacheNames = "dept")
public class DeptServiceImpl implements DeptService {

    @Resource
    private DeptMapper deptMapper;

    @CacheEvict(allEntries = true)
    @Override
    public int insert(DeptInsertDTO dto) {
        String info = dto.getInfo();
        // 拷贝属性
        Dept dept = BeanUtil.copyProperties(dto, Dept.class);
        // 设置默认值
        dept.setInfo(StrUtil.isBlank(info) ? "暂无描述" : info);
        dept.setVersion(0L);
        dept.setDeleted(0);
        dept.setCreated(LocalDateTime.now());
        dept.setUpdated(LocalDateTime.now());
        // DB添加
        int result = deptMapper.insert(dept);
        if (result <= 0) {
            throw new ServerErrorException("DB添加失败");
        }
        return result;
    }

    @Cacheable(key = "#p0", condition = "#p0 != null", unless = "#result == null")
    @Override
    public Dept select(Long id) {
        Dept result = deptMapper.select(id);
        if (ObjectUtil.isNull(result)) {
            throw new ServerErrorException("记录不存在");
        }
        return result;
    }

    @Cacheable(key = "#root.methodName", unless = "#result == null")
    @Override
    public List<DeptVO> list() {
        return deptMapper.list(new DeptPageDTO())
                .stream()
                .map(dept -> BeanUtil.copyProperties(dept, DeptVO.class))
                .collect(Collectors.toList());
    }

    @Cacheable(key = "#root.methodName + ':' + #p0.toString()",
            condition = "#p0 != null",
            unless = "#result == null")
    @Override
    public PageInfo<Dept> page(DeptPageDTO dto) {
        PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
        return new PageInfo<>(deptMapper.list(dto));
    }

    @CacheEvict(allEntries = true)
    @Transactional
    @Retryable(retryFor = VersionException.class)
    @Override
    public int update(DeptUpdateDTO dto) {
        Dept dept = deptMapper.select(dto.getId());
        if (ObjectUtil.isNull(dept)) {
            throw new ServerErrorException("记录不存在");
        }
        BeanUtil.copyProperties(dto, dept);
        // 设置默认值
        dept.setUpdated(LocalDateTime.now());
        // DB修改
        int result = deptMapper.update(dept);
        if (result <= 0) {
            throw new VersionException("DB修改失败");
        }
        return result;
    }

    @CacheEvict(allEntries = true)
    @Override
    public int delete(Long id) {
        int result = deptMapper.delete(id);
        if (result <= 0) {
            throw new ServerErrorException("DB逻辑删除失败");
        }
        return result;
    }

    @CacheEvict(allEntries = true)
    @Override
    public int deleteBatch(List<Long> ids) {
        int result = deptMapper.deleteBatch(ids);
        if (result <= 0) {
            throw new ServerErrorException("DB逻辑批删失败");
        }
        return result;
    }
}
  1. 开发控制层代码:
package com.joezhou.controller;

/** @author 周航宇 */
@Tag(name = "部门模块")
@RestController
@RequestMapping("/api/v1/dept")
public class DeptController {

    @Resource
    private DeptService deptService;

    @Operation(summary = "新增 - 单条新增")
    @PostMapping("insert")
    public Result<Integer> insert(@RequestBody @Validated DeptInsertDTO dto) {
        return new Result<>(deptService.insert(dto));
    }

    @Operation(summary = "查询 - 单条查询")
    @GetMapping("select/{id}")
    public Result<Dept> select(@PathVariable("id") Long id) {
        return new Result<>(deptService.select(id));
    }

    @Operation(summary = "查询 - 全部记录")
    @GetMapping("list")
    public Result<List<DeptVO>> list() {
        return new Result<>(deptService.list());
    }

    @Operation(summary = "查询 - 分页查询")
    @GetMapping("page")
    public Result<PageInfo<Dept>> page(@Validated DeptPageDTO dto) {
        return new Result<>(deptService.page(dto));
    }

    @Operation(summary = "修改 - 单条修改")
    @PutMapping("update")
    public Result<Integer> update(@RequestBody @Validated DeptUpdateDTO dto) {
        return new Result<>(deptService.update(dto));
    }

    @Operation(summary = "删除 - 单条删除")
    @DeleteMapping("delete/{id}")
    public Result<Integer> delete(@PathVariable("id") Long id) {
        return new Result<>(deptService.delete(id));
    }

    @Operation(summary = "删除 - 批量删除")
    @DeleteMapping("deleteBatch")
    public Result<Integer> deleteBatch(@RequestParam("ids") List<Long> ids) {
        return new Result<>(deptService.deleteBatch(ids));
    }
}

2. 下载数据报表

  1. 开发 DTO 实体类:
package com.joezhou.excel;

/** @author 周航宇 */
@ColumnWidth(20)
@HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.LEFT, verticalAlignment = VerticalAlignmentEnum.CENTER)
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.LEFT, verticalAlignment = VerticalAlignmentEnum.CENTER)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DeptExcel implements Serializable {
    @ExcelProperty(value = {"部门数据统计表", "部门标题"})
    private String title;
    @ExcelProperty(value = {"部门数据统计表", "部门描述"})
    private String info;
    @ExcelProperty(value = {"部门数据统计表", "所在房间"})
    private String roomTitle;
    @ExcelProperty(value = {"部门数据统计表", "首次创建日期"})
    @DateTimeFormat("yyyy/MM/dd HH:mm:ss")
    private LocalDateTime created;
    @ExcelProperty(value = {"部门数据统计表", "最后创建日期"})
    @DateTimeFormat("yyyy/MM/dd HH:mm:ss")
    private LocalDateTime updated;
}
  1. 开发业务层代码:
package com.joezhou.service;

/**
 * 获取部门记录的Excel数据
 *
 * @return 部门记录的Excel数据列表
 */
List<DeptExcel> getExcelData();
package com.joezhou.service.impl;

@Override
public List<DeptExcel> getExcelData() {
	return deptMapper.list(new DeptPageDTO())
			.stream()
			.map(dept -> {
				DeptExcel deptExcel = BeanUtil.copyProperties(dept, DeptExcel.class);
				if (ObjectUtil.isNotNull(dept.getRoom())) {
					deptExcel.setRoomTitle(dept.getRoom().getTitle());
				}
				return deptExcel;
			})
			.collect(Collectors.toList());
}
  1. 开发控制层代码:
package com.joezhou.controller;

@Operation(summary = "查询 - 报表打印")
@SneakyThrows
@GetMapping("/excel")
public void excel(HttpServletResponse resp) {
	EasyExcelUtil.download(resp, "部门统计表", deptService.getExcelData());
}

E02. 开发员工接口

心法:员工记录需要关联部门记录,所以需要事先对实体类进行改造。

改造如下:

package com.joezhou.entity;

/** @author 周航宇 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp implements Serializable {

	...
    
    /** 每条员工记录对应 1 条部门记录 */
    private Dept dept;
}

1. 基础增删改查

  1. 开发 DTO 实体类:

负责(添加)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@Schema(description = "员工添加DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmpInsertDTO implements Serializable {

    @Schema(description = "登录账号")
    @NotEmpty(message = "登录账号不能为空")
    @Pattern(regexp = MC.Regex.USERNAME_RE, message = MC.Regex.USERNAME_RE_MSG)
    private String username;

    @Schema(description = "登录密码")
    @NotEmpty(message = "登录密码不能为空")
    @Pattern(regexp = MC.Regex.PASSWORD_RE, message = MC.Regex.PASSWORD_RE_MSG)
    private String password;

    @Schema(description = "手机号码")
    @NotEmpty(message = "手机号码不能为空")
    @Pattern(regexp = MC.Regex.PHONE_RE, message = MC.Regex.PHONE_RE_MSG)
    private String phone;

    @Schema(description = "微信号码")
    @NotEmpty(message = "微信号码不能为空")
    private String wechat;

    @Schema(description = "电子邮箱")
    @NotEmpty(message = "电子邮箱不能为空")
    @Pattern(regexp = MC.Regex.EMAIL_RE, message = MC.Regex.EMAIL_RE_MSG)
    private String email;

    @Schema(description = "真实姓名")
    @NotEmpty(message = "真实姓名不能为空")
    @Pattern(regexp = MC.Regex.REALNAME_RE, message = MC.Regex.REALNAME_RE_MSG)
    private String realname;

    @Schema(description = "身份证号")
    @Pattern(regexp = MC.Regex.ID_CARD_RE, message = MC.Regex.ID_CARD_RE_MSG)
    @NotEmpty(message = "身份证号不能为空")
    private String idcard;

    @Schema(description = "部门外键")
    @NotNull(message = "部门外键不能为空")
    private Long fkDeptId;

    @Schema(description = "员工描述")
    @Pattern(regexp = MC.Regex.INFO_RE, message = MC.Regex.INFO_RE_MSG)
    private String info;

    @Schema(description = "入职日期", example = "2023-10-05T12:12:12Z")
    @NotNull(message = "入职日期不能为空")
    private LocalDateTime hiredate;
}

负责(修改)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@Schema(description = "员工修改DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmpUpdateDTO implements Serializable {

    @Schema(description = "主键")
    @NotNull(message = "主键不能为空")
    private Long id;

    @Schema(description = "手机号码")
    @Pattern(regexp = MC.Regex.PHONE_RE, message = MC.Regex.PHONE_RE_MSG)
    private String phone;

    @Schema(description = "微信号码")
    private String wechat;

    @Schema(description = "电子邮件")
    @Pattern(regexp = MC.Regex.EMAIL_RE, message = MC.Regex.EMAIL_RE_MSG)
    private String email;

    @Schema(description = "员工性别")
    @Min(value = 0, message = "性别必须为0、1或2")
    @Max(value = 2, message = "性别必须为0、1或2")
    private Integer gender;

    @Schema(description = "员工年龄")
    @Min(value = 18, message = "年龄不能小于18岁")
    @Max(value = 60, message = "年龄不能大于60岁")
    private Integer age;

    @Schema(description = "所在省份")
    @Pattern(regexp = MC.Regex.PROVINCE_RE, message = MC.Regex.PROVINCE_RE_MSG)
    private String province;

    @Schema(description = "详细住址")
    @Pattern(regexp = MC.Regex.ADDRESS_RE, message = MC.Regex.ADDRESS_RE_MSG)
    private String address;

    @Schema(description = "真实姓名")
    @Pattern(regexp = MC.Regex.REALNAME_RE, message = MC.Regex.REALNAME_RE_MSG)
    private String realname;

    @Schema(description = "身份证号")
    @Pattern(regexp = MC.Regex.ID_CARD_RE, message = MC.Regex.ID_CARD_RE_MSG)
    private String idcard;

    @Schema(description = "部门外键")
    private Long fkDeptId;

    @Schema(description = "描述信息")
    @Pattern(regexp = MC.Regex.INFO_RE, message = MC.Regex.INFO_RE_MSG)
    private String info;

    @Schema(description = "入职日期")
    private LocalDateTime hiredate;
}

负责(分页)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Schema(description = "按条件分页搜索员工DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmpPageDTO extends PageDTO {
    @Schema(description = "登录账号")
    private String username;
    @Schema(description = "手机号码")
    private String phone;
    @Schema(description = "身份证号")
    private String idcard;
    @Schema(description = "真实姓名")
    private String realname;
    @Schema(description = "部门外键")
    private Long fkDeptId;
}

负责(全查)业务的实体类

package com.joezhou.vo;  
  
/** @author 周航宇 */  
@Data  
@NoArgsConstructor  
@AllArgsConstructor  
public class EmpVO implements Serializable {  
    /** 主键 */  
    private Long id;  
    /** 真实姓名 */  
    private String realname;  
}
  1. 开发数据层代码:
package com.joezhou.mapper;

/** @author 周航宇 */
@Repository
public interface EmpMapper {

    @Insert("""
            insert into ums_emp (username, password, avatar, phone, wechat, email, gender, age, province, address, realname, idcard, info, fk_dept_id, hiredate, version, deleted, created, updated)
            values (#{username}, #{password}, #{avatar}, #{phone}, #{wechat}, #{email}, #{gender}, #{age}, #{province}, #{address}, #{realname}, #{idcard}, #{info}, #{fkDeptId}, #{hiredate}, #{version}, #{deleted}, #{created}, #{updated})
            """)
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(Emp emp);

    @Results(id = "empResultMap", value = {
            @Result(property = "id", column = "id", id = true),
            @Result(property = "fkDeptId", column = "fk_dept_id"),
            @Result(property = "dept", column = "fk_dept_id",
                    one = @One(select = "com.joezhou.mapper.DeptMapper.select")),
    })
    @Select("""
            select * from ums_emp t
            where t.id = #{param1} and t.deleted = 0
            """)
    Emp select(Long id);

    @ResultMap("empResultMap")
    @Select("""
            <script>
            select * from ums_emp t
            <where>
                <if test='username != null'> username like concat('%', #{username}, '%') and </if>
                <if test='realname != null'> realname like concat('%', #{realname}, '%') and </if>
                <if test='phone != null'> phone = #{phone} and </if>
                <if test='idcard != null'> idcard = #{idcard} and </if>
                <if test='fkDeptId != null'> fk_dept_id = #{fkDeptId} and </if>
                t.deleted = 0
            </where>
            </script>
            """)
    List<Emp> list(EmpPageDTO dto);

    @Update("""
            <script>
            update ums_emp
            <set>
            <if test='username != null'> username = #{username}, </if>
            <if test='password != null'> password = #{password}, </if>
            <if test='avatar != null'> avatar = #{avatar}, </if>
            <if test='phone != null'> phone = #{phone}, </if>
            <if test='wechat != null'> wechat = #{wechat}, </if>
            <if test='email != null'> email = #{email}, </if>
            <if test='gender != null'> gender = #{gender}, </if>
            <if test='age != null'> age = #{age}, </if>
            <if test='province != null'> province = #{province}, </if>
            <if test='address != null'> address = #{address}, </if>
            <if test='realname != null'> realname = #{realname}, </if>
            <if test='idcard != null'> idcard = #{idcard}, </if>
            <if test='fkDeptId != null'> fk_dept_id = #{fkDeptId}, </if>
            <if test='info != null'> info = #{info}, </if>
            <if test='hiredate != null'> hiredate = #{hiredate}, </if>
            <if test='deleted != null'> deleted = #{deleted}, </if>
            <if test='created != null'> created = #{created}, </if>
            <if test='updated != null'> updated = #{updated}, </if>
            version = version + 1
            </set>
            where id = #{id} and deleted = 0 and version = #{version}
            </script>
            """)
    int update(Emp emp);

    @Update("""
            update ums_emp set deleted = 1, updated = current_timestamp
            where id = #{param1}
            """)
    int delete(Long id);

    @Update("""
            <script>
            update ums_emp set deleted = 1, updated = current_timestamp
            where id in
            <foreach collection='list' item='e' open='(' close=')' separator=','>
                ${e}
            </foreach>
            </script>
            """)
    int deleteBatch(List<Long> ids);
}
  1. 开发业务层代码:
package com.joezhou.service;

/** @author 周航宇 */
public interface EmpService {
    int insert(EmpInsertDTO dto);
    Emp select(Long id);
    List<EmpVO> list();
    PageInfo<Emp> page(EmpPageDTO dto);
    int update(EmpUpdateDTO dto);
    int delete(Long id);
    int deleteBatch(List<Long> ids);
}
package com.joezhou.service.impl;

/** @author 周航宇 */
@Service
@CacheConfig(cacheNames = "emp")
public class EmpServiceImpl implements EmpService {

    @Resource
    private EmpMapper empMapper;

    @CacheEvict(allEntries = true)
    @Override
    public int insert(EmpInsertDTO dto) {
        String info = dto.getInfo();
        String idcard = dto.getIdcard();
        // 拷贝属性
        Emp emp = BeanUtil.copyProperties(dto, Emp.class);
        // 密码加密
        emp.setPassword(SecureUtil.md5(emp.getPassword()));
        // 设置默认值
        emp.setInfo(StrUtil.isBlank(info) ? "暂无描述" : info);
        emp.setAvatar(MC.Emp.DEFAULT_AVATAR);
        emp.setGender(IdcardUtil.getGenderByIdCard(idcard));
        emp.setAge(IdcardUtil.getAgeByIdCard(idcard));
        emp.setProvince(IdcardUtil.getProvinceByIdCard(idcard));
        emp.setAddress("暂未添加详细住址");
        emp.setVersion(0L);
        emp.setDeleted(0);
        emp.setCreated(LocalDateTime.now());
        emp.setUpdated(LocalDateTime.now());
        // DB添加
        int result = empMapper.insert(emp);
        if (result <= 0) {
            throw new ServerErrorException("DB添加失败");
        }
        return result;
    }

    @Cacheable(key = "#p0", condition = "#p0 != null", unless = "#result == null")
    @Override
    public Emp select(Long id) {
        Emp result = empMapper.select(id);
        if (ObjectUtil.isNull(result)) {
            throw new ServerErrorException("记录不存在");
        }
        return result;
    }

    @Cacheable(key = "#root.methodName", unless = "#result == null")
    @Override
    public List<EmpVO> list() {
        return empMapper.list(new EmpPageDTO())
                .stream()
                .map(emp -> BeanUtil.copyProperties(emp, EmpVO.class))
                .collect(Collectors.toList());
    }

    @Cacheable(key = "#root.methodName + ':' + #p0.toString()",
            condition = "#p0 != null",
            unless = "#result == null")
    @Override
    public PageInfo<Emp> page(EmpPageDTO dto) {
        PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
        return new PageInfo<>(empMapper.list(dto));
    }

    @CacheEvict(allEntries = true)
    @Transactional
    @Retryable(retryFor = VersionException.class)
    @Override
    public int update(EmpUpdateDTO dto) {
        Emp emp = empMapper.select(dto.getId());
        if (ObjectUtil.isNull(emp)) {
            throw new ServerErrorException("记录不存在");
        }
        BeanUtil.copyProperties(dto, emp);
        // 设置默认值
        emp.setUpdated(LocalDateTime.now());
        // DB修改
        int result = empMapper.update(emp);
        if (result <= 0) {
            throw new VersionException("DB修改失败");
        }
        return result;
    }

    @CacheEvict(allEntries = true)
    @Override
    public int delete(Long id) {
        int result = empMapper.delete(id);
        if (result <= 0) {
            throw new ServerErrorException("DB逻辑删除失败");
        }
        return result;
    }

    @CacheEvict(allEntries = true)
    @Override
    public int deleteBatch(List<Long> ids) {
        int result = empMapper.deleteBatch(ids);
        if (result <= 0) {
            throw new ServerErrorException("DB逻辑批删失败");
        }
        return result;
    }
}
  1. 开发控制层代码:
package com.joezhou.controller;

/** @author 周航宇 */
@Tag(name = "员工模块")
@RestController
@RequestMapping("/api/v1/emp")
public class EmpController {

    @Resource
    private EmpService empService;

    @Operation(summary = "新增 - 单条新增")
    @PostMapping("insert")
    public Result<Integer> insert(@RequestBody @Validated EmpInsertDTO dto) {
        return new Result<>(empService.insert(dto));
    }

    @Operation(summary = "查询 - 单条查询")
    @GetMapping("select/{id}")
    public Result<Emp> select(@PathVariable("id") Long id) {
        return new Result<>(empService.select(id));
    }

    @Operation(summary = "查询 - 全部记录")
    @GetMapping("list")
    public Result<List<EmpVO>> list() {
        return new Result<>(empService.list());
    }

    @Operation(summary = "查询 - 分页查询")
    @GetMapping("page")
    public Result<PageInfo<Emp>> page(@Validated EmpPageDTO dto) {
        return new Result<>(empService.page(dto));
    }

    @Operation(summary = "修改 - 单条修改")
    @PutMapping("update")
    public Result<Integer> update(@RequestBody @Validated EmpUpdateDTO dto) {
        return new Result<>(empService.update(dto));
    }

    @Operation(summary = "删除 - 单条删除")
    @DeleteMapping("delete/{id}")
    public Result<Integer> delete(@PathVariable("id") Long id) {
        return new Result<>(empService.delete(id));
    }

    @Operation(summary = "删除 - 批量删除")
    @DeleteMapping("deleteBatch")
    public Result<Integer> deleteBatch(@RequestParam("ids") List<Long> ids) {
        return new Result<>(empService.deleteBatch(ids));
    }
}

2. 下载数据报表

  1. 开发 DTO 实体类:
package com.joezhou.excel;

/** @author 周航宇 */
@ColumnWidth(20)
@HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.LEFT, verticalAlignment = VerticalAlignmentEnum.CENTER)  
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.LEFT, verticalAlignment = VerticalAlignmentEnum.CENTER)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmpExcel implements Serializable {
    @ExcelProperty(value = {"员工数据统计表", "工作信息", "真实姓名"})
    private String realname;
	@ExcelProperty(value = {"员工数据统计表", "工作信息", "所在部门"})
    private String deptName;
    @ExcelProperty(value = {"员工数据统计表", "工作信息", "入职时间"})
    @DateTimeFormat("yyyy/MM/dd HH:mm:ss")
    private LocalDateTime hiredate;
    @ExcelProperty(value = {"员工数据统计表", "个人信息", "手机号码"})
    private String phone;
    @ExcelProperty(value = {"员工数据统计表", "个人信息", "微信号码"})
    private String wechat;
    @ExcelProperty(value = {"员工数据统计表", "个人信息", "邮箱地址"})
    private String email;
    @ExcelProperty(value = {"员工数据统计表", "个人信息", "用户性别"})
    private String gender;
    @ExcelProperty(value = {"员工数据统计表", "个人信息", "用户年龄"})
    private Integer age;
    @ExcelProperty(value = {"员工数据统计表", "个人信息", "籍贯省份"})
    private String province;
    @ExcelProperty(value = {"员工数据统计表", "个人信息", "现居住地"})
    private String address;
    @ExcelProperty(value = {"员工数据统计表", "个人信息", "身份证号"})
    private String idcard;
    @ExcelProperty(value = {"员工数据统计表", "个人信息", "描述信息"})
    private String info;
    @ExcelProperty(value = {"员工数据统计表", "账号信息", "员工账号"})
    private String username;
    @ExcelProperty(value = {"员工数据统计表", "账号信息", "创建时间"})
    @DateTimeFormat("yyyy/MM/dd HH:mm:ss")
    private LocalDateTime created;
    @ExcelProperty(value = {"员工数据统计表", "账号信息", "修改时间"})
    @DateTimeFormat("yyyy/MM/dd HH:mm:ss")
    private LocalDateTime updated;
}
  1. 开发业务层代码:
package com.joezhou.service;

/**
 * 获取员工记录的Excel数据
 *
 * @return 员工记录的Excel数据列表
 */
List<EmpExcel> getExcelData();
package com.joezhou.service.impl;

@Override
public List<EmpExcel> getExcelData() {
	return empMapper.list(new EmpPageDTO())
			.stream()
			.map(emp -> {
				EmpExcel empExcel = BeanUtil.copyProperties(emp, EmpExcel.class);
				if (ObjectUtil.isNotNull(emp.getDept())) {
					empExcel.setDeptName(emp.getDept().getTitle());
				}
				empExcel.setGender(MC.Emp.genderFormat(emp.getGender()));
				return empExcel;
			})
			.collect(Collectors.toList());
}
  1. 开发控制层代码:
package com.joezhou.controller;

@Operation(summary = "查询 - 报表打印")
@SneakyThrows
@GetMapping("/excel")
public void excel(HttpServletResponse resp) {
	EasyExcelUtil.download(resp, "员工统计表", empService.getExcelData());
}

3. 上传员工头像

  1. 开发业务层代码:
package com.joezhou.service;

/**
 * 上传员工头像
 *
 * @param newFile 上传员工头像DTO
 * @param id      员工主键
 * @return 文件名
 */
String uploadAvatar(MultipartFile newFile, Long id);
package com.joezhou.service.impl;

@Transactional(rollbackFor = RuntimeException.class)
@CacheEvict(allEntries = true)
@Override
public String uploadAvatar(MultipartFile newFile, Long id) {
	// 按主键查询记录
	Emp emp = empMapper.select(id);
	if (ObjectUtil.isNull(emp)) {
		throw new ServerErrorException("记录不存在");
	}
	// 备份旧文件
	String oldFile = emp.getAvatar();
	// 生成新文件名
	String newFileName = MinioUtil.randomFilename(newFile);
	// DB更新文件名
	emp.setAvatar(newFileName);
	if (empMapper.update(emp) <= 0) {
		throw new ServerErrorException("DB更新失败");
	}

	try {
		// MinIO删除旧文件(默认文件不删除)
		if (!MC.Emp.DEFAULT_AVATAR.equals(oldFile)) {
			MinioUtil.delete(oldFile, MC.MinIO.AVATAR_DIR, MC.MinIO.BUCKET_NAME);
		}
		// MinIO上传新文件
		MinioUtil.upload(newFile, newFileName, MC.MinIO.AVATAR_DIR, MC.MinIO.BUCKET_NAME);
	} catch (Exception e) {
		throw new ServerErrorException("MinIO操作失败:" + e.getMessage());
	}
	// 返回新文件名
	return newFileName;
}
  1. 开发控制层代码:注意上传文件不是 JSON 参数,而是二进制参数,不能使用 @RequestBody 注解:
package com.joezhou.controller;

@Operation(summary = "上传 - 员工头像")
@PostMapping("/uploadAvatar/{id}")
public Result<String> uploadAvatar(@RequestParam("avatarFile") MultipartFile avatarFile,
									@PathVariable("id") Long id) {
	return new Result<>(empService.uploadAvatar(avatarFile, id));
}

4. 修改登录密码

  1. 开发 DTO 实体类:
package com.joezhou.dto;

/** @author 周航宇 */
@Schema(description = "修改登录密码DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmpUpdatePasswordDTO implements Serializable {

    @NotNull(message = "员工主键必须不为空")
    @Schema(description = "员工主键")
    private Long id;

    @NotEmpty(message = "旧密码不能为空")
    @Schema(description = "旧密码")
    private String oldPassword;

    @NotEmpty(message = "新密码不能为空")
    @Schema(description = "新密码")
    private String newPassword;
}
  1. 开发数据层代码:
package com.joezhou.mapper;

@Update("""
		update ums_emp set password = #{param1}, updated = now()
		where password = #{param2} and id = #{param3} and deleted = 0
		""")
int updatePassword(String newPassword, String oldPassword, Long id);
  1. 开发业务层代码:
package com.joezhou.service;

/**
 * 修改登录密码
 *
 * @param dto 登录密码修改DTO
 * @return 影响条目数
 */
int updatePassword(EmpUpdatePasswordDTO dto);
package com.joezhou.service.impl;

@CacheEvict(allEntries = true)
@Override
public int updatePassword(EmpUpdatePasswordDTO dto) {
    String newPassword = SecureUtil.md5(dto.getNewPassword());
    String oldPassword = SecureUtil.md5(dto.getOldPassword());
    Long id = dto.getId();
    int result = empMapper.updatePassword(newPassword, oldPassword, id);
    if(result <= 0){
        throw new ServiceException("修改密码失败");
    }
    return result;
}
  1. 开发控制层代码:
package com.joezhou.controller;

@Operation(summary = "修改 - 员工密码")
@PutMapping("updatePassword")
public Result<Integer> updatePassword(@RequestBody EmpUpdatePasswordDTO dto) {
	return new Result<>(empService.updatePassword(dto));
}

5. 账号密码登录

  1. 开发 DTO 实体类:
package com.joezhou.dto;

/** @author 周航宇 */
@Schema(description = "员工登录DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginDTO implements Serializable {

    @Schema(description = "登录账号")
    @NotEmpty(message = "登录账号不能为空")
    @Pattern(regexp = MC.Regex.USERNAME_RE, message = MC.Regex.USERNAME_RE_MSG)
    private String username;

    @Schema(description = "登录密码")
    @NotEmpty(message = "登录密码不能为空")
    @Pattern(regexp = MC.Regex.PASSWORD_RE, message = MC.Regex.PASSWORD_RE_MSG)
    private String password;
}
package com.joezhou.vo;

/** @author 周航宇 */
@Schema(description = "员工登录VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginVO implements Serializable {
    /** Token令牌 */
    private String token;
    /** 该员工信息 */
    private Emp emp;
}
  1. 开发数据层代码:
package com.joezhou.mapper;

@ResultMap("empResultMap")
@Select("""
		select * from ums_emp t
		where t.username = #{param1} and t.password = #{param2} and t.deleted = 0
		""")
Emp selectByAccount(String username, String password);
  1. 开发业务层代码:
package com.joezhou.service;

/**
 * 按账号密码登录
 *
 * @param dto 登录DTO
 * @return 登录VO,包含Token和员工菜单列表
 */
LoginVO loginByAccount(LoginDTO dto);
package com.joezhou.service.impl;

@Override
public LoginVO loginByAccount(LoginDTO dto) {
	String username = dto.getUsername();
	// 密码加密
	String password = SecureUtil.md5(dto.getPassword());
	// DB查询
	Emp emp = empMapper.selectByAccount(username, password);
	if (ObjectUtil.isNull(emp)) {
		throw new ServerErrorException("账号或密码有误");
	}
	// 生成Token令牌
	String token = JwtUtil.build(emp.getId(), emp.getRealname(), emp.getAvatar());
	// 组装VO实体类
	LoginVO loginVO = new LoginVO();
	loginVO.setToken(token);
	loginVO.setEmp(emp);
	return loginVO;
}
  1. 开发控制层代码:
package com.joezhou.controller;

@Operation(summary = "登录 - 账号密码")
@PostMapping("loginByAccount")
public Result<LoginVO> loginByAccount(@RequestBody @Validated LoginDTO dto) {
	return new Result<>(empService.loginByAccount(dto));
}
  1. 开发 Token 拦截器:
package com.joezhou.component;

/** @author 周航宇 */
@Component
public class TokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(@NotNull HttpServletRequest request,
                             @NotNull HttpServletResponse response,
                             @NotNull Object handler) {
        // 若目标方法不是Controller方法,则直接放行
        if (!(handler instanceof HandlerMethod)) return true;
        // 若请求头中不存在Token令牌,则返回失效提示
        String token = request.getHeader("token");
        if (StrUtil.isEmpty(token)) {
            throw new TokenExpiredException("Token令牌不存在");
        }
        // 解析Token令牌
        Map<String, Object> verifyResult = JwtUtil.parse(token);
        // 若Token令牌即将过期,则返回过期提示以及一个新的Token令牌
        if ((boolean) verifyResult.get("expiringSoon")) {
            throw new TokenExpiredSoonException((String) verifyResult.get("newToken"));
        }
        return true;
    }
}
  1. 配置 Token 拦截器:
package com.joezhou.config;

/** @author 周航宇 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Resource
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/api/v1/**")
                .excludePathPatterns("/api/v1/emp/loginByAccount");
    }
}

E03. 开发角色接口

1. 基础增删改查

  1. 开发 DTO 实体类:

负责(添加)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@Schema(description = "角色添加DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RoleInsertDTO implements Serializable {

    @Schema(description = "角色名称")
    @NotEmpty(message = "角色名称不能为空")
    @Pattern(regexp = MC.Regex.TITLE_RE, message = MC.Regex.TITLE_RE_MSG)
    private String title;
    
    @Schema(description = "角色描述")
    @Pattern(regexp = MC.Regex.INFO_RE, message = MC.Regex.INFO_RE_MSG)
    private String info;
}

负责(修改)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@Schema(description = "角色修改DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RoleUpdateDTO implements Serializable {
    
    @NotNull(message = "主键不能为空")
    @Schema(description = "主键")
    private Long id;
    
    @Schema(description = "角色名称")
    @Pattern(regexp = MC.Regex.TITLE_RE, message = MC.Regex.TITLE_RE_MSG)
    private String title;
    
    @Schema(description = "角色描述")
    @Pattern(regexp = MC.Regex.INFO_RE, message = MC.Regex.INFO_RE_MSG)
    private String info;
}

负责(分页)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Schema(description = "按条件分页搜索角色DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RolePageDTO extends PageDTO {
    @Schema(description = "角色名称")
    @Pattern(regexp = MC.Regex.TITLE_RE, message = MC.Regex.TITLE_RE_MSG)
    private String title;
}

负责(全查)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RoleVO implements Serializable {
    /** 主键 */
    private Long id;
    /** 角色名称 */
    private String title;
}
  1. 开发数据层代码:
package com.joezhou.mapper;

/** @author 周航宇 */
@Repository
public interface RoleMapper {

    @Insert("""
            insert into num_role (title, info, version, deleted, created, updated)
            values (#{title}, #{info}, #{version}, #{deleted}, #{created}, #{updated})
            """)
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(Role role);

    @Select("""
            select * from ums_role t
            where t.id = #{param1} and t.deleted = 0
            """)
    Role select(Long id);

    @Select("""
            <script>
            select * from ums_role t
            <where>
                <if test='title != null'> title like concat('%', #{title}, '%') and </if>
                t.deleted = 0
            </where>
            </script>
            """)
    List<Role> list(RolePageDTO dto);

    @Update("""
            <script>
            update ums_role
            <set>
            <if test='title != null'> title = #{title}, </if>
            <if test='info != null'> info = #{info}, </if>
            <if test='deleted != null'> deleted = #{deleted}, </if>
            <if test='created != null'> created = #{created}, </if>
            <if test='updated != null'> updated = #{updated}, </if>
            version = version + 1
            </set>
            where id = #{id} and deleted = 0 and version = #{version}
            </script>
            """)
    int update(Role role);

    @Update("""
            update ums_role set deleted = 1, updated = current_timestamp
            where id = #{param1}
            """)
    int delete(Long id);

    @Update("""
            <script>
            update ums_role set deleted = 1, updated = current_timestamp
            where id in
            <foreach collection='list' item='e' open='(' close=')' separator=','>
                ${e}
            </foreach>
            </script>
            """)
    int deleteBatch(List<Long> ids);
}
  1. 开发业务层代码:
package com.joezhou.service;

/** @author 周航宇 */
public interface RoleService {
    int insert(RoleInsertDTO dto);
    Role select(Long id);
    List<RoleVO> list();
    PageInfo<Role> page(RolePageDTO dto);
    int update(RoleUpdateDTO dto);
    int delete(Long id);
    int deleteBatch(List<Long> ids);
}
package com.joezhou.service.impl;

/** @author 周航宇 */
@Service
@CacheConfig(cacheNames = "role")
public class RoleServiceImpl implements RoleService {

    @Resource
    private RoleMapper roleMapper;

    @CacheEvict(allEntries = true)
    @Override
    public int insert(RoleInsertDTO dto) {
        String info = dto.getInfo();
        // 拷贝属性
        Role role = BeanUtil.copyProperties(dto, Role.class);
        // 设置默认值
        role.setInfo(StrUtil.isBlank(info) ? "暂无描述" : info);
        role.setVersion(0L);
        role.setDeleted(0);
        role.setCreated(LocalDateTime.now());
        role.setUpdated(LocalDateTime.now());
        // DB添加
        int result = roleMapper.insert(role);
        if (result <= 0) {
            throw new ServerErrorException("DB添加失败");
        }
        return result;
    }

    @Cacheable(key = "#p0", condition = "#p0 != null", unless = "#result == null")
    @Override
    public Role select(Long id) {
        Role result = roleMapper.select(id);
        if (ObjectUtil.isNull(result)) {
            throw new ServerErrorException("记录不存在");
        }
        return result;
    }

    @Cacheable(key = "#root.methodName", unless = "#result == null")
    @Override
    public List<RoleVO> list() {
        return roleMapper.list(new RolePageDTO())
                .stream()
                .map(role -> BeanUtil.copyProperties(role, RoleVO.class))
                .collect(Collectors.toList());
    }

    @Cacheable(key = "#root.methodName + ':' + #p0.toString()",
            condition = "#p0 != null",
            unless = "#result == null")
    @Override
    public PageInfo<Role> page(RolePageDTO dto) {
        PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
        return new PageInfo<>(roleMapper.list(dto));
    }

    @CacheEvict(allEntries = true)
    @Transactional
    @Retryable(retryFor = VersionException.class)
    @Override
    public int update(RoleUpdateDTO dto) {
        Role role = roleMapper.select(dto.getId());
        if (ObjectUtil.isNull(role)) {
            throw new ServerErrorException("记录不存在");
        }
        BeanUtil.copyProperties(dto, role);
        // 设置默认值
        role.setUpdated(LocalDateTime.now());
        // DB修改
        int result = roleMapper.update(role);
        if (result <= 0) {
            throw new VersionException("DB修改失败");
        }
        return result;
    }

    @CacheEvict(allEntries = true)
    @Override
    public int delete(Long id) {
        int result = roleMapper.delete(id);
        if (result <= 0) {
            throw new ServerErrorException("DB逻辑删除失败");
        }
        return result;
    }

    @CacheEvict(allEntries = true)
    @Override
    public int deleteBatch(List<Long> ids) {
        int result = roleMapper.deleteBatch(ids);
        if (result <= 0) {
            throw new ServerErrorException("DB逻辑批删失败");
        }
        return result;
    }
}
  1. 开发控制层代码:
package com.joezhou.controller;

/** @author 周航宇 */
@Tag(name = "角色模块")
@RestController
@RequestMapping("/api/v1/role")
public class RoleController {

    @Resource
    private RoleService roleService;

    @Operation(summary = "新增 - 单条新增")
    @PostMapping("insert")
    public Result<Integer> insert(@RequestBody @Validated RoleInsertDTO dto) {
        return new Result<>(roleService.insert(dto));
    }

    @Operation(summary = "查询 - 单条查询")
    @GetMapping("select/{id}")
    public Result<Role> select(@PathVariable("id") Long id) {
        return new Result<>(roleService.select(id));
    }

    @Operation(summary = "查询 - 全部记录")
    @GetMapping("list")
    public Result<List<RoleVO>> list() {
        return new Result<>(roleService.list());
    }

    @Operation(summary = "查询 - 分页查询")
    @GetMapping("page")
    public Result<PageInfo<Role>> page(@Validated RolePageDTO dto) {
        return new Result<>(roleService.page(dto));
    }

    @Operation(summary = "修改 - 单条修改")
    @PutMapping("update")
    public Result<Integer> update(@RequestBody @Validated RoleUpdateDTO dto) {
        return new Result<>(roleService.update(dto));
    }

    @Operation(summary = "删除 - 单条删除")
    @DeleteMapping("delete/{id}")
    public Result<Integer> delete(@PathVariable("id") Long id) {
        return new Result<>(roleService.delete(id));
    }

    @Operation(summary = "删除 - 批量删除")
    @DeleteMapping("deleteBatch")
    public Result<Integer> deleteBatch(@RequestParam("ids") List<Long> ids) {
        return new Result<>(roleService.deleteBatch(ids));
    }
}

2. 下载数据报表

  1. 开发 DTO 实体类:
package com.joezhou.excel;

/** @author 周航宇 */
@HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.LEFT, verticalAlignment = VerticalAlignmentEnum.CENTER)
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.LEFT, verticalAlignment = VerticalAlignmentEnum.CENTER)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoleExcel implements Serializable {
    @ExcelProperty(value = {"角色数据统计表", "角色标题"})
    private String title;
    @ExcelProperty(value = {"角色数据统计表", "角色描述"})
    private String info;
    @ExcelProperty(value = {"角色数据统计表", "首次创建日期"})
    @DateTimeFormat("yyyy/MM/dd HH:mm:ss")
    private LocalDateTime created;
    @ExcelProperty(value = {"角色数据统计表", "最后创建日期"})
    @DateTimeFormat("yyyy/MM/dd HH:mm:ss")
    private LocalDateTime updated;
}
  1. 开发业务层代码:
package com.joezhou.service;

/**
 * 导出角色记录的Excel数据
 *
 * @return 角色记录的Excel数据列表
 */
List<RoleExcel> getExcelData();
package com.joezhou.service.impl;

@Override
public List<RoleExcel> getExcelData() {
	return roleMapper.list(new RolePageDTO())
			.stream()
			.map(role -> BeanUtil.copyProperties(role, RoleExcel.class))
			.collect(Collectors.toList());
}
  1. 开发控制层代码:
package com.joezhou.controller;

@Operation(summary = "查询 - 报表打印")
@SneakyThrows
@GetMapping("/excel")
public void excel(HttpServletResponse resp) {
	EasyExcelUtil.download(resp, "角色统计表", roleService.getExcelData());
}

3. 查询员工角色

武技:根据员工的 ID 查询该员工的全部角色列表。

  1. 开发数据层代码:
package com.joezhou.mapper;

@Select("""
		select * from ums_role t2 where id in (
			select fk_role_id from ums_emp_role t1
			where t1.fk_emp_id = #{param1} and t1.deleted = 0
		) and t2.deleted = 0
		""")
List<Role> listByEmpId(Long empId);
  1. 开发业务层代码:
package com.joezhou.service;

/**
 * 根据员工主键查询该员工的全部角色列表
 *
 * @param empId 员工主键
 * @return 该员工的全部角色列表
 */
List<Role> listByEmpId(Long empId);
package com.joezhou.service.impl;

@Cacheable(key = "#root.methodName + ':' + #p0", 
		condition = "#p0 != null",
		unless = "#result == null")
@Override
public List<Role> listByEmpId(Long empId) {
	return roleMapper.listByEmpId(empId);
}
  1. 开发控制层代码:
package com.joezhou.controller;

@Operation(summary = "查询 - 员工角色")
@GetMapping("listByEmpId/{empId}")
public Result<List<Role>> listByEmpId(@PathVariable("empId") Long empId) {
	return new Result<>(roleService.listByEmpId(empId));
}

4. 修改员工角色

  1. 开发数据方法:
package com.joezhou.mapper;

@Update("""
		update ums_emp_role set deleted = 1, updated = now() where fk_emp_id = #{param1}
		""")
int deleteEmpRoleByEmpId(Long empId);

@Insert("""
		<script>
		insert into ums_emp_role (fk_emp_id, fk_role_id, version, deleted, created, updated)
		values
		<foreach collection='list' item='e' separator=','>
			(#{e.fkEmpId}, #{e.fkRoleId}, #{e.version}, #{e.deleted}, #{e.created}, #{e.updated})
		</foreach>
		</script>
		""")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertEmpRoleBatch(List<EmpRole> empRoles);
  1. 开发业务层代码:
package com.joezhou.serivce;

/**
 * 根据员工主键修改该员工的角色列表
 *
 * @param empId   员工主键
 * @param roleIds 角色主键列表
 * @return 影响条目数
 */
int updateByEmpId(Long empId, List<Long> roleIds);
package com.joezhou.serivce.impl;

@CacheEvict(allEntries = true)
@Transactional
@Retryable(retryFor = VersionException.class)
@Override
public int updateByEmpId(Long empId, List<Long> roleIds) {
    // 删除该员工的全部中间表记录
    int deleteResult = roleMapper.deleteEmpRoleByEmpId(empId);
    // 创建该员工的新角色列表
    List<EmpRole> empRoles = new ArrayList<>();
    for (Long roleId : roleIds) {
        EmpRole empRole = new EmpRole();
        empRole.setFkRoleId(roleId);
        empRole.setFkEmpId(empId);
        empRole.setVersion(0L);
        empRole.setDeleted(0);
        empRole.setCreated(LocalDateTime.now());
        empRole.setUpdated(LocalDateTime.now());
        empRoles.add(empRole);
    }
    // 批量添加该员工的角色记录(中间表记录)
    int insertResult = roleMapper.insertEmpRoleBatch(empRoles);
    return deleteResult + insertResult;
}
  1. 开发控制层代码:
package com.joezhou.controller;

@Operation(summary = "修改 - 员工角色")
@PutMapping("updateByEmpId")
public Result<Integer> updateByEmpId(@RequestParam("empId") Long empId,
									 @RequestParam("roleIds") List<Long> roleIds) {
	return new Result<>(roleService.updateByEmpId(empId, roleIds));
}

E04. 开发菜单接口

心法:菜单记录需要关联父菜单记录,所以需要事先对实体类进行改造。

改造如下:

package com.joezhou.entity;

/** @author 周航宇 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Menu implements Serializable {

	...

    /** 每条菜单记录对应 1 条父菜单记录 */
    private Menu parent;
}

1. 基础增删改查

  1. 开发 DTO 实体类:

负责(添加)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@Schema(description = "菜单添加DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MenuInsertDTO implements Serializable {

    @Schema(description = "菜单名称")
    @NotEmpty(message = "菜单名称不能为空")
    @Pattern(regexp = MC.Regex.TITLE_RE, message = MC.Regex.TITLE_RE_MSG)
    private String title;

    @Schema(description = "菜单地址")
    @Pattern(regexp = MC.Regex.MENU_URL_RE, message = MC.Regex.MENU_URL_RE_MSG)
    private String url;

    @Schema(description = "菜单图标")
    @Pattern(regexp = MC.Regex.MENU_ICON_RE, message = MC.Regex.MENU_ICON_RE_MSG)
    private String icon;
    
    @Schema(description = "父菜单ID")
    @Min(value = 0, message = "父菜单ID不能小于0")
    private Long pid;

    @Schema(description = "菜单描述")
    @Pattern(regexp = MC.Regex.INFO_RE, message = MC.Regex.INFO_RE_MSG)
    private String info;
}

负责(修改)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@Schema(description = "菜单修改DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MenuUpdateDTO implements Serializable {

    @NotNull(message = "主键不能为空")
    @Schema(description = "主键")
    private Long id;

    @Schema(description = "菜单名称")
    @Pattern(regexp = MC.Regex.TITLE_RE, message = MC.Regex.TITLE_RE_MSG)
    private String title;

    @Schema(description = "菜单地址")
    @Pattern(regexp = MC.Regex.MENU_URL_RE, message = MC.Regex.MENU_URL_RE_MSG)
    private String url;

    @Schema(description = "菜单图标")
    @Pattern(regexp = MC.Regex.MENU_ICON_RE, message = MC.Regex.MENU_ICON_RE_MSG)
    private String icon;

    @Schema(description = "父菜单ID")
    @Min(value = 0, message = "父菜单ID不能小于0")
    private Long pid;

    @Schema(description = "菜单描述")
    @Pattern(regexp = MC.Regex.INFO_RE, message = MC.Regex.INFO_RE_MSG)
    private String info;
}

负责(分页)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Schema(description = "按条件分页搜索权限DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MenuPageDTO extends PageDTO {
    @Schema(description = "菜单名称")
    @Pattern(regexp = MC.Regex.TITLE_RE, message = MC.Regex.TITLE_RE_MSG)
    private String title;

    @Schema(description = "父菜单ID")
    @Min(value = 0, message = "父菜单ID必须大于0")
    private Long pid;
}

负责(全查)业务的实体类

package com.joezhou.dto;

/** @author 周航宇 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MenuVO implements Serializable {
    /** 主键 */
    private Long id;
    /** 菜单名称 */
    private String title;
    /** 父菜单ID */
    private Long pid;
    /** 父菜单名称 */
    private String parentTitle;
}
  1. 开发数据层代码:
package com.joezhou.mapper;

/** @author 周航宇 */
@Repository
public interface MenuMapper {

    @Insert("""
            insert into ums_menu (title, url, icon, pid, info, version, deleted, created, updated)
            values (#{title}, #{url}, #{icon}, #{pid}, #{info}, #{version}, #{deleted}, #{created}, #{updated})
            """)
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(Menu menu);

    @Results(id = "menuResultMap", value = {
            @Result(column = "id", property = "id", id = true),
            @Result(column = "pid", property = "pid"),
            @Result(column = "pid", property = "parent",
                    one = @One(select = "com.joezhou.mapper.MenuMapper.select")),
    })
    @Select("""
            select * from ums_menu t where t.id = #{param1} and t.deleted = 0
            """)
    Menu select(Long id);

    @ResultMap("menuResultMap")
    @Select("""
            <script>
            select * from ums_menu t
            <where>
                <if test='title != null'> title like concat('%', #{title}, '%') and </if>
                <if test='pid != null'> pid = #{pid} and </if>
                t.deleted = 0
            </where>
            </script>
            """)
    List<Menu> list(MenuPageDTO dto);

    @Update("""
            <script>
            update ums_menu
            <set>
            <if test='title != null'> title = #{title}, </if>
            <if test='url != null'> url = #{url}, </if>
            <if test='icon != null'> icon = #{icon}, </if>
            <if test='pid != null'> pid = #{pid}, </if>
            <if test='info != null'> info = #{info}, </if>
            <if test='deleted != null'> deleted = #{deleted}, </if>
            <if test='created != null'> created = #{created}, </if>
            <if test='updated != null'> updated = #{updated}, </if>
            version = version + 1
            </set>
            where id = #{id} and deleted = 0 and version = #{version}
            </script>
            """)
    int update(Menu menu);

    @Update("""
            update ums_menu set deleted = 1, updated = current_timestamp where id = #{param1}
            """)
    int delete(Long id);

    @Update("""
            <script>
            update ums_menu set deleted = 1, updated = current_timestamp
            where id in
            <foreach collection='list' item='e' open='(' close=')' separator=','>
                ${e}
            </foreach>
            </script>
            """)
    int deleteBatch(List<Long> ids);
}
  1. 开发业务层代码:
package com.joezhou.service;

/** @author 周航宇 */
public interface MenuService {
    int insert(MenuInsertDTO dto);
    Menu select(Long id);
    List<MenuVO> list();
    PageInfo<Menu> page(MenuPageDTO dto);
    int update(MenuUpdateDTO dto);
    int delete(Long id);
    int deleteBatch(List<Long> ids);
}
package com.joezhou.service.impl;

/** @author 周航宇 */
@Service
@CacheConfig(cacheNames = "menu")
public class MenuServiceImpl implements MenuService {

    @Resource
    private MenuMapper menuMapper;

    @CacheEvict(allEntries = true)
    @Override
    public int insert(MenuInsertDTO dto) {
        String info = dto.getInfo();
        // 拷贝属性
        Menu menu = BeanUtil.copyProperties(dto, Menu.class);

        // 处理父权限
        if (ObjectUtil.isNull(menu.getPid()) || menu.getPid() == 0) {
            menu.setPid(0L);
            menu.setUrl("/");
        }

        // 处理权限路径:必须以 "/" 开头
        String url = menu.getUrl();
        if (ObjectUtil.isNotNull(url) && !url.startsWith("/")) {
            menu.setUrl("/" + url);
        }

        // 设置默认值
        menu.setVersion(0L);
        menu.setDeleted(0);
        menu.setInfo(StrUtil.isBlank(info) ? "暂无描述" : info);
        menu.setCreated(LocalDateTime.now());
        menu.setUpdated(LocalDateTime.now());
        // DB添加
        int result = menuMapper.insert(menu);
        if (result <= 0) {
            throw new ServerErrorException("DB添加失败");
        }
        return result;
    }

    @Cacheable(key = "#p0", condition = "#p0 != null", unless = "#result == null")
    @Override
    public Menu select(Long id) {
        Menu result = menuMapper.select(id);
        if (ObjectUtil.isNull(result)) {
            throw new ServerErrorException("记录不存在");
        }
        return result;
    }

    @Cacheable(key = "#root.methodName", unless = "#result == null")
    @Override
    public List<MenuVO> list() {
        return menuMapper.list(new MenuPageDTO())
                .stream()
                .map(menu -> {
                    MenuVO menuVO = BeanUtil.copyProperties(menu, MenuVO.class);
                    if (ObjectUtil.isNotNull(menu.getParent())) {
                        menuVO.setParentTitle(menu.getParent().getTitle());
                    } else {
                        menuVO.setParentTitle("无");
                    }
                    return menuVO;
                })
                .collect(Collectors.toList());
    }

    @Cacheable(key = "#root.methodName + ':' + #p0.toString()",
            condition = "#p0 != null",
            unless = "#result == null")
    @Override
    public PageInfo<Menu> page(MenuPageDTO dto) {
        PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
        return new PageInfo<>(menuMapper.list(dto));
    }

    @CacheEvict(allEntries = true)
    @Transactional
    @Retryable(retryFor = VersionException.class)
    @Override
    public int update(MenuUpdateDTO dto) {
        Menu menu = menuMapper.select(dto.getId());
        if (ObjectUtil.isNull(menu)) {
            throw new ServerErrorException("记录不存在");
        }
        BeanUtil.copyProperties(dto, menu);
        // 设置默认值
        menu.setUpdated(LocalDateTime.now());
        // DB修改
        int result = menuMapper.update(menu);
        if (result <= 0) {
            throw new VersionException("DB修改失败");
        }
        return result;
    }

    @CacheEvict(allEntries = true)
    @Override
    public int delete(Long id) {
        int result = menuMapper.delete(id);
        if (result <= 0) {
            throw new ServerErrorException("DB逻辑删除失败");
        }
        return result;
    }

    @CacheEvict(allEntries = true)
    @Override
    public int deleteBatch(List<Long> ids) {
        int result = menuMapper.deleteBatch(ids);
        if (result <= 0) {
            throw new ServerErrorException("DB逻辑批删失败");
        }
        return result;
    }
}
  1. 开发控制层代码:
package com.joezhou.controller;

/** @author 周航宇 */
@Tag(name = "菜单模块")
@RestController
@RequestMapping("/api/v1/menu")
public class MenuController {

    @Resource
    private MenuService menuService;

    @Operation(summary = "新增 - 单条新增")
    @PostMapping("insert")
    public Result<Integer> insert(@RequestBody @Validated MenuInsertDTO dto) {
        return new Result<>(menuService.insert(dto));
    }

    @Operation(summary = "查询 - 单条查询")
    @GetMapping("select/{id}")
    public Result<Menu> select(@PathVariable("id") Long id) {
        return new Result<>(menuService.select(id));
    }

    @Operation(summary = "查询 - 全部记录")
    @GetMapping("list")
    public Result<List<MenuVO>> list() {
        return new Result<>(menuService.list());
    }

    @Operation(summary = "查询 - 分页查询")
    @GetMapping("page")
    public Result<PageInfo<Menu>> page(@Validated MenuPageDTO dto) {
        return new Result<>(menuService.page(dto));
    }

    @Operation(summary = "修改 - 单条修改")
    @PutMapping("update")
    public Result<Integer> update(@RequestBody @Validated MenuUpdateDTO dto) {
        return new Result<>(menuService.update(dto));
    }


    @Operation(summary = "删除 - 单条删除")
    @DeleteMapping("delete/{id}")
    public Result<Integer> delete(@PathVariable("id") Long id) {
        return new Result<>(menuService.delete(id));
    }

    @Operation(summary = "删除 - 批量删除")
    @DeleteMapping("deleteBatch")
    public Result<Integer> deleteBatch(@RequestParam("ids") List<Long> ids) {
        return new Result<>(menuService.deleteBatch(ids));
    }
}

2. 下载数据报表

  1. 开发 DTO 实体类:
package com.joezhou.excel;

/** @author 周航宇 */
@HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.LEFT, verticalAlignment = VerticalAlignmentEnum.CENTER)
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.LEFT, verticalAlignment = VerticalAlignmentEnum.CENTER)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MenuExcel implements Serializable {
    @ExcelProperty(value = {"菜单数据统计表", "菜单标题"})
    private String title;
    @ExcelProperty(value = {"菜单数据统计表", "上级标题"})
    private String parentTitle;
    @ExcelProperty(value = {"菜单数据统计表", "菜单图标"})
    private String icon;
    @ExcelProperty(value = {"菜单数据统计表", "菜单地址"})
    private String url;
    @ExcelProperty(value = {"菜单数据统计表", "菜单描述"})
    private String info;
    @ExcelProperty(value = {"菜单数据统计表", "首次创建日期"})
    @DateTimeFormat("yyyy/MM/dd HH:mm:ss")
    private LocalDateTime created;
    @ExcelProperty(value = {"菜单数据统计表", "最后创建日期"})
    @DateTimeFormat("yyyy/MM/dd HH:mm:ss")
    private LocalDateTime updated;
}
  1. 开发业务层代码:
package com.joezhou.service;

/**
 * 导出菜单记录的Excel数据
 *
 * @return 菜单记录的Excel数据列表
 */
List<MenuExcel> getExcelData();
package com.joezhou.service.impl;

@Override
public List<MenuExcel> getExcelData() {
	return menuMapper.list(new MenuPageDTO())
			.stream()
			.map(menu -> {
				MenuExcel menuExcel = BeanUtil.copyProperties(menu, MenuExcel.class);
				if (menu.getPid().equals(MC.Menu.ROOT_ID)) {
					menuExcel.setParentTitle("无");
				} else {
					menuExcel.setParentTitle(menu.getParent().getTitle());
				}
				return menuExcel;
			})
			.collect(Collectors.toList());
}
  1. 开发控制层代码:
package com.joezhou.controller;

@Operation(summary = "查询 - 报表打印")
@SneakyThrows
@GetMapping("/excel")
public void excel(HttpServletResponse resp) {
	EasyExcelUtil.download(resp, "菜单统计表", menuService.getExcelData());
}

3. 查询角色菜单

武技:根据角色的 ID 查询该角色的全部菜单列表。

  1. 开发数据层代码:
@Select("""
		select * from ums_menu t2 where id in (
			select fk_menu_id from ums_role_menu t1
			where t1.fk_role_id = #{param1} and t1.deleted = 0
		) and t2.deleted = 0
		""")
List<Menu> listByRoleId(Long roleId);
  1. 开发业务层代码:
package com.joezhou.service;

/**
 * 根据角色主键查询该角色的全部菜单列表
 *
 * @param roleId 角色主键
 * @return 该角色的全部菜单列表
 */
List<Menu> listByRoleId(Long roleId);
package com.joezhou.service.impl;

@Cacheable(key = "#root.methodName + ':' + #p0",
		condition = "#p0 != null",
		unless = "#result == null")
@Override
public List<Menu> listByRoleId(Long roleId) {
	return menuMapper.listByRoleId(roleId);
}
  1. 开发控制层代码:
package com.joezhou.controller;

@Operation(summary = "查询 - 角色菜单")
@GetMapping("listByRoleId/{roleId}")
public Result<List<Menu>> listByRoleId(@PathVariable("roleId") Long roleId) {
	return new Result<>(menuService.listByRoleId(roleId));
}

4. 修改角色菜单

  1. 开发数据层代码:
package com.joezhou.mapper;

@Update("""
		update ums_role_menu set deleted = 1, updated = now() where fk_role_id = #{param1}
		""")
int deleteRoleMenuByRoleId(Long roleId);

@Insert("""
        <script>
        insert into ums_role_menu (fk_role_id, fk_menu_id, version, deleted, created, updated)
        values
        <foreach collection='list' item='e' separator=','>
            (#{e.fkRoleId}, #{e.fkMenuId}, #{e.version}, #{e.deleted}, #{e.created}, #{e.updated})
        </foreach>
        </script>
        """)
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertRoleMenuBatch(List<RoleMenu> roleMenus);
  1. 开发业务层代码:
package com.joezhou.service;

/**
 * 根据角色主键修改该角色的菜单列表
 *
 * @param roleId 角色主键
 * @param menuIds 菜单主键列表
 * @return 影响条目数
 */
int updateByRoleId(Long roleId, List<Long> menuIds);
package com.joezhou.service.impl;

@CacheEvict(allEntries = true)
@Transactional
@Retryable(retryFor = VersionException.class)
@Override
public int updateByRoleId(Long roleId, List<Long> menuIds) {
    // 删除该角色的全部中间表记录
    int deleteResult = menuMapper.deleteRoleMenuByRoleId(roleId);
    // 创建该角色的新菜单列表
    List<RoleMenu> roleMenus = new ArrayList<>();
    for (Long menuId : menuIds) {
        RoleMenu roleMenu = new RoleMenu();
        roleMenu.setFkRoleId(roleId);
        roleMenu.setFkMenuId(menuId);
        roleMenu.setVersion(0L);
        roleMenu.setDeleted(0);
        roleMenu.setCreated(LocalDateTime.now());
        roleMenu.setUpdated(LocalDateTime.now());
        roleMenus.add(roleMenu);
    }
    // 批量添加该角色的菜单记录(中间表记录)
    int insertResult = menuMapper.insertRoleMenuBatch(roleMenus);
    return deleteResult + insertResult;
}
  1. 开发控制层代码:
package com.joezhou.controller;

@Operation(summary = "修改 - 角色菜单")
@PutMapping("updateByRoleId")
public Result<Integer> updateByRoleId(@RequestParam("roleId") Long roleId,
									  @RequestParam("menuIds") List<Long> menuIds) {
	return new Result<>(menuService.updateByRoleId(roleId, menuIds));
}

5. 查询员工菜单

  1. 开发数据层代码:
package com.joezhou.mapper;

@Select("""
		select * from ums_menu t1 where id in(
			select t2.fk_menu_id from ums_role_menu t2 where t2.fk_role_id in (
				select t1.fk_role_id from ums_emp_role t1
				where t1.fk_emp_id = #{param1} and t1.deleted = 0
			) and t2.deleted = 0
		) and t1.deleted = 0
		""")
List<Menu> listByEmpId(Long empId);
  1. 开发业务层代码:
package com.joezhou.service;

/**
 * 根据员工主键查询该员工的全部菜单列表
 *
 * @param empId 员工主键
 * @return 该员工的全部菜单列表
 */
List<Menu> listByEmpId(Long empId);
package com.joezhou.service.impl;

@Cacheable(key = "#root.methodName + ':' + #p0",
		condition = "#p0 != null",
		unless = "#result == null")
@Override
public List<Menu> listByEmpId(Long empId) {
	return menuMapper.listByEmpId(empId);
}
  1. 开发控制层代码:
package com.joezhou.controller;

@Operation(summary = "查询 - 员工菜单")
@GetMapping("listByEmpId/{empId}")
public Result<List<Menu>> listByEmpId(@PathVariable("empId") Long empId) {
	return new Result<>(menuService.listByEmpId(empId));
}

Java道经 - 项目 - MyClub - 后台后端(二)


传送门:JP3-1-MyClub项目简介
传送门:JP3-2-MyClub公共服务
传送门:JP3-3-MyClub后台后端(一)
传送门:JP3-3-MyClub后台后端(二)


网站公告

今日签到

点亮在社区的每一天
去签到