diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainDormitoryInfoController.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainDormitoryInfoController.java index eca0456f8..1b6b06b1c 100644 --- a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainDormitoryInfoController.java +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainDormitoryInfoController.java @@ -1,104 +1,116 @@ package com.ruoyi.web.controller.train; +import java.util.ArrayList; import java.util.List; -import javax.servlet.http.HttpServletResponse; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; -import com.ruoyi.common.enums.BusinessType; -import com.ruoyi.train.domain.TrainDormitoryInfo; -import com.ruoyi.train.service.ITrainDormitoryInfoService; -import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.train.domain.TrainDormitoryInfo; +import com.ruoyi.train.domain.dto.DormitoryCreateDTO; +import com.ruoyi.train.domain.vo.DormOptionsVO; +import com.ruoyi.train.domain.vo.DormitoryDetailVO; +import com.ruoyi.train.domain.vo.DormitoryItemVO; +import com.ruoyi.train.service.ITrainDormitoryInfoService; /** - * 宿舍信息Controller - * + * 宿舍房间基础资源 Controller (ISSUE-01-05 / API Contract V1.0.2) + * + * 冻结公开物理 Endpoint 共且仅有 4 个正式 P0: + * 1. GET /train/dormitory/list + * 2. GET /train/dormitory/{dormId} + * 3. POST /train/dormitory + * 4. GET /train/dormitory/options + * * @author ruoyi - * @date 2026-09-08 + * @date 2026-09-14 */ @RestController -@RequestMapping({"/train/dorminfo", "/train/dormitory"}) +@RequestMapping("/train/dormitory") public class TrainDormitoryInfoController extends BaseController { @Autowired private ITrainDormitoryInfoService trainDormitoryInfoService; /** - * 查询宿舍信息列表 + * 分页查询宿舍房间列表 (附带容量、已住人数、剩余床位) + * GET /train/dormitory/list */ - @PreAuthorize("@ss.hasPermi('system:info:list')") + @PreAuthorize("@ss.hasPermi('train:dormitory:list')") @GetMapping("/list") - public TableDataInfo list(TrainDormitoryInfo trainDormitoryInfo) + public TableDataInfo list(TrainDormitoryInfo query) { startPage(); - List list = trainDormitoryInfoService.selectTrainDormitoryInfoList(trainDormitoryInfo); - return getDataTable(list); + List list = trainDormitoryInfoService.selectTrainDormitoryInfoList(query); + List voList = new ArrayList<>(); + if (list != null) + { + for (TrainDormitoryInfo entity : list) + { + DormitoryItemVO vo = new DormitoryItemVO(); + vo.setDormId(entity.getDormId()); + vo.setDormBuilding(entity.getDormBuilding()); + vo.setDormFloor(entity.getDormFloor()); + vo.setDormRoom(entity.getDormRoom()); + vo.setCapacity(entity.getCapacity()); + vo.setUsedCount(entity.getUsedCount()); + vo.setRemainingBeds(entity.getRemainingBeds()); + vo.setGenderType(entity.getGenderType()); + vo.setLockDeviceSn(entity.getLockDeviceSn()); + vo.setDormStatus(entity.getDormStatus()); + vo.setRemark(entity.getRemark()); + vo.setCreateTime(entity.getCreateTime()); + voList.add(vo); + } + } + return getDataTable(voList); } /** - * 导出宿舍信息列表 + * 获取指定房间详情及当前入住人员列表 + * GET /train/dormitory/{dormId} */ - @PreAuthorize("@ss.hasPermi('system:info:export')") - @Log(title = "宿舍信息", businessType = BusinessType.EXPORT) - @PostMapping("/export") - public void export(HttpServletResponse response, TrainDormitoryInfo trainDormitoryInfo) - { - List list = trainDormitoryInfoService.selectTrainDormitoryInfoList(trainDormitoryInfo); - ExcelUtil util = new ExcelUtil(TrainDormitoryInfo.class); - util.exportExcel(response, list, "宿舍信息数据"); - } - - /** - * 获取宿舍信息详细信息 - */ - @PreAuthorize("@ss.hasPermi('system:info:query')") - @GetMapping(value = "/{dormId}") + @PreAuthorize("@ss.hasPermi('train:dormitory:list')") + @GetMapping("/{dormId}") public AjaxResult getInfo(@PathVariable("dormId") String dormId) { - return success(trainDormitoryInfoService.selectTrainDormitoryInfoByDormId(dormId)); + DormitoryDetailVO detail = trainDormitoryInfoService.selectDormitoryDetailByDormId(dormId); + return success(detail); } /** - * 新增宿舍信息 + * 新增宿舍房间 (同楼栋房号重复拦截抛 40006) + * POST /train/dormitory */ - @PreAuthorize("@ss.hasPermi('system:info:add')") - @Log(title = "宿舍信息", businessType = BusinessType.INSERT) + @PreAuthorize("@ss.hasPermi('train:dormitory:edit')") + @Log(title = "宿舍房间管理", businessType = BusinessType.INSERT) @PostMapping - public AjaxResult add(@RequestBody TrainDormitoryInfo trainDormitoryInfo) + public AjaxResult add(@Validated @RequestBody DormitoryCreateDTO dto) { - return toAjax(trainDormitoryInfoService.insertTrainDormitoryInfo(trainDormitoryInfo)); + trainDormitoryInfoService.insertTrainDormitory(dto, SecurityUtils.getUsername()); + return success(); } /** - * 修改宿舍信息 + * 获取楼栋、楼层级联下拉数据 (分房交互必需) + * GET /train/dormitory/options */ - @PreAuthorize("@ss.hasPermi('system:info:edit')") - @Log(title = "宿舍信息", businessType = BusinessType.UPDATE) - @PutMapping - public AjaxResult edit(@RequestBody TrainDormitoryInfo trainDormitoryInfo) + @PreAuthorize("@ss.hasPermi('train:dormitory:list')") + @GetMapping("/options") + public AjaxResult options() { - return toAjax(trainDormitoryInfoService.updateTrainDormitoryInfo(trainDormitoryInfo)); - } - - /** - * 删除宿舍信息 - */ - @PreAuthorize("@ss.hasPermi('system:info:remove')") - @Log(title = "宿舍信息", businessType = BusinessType.DELETE) - @DeleteMapping("/{dormIds}") - public AjaxResult remove(@PathVariable String[] dormIds) - { - return toAjax(trainDormitoryInfoService.deleteTrainDormitoryInfoByDormIds(dormIds)); + List tree = trainDormitoryInfoService.selectDormitoryTreeOptions(); + return success(tree); } } diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/TrainDormitoryInfo.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/TrainDormitoryInfo.java index 212d798a3..1fb9373df 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/TrainDormitoryInfo.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/TrainDormitoryInfo.java @@ -6,138 +6,180 @@ import com.ruoyi.common.annotation.Excel; import com.ruoyi.common.core.domain.BaseEntity; /** - * 宿舍信息对象 train_dormitory_info - * + * 宿舍信息对象 train_dormitory_info (Database V2 / ISSUE-01-05) + * * @author ruoyi - * @date 2026-09-08 + * @date 2026-09-14 */ public class TrainDormitoryInfo extends BaseEntity { private static final long serialVersionUID = 1L; - /** 宿舍ID主键 */ + /** 宿舍ID主键 (32位UUID) */ private String dormId; /** 宿舍楼栋 */ @Excel(name = "宿舍楼栋") private String dormBuilding; + /** 所在楼层 */ + @Excel(name = "所在楼层") + private String dormFloor; + /** 宿舍房间号 */ @Excel(name = "宿舍房间号") private String dormRoom; - /** 宿舍总床位 */ - @Excel(name = "宿舍总床位") - private Long bedTotal; + /** 核定床位容量 (正整数,无默认值,1~8) */ + @Excel(name = "核定床位容量") + private Integer capacity; - /** 已占用床位 */ - @Excel(name = "已占用床位") - private Long bedUsed; + /** 适用性别 (0:不限, 1:男, 2:女) */ + @Excel(name = "适用性别", readConverterExp = "0=不限,1=男,2=女") + private Integer genderType; - /** 宿舍人脸智能锁设备序列号,对接智能锁 */ - @Excel(name = "宿舍人脸智能锁设备序列号,对接智能锁") + /** 智能门锁设备序列号 */ + @Excel(name = "智能门锁设备序列号") private String lockDeviceSn; - /** 0 禁用 1 可用 */ - @Excel(name = "0 禁用 1 可用") + /** 房间状态 (0: 禁用, 1: 可用) */ + @Excel(name = "房间状态", readConverterExp = "0=禁用,1=可用") private Integer dormStatus; - /** 创建管理员账号 */ - @Excel(name = "创建管理员账号") - private String createUser; + /** 删除标志 (0: 存在, 2: 删除) */ + private String delFlag; - public void setDormId(String dormId) + /** 已住人数 (实时统计自 train_accommodation,非持久化列) */ + private Integer usedCount; + + /** 剩余床位 (capacity - usedCount,非持久化列) */ + private Integer remainingBeds; + + public void setDormId(String dormId) { this.dormId = dormId; } - public String getDormId() + public String getDormId() { return dormId; } - public void setDormBuilding(String dormBuilding) + public void setDormBuilding(String dormBuilding) { this.dormBuilding = dormBuilding; } - public String getDormBuilding() + public String getDormBuilding() { return dormBuilding; } - public void setDormRoom(String dormRoom) + public void setDormFloor(String dormFloor) + { + this.dormFloor = dormFloor; + } + + public String getDormFloor() + { + return dormFloor; + } + + public void setDormRoom(String dormRoom) { this.dormRoom = dormRoom; } - public String getDormRoom() + public String getDormRoom() { return dormRoom; } - public void setBedTotal(Long bedTotal) + public void setCapacity(Integer capacity) { - this.bedTotal = bedTotal; + this.capacity = capacity; } - public Long getBedTotal() + public Integer getCapacity() { - return bedTotal; + return capacity; } - public void setBedUsed(Long bedUsed) + public void setGenderType(Integer genderType) { - this.bedUsed = bedUsed; + this.genderType = genderType; } - public Long getBedUsed() + public Integer getGenderType() { - return bedUsed; + return genderType; } - public void setLockDeviceSn(String lockDeviceSn) + public void setLockDeviceSn(String lockDeviceSn) { this.lockDeviceSn = lockDeviceSn; } - public String getLockDeviceSn() + public String getLockDeviceSn() { return lockDeviceSn; } - public void setDormStatus(Integer dormStatus) + public void setDormStatus(Integer dormStatus) { this.dormStatus = dormStatus; } - public Integer getDormStatus() + public Integer getDormStatus() { return dormStatus; } - public void setCreateUser(String createUser) + public void setDelFlag(String delFlag) { - this.createUser = createUser; + this.delFlag = delFlag; } - public String getCreateUser() + public String getDelFlag() { - return createUser; + return delFlag; + } + + public Integer getUsedCount() + { + return usedCount; + } + + public void setUsedCount(Integer usedCount) + { + this.usedCount = usedCount; + } + + public Integer getRemainingBeds() + { + return remainingBeds; + } + + public void setRemainingBeds(Integer remainingBeds) + { + this.remainingBeds = remainingBeds; } @Override public String toString() { - return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) .append("dormId", getDormId()) .append("dormBuilding", getDormBuilding()) + .append("dormFloor", getDormFloor()) .append("dormRoom", getDormRoom()) - .append("bedTotal", getBedTotal()) - .append("bedUsed", getBedUsed()) + .append("capacity", getCapacity()) + .append("genderType", getGenderType()) .append("lockDeviceSn", getLockDeviceSn()) .append("dormStatus", getDormStatus()) - .append("createUser", getCreateUser()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) .append("updateTime", getUpdateTime()) .append("remark", getRemark()) .toString(); diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/DormitoryCreateDTO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/DormitoryCreateDTO.java new file mode 100644 index 000000000..df7173e46 --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/DormitoryCreateDTO.java @@ -0,0 +1,121 @@ +package com.ruoyi.train.domain.dto; + +import java.io.Serializable; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 宿舍房间创建 DTO (ISSUE-01-05 / API Contract V1.0.2) + * + * @author ruoyi + */ +public class DormitoryCreateDTO implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 宿舍楼栋 */ + @NotBlank(message = "宿舍楼栋不能为空") + @Size(max = 50, message = "宿舍楼栋长度不能超过50个字符") + private String dormBuilding; + + /** 所在楼层 */ + @NotBlank(message = "所在楼层不能为空") + @Size(max = 20, message = "所在楼层长度不能超过20个字符") + private String dormFloor; + + /** 房间号 */ + @NotBlank(message = "房间号不能为空") + @Size(max = 30, message = "房间号长度不能超过30个字符") + private String dormRoom; + + /** 核定床位容量 (1~8) */ + @NotNull(message = "床位容量不能为空") + @Min(value = 1, message = "床位容量最小为1") + @Max(value = 8, message = "床位容量最大为8") + private Integer capacity; + + /** 适用性别 (0:不限, 1:男, 2:女) */ + @NotNull(message = "适用性别不能为空") + private Integer genderType; + + /** 智能门锁SN */ + @Size(max = 64, message = "智能锁设备序列号长度不能超过64个字符") + private String lockDeviceSn; + + /** 备注 */ + @Size(max = 500, message = "备注长度不能超过500个字符") + private String remark; + + public String getDormBuilding() + { + return dormBuilding; + } + + public void setDormBuilding(String dormBuilding) + { + this.dormBuilding = dormBuilding; + } + + public String getDormFloor() + { + return dormFloor; + } + + public void setDormFloor(String dormFloor) + { + this.dormFloor = dormFloor; + } + + public String getDormRoom() + { + return dormRoom; + } + + public void setDormRoom(String dormRoom) + { + this.dormRoom = dormRoom; + } + + public Integer getCapacity() + { + return capacity; + } + + public void setCapacity(Integer capacity) + { + this.capacity = capacity; + } + + public Integer getGenderType() + { + return genderType; + } + + public void setGenderType(Integer genderType) + { + this.genderType = genderType; + } + + public String getLockDeviceSn() + { + return lockDeviceSn; + } + + public void setLockDeviceSn(String lockDeviceSn) + { + this.lockDeviceSn = lockDeviceSn; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormOptionsVO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormOptionsVO.java new file mode 100644 index 000000000..5d86b87f8 --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormOptionsVO.java @@ -0,0 +1,197 @@ +package com.ruoyi.train.domain.vo; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 宿舍楼栋/楼层/房间级联下拉选项 VO (ISSUE-01-05 / API Contract V1.0.2) + * + * @author ruoyi + */ +public class DormOptionsVO implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 节点取值 (楼栋名 / 楼层名 / dormId) */ + private String value; + + /** 节点显示名称 (楼栋名 / 楼层名 / 房间号) */ + private String label; + + /** 节点类型 ("building", "floor", "room") */ + private String type; + + /** 子节点集合 (无子节点时为空列表或 null) */ + private List children; + + // 以下为房间叶子节点扩展属性 (分房交互便利使用) + /** 房间ID */ + private String dormId; + + /** 楼栋名称 */ + private String dormBuilding; + + /** 所在楼层 */ + private String dormFloor; + + /** 房间号 */ + private String dormRoom; + + /** 核定床位容量 */ + private Integer capacity; + + /** 已住人数 */ + private Integer usedCount; + + /** 剩余床位 */ + private Integer remainingBeds; + + /** 适用性别 (0:不限, 1:男, 2:女) */ + private Integer genderType; + + /** 智能锁序列号 */ + private String lockDeviceSn; + + public DormOptionsVO() + { + } + + public DormOptionsVO(String value, String label, String type) + { + this.value = value; + this.label = label; + this.type = type; + this.children = new ArrayList<>(); + } + + public String getValue() + { + return value; + } + + public void setValue(String value) + { + this.value = value; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public String getType() + { + return type; + } + + public void setType(String type) + { + this.type = type; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + public String getDormId() + { + return dormId; + } + + public void setDormId(String dormId) + { + this.dormId = dormId; + } + + public String getDormBuilding() + { + return dormBuilding; + } + + public void setDormBuilding(String dormBuilding) + { + this.dormBuilding = dormBuilding; + } + + public String getDormFloor() + { + return dormFloor; + } + + public void setDormFloor(String dormFloor) + { + this.dormFloor = dormFloor; + } + + public String getDormRoom() + { + return dormRoom; + } + + public void setDormRoom(String dormRoom) + { + this.dormRoom = dormRoom; + } + + public Integer getCapacity() + { + return capacity; + } + + public void setCapacity(Integer capacity) + { + this.capacity = capacity; + } + + public Integer getUsedCount() + { + return usedCount; + } + + public void setUsedCount(Integer usedCount) + { + this.usedCount = usedCount; + } + + public Integer getRemainingBeds() + { + return remainingBeds; + } + + public void setRemainingBeds(Integer remainingBeds) + { + this.remainingBeds = remainingBeds; + } + + public Integer getGenderType() + { + return genderType; + } + + public void setGenderType(Integer genderType) + { + this.genderType = genderType; + } + + public String getLockDeviceSn() + { + return lockDeviceSn; + } + + public void setLockDeviceSn(String lockDeviceSn) + { + this.lockDeviceSn = lockDeviceSn; + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormitoryDetailVO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormitoryDetailVO.java new file mode 100644 index 000000000..184b96533 --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormitoryDetailVO.java @@ -0,0 +1,188 @@ +package com.ruoyi.train.domain.vo; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; + +/** + * 宿舍房间详情 VO (ISSUE-01-05 / API Contract V1.0.2) + * 包含房间基础属性与当前入住人员列表 + * + * @author ruoyi + */ +public class DormitoryDetailVO implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 宿舍ID主键 */ + private String dormId; + + /** 宿舍楼栋 */ + private String dormBuilding; + + /** 所在楼层 */ + private String dormFloor; + + /** 房间号 */ + private String dormRoom; + + /** 核定床位容量 */ + private Integer capacity; + + /** 已住人数 (实时统计自 train_accommodation) */ + private Integer usedCount; + + /** 剩余床位 (capacity - usedCount) */ + private Integer remainingBeds; + + /** 适用性别 (0:不限, 1:男, 2:女) */ + private Integer genderType; + + /** 智能门锁SN */ + private String lockDeviceSn; + + /** 房间状态 (0: 禁用, 1: 可用) */ + private Integer dormStatus; + + /** 备注 */ + private String remark; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 当前入住人员列表 (无入住人员时返回空列表 []) */ + private List occupants = new ArrayList<>(); + + public String getDormId() + { + return dormId; + } + + public void setDormId(String dormId) + { + this.dormId = dormId; + } + + public String getDormBuilding() + { + return dormBuilding; + } + + public void setDormBuilding(String dormBuilding) + { + this.dormBuilding = dormBuilding; + } + + public String getDormFloor() + { + return dormFloor; + } + + public void setDormFloor(String dormFloor) + { + this.dormFloor = dormFloor; + } + + public String getDormRoom() + { + return dormRoom; + } + + public void setDormRoom(String dormRoom) + { + this.dormRoom = dormRoom; + } + + public Integer getCapacity() + { + return capacity; + } + + public void setCapacity(Integer capacity) + { + this.capacity = capacity; + } + + public Integer getUsedCount() + { + return usedCount; + } + + public void setUsedCount(Integer usedCount) + { + this.usedCount = usedCount; + } + + public Integer getRemainingBeds() + { + return remainingBeds; + } + + public void setRemainingBeds(Integer remainingBeds) + { + this.remainingBeds = remainingBeds; + } + + public Integer getGenderType() + { + return genderType; + } + + public void setGenderType(Integer genderType) + { + this.genderType = genderType; + } + + public String getLockDeviceSn() + { + return lockDeviceSn; + } + + public void setLockDeviceSn(String lockDeviceSn) + { + this.lockDeviceSn = lockDeviceSn; + } + + public Integer getDormStatus() + { + return dormStatus; + } + + public void setDormStatus(Integer dormStatus) + { + this.dormStatus = dormStatus; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } + + public Date getCreateTime() + { + return createTime; + } + + public void setCreateTime(Date createTime) + { + this.createTime = createTime; + } + + public List getOccupants() + { + return occupants; + } + + public void setOccupants(List occupants) + { + this.occupants = occupants != null ? occupants : new ArrayList<>(); + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormitoryItemVO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormitoryItemVO.java new file mode 100644 index 000000000..2964e9c7a --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormitoryItemVO.java @@ -0,0 +1,173 @@ +package com.ruoyi.train.domain.vo; + +import java.io.Serializable; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; + +/** + * 宿舍房间列表项 VO (ISSUE-01-05 / API Contract V1.0.2) + * 包含容量、已住人数、剩余床位 + * + * @author ruoyi + */ +public class DormitoryItemVO implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 宿舍ID主键 */ + private String dormId; + + /** 宿舍楼栋 */ + private String dormBuilding; + + /** 所在楼层 */ + private String dormFloor; + + /** 房间号 */ + private String dormRoom; + + /** 核定床位容量 */ + private Integer capacity; + + /** 已住人数 (实时统计自 train_accommodation) */ + private Integer usedCount; + + /** 剩余床位 (capacity - usedCount) */ + private Integer remainingBeds; + + /** 适用性别 (0:不限, 1:男, 2:女) */ + private Integer genderType; + + /** 智能门锁SN */ + private String lockDeviceSn; + + /** 房间状态 (0: 禁用, 1: 可用) */ + private Integer dormStatus; + + /** 备注 */ + private String remark; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + public String getDormId() + { + return dormId; + } + + public void setDormId(String dormId) + { + this.dormId = dormId; + } + + public String getDormBuilding() + { + return dormBuilding; + } + + public void setDormBuilding(String dormBuilding) + { + this.dormBuilding = dormBuilding; + } + + public String getDormFloor() + { + return dormFloor; + } + + public void setDormFloor(String dormFloor) + { + this.dormFloor = dormFloor; + } + + public String getDormRoom() + { + return dormRoom; + } + + public void setDormRoom(String dormRoom) + { + this.dormRoom = dormRoom; + } + + public Integer getCapacity() + { + return capacity; + } + + public void setCapacity(Integer capacity) + { + this.capacity = capacity; + } + + public Integer getUsedCount() + { + return usedCount; + } + + public void setUsedCount(Integer usedCount) + { + this.usedCount = usedCount; + } + + public Integer getRemainingBeds() + { + return remainingBeds; + } + + public void setRemainingBeds(Integer remainingBeds) + { + this.remainingBeds = remainingBeds; + } + + public Integer getGenderType() + { + return genderType; + } + + public void setGenderType(Integer genderType) + { + this.genderType = genderType; + } + + public String getLockDeviceSn() + { + return lockDeviceSn; + } + + public void setLockDeviceSn(String lockDeviceSn) + { + this.lockDeviceSn = lockDeviceSn; + } + + public Integer getDormStatus() + { + return dormStatus; + } + + public void setDormStatus(Integer dormStatus) + { + this.dormStatus = dormStatus; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } + + public Date getCreateTime() + { + return createTime; + } + + public void setCreateTime(Date createTime) + { + this.createTime = createTime; + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormitoryOccupantVO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormitoryOccupantVO.java new file mode 100644 index 000000000..0cd9c4016 --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/DormitoryOccupantVO.java @@ -0,0 +1,120 @@ +package com.ruoyi.train.domain.vo; + +import java.io.Serializable; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; + +/** + * 房间当前入住学员 VO (ISSUE-01-05 / API Contract V1.0.2) + * + * @author ruoyi + */ +public class DormitoryOccupantVO implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 参训档案ID */ + private Long enrollmentId; + + /** 学员ID */ + private String studentId; + + /** 学员姓名 */ + private String studentName; + + /** 学员性别 */ + private String gender; + + /** 所属单位/部门 */ + private String orgName; + + /** 分配床位号 (可选) */ + private String bedNo; + + /** 分配时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date assignedTime; + + /** 住宿状态 (0:预分配, 1:已入住) */ + private Integer accommodationStatus; + + public Long getEnrollmentId() + { + return enrollmentId; + } + + public void setEnrollmentId(Long enrollmentId) + { + this.enrollmentId = enrollmentId; + } + + public String getStudentId() + { + return studentId; + } + + public void setStudentId(String studentId) + { + this.studentId = studentId; + } + + public String getStudentName() + { + return studentName; + } + + public void setStudentName(String studentName) + { + this.studentName = studentName; + } + + public String getGender() + { + return gender; + } + + public void setGender(String gender) + { + this.gender = gender; + } + + public String getOrgName() + { + return orgName; + } + + public void setOrgName(String orgName) + { + this.orgName = orgName; + } + + public String getBedNo() + { + return bedNo; + } + + public void setBedNo(String bedNo) + { + this.bedNo = bedNo; + } + + public Date getAssignedTime() + { + return assignedTime; + } + + public void setAssignedTime(Date assignedTime) + { + this.assignedTime = assignedTime; + } + + public Integer getAccommodationStatus() + { + return accommodationStatus; + } + + public void setAccommodationStatus(Integer accommodationStatus) + { + this.accommodationStatus = accommodationStatus; + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainDormitoryInfoMapper.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainDormitoryInfoMapper.java index 3747c1b05..280d29100 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainDormitoryInfoMapper.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainDormitoryInfoMapper.java @@ -1,35 +1,62 @@ package com.ruoyi.train.mapper; import java.util.List; +import org.apache.ibatis.annotations.Param; import com.ruoyi.train.domain.TrainDormitoryInfo; +import com.ruoyi.train.domain.vo.DormitoryOccupantVO; /** - * 宿舍信息Mapper接口 - * + * 宿舍信息Mapper接口 (ISSUE-01-05 / API Contract V1.0.2) + * * @author ruoyi - * @date 2026-09-08 + * @date 2026-09-14 */ -public interface TrainDormitoryInfoMapper +public interface TrainDormitoryInfoMapper { /** * 查询宿舍信息 - * + * * @param dormId 宿舍信息主键 * @return 宿舍信息 */ public TrainDormitoryInfo selectTrainDormitoryInfoByDormId(String dormId); /** - * 查询宿舍信息列表 - * + * 查询宿舍信息列表 (包含实时聚合的 used_count 与 remaining_beds) + * * @param trainDormitoryInfo 宿舍信息 * @return 宿舍信息集合 */ public List selectTrainDormitoryInfoList(TrainDormitoryInfo trainDormitoryInfo); + /** + * 检查同楼栋下房间号是否唯一 + * + * @param dormBuilding 楼栋名称 + * @param dormRoom 房间号 + * @param dormId 排除的宿舍ID (新增时传 null) + * @return 匹配的宿舍实体 (若无返回 null) + */ + public TrainDormitoryInfo checkBuildingRoomUnique(@Param("dormBuilding") String dormBuilding, @Param("dormRoom") String dormRoom, @Param("dormId") String dormId); + + /** + * 查询指定宿舍当前有效入住的学员列表 + * + * @param dormId 宿舍ID + * @return 入住学员列表 + */ + public List selectOccupantsByDormId(@Param("dormId") String dormId); + + /** + * 查询所有未删除且可用的宿舍列表 (用于生成级联树) + * + * @return 宿舍列表 + */ + public List selectAllAvailableDormitories(); + /** * 新增宿舍信息 - * + * * @param trainDormitoryInfo 宿舍信息 * @return 结果 */ @@ -37,23 +64,23 @@ public interface TrainDormitoryInfoMapper /** * 修改宿舍信息 - * + * * @param trainDormitoryInfo 宿舍信息 * @return 结果 */ public int updateTrainDormitoryInfo(TrainDormitoryInfo trainDormitoryInfo); /** - * 删除宿舍信息 - * + * 软删除宿舍信息 + * * @param dormId 宿舍信息主键 * @return 结果 */ public int deleteTrainDormitoryInfoByDormId(String dormId); /** - * 批量删除宿舍信息 - * + * 批量软删除宿舍信息 + * * @param dormIds 需要删除的数据主键集合 * @return 结果 */ diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainDormitoryInfoService.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainDormitoryInfoService.java index 85b4c53c3..48887d414 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainDormitoryInfoService.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainDormitoryInfoService.java @@ -2,34 +2,64 @@ package com.ruoyi.train.service; import java.util.List; import com.ruoyi.train.domain.TrainDormitoryInfo; +import com.ruoyi.train.domain.dto.DormitoryCreateDTO; +import com.ruoyi.train.domain.vo.DormOptionsVO; +import com.ruoyi.train.domain.vo.DormitoryDetailVO; /** - * 宿舍信息Service接口 - * + * 宿舍信息Service接口 (ISSUE-01-05 / API Contract V1.0.2) + * * @author ruoyi - * @date 2026-09-08 + * @date 2026-09-14 */ -public interface ITrainDormitoryInfoService +public interface ITrainDormitoryInfoService { /** - * 查询宿舍信息 - * + * 查询宿舍信息 (基础Domain) + * * @param dormId 宿舍信息主键 * @return 宿舍信息 */ public TrainDormitoryInfo selectTrainDormitoryInfoByDormId(String dormId); + /** + * 查询宿舍房间详情 (包含基础属性与当前入住人员列表) + * + * @param dormId 宿舍信息主键 + * @return 宿舍详情VO + */ + public DormitoryDetailVO selectDormitoryDetailByDormId(String dormId); + /** * 查询宿舍信息列表 - * + * * @param trainDormitoryInfo 宿舍信息 * @return 宿舍信息集合 */ public List selectTrainDormitoryInfoList(TrainDormitoryInfo trainDormitoryInfo); /** - * 新增宿舍信息 - * + * 校验同楼栋下房间号是否唯一 + * + * @param dormBuilding 楼栋名称 + * @param dormRoom 房间号 + * @param dormId 排除的宿舍ID (新增时传 null) + * @return true 唯一 (无冲突), false 不唯一 (已存在) + */ + public boolean checkBuildingRoomUnique(String dormBuilding, String dormRoom, String dormId); + + /** + * 新增宿舍房间 (带严格校验与查重防重) + * + * @param dto 宿舍创建DTO + * @param operator 操作人 + * @return 结果 + */ + public int insertTrainDormitory(DormitoryCreateDTO dto, String operator); + + /** + * 新增宿舍信息 (兼容底层Entity) + * * @param trainDormitoryInfo 宿舍信息 * @return 结果 */ @@ -37,7 +67,7 @@ public interface ITrainDormitoryInfoService /** * 修改宿舍信息 - * + * * @param trainDormitoryInfo 宿舍信息 * @return 结果 */ @@ -45,17 +75,24 @@ public interface ITrainDormitoryInfoService /** * 批量删除宿舍信息 - * + * * @param dormIds 需要删除的宿舍信息主键集合 * @return 结果 */ public int deleteTrainDormitoryInfoByDormIds(String[] dormIds); /** - * 删除宿舍信息信息 - * + * 删除宿舍信息 + * * @param dormId 宿舍信息主键 * @return 结果 */ public int deleteTrainDormitoryInfoByDormId(String dormId); + + /** + * 查询楼栋 -> 楼层 -> 房间结构化级联选项树 + * + * @return 级联树集合 + */ + public List selectDormitoryTreeOptions(); } diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainDormitoryInfoServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainDormitoryInfoServiceImpl.java index 32a07f2f6..eb9708c01 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainDormitoryInfoServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainDormitoryInfoServiceImpl.java @@ -1,28 +1,39 @@ package com.ruoyi.train.service.impl; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; -import com.ruoyi.common.utils.DateUtils; +import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; -import com.ruoyi.train.mapper.TrainDormitoryInfoMapper; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; import com.ruoyi.train.domain.TrainDormitoryInfo; +import com.ruoyi.train.domain.dto.DormitoryCreateDTO; +import com.ruoyi.train.domain.vo.DormOptionsVO; +import com.ruoyi.train.domain.vo.DormitoryDetailVO; +import com.ruoyi.train.domain.vo.DormitoryOccupantVO; +import com.ruoyi.train.mapper.TrainDormitoryInfoMapper; import com.ruoyi.train.service.ITrainDormitoryInfoService; /** - * 宿舍信息Service业务层处理 - * + * 宿舍信息Service业务层处理 (ISSUE-01-05 / API Contract V1.0.2) + * * @author ruoyi - * @date 2026-09-08 + * @date 2026-09-14 */ @Service -public class TrainDormitoryInfoServiceImpl implements ITrainDormitoryInfoService +public class TrainDormitoryInfoServiceImpl implements ITrainDormitoryInfoService { @Autowired private TrainDormitoryInfoMapper trainDormitoryInfoMapper; /** - * 查询宿舍信息 - * + * 查询宿舍信息 (基础Domain) + * * @param dormId 宿舍信息主键 * @return 宿舍信息 */ @@ -33,8 +44,44 @@ public class TrainDormitoryInfoServiceImpl implements ITrainDormitoryInfoService } /** - * 查询宿舍信息列表 - * + * 查询宿舍房间详情 (包含基础属性与当前入住人员列表) + * + * @param dormId 宿舍信息主键 + * @return 宿舍详情VO + */ + @Override + public DormitoryDetailVO selectDormitoryDetailByDormId(String dormId) + { + TrainDormitoryInfo dorm = trainDormitoryInfoMapper.selectTrainDormitoryInfoByDormId(dormId); + if (dorm == null) + { + return null; + } + + DormitoryDetailVO detailVO = new DormitoryDetailVO(); + detailVO.setDormId(dorm.getDormId()); + detailVO.setDormBuilding(dorm.getDormBuilding()); + detailVO.setDormFloor(dorm.getDormFloor()); + detailVO.setDormRoom(dorm.getDormRoom()); + detailVO.setCapacity(dorm.getCapacity()); + detailVO.setUsedCount(dorm.getUsedCount()); + detailVO.setRemainingBeds(dorm.getRemainingBeds()); + detailVO.setGenderType(dorm.getGenderType()); + detailVO.setLockDeviceSn(dorm.getLockDeviceSn()); + detailVO.setDormStatus(dorm.getDormStatus()); + detailVO.setRemark(dorm.getRemark()); + detailVO.setCreateTime(dorm.getCreateTime()); + + // 查询当前有效入住学员列表 + List occupants = trainDormitoryInfoMapper.selectOccupantsByDormId(dormId); + detailVO.setOccupants(occupants != null ? occupants : new ArrayList<>()); + + return detailVO; + } + + /** + * 查询宿舍信息列表 (包含实时聚合的 used_count 与 remaining_beds) + * * @param trainDormitoryInfo 宿舍信息 * @return 宿舍信息 */ @@ -45,21 +92,103 @@ public class TrainDormitoryInfoServiceImpl implements ITrainDormitoryInfoService } /** - * 新增宿舍信息 - * + * 校验同楼栋下房间号是否唯一 + * + * @param dormBuilding 楼栋名称 + * @param dormRoom 房间号 + * @param dormId 排除的宿舍ID (新增时传 null) + * @return true 唯一 (无冲突), false 不唯一 (已存在) + */ + @Override + public boolean checkBuildingRoomUnique(String dormBuilding, String dormRoom, String dormId) + { + if (StringUtils.isEmpty(dormBuilding) || StringUtils.isEmpty(dormRoom)) + { + return true; + } + TrainDormitoryInfo info = trainDormitoryInfoMapper.checkBuildingRoomUnique(dormBuilding, dormRoom, dormId); + return info == null; + } + + /** + * 新增宿舍房间 (带严格校验与查重防重) + * + * @param dto 宿舍创建DTO + * @param operator 操作人 + * @return 结果 + */ + @Override + public int insertTrainDormitory(DormitoryCreateDTO dto, String operator) + { + // 1. 容量校验:必须在 1~8 之间 + if (dto.getCapacity() == null || dto.getCapacity() < 1 || dto.getCapacity() > 8) + { + throw new ServiceException("房间床位容量必须在1至8之间"); + } + + // 2. 适用性别校验:0:不限, 1:男, 2:女 + if (dto.getGenderType() == null || dto.getGenderType() < 0 || dto.getGenderType() > 2) + { + throw new ServiceException("适用性别取值不合法"); + } + + String building = dto.getDormBuilding().trim(); + String room = dto.getDormRoom().trim(); + + // 3. 同楼栋下房间号查重 (抛出错误码 40006) + if (!checkBuildingRoomUnique(building, room, null)) + { + throw new ServiceException("同一楼栋下该宿舍房号已存在", 40006); + } + + // 4. 构建实体 + TrainDormitoryInfo info = new TrainDormitoryInfo(); + info.setDormId(IdUtils.fastSimpleUUID()); + info.setDormBuilding(building); + info.setDormFloor(dto.getDormFloor().trim()); + info.setDormRoom(room); + info.setCapacity(dto.getCapacity()); + info.setGenderType(dto.getGenderType()); + info.setLockDeviceSn(StringUtils.trimToNull(dto.getLockDeviceSn())); + info.setDormStatus(1); // 默认可用 + info.setDelFlag("0"); + info.setCreateBy(operator); + info.setCreateTime(DateUtils.getNowDate()); + try + { + return trainDormitoryInfoMapper.insertTrainDormitoryInfo(info); + } + catch (DuplicateKeyException e) + { + throw new ServiceException("同一楼栋下该宿舍房号已存在", 40006); + } + } + + /** + * 新增宿舍信息 (兼容底层Entity) + * * @param trainDormitoryInfo 宿舍信息 * @return 结果 */ @Override public int insertTrainDormitoryInfo(TrainDormitoryInfo trainDormitoryInfo) { + if (StringUtils.isEmpty(trainDormitoryInfo.getDormId())) + { + trainDormitoryInfo.setDormId(IdUtils.fastSimpleUUID()); + } + if (trainDormitoryInfo.getDormStatus() == null) + { + trainDormitoryInfo.setDormStatus(1); + } + trainDormitoryInfo.setDelFlag("0"); trainDormitoryInfo.setCreateTime(DateUtils.getNowDate()); return trainDormitoryInfoMapper.insertTrainDormitoryInfo(trainDormitoryInfo); } /** * 修改宿舍信息 - * + * * @param trainDormitoryInfo 宿舍信息 * @return 结果 */ @@ -72,7 +201,7 @@ public class TrainDormitoryInfoServiceImpl implements ITrainDormitoryInfoService /** * 批量删除宿舍信息 - * + * * @param dormIds 需要删除的宿舍信息主键 * @return 结果 */ @@ -83,8 +212,8 @@ public class TrainDormitoryInfoServiceImpl implements ITrainDormitoryInfoService } /** - * 删除宿舍信息信息 - * + * 删除宿舍信息 + * * @param dormId 宿舍信息主键 * @return 结果 */ @@ -93,4 +222,75 @@ public class TrainDormitoryInfoServiceImpl implements ITrainDormitoryInfoService { return trainDormitoryInfoMapper.deleteTrainDormitoryInfoByDormId(dormId); } + + /** + * 查询楼栋 -> 楼层 -> 房间结构化级联选项树 + * + * @return 级联树集合 + */ + @Override + public List selectDormitoryTreeOptions() + { + List allDorms = trainDormitoryInfoMapper.selectAllAvailableDormitories(); + if (allDorms == null || allDorms.isEmpty()) + { + return new ArrayList<>(); + } + + // 使用 LinkedHashMap 保持有序分组:Building -> Floor -> List + Map>> buildingMap = new LinkedHashMap<>(); + + for (TrainDormitoryInfo dorm : allDorms) + { + String building = dorm.getDormBuilding(); + String floor = dorm.getDormFloor(); + + buildingMap + .computeIfAbsent(building, k -> new LinkedHashMap<>()) + .computeIfAbsent(floor, k -> new ArrayList<>()) + .add(dorm); + } + + List resultTree = new ArrayList<>(); + + for (Map.Entry>> bEntry : buildingMap.entrySet()) + { + String buildingName = bEntry.getKey(); + DormOptionsVO buildingNode = new DormOptionsVO(buildingName, buildingName, "building"); + + List floorNodeList = new ArrayList<>(); + for (Map.Entry> fEntry : bEntry.getValue().entrySet()) + { + String floorName = fEntry.getKey(); + String floorLabel = floorName.endsWith("层") || floorName.endsWith("楼") ? floorName : floorName + "层"; + DormOptionsVO floorNode = new DormOptionsVO(floorName, floorLabel, "floor"); + + List roomNodeList = new ArrayList<>(); + for (TrainDormitoryInfo dorm : fEntry.getValue()) + { + String roomLabel = dorm.getDormRoom().endsWith("室") ? dorm.getDormRoom() : dorm.getDormRoom() + "室"; + DormOptionsVO roomNode = new DormOptionsVO(dorm.getDormId(), roomLabel, "room"); + roomNode.setDormId(dorm.getDormId()); + roomNode.setDormBuilding(dorm.getDormBuilding()); + roomNode.setDormFloor(dorm.getDormFloor()); + roomNode.setDormRoom(dorm.getDormRoom()); + roomNode.setCapacity(dorm.getCapacity()); + roomNode.setUsedCount(dorm.getUsedCount()); + roomNode.setRemainingBeds(dorm.getRemainingBeds()); + roomNode.setGenderType(dorm.getGenderType()); + roomNode.setLockDeviceSn(dorm.getLockDeviceSn()); + // 房间节点为叶子节点,children 置为 null 便于前端 Cascader 渲染 + roomNode.setChildren(null); + + roomNodeList.add(roomNode); + } + floorNode.setChildren(roomNodeList); + floorNodeList.add(floorNode); + } + buildingNode.setChildren(floorNodeList); + resultTree.add(buildingNode); + } + + return resultTree; + } } diff --git a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainDormitoryInfoMapper.xml b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainDormitoryInfoMapper.xml index 9731555b2..29aca8a72 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainDormitoryInfoMapper.xml +++ b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainDormitoryInfoMapper.xml @@ -3,98 +3,177 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> - + - - - - - - - - - - - + + + + + + + + + + + + + + + + - select dorm_id, dorm_building, dorm_room, bed_total, bed_used, lock_device_sn, dorm_status, create_user, create_time, update_time, remark from train_dormitory_info + SELECT + d.dorm_id, + d.dorm_building, + d.dorm_floor, + d.dorm_room, + d.capacity, + d.gender_type, + d.lock_device_sn, + d.dorm_status, + d.del_flag, + d.create_by, + d.create_time, + d.update_by, + d.update_time, + d.remark, + COALESCE(acc.used_count, 0) AS used_count, + (d.capacity - COALESCE(acc.used_count, 0)) AS remaining_beds + FROM train_dormitory_info d + LEFT JOIN ( + SELECT dorm_id, COUNT(1) AS used_count + FROM train_accommodation + WHERE accommodation_status IN (0, 1) + GROUP BY dorm_id + ) acc ON d.dorm_id = acc.dorm_id - + + + + + + + - insert into train_dormitory_info + INSERT INTO train_dormitory_info dorm_id, - dorm_building, + dorm_building, + dorm_floor, dorm_room, - bed_total, - bed_used, + capacity, + gender_type, lock_device_sn, dorm_status, - create_user, + del_flag, + create_by, create_time, + update_by, update_time, remark, - + #{dormId}, - #{dormBuilding}, + #{dormBuilding}, + #{dormFloor}, #{dormRoom}, - #{bedTotal}, - #{bedUsed}, + #{capacity}, + #{genderType}, #{lockDeviceSn}, #{dormStatus}, - #{createUser}, + #{delFlag}, + #{createBy}, #{createTime}, + #{updateBy}, #{updateTime}, #{remark}, - + - update train_dormitory_info + UPDATE train_dormitory_info - dorm_building = #{dormBuilding}, + dorm_building = #{dormBuilding}, + dorm_floor = #{dormFloor}, dorm_room = #{dormRoom}, - bed_total = #{bedTotal}, - bed_used = #{bedUsed}, + capacity = #{capacity}, + gender_type = #{genderType}, lock_device_sn = #{lockDeviceSn}, dorm_status = #{dormStatus}, - create_user = #{createUser}, - create_time = #{createTime}, + del_flag = #{delFlag}, + update_by = #{updateBy}, update_time = #{updateTime}, remark = #{remark}, - where dorm_id = #{dormId} + WHERE dorm_id = #{dormId} - - delete from train_dormitory_info where dorm_id = #{dormId} - + + UPDATE train_dormitory_info SET del_flag = '2' WHERE dorm_id = #{dormId} + - - delete from train_dormitory_info where dorm_id in + + UPDATE train_dormitory_info SET del_flag = '2' WHERE dorm_id IN #{dormId} - + \ No newline at end of file diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/DormitoryServiceTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/DormitoryServiceTest.java new file mode 100644 index 000000000..1ee8a2645 --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/DormitoryServiceTest.java @@ -0,0 +1,426 @@ +package com.ruoyi.train.service.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.train.domain.TrainDormitoryInfo; +import com.ruoyi.train.domain.dto.DormitoryCreateDTO; +import com.ruoyi.train.domain.vo.DormOptionsVO; +import com.ruoyi.train.domain.vo.DormitoryDetailVO; +import com.ruoyi.train.domain.vo.DormitoryOccupantVO; +import com.ruoyi.train.mapper.TrainDormitoryInfoMapper; + +/** + * ISSUE-01-05 宿舍房间基础资源单元测试 (DormitoryApiTest / ServiceTest) + * + * @author ruoyi + */ +public class DormitoryServiceTest +{ + private TrainDormitoryInfoServiceImpl dormitoryService; + private MockDormitoryMapper mockMapper; + + @Before + public void setUp() throws Exception + { + dormitoryService = new TrainDormitoryInfoServiceImpl(); + mockMapper = new MockDormitoryMapper(); + Field field = TrainDormitoryInfoServiceImpl.class.getDeclaredField("trainDormitoryInfoMapper"); + field.setAccessible(true); + field.set(dormitoryService, mockMapper); + } + + /** + * AC-01 & testAdd: 正常录入与同楼栋重复房号拦截抛 40006 + */ + @Test + public void testAddAndDuplicateInterception() + { + DormitoryCreateDTO dto = new DormitoryCreateDTO(); + dto.setDormBuilding("A栋"); + dto.setDormFloor("1"); + dto.setDormRoom("101"); + dto.setCapacity(4); + dto.setGenderType(1); // 男 + dto.setLockDeviceSn("LOCK-SN-101"); + dto.setRemark("标准4人间"); + + // 1. 首次新增成功 + int result = dormitoryService.insertTrainDormitory(dto, "admin"); + assertEquals(1, result); + + TrainDormitoryInfo saved = mockMapper.findByBuildingAndRoom("A栋", "101"); + assertNotNull(saved); + assertEquals("A栋", saved.getDormBuilding()); + assertEquals("101", saved.getDormRoom()); + assertEquals(Integer.valueOf(4), saved.getCapacity()); + assertEquals(Integer.valueOf(1), saved.getGenderType()); + assertEquals("LOCK-SN-101", saved.getLockDeviceSn()); + assertEquals("admin", saved.getCreateBy()); + + // 2. 再次提交相同楼栋与房号 A栋-101,必须拦截并抛出错误码 40006 (DORM_ROOM_DUPLICATED) + try + { + dormitoryService.insertTrainDormitory(dto, "admin"); + fail("同楼栋房号重复未拦截"); + } + catch (ServiceException e) + { + assertEquals(Integer.valueOf(40006), e.getCode()); + assertTrue(e.getMessage().contains("同一楼栋下该宿舍房号已存在")); + } + + // 3. 提交不同楼栋同房号 B栋-101,允许新增成功 + DormitoryCreateDTO dtoB = new DormitoryCreateDTO(); + dtoB.setDormBuilding("B栋"); + dtoB.setDormFloor("1"); + dtoB.setDormRoom("101"); + dtoB.setCapacity(4); + dtoB.setGenderType(2); // 女 + int resultB = dormitoryService.insertTrainDormitory(dtoB, "admin"); + assertEquals(1, resultB); + } + + /** + * 验证高并发底层唯一约束冲突 DuplicateKeyException 是否安全转换为 40006 业务异常 + */ + @Test + public void testConcurrentInsertDuplicateKeyConversion() + { + DormitoryCreateDTO dto = new DormitoryCreateDTO(); + dto.setDormBuilding("D栋"); + dto.setDormFloor("1"); + dto.setDormRoom("101"); + dto.setCapacity(4); + dto.setGenderType(1); + + // 模拟高并发下绕过前置查重、底层数据库抛出唯一约束冲突异常 + mockMapper.setSimulateDuplicateKey(true); + + try + { + dormitoryService.insertTrainDormitory(dto, "admin"); + fail("底层唯一键冲突未转换为 40006 业务异常"); + } + catch (ServiceException e) + { + assertEquals(Integer.valueOf(40006), e.getCode()); + assertTrue(e.getMessage().contains("同一楼栋下该宿舍房号已存在")); + } + } + + /** + * 容量与性别边界校验 + */ + @Test + public void testCapacityAndGenderValidation() + { + DormitoryCreateDTO dto = new DormitoryCreateDTO(); + dto.setDormBuilding("A栋"); + dto.setDormFloor("2"); + dto.setDormRoom("201"); + dto.setGenderType(1); + + // 容量为0 + dto.setCapacity(0); + try + { + dormitoryService.insertTrainDormitory(dto, "admin"); + fail("容量为0未拦截"); + } + catch (ServiceException e) + { + assertTrue(e.getMessage().contains("必须在1至8之间")); + } + + // 容量为9 + dto.setCapacity(9); + try + { + dormitoryService.insertTrainDormitory(dto, "admin"); + fail("容量为9未拦截"); + } + catch (ServiceException e) + { + assertTrue(e.getMessage().contains("必须在1至8之间")); + } + + // 性别非法 + dto.setCapacity(4); + dto.setGenderType(5); + try + { + dormitoryService.insertTrainDormitory(dto, "admin"); + fail("非法性别未拦截"); + } + catch (ServiceException e) + { + assertTrue(e.getMessage().contains("适用性别取值不合法")); + } + } + + /** + * testList: 分页返回房间、核定床位、已住与剩余 + */ + @Test + public void testList() + { + // 准备一间房间,capacity = 4,已有 3 人入住 + TrainDormitoryInfo dorm = new TrainDormitoryInfo(); + dorm.setDormId("dorm-001"); + dorm.setDormBuilding("A栋"); + dorm.setDormFloor("1"); + dorm.setDormRoom("102"); + dorm.setCapacity(4); + dorm.setGenderType(1); + dorm.setUsedCount(3); + dorm.setRemainingBeds(1); + dorm.setDormStatus(1); + dorm.setDelFlag("0"); + mockMapper.save(dorm); + + List list = dormitoryService.selectTrainDormitoryInfoList(new TrainDormitoryInfo()); + assertNotNull(list); + assertEquals(1, list.size()); + TrainDormitoryInfo item = list.get(0); + assertEquals(Integer.valueOf(4), item.getCapacity()); + assertEquals(Integer.valueOf(3), item.getUsedCount()); + assertEquals(Integer.valueOf(1), item.getRemainingBeds()); + } + + /** + * testGet: 回显房间当前入住学员列表 + */ + @Test + public void testGet() + { + TrainDormitoryInfo dorm = new TrainDormitoryInfo(); + dorm.setDormId("dorm-detail-01"); + dorm.setDormBuilding("C栋"); + dorm.setDormFloor("3"); + dorm.setDormRoom("301"); + dorm.setCapacity(4); + dorm.setGenderType(1); + dorm.setUsedCount(1); + dorm.setRemainingBeds(3); + dorm.setDormStatus(1); + dorm.setDelFlag("0"); + mockMapper.save(dorm); + + DormitoryOccupantVO occupant = new DormitoryOccupantVO(); + occupant.setEnrollmentId(1001L); + occupant.setStudentId("stu-01"); + occupant.setStudentName("张三"); + occupant.setGender("0"); // 男 + occupant.setOrgName("上海机务段"); + occupant.setBedNo("1号床"); + occupant.setAssignedTime(new Date()); + occupant.setAccommodationStatus(1); + mockMapper.addOccupant("dorm-detail-01", occupant); + + DormitoryDetailVO detail = dormitoryService.selectDormitoryDetailByDormId("dorm-detail-01"); + assertNotNull(detail); + assertEquals("C栋", detail.getDormBuilding()); + assertEquals("301", detail.getDormRoom()); + assertEquals(1, detail.getOccupants().size()); + assertEquals("张三", detail.getOccupants().get(0).getStudentName()); + assertEquals("1号床", detail.getOccupants().get(0).getBedNo()); + } + + /** + * AC-02 & testOptions: 结构化返回楼栋-楼层-房间级联树 + */ + @Test + public void testOptions() + { + // 构造 A栋1层101, A栋1层102, A栋2层201, B栋1层101 + createDormEntity("d1", "A栋", "1", "101", 4, 1); + createDormEntity("d2", "A栋", "1", "102", 4, 1); + createDormEntity("d3", "A栋", "2", "201", 4, 1); + createDormEntity("d4", "B栋", "1", "101", 2, 2); + + List tree = dormitoryService.selectDormitoryTreeOptions(); + assertNotNull(tree); + // 两个楼栋:A栋与B栋 + assertEquals(2, tree.size()); + + DormOptionsVO buildingA = tree.get(0); + assertEquals("A栋", buildingA.getLabel()); + assertEquals("building", buildingA.getType()); + assertNotNull(buildingA.getChildren()); + assertEquals(2, buildingA.getChildren().size()); // 1层、2层 + + DormOptionsVO floor1 = buildingA.getChildren().get(0); + assertEquals("1", floor1.getValue()); + assertEquals("1层", floor1.getLabel()); + assertEquals("floor", floor1.getType()); + assertNotNull(floor1.getChildren()); + assertEquals(2, floor1.getChildren().size()); // 101室、102室 + + DormOptionsVO room101 = floor1.getChildren().get(0); + assertEquals("d1", room101.getValue()); + assertEquals("101室", room101.getLabel()); + assertEquals("room", room101.getType()); + assertNull(room101.getChildren()); // 叶子节点无 children + } + + private void createDormEntity(String id, String building, String floor, String room, int capacity, int gender) + { + TrainDormitoryInfo dorm = new TrainDormitoryInfo(); + dorm.setDormId(id); + dorm.setDormBuilding(building); + dorm.setDormFloor(floor); + dorm.setDormRoom(room); + dorm.setCapacity(capacity); + dorm.setGenderType(gender); + dorm.setUsedCount(0); + dorm.setRemainingBeds(capacity); + dorm.setDormStatus(1); + dorm.setDelFlag("0"); + mockMapper.save(dorm); + } + + /** + * 内存 Mock Mapper 实现 + */ + private static class MockDormitoryMapper implements TrainDormitoryInfoMapper + { + private final Map store = new java.util.LinkedHashMap<>(); + private final Map> occupantsMap = new HashMap<>(); + private boolean simulateDuplicateKey = false; + + public void setSimulateDuplicateKey(boolean simulateDuplicateKey) + { + this.simulateDuplicateKey = simulateDuplicateKey; + } + + public void save(TrainDormitoryInfo dorm) + { + store.put(dorm.getDormId(), dorm); + } + + public void addOccupant(String dormId, DormitoryOccupantVO occupant) + { + occupantsMap.computeIfAbsent(dormId, k -> new ArrayList<>()).add(occupant); + } + + public TrainDormitoryInfo findByBuildingAndRoom(String building, String room) + { + for (TrainDormitoryInfo d : store.values()) + { + if ("0".equals(d.getDelFlag()) && building.equals(d.getDormBuilding()) && room.equals(d.getDormRoom())) + { + return d; + } + } + return null; + } + + @Override + public TrainDormitoryInfo selectTrainDormitoryInfoByDormId(String dormId) + { + return store.get(dormId); + } + + @Override + public List selectTrainDormitoryInfoList(TrainDormitoryInfo query) + { + List list = new ArrayList<>(); + for (TrainDormitoryInfo d : store.values()) + { + if ("0".equals(d.getDelFlag())) + { + list.add(d); + } + } + return list; + } + + @Override + public TrainDormitoryInfo checkBuildingRoomUnique(String dormBuilding, String dormRoom, String dormId) + { + for (TrainDormitoryInfo d : store.values()) + { + if ("0".equals(d.getDelFlag()) && dormBuilding.equals(d.getDormBuilding()) && dormRoom.equals(d.getDormRoom())) + { + if (dormId == null || !dormId.equals(d.getDormId())) + { + return d; + } + } + } + return null; + } + + @Override + public List selectOccupantsByDormId(String dormId) + { + return occupantsMap.getOrDefault(dormId, new ArrayList<>()); + } + + @Override + public List selectAllAvailableDormitories() + { + List list = new ArrayList<>(); + for (TrainDormitoryInfo d : store.values()) + { + if ("0".equals(d.getDelFlag()) && Integer.valueOf(1).equals(d.getDormStatus())) + { + list.add(d); + } + } + return list; + } + + @Override + public int insertTrainDormitoryInfo(TrainDormitoryInfo trainDormitoryInfo) + { + if (simulateDuplicateKey) + { + throw new org.springframework.dao.DuplicateKeyException("Mock unique constraint violation (dorm_building, dorm_room)"); + } + store.put(trainDormitoryInfo.getDormId(), trainDormitoryInfo); + return 1; + } + + @Override + public int updateTrainDormitoryInfo(TrainDormitoryInfo trainDormitoryInfo) + { + store.put(trainDormitoryInfo.getDormId(), trainDormitoryInfo); + return 1; + } + + @Override + public int deleteTrainDormitoryInfoByDormId(String dormId) + { + TrainDormitoryInfo d = store.get(dormId); + if (d != null) + { + d.setDelFlag("2"); + } + return 1; + } + + @Override + public int deleteTrainDormitoryInfoByDormIds(String[] dormIds) + { + for (String id : dormIds) + { + deleteTrainDormitoryInfoByDormId(id); + } + return dormIds.length; + } + } +} diff --git a/build_ui/src/api/train/dormitory.js b/build_ui/src/api/train/dormitory.js index 59fc780e3..bf42dfb94 100644 --- a/build_ui/src/api/train/dormitory.js +++ b/build_ui/src/api/train/dormitory.js @@ -1,53 +1,35 @@ import request from '@/utils/request' -// 查询宿舍信息列表 +// 分页查询宿舍房间列表 (P0) export function listDormitory(query) { return request({ - url: '/train/dorminfo/list', + url: '/train/dormitory/list', method: 'get', params: query }) } -// 查询宿舍信息详细 +// 查询宿舍详细信息及当前入住人员列表 (P0) export function getDormitory(dormId) { return request({ - url: '/train/dorminfo/' + dormId, + url: '/train/dormitory/' + dormId, method: 'get' }) } -// 新增宿舍信息 +// 新增宿舍房间 (P0) export function addDormitory(data) { return request({ - url: '/train/dorminfo', + url: '/train/dormitory', method: 'post', data: data }) } -// 修改宿舍信息 -export function updateDormitory(data) { +// 获取楼栋、楼层级联下拉数据 (P0) +export function getDormitoryOptions() { return request({ - url: '/train/dorminfo', - method: 'put', - data: data - }) -} - -// 删除宿舍信息 -export function delDormitory(dormId) { - return request({ - url: '/train/dorminfo/' + dormId, - method: 'delete' - }) -} - -// 导出宿舍信息 -export function exportDormitory(query) { - return request({ - url: '/train/dorminfo/export', - method: 'post', - params: query + url: '/train/dormitory/options', + method: 'get' }) } diff --git a/build_ui/src/views/train/dormitory/index.vue b/build_ui/src/views/train/dormitory/index.vue index 001782e1c..f39f06d2c 100644 --- a/build_ui/src/views/train/dormitory/index.vue +++ b/build_ui/src/views/train/dormitory/index.vue @@ -1,6 +1,6 @@