Browse Source
- rename RoleDetailVO -> RoleDTO extending BaseDTO, add toEntity() - new RoleParam extends BaseParam; page() now uses PageConverter (size clamp + orderBy anti-injection), fixing pagination bypass - saveOrUpdate 6 @RequestParam -> RoleDTO; detail returns RoleDTO - add PageConverterTest covering size clamp + orderBy injection guardmaster
9 changed files with 204 additions and 112 deletions
@ -0,0 +1,69 @@ |
|||||
|
package com.crm.auth.domain.dto; |
||||
|
|
||||
|
import com.crm.auth.domain.entity.SysRole; |
||||
|
import com.crm.base.domain.dto.BaseDTO; |
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 角色传输对象(ADR-0012 / ADR-0017 Route A:saveOrUpdate 写入参 + detail 出参双向) |
||||
|
* <p>继承 {@link BaseDTO} 白拿 id/createTime/updateTime;createTime/updateTime 由全局 |
||||
|
* @InitBinder 在入参侧 strip,出参侧展示原值。builtin 亦由 @InitBinder strip(内置角色标记服务端裁定)。</p> |
||||
|
* <p>resourceIds 为已补全祖先的完整授权集合(ADR-0005),仅 detail 出参填充。</p> |
||||
|
*/ |
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
public class RoleDTO extends BaseDTO { |
||||
|
|
||||
|
@Schema(description = "角色名称") |
||||
|
private String roleName; |
||||
|
|
||||
|
@Schema(description = "角色编码") |
||||
|
private String roleCode; |
||||
|
|
||||
|
@Schema(description = "数据范围 1=本人 2=本部门 3=本部门及子部门 4=全部") |
||||
|
private Integer dataScope; |
||||
|
|
||||
|
@Schema(description = "排序") |
||||
|
private Integer sort; |
||||
|
|
||||
|
@Schema(description = "备注") |
||||
|
private String remark; |
||||
|
|
||||
|
@Schema(description = "是否内置角色") |
||||
|
private Boolean builtin; |
||||
|
|
||||
|
@Schema(description = "已补全祖先的完整授权集合(存储态)") |
||||
|
private List<Long> resourceIds; |
||||
|
|
||||
|
/** SysRole → RoleDTO 基本字段映射(不含 resourceIds) */ |
||||
|
public static RoleDTO fromEntity(SysRole entity) { |
||||
|
if (entity == null) { |
||||
|
return null; |
||||
|
} |
||||
|
RoleDTO dto = new RoleDTO(); |
||||
|
dto.setId(entity.getId()); |
||||
|
dto.setRoleName(entity.getRoleName()); |
||||
|
dto.setRoleCode(entity.getRoleCode()); |
||||
|
dto.setDataScope(entity.getDataScope()); |
||||
|
dto.setSort(entity.getSort()); |
||||
|
dto.setRemark(entity.getRemark()); |
||||
|
dto.setBuiltin(entity.getBuiltin()); |
||||
|
return dto; |
||||
|
} |
||||
|
|
||||
|
/** RoleDTO → SysRole(写入参转换,Service 层调用;builtin/审计字段不映射,由服务端裁定) */ |
||||
|
public SysRole toEntity() { |
||||
|
SysRole entity = new SysRole(); |
||||
|
entity.setId(this.getId()); |
||||
|
entity.setRoleName(this.roleName); |
||||
|
entity.setRoleCode(this.roleCode); |
||||
|
entity.setDataScope(this.dataScope); |
||||
|
entity.setSort(this.sort); |
||||
|
entity.setRemark(this.remark); |
||||
|
return entity; |
||||
|
} |
||||
|
} |
||||
@ -1,60 +0,0 @@ |
|||||
package com.crm.auth.domain.dto; |
|
||||
|
|
||||
import com.crm.auth.domain.entity.SysRole; |
|
||||
import io.swagger.v3.oas.annotations.media.Schema; |
|
||||
import lombok.Data; |
|
||||
|
|
||||
import java.io.Serial; |
|
||||
import java.io.Serializable; |
|
||||
import java.util.List; |
|
||||
|
|
||||
/** |
|
||||
* 角色详情 DTO(ADR-0012) |
|
||||
* <p>角色管理详情接口的输出载体;resourceIds 为已补全祖先的完整授权集合(ADR-0005)</p> |
|
||||
*/ |
|
||||
@Data |
|
||||
public class RoleDetailVO implements Serializable { |
|
||||
|
|
||||
@Serial |
|
||||
private static final long serialVersionUID = 1L; |
|
||||
|
|
||||
@Schema(description = "角色 ID") |
|
||||
private Long id; |
|
||||
|
|
||||
@Schema(description = "角色名称") |
|
||||
private String roleName; |
|
||||
|
|
||||
@Schema(description = "角色编码") |
|
||||
private String roleCode; |
|
||||
|
|
||||
@Schema(description = "数据范围 1=本人 2=本部门 3=本部门及子部门 4=全部") |
|
||||
private Integer dataScope; |
|
||||
|
|
||||
@Schema(description = "排序") |
|
||||
private Integer sort; |
|
||||
|
|
||||
@Schema(description = "备注") |
|
||||
private String remark; |
|
||||
|
|
||||
@Schema(description = "是否内置角色") |
|
||||
private Boolean builtin; |
|
||||
|
|
||||
@Schema(description = "已补全祖先的完整授权集合(存储态)") |
|
||||
private List<Long> resourceIds; |
|
||||
|
|
||||
/** SysRole → RoleDetailVO 基本字段映射(不含 resourceIds) */ |
|
||||
public static RoleDetailVO fromEntity(SysRole entity) { |
|
||||
if (entity == null) { |
|
||||
return null; |
|
||||
} |
|
||||
RoleDetailVO vo = new RoleDetailVO(); |
|
||||
vo.setId(entity.getId()); |
|
||||
vo.setRoleName(entity.getRoleName()); |
|
||||
vo.setRoleCode(entity.getRoleCode()); |
|
||||
vo.setDataScope(entity.getDataScope()); |
|
||||
vo.setSort(entity.getSort()); |
|
||||
vo.setRemark(entity.getRemark()); |
|
||||
vo.setBuiltin(entity.getBuiltin()); |
|
||||
return vo; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,15 @@ |
|||||
|
package com.crm.auth.domain.param; |
||||
|
|
||||
|
import com.crm.base.domain.param.BaseParam; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
|
||||
|
/** |
||||
|
* 角色管理-分页查询入参(ADR-0017) |
||||
|
* <p>keyword 匹配角色名称,current/size/orderBy/asc 继承自 {@link BaseParam}, |
||||
|
* 经 {@link com.crm.base.utils.PageConverter#toMpPage} 转换(size 上限收敛 + 排序字段防注入)。</p> |
||||
|
*/ |
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
public class RoleParam extends BaseParam { |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
package com.crm.base.utils; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
||||
|
import com.crm.base.constant.CommonConstants; |
||||
|
import com.crm.base.domain.param.BaseParam; |
||||
|
import org.junit.jupiter.api.DisplayName; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
|
||||
|
/** |
||||
|
* {@link PageConverter#toMpPage} 单元测试:分页安全收敛(size 上限、排序字段防注入)。 |
||||
|
* <p>这是所有分页接口(含角色 page,ADR-0017 ticket 06)的共享护栏。</p> |
||||
|
*/ |
||||
|
@DisplayName("PageConverter 分页参数收敛") |
||||
|
class PageConverterTest { |
||||
|
|
||||
|
private static BaseParam param(Integer current, Integer size, String orderBy, Boolean asc) { |
||||
|
BaseParam p = new BaseParam(); |
||||
|
p.setCurrent(current); |
||||
|
p.setSize(size); |
||||
|
p.setOrderBy(orderBy); |
||||
|
p.setAsc(asc); |
||||
|
return p; |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@DisplayName("size 超过上限 500 被收敛为 500") |
||||
|
void size_exceedsMax_clampedToMax() { |
||||
|
Page<Object> page = PageConverter.toMpPage(param(1, 100_000, null, null)); |
||||
|
assertThat(page.getSize()).isEqualTo(CommonConstants.MAX_PAGE_SIZE); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@DisplayName("size 为 null 或 <1 回落默认值 10") |
||||
|
void size_nullOrNonPositive_fallsBackToDefault() { |
||||
|
assertThat(PageConverter.toMpPage(param(1, null, null, null)).getSize()) |
||||
|
.isEqualTo(CommonConstants.DEFAULT_PAGE_SIZE); |
||||
|
assertThat(PageConverter.toMpPage(param(1, 0, null, null)).getSize()) |
||||
|
.isEqualTo(CommonConstants.DEFAULT_PAGE_SIZE); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@DisplayName("current <1 回落到 1") |
||||
|
void current_nonPositive_fallsBackToOne() { |
||||
|
assertThat(PageConverter.toMpPage(param(0, 10, null, null)).getCurrent()).isEqualTo(1); |
||||
|
assertThat(PageConverter.toMpPage(param(null, 10, null, null)).getCurrent()).isEqualTo(1); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@DisplayName("合法 orderBy 驼峰转下划线并生成排序项") |
||||
|
void orderBy_validCamel_convertedToUnderlineOrder() { |
||||
|
Page<Object> page = PageConverter.toMpPage(param(1, 10, "createTime", true)); |
||||
|
assertThat(page.orders()).hasSize(1); |
||||
|
assertThat(page.orders().get(0).getColumn()).isEqualTo("create_time"); |
||||
|
assertThat(page.orders().get(0).isAsc()).isTrue(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@DisplayName("非法 orderBy(含注入字符)被忽略,不产生排序项") |
||||
|
void orderBy_injectionChars_ignored() { |
||||
|
Page<Object> page = PageConverter.toMpPage(param(1, 10, "id; DROP TABLE sys_role", null)); |
||||
|
assertThat(page.orders()).isEmpty(); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue