Commit bda3515f26e235ea1266c78a64f4e42f90c7490a
1 parent
405982ff
feat(usr): SysUserMapper 动态查询 XML + JOIN 员工/部门 REQ-USR-004
Showing
5 changed files
with
209 additions
and
3 deletions
backend/src/main/java/com/xly/erp/module/usr/mapper/SysUserMapper.java
| ... | ... | @@ -2,11 +2,14 @@ package com.xly.erp.module.usr.mapper; |
| 2 | 2 | |
| 3 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| 4 | 4 | import com.xly.erp.module.usr.entity.SysUser; |
| 5 | +import com.xly.erp.module.usr.vo.UserListItemVo; | |
| 5 | 6 | import org.apache.ibatis.annotations.Mapper; |
| 6 | 7 | import org.apache.ibatis.annotations.Param; |
| 7 | 8 | import org.apache.ibatis.annotations.Select; |
| 8 | 9 | import org.apache.ibatis.annotations.Update; |
| 9 | 10 | |
| 11 | +import java.util.List; | |
| 12 | + | |
| 10 | 13 | @Mapper |
| 11 | 14 | public interface SysUserMapper extends BaseMapper<SysUser> { |
| 12 | 15 | |
| ... | ... | @@ -20,10 +23,7 @@ public interface SysUserMapper extends BaseMapper<SysUser> { |
| 20 | 23 | /** |
| 21 | 24 | * 原子累加失败登录次数;达到阈值 maxCount 时同步写 tLockUntil = NOW() + lockMinutes 分钟。 |
| 22 | 25 | * 单 SQL,DB 层保证并发安全。返回受影响行数(应为 1)。 |
| 23 | - */ | |
| 24 | - /* | |
| 25 | 26 | * MySQL 按 SET 子句从左到右求值,所以放在 +1 之后的引用看到的是新值。 |
| 26 | - * 第二条 SET 用 `iFailedLoginCount >= maxCount` 即等价于"新计数 >= 阈值"判定。 | |
| 27 | 27 | */ |
| 28 | 28 | @Update("UPDATE sys_user " + |
| 29 | 29 | "SET iFailedLoginCount = iFailedLoginCount + 1, " + |
| ... | ... | @@ -53,4 +53,12 @@ public interface SysUserMapper extends BaseMapper<SysUser> { |
| 53 | 53 | "WHERE sUserCode = #{userCode} AND iIncrement <> #{excludedUserId})") |
| 54 | 54 | boolean existsByUserCodeExcludingId(@Param("userCode") String userCode, |
| 55 | 55 | @Param("excludedUserId") Integer excludedUserId); |
| 56 | + | |
| 57 | + /** | |
| 58 | + * REQ-USR-004 动态查询。SQL 在 SysUserMapper.xml 定义。 | |
| 59 | + * QueryParams 必须已通过 service 层白名单校验。 | |
| 60 | + */ | |
| 61 | + List<UserListItemVo> selectByQuery(@Param("p") UserQueryParams p); | |
| 62 | + | |
| 63 | + long countByQuery(@Param("p") UserQueryParams p); | |
| 56 | 64 | } | ... | ... |
backend/src/main/java/com/xly/erp/module/usr/mapper/UserQueryParams.java
0 → 100644
| 1 | +package com.xly.erp.module.usr.mapper; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * SysUserMapper.selectByQuery / countByQuery 入参(service 层规范化白名单后填入)。 | |
| 5 | + * sqlSortField / sqlSortOrder / sqlQueryColumn 必须已通过白名单校验; | |
| 6 | + * mapper XML 直接用 ${} 拼接到 SQL。 | |
| 7 | + */ | |
| 8 | +public class UserQueryParams { | |
| 9 | + public String sqlSortField; | |
| 10 | + public String sqlSortOrder; | |
| 11 | + public String sqlQueryColumn; // null 表示无 queryField 条件 | |
| 12 | + public String matchMode; // contains / notContains / equals | |
| 13 | + public String queryValue; // null/"" 表示跳过该条件 | |
| 14 | + public String userType; // null 表示不过滤 | |
| 15 | + public Integer isDeleted; // null 不过滤;0 / 1 过滤 | |
| 16 | + public Integer offset; | |
| 17 | + public Integer limit; | |
| 18 | +} | ... | ... |
backend/src/main/resources/application.yml
backend/src/main/resources/mapper/usr/SysUserMapper.xml
0 → 100644
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> | |
| 3 | +<mapper namespace="com.xly.erp.module.usr.mapper.SysUserMapper"> | |
| 4 | + | |
| 5 | + <sql id="baseFrom"> | |
| 6 | + FROM sys_user u | |
| 7 | + LEFT JOIN sys_employee e ON e.iIncrement = u.iEmployeeId | |
| 8 | + LEFT JOIN sys_department d ON d.iIncrement = e.iDepartmentId | |
| 9 | + </sql> | |
| 10 | + | |
| 11 | + <sql id="whereClause"> | |
| 12 | + <where> | |
| 13 | + <if test="p.sqlQueryColumn != null and p.queryValue != null and p.queryValue != ''"> | |
| 14 | + <choose> | |
| 15 | + <when test="'contains'.equals(p.matchMode)"> | |
| 16 | + AND ${p.sqlQueryColumn} LIKE CONCAT('%', #{p.queryValue}, '%') | |
| 17 | + </when> | |
| 18 | + <when test="'notContains'.equals(p.matchMode)"> | |
| 19 | + AND (${p.sqlQueryColumn} NOT LIKE CONCAT('%', #{p.queryValue}, '%') | |
| 20 | + OR ${p.sqlQueryColumn} IS NULL) | |
| 21 | + </when> | |
| 22 | + <otherwise> | |
| 23 | + AND ${p.sqlQueryColumn} = #{p.queryValue} | |
| 24 | + </otherwise> | |
| 25 | + </choose> | |
| 26 | + </if> | |
| 27 | + <if test="p.userType != null"> | |
| 28 | + AND u.sUserType = #{p.userType} | |
| 29 | + </if> | |
| 30 | + <if test="p.isDeleted != null"> | |
| 31 | + AND u.iIsDeleted = #{p.isDeleted} | |
| 32 | + </if> | |
| 33 | + </where> | |
| 34 | + </sql> | |
| 35 | + | |
| 36 | + <resultMap id="UserListItemVoMap" type="com.xly.erp.module.usr.vo.UserListItemVo"> | |
| 37 | + <id property="userId" column="userId"/> | |
| 38 | + <result property="username" column="username"/> | |
| 39 | + <result property="employeeName" column="employeeName"/> | |
| 40 | + <result property="userCode" column="userCode"/> | |
| 41 | + <result property="departmentName" column="departmentName"/> | |
| 42 | + <result property="userType" column="userType"/> | |
| 43 | + <result property="language" column="language"/> | |
| 44 | + <result property="isDeleted" column="isDeleted" javaType="java.lang.Boolean"/> | |
| 45 | + <result property="lastLoginDate" column="lastLoginDate"/> | |
| 46 | + <result property="createdBy" column="createdBy"/> | |
| 47 | + <result property="createdDate" column="createdDate"/> | |
| 48 | + </resultMap> | |
| 49 | + | |
| 50 | + <select id="selectByQuery" resultMap="UserListItemVoMap"> | |
| 51 | + SELECT u.iIncrement AS userId, | |
| 52 | + u.sUsername AS username, | |
| 53 | + e.sEmployeeName AS employeeName, | |
| 54 | + u.sUserCode AS userCode, | |
| 55 | + d.sDepartmentName AS departmentName, | |
| 56 | + u.sUserType AS userType, | |
| 57 | + u.sLanguage AS language, | |
| 58 | + u.iIsDeleted AS isDeleted, | |
| 59 | + u.tLastLoginDate AS lastLoginDate, | |
| 60 | + u.sCreatedBy AS createdBy, | |
| 61 | + u.tCreateDate AS createdDate | |
| 62 | + <include refid="baseFrom"/> | |
| 63 | + <include refid="whereClause"/> | |
| 64 | + ORDER BY u.${p.sqlSortField} ${p.sqlSortOrder} | |
| 65 | + LIMIT #{p.offset}, #{p.limit} | |
| 66 | + </select> | |
| 67 | + | |
| 68 | + <select id="countByQuery" resultType="long"> | |
| 69 | + SELECT COUNT(*) | |
| 70 | + <include refid="baseFrom"/> | |
| 71 | + <include refid="whereClause"/> | |
| 72 | + </select> | |
| 73 | + | |
| 74 | +</mapper> | ... | ... |
backend/src/test/java/com/xly/erp/module/usr/mapper/SysUserMapperQueryTest.java
0 → 100644
| 1 | +package com.xly.erp.module.usr.mapper; | |
| 2 | + | |
| 3 | +import com.xly.erp.module.usr.support.LoginTestSeeder; | |
| 4 | +import com.xly.erp.module.usr.vo.UserListItemVo; | |
| 5 | +import org.junit.jupiter.api.BeforeEach; | |
| 6 | +import org.junit.jupiter.api.Test; | |
| 7 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 8 | +import org.springframework.boot.test.context.SpringBootTest; | |
| 9 | +import org.springframework.test.context.ActiveProfiles; | |
| 10 | + | |
| 11 | +import java.util.List; | |
| 12 | + | |
| 13 | +import static org.junit.jupiter.api.Assertions.*; | |
| 14 | + | |
| 15 | +@SpringBootTest | |
| 16 | +@ActiveProfiles("test") | |
| 17 | +class SysUserMapperQueryTest { | |
| 18 | + | |
| 19 | + @Autowired private SysUserMapper mapper; | |
| 20 | + @Autowired private LoginTestSeeder seeder; | |
| 21 | + | |
| 22 | + @BeforeEach | |
| 23 | + void setUp() { | |
| 24 | + seeder.reset(); | |
| 25 | + } | |
| 26 | + | |
| 27 | + private UserQueryParams baseParams() { | |
| 28 | + UserQueryParams p = new UserQueryParams(); | |
| 29 | + p.sqlSortField = "tCreateDate"; | |
| 30 | + p.sqlSortOrder = "desc"; | |
| 31 | + p.matchMode = "contains"; | |
| 32 | + p.offset = 0; | |
| 33 | + p.limit = 100; | |
| 34 | + return p; | |
| 35 | + } | |
| 36 | + | |
| 37 | + @Test | |
| 38 | + void count_noFilters_returnsAllRows() { | |
| 39 | + long total = mapper.countByQuery(baseParams()); | |
| 40 | + // seeder 插入 alice + admin + bob_deleted = 3 行 | |
| 41 | + assertEquals(3, total); | |
| 42 | + } | |
| 43 | + | |
| 44 | + @Test | |
| 45 | + void select_withSortByUsername_ascending() { | |
| 46 | + UserQueryParams p = baseParams(); | |
| 47 | + p.sqlSortField = "sUsername"; | |
| 48 | + p.sqlSortOrder = "asc"; | |
| 49 | + List<UserListItemVo> rows = mapper.selectByQuery(p); | |
| 50 | + assertEquals(3, rows.size()); | |
| 51 | + // 期望升序:admin / alice / bob_deleted | |
| 52 | + assertEquals(LoginTestSeeder.USER_ADMIN, rows.get(0).getUsername()); | |
| 53 | + assertEquals(LoginTestSeeder.USER_OK, rows.get(1).getUsername()); | |
| 54 | + assertEquals(LoginTestSeeder.USER_DELETED, rows.get(2).getUsername()); | |
| 55 | + } | |
| 56 | + | |
| 57 | + @Test | |
| 58 | + void select_withQueryFieldUsername_contains() { | |
| 59 | + UserQueryParams p = baseParams(); | |
| 60 | + p.sqlQueryColumn = "u.sUsername"; | |
| 61 | + p.matchMode = "contains"; | |
| 62 | + p.queryValue = "ali"; | |
| 63 | + List<UserListItemVo> rows = mapper.selectByQuery(p); | |
| 64 | + assertEquals(1, rows.size()); | |
| 65 | + assertEquals(LoginTestSeeder.USER_OK, rows.get(0).getUsername()); | |
| 66 | + } | |
| 67 | + | |
| 68 | + @Test | |
| 69 | + void select_joinsEmployeeAndDepartment_returnsBothNames() { | |
| 70 | + UserQueryParams p = baseParams(); | |
| 71 | + p.sqlQueryColumn = "u.sUsername"; | |
| 72 | + p.matchMode = "equals"; | |
| 73 | + p.queryValue = LoginTestSeeder.USER_OK; | |
| 74 | + List<UserListItemVo> rows = mapper.selectByQuery(p); | |
| 75 | + assertEquals(1, rows.size()); | |
| 76 | + assertEquals("张三", rows.get(0).getEmployeeName()); | |
| 77 | + assertEquals("技术部", rows.get(0).getDepartmentName()); | |
| 78 | + } | |
| 79 | + | |
| 80 | + @Test | |
| 81 | + void select_withIsDeletedFilter_returnsOnlyMatching() { | |
| 82 | + UserQueryParams p = baseParams(); | |
| 83 | + p.isDeleted = 1; | |
| 84 | + List<UserListItemVo> rows = mapper.selectByQuery(p); | |
| 85 | + assertEquals(1, rows.size()); | |
| 86 | + assertEquals(LoginTestSeeder.USER_DELETED, rows.get(0).getUsername()); | |
| 87 | + } | |
| 88 | + | |
| 89 | + @Test | |
| 90 | + void select_withUserTypeFilter_returnsOnlyAdmin() { | |
| 91 | + UserQueryParams p = baseParams(); | |
| 92 | + p.userType = "SUPER_ADMIN"; | |
| 93 | + List<UserListItemVo> rows = mapper.selectByQuery(p); | |
| 94 | + assertEquals(1, rows.size()); | |
| 95 | + assertEquals(LoginTestSeeder.USER_ADMIN, rows.get(0).getUsername()); | |
| 96 | + } | |
| 97 | + | |
| 98 | + @Test | |
| 99 | + void select_pagination_limitsResults() { | |
| 100 | + UserQueryParams p = baseParams(); | |
| 101 | + p.limit = 2; | |
| 102 | + List<UserListItemVo> rows = mapper.selectByQuery(p); | |
| 103 | + assertEquals(2, rows.size()); | |
| 104 | + } | |
| 105 | +} | ... | ... |