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,11 +2,14 @@ package com.xly.erp.module.usr.mapper; | ||
| 2 | 2 | ||
| 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| 4 | import com.xly.erp.module.usr.entity.SysUser; | 4 | import com.xly.erp.module.usr.entity.SysUser; |
| 5 | +import com.xly.erp.module.usr.vo.UserListItemVo; | ||
| 5 | import org.apache.ibatis.annotations.Mapper; | 6 | import org.apache.ibatis.annotations.Mapper; |
| 6 | import org.apache.ibatis.annotations.Param; | 7 | import org.apache.ibatis.annotations.Param; |
| 7 | import org.apache.ibatis.annotations.Select; | 8 | import org.apache.ibatis.annotations.Select; |
| 8 | import org.apache.ibatis.annotations.Update; | 9 | import org.apache.ibatis.annotations.Update; |
| 9 | 10 | ||
| 11 | +import java.util.List; | ||
| 12 | + | ||
| 10 | @Mapper | 13 | @Mapper |
| 11 | public interface SysUserMapper extends BaseMapper<SysUser> { | 14 | public interface SysUserMapper extends BaseMapper<SysUser> { |
| 12 | 15 | ||
| @@ -20,10 +23,7 @@ public interface SysUserMapper extends BaseMapper<SysUser> { | @@ -20,10 +23,7 @@ public interface SysUserMapper extends BaseMapper<SysUser> { | ||
| 20 | /** | 23 | /** |
| 21 | * 原子累加失败登录次数;达到阈值 maxCount 时同步写 tLockUntil = NOW() + lockMinutes 分钟。 | 24 | * 原子累加失败登录次数;达到阈值 maxCount 时同步写 tLockUntil = NOW() + lockMinutes 分钟。 |
| 22 | * 单 SQL,DB 层保证并发安全。返回受影响行数(应为 1)。 | 25 | * 单 SQL,DB 层保证并发安全。返回受影响行数(应为 1)。 |
| 23 | - */ | ||
| 24 | - /* | ||
| 25 | * MySQL 按 SET 子句从左到右求值,所以放在 +1 之后的引用看到的是新值。 | 26 | * MySQL 按 SET 子句从左到右求值,所以放在 +1 之后的引用看到的是新值。 |
| 26 | - * 第二条 SET 用 `iFailedLoginCount >= maxCount` 即等价于"新计数 >= 阈值"判定。 | ||
| 27 | */ | 27 | */ |
| 28 | @Update("UPDATE sys_user " + | 28 | @Update("UPDATE sys_user " + |
| 29 | "SET iFailedLoginCount = iFailedLoginCount + 1, " + | 29 | "SET iFailedLoginCount = iFailedLoginCount + 1, " + |
| @@ -53,4 +53,12 @@ public interface SysUserMapper extends BaseMapper<SysUser> { | @@ -53,4 +53,12 @@ public interface SysUserMapper extends BaseMapper<SysUser> { | ||
| 53 | "WHERE sUserCode = #{userCode} AND iIncrement <> #{excludedUserId})") | 53 | "WHERE sUserCode = #{userCode} AND iIncrement <> #{excludedUserId})") |
| 54 | boolean existsByUserCodeExcludingId(@Param("userCode") String userCode, | 54 | boolean existsByUserCodeExcludingId(@Param("userCode") String userCode, |
| 55 | @Param("excludedUserId") Integer excludedUserId); | 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
| @@ -20,6 +20,7 @@ spring: | @@ -20,6 +20,7 @@ spring: | ||
| 20 | validate-on-migrate: true | 20 | validate-on-migrate: true |
| 21 | 21 | ||
| 22 | mybatis-plus: | 22 | mybatis-plus: |
| 23 | + mapper-locations: classpath*:/mapper/**/*.xml | ||
| 23 | configuration: | 24 | configuration: |
| 24 | map-underscore-to-camel-case: false | 25 | map-underscore-to-camel-case: false |
| 25 | log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl | 26 | log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl |
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 | +} |