新增住宿:对齐住宿与门禁授权领域模型
This commit is contained in:
1 parent
cc911186aa
commit
5de1d2a142
21 files changed
+3345
-231
No files matched your search
+160
@@ -0,0 +1,160 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
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.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.train.domain.TrainAccommodation;
|
||||
import com.ruoyi.train.service.ITrainAccommodationService;
|
||||
|
||||
/**
|
||||
* 住宿安排 Controller
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-15
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/train/accommodation")
|
||||
public class TrainAccommodationController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ITrainAccommodationService trainAccommodationService;
|
||||
|
||||
/**
|
||||
* 查询住宿安排列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:accommodation:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(TrainAccommodation accommodation)
|
||||
{
|
||||
startPage();
|
||||
List<TrainAccommodation> list = trainAccommodationService.selectTrainAccommodationList(accommodation);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取住宿安排详细信息
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:accommodation:query')")
|
||||
@GetMapping("/{accommodationId}")
|
||||
public AjaxResult getInfo(@PathVariable("accommodationId") Long accommodationId)
|
||||
{
|
||||
return success(trainAccommodationService.selectTrainAccommodationByAccommodationId(accommodationId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参训档案ID查询住宿安排
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:accommodation:query')")
|
||||
@GetMapping("/enrollment/{enrollmentId}")
|
||||
public AjaxResult getByEnrollmentId(@PathVariable("enrollmentId") Long enrollmentId)
|
||||
{
|
||||
return success(trainAccommodationService.selectActiveAccommodationByEnrollmentId(enrollmentId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参训档案ID查询该学员全部住宿历史
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:accommodation:query')")
|
||||
@GetMapping("/enrollment/{enrollmentId}/history")
|
||||
public AjaxResult getHistoryByEnrollmentId(@PathVariable("enrollmentId") Long enrollmentId)
|
||||
{
|
||||
return success(trainAccommodationService.selectAccommodationHistoryByEnrollmentId(enrollmentId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增住宿安排
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:accommodation:add')")
|
||||
@Log(title = "住宿安排", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody TrainAccommodation accommodation)
|
||||
{
|
||||
accommodation.setCreateBy(getUsernameSafe());
|
||||
return toAjax(trainAccommodationService.insertTrainAccommodation(accommodation));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改住宿安排
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:accommodation:edit')")
|
||||
@Log(title = "住宿安排", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody TrainAccommodation accommodation)
|
||||
{
|
||||
accommodation.setUpdateBy(getUsernameSafe());
|
||||
return toAjax(trainAccommodationService.updateTrainAccommodation(accommodation));
|
||||
}
|
||||
|
||||
/**
|
||||
* 办理入住
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:accommodation:edit')")
|
||||
@Log(title = "住宿安排-办理入住", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{accommodationId}/checkin")
|
||||
public AjaxResult checkIn(@PathVariable("accommodationId") Long accommodationId)
|
||||
{
|
||||
return toAjax(trainAccommodationService.checkIn(accommodationId, getUsernameSafe()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 办理退宿
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:accommodation:edit')")
|
||||
@Log(title = "住宿安排-办理退宿", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{accommodationId}/checkout")
|
||||
public AjaxResult checkOut(@PathVariable("accommodationId") Long accommodationId)
|
||||
{
|
||||
return toAjax(trainAccommodationService.checkOut(accommodationId, getUsernameSafe()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 办理调房
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:accommodation:edit')")
|
||||
@Log(title = "住宿安排-调房", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{accommodationId}/transfer")
|
||||
public AjaxResult transfer(@PathVariable("accommodationId") Long accommodationId,
|
||||
@RequestParam("newDormId") String newDormId,
|
||||
@RequestParam("newBedNo") String newBedNo)
|
||||
{
|
||||
return success(trainAccommodationService.transfer(accommodationId, newDormId, newBedNo, getUsernameSafe()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除住宿安排
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:accommodation:remove')")
|
||||
@Log(title = "住宿安排", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{accommodationIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] accommodationIds)
|
||||
{
|
||||
return toAjax(trainAccommodationService.deleteTrainAccommodationByAccommodationIds(accommodationIds));
|
||||
}
|
||||
|
||||
private String getUsernameSafe()
|
||||
{
|
||||
try
|
||||
{
|
||||
return SecurityUtils.getUsername();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return "admin";
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
-4
@@ -20,12 +20,13 @@ import com.ruoyi.train.domain.TrainDormAuth;
|
||||
import com.ruoyi.train.service.ITrainDormAuthService;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
|
||||
/**
|
||||
* 宿舍权限分配Controller
|
||||
*
|
||||
* 宿舍门禁授权Controller (Database V2)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-08
|
||||
* @date 2026-09-15
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/train/auth")
|
||||
@@ -69,6 +70,16 @@ public class TrainDormAuthController extends BaseController
|
||||
return success(trainDormAuthService.selectTrainDormAuthByAuthId(authId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据住宿安排ID获取门禁授权详细信息
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:auth:query')")
|
||||
@GetMapping(value = "/accommodation/{accommodationId}")
|
||||
public AjaxResult getInfoByAccommodationId(@PathVariable("accommodationId") Long accommodationId)
|
||||
{
|
||||
return success(trainDormAuthService.selectTrainDormAuthByAccommodationId(accommodationId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增宿舍权限分配
|
||||
*/
|
||||
@@ -77,6 +88,7 @@ public class TrainDormAuthController extends BaseController
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody TrainDormAuth trainDormAuth)
|
||||
{
|
||||
trainDormAuth.setCreateBy(getUsernameSafe());
|
||||
return toAjax(trainDormAuthService.insertTrainDormAuth(trainDormAuth));
|
||||
}
|
||||
|
||||
@@ -88,17 +100,41 @@ public class TrainDormAuthController extends BaseController
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody TrainDormAuth trainDormAuth)
|
||||
{
|
||||
trainDormAuth.setUpdateBy(getUsernameSafe());
|
||||
return toAjax(trainDormAuthService.updateTrainDormAuth(trainDormAuth));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销宿舍权限分配
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:auth:edit')")
|
||||
@Log(title = "宿舍权限分配-注销", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{authId}/revoke")
|
||||
public AjaxResult revoke(@PathVariable("authId") Long authId)
|
||||
{
|
||||
return toAjax(trainDormAuthService.revokeAuth(authId, getUsernameSafe()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除宿舍权限分配
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:auth:remove')")
|
||||
@Log(title = "宿舍权限分配", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{authIds}")
|
||||
@DeleteMapping("/{authIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] authIds)
|
||||
{
|
||||
return toAjax(trainDormAuthService.deleteTrainDormAuthByAuthIds(authIds));
|
||||
}
|
||||
|
||||
private String getUsernameSafe()
|
||||
{
|
||||
try
|
||||
{
|
||||
return SecurityUtils.getUsername();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return "admin";
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import com.ruoyi.train.domain.TrainAccommodation;
|
||||
import com.ruoyi.train.domain.TrainDormAuth;
|
||||
|
||||
/**
|
||||
* R.3B Wave 0 - 住宿与门禁授权 Controller API 契约与权限边界测试
|
||||
*/
|
||||
public class AccommodationAndAuthApiControllerContractTest
|
||||
{
|
||||
/**
|
||||
* 验证 TrainAccommodationController 的路径与权限契约
|
||||
*/
|
||||
@Test
|
||||
public void testAccommodationControllerContract() throws Exception
|
||||
{
|
||||
Class<TrainAccommodationController> clazz = TrainAccommodationController.class;
|
||||
RequestMapping classMapping = clazz.getAnnotation(RequestMapping.class);
|
||||
assertNotNull(classMapping);
|
||||
assertEquals("/train/accommodation", classMapping.value()[0]);
|
||||
|
||||
// 1. GET /train/accommodation/list
|
||||
Method listMethod = clazz.getMethod("list", TrainAccommodation.class);
|
||||
assertNotNull(listMethod.getAnnotation(GetMapping.class));
|
||||
assertEquals("/list", listMethod.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:accommodation:list')", listMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 2. GET /train/accommodation/{accommodationId}
|
||||
Method infoMethod = clazz.getMethod("getInfo", Long.class);
|
||||
assertNotNull(infoMethod.getAnnotation(GetMapping.class));
|
||||
assertEquals("/{accommodationId}", infoMethod.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:accommodation:query')", infoMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 3. GET /train/accommodation/enrollment/{enrollmentId}
|
||||
Method enrollMethod = clazz.getMethod("getByEnrollmentId", Long.class);
|
||||
assertNotNull(enrollMethod.getAnnotation(GetMapping.class));
|
||||
assertEquals("/enrollment/{enrollmentId}", enrollMethod.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:accommodation:query')", enrollMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 3b. GET /train/accommodation/enrollment/{enrollmentId}/history
|
||||
Method historyMethod = clazz.getMethod("getHistoryByEnrollmentId", Long.class);
|
||||
assertNotNull(historyMethod.getAnnotation(GetMapping.class));
|
||||
assertEquals("/enrollment/{enrollmentId}/history", historyMethod.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:accommodation:query')", historyMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 4. POST /train/accommodation
|
||||
Method addMethod = clazz.getMethod("add", TrainAccommodation.class);
|
||||
assertNotNull(addMethod.getAnnotation(PostMapping.class));
|
||||
assertEquals("@ss.hasPermi('train:accommodation:add')", addMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 5. POST /train/accommodation/{accommodationId}/checkin
|
||||
Method checkInMethod = clazz.getMethod("checkIn", Long.class);
|
||||
assertNotNull(checkInMethod.getAnnotation(PostMapping.class));
|
||||
assertEquals("/{accommodationId}/checkin", checkInMethod.getAnnotation(PostMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:accommodation:edit')", checkInMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 6. POST /train/accommodation/{accommodationId}/checkout
|
||||
Method checkOutMethod = clazz.getMethod("checkOut", Long.class);
|
||||
assertNotNull(checkOutMethod.getAnnotation(PostMapping.class));
|
||||
assertEquals("/{accommodationId}/checkout", checkOutMethod.getAnnotation(PostMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:accommodation:edit')", checkOutMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 7. POST /train/accommodation/{accommodationId}/transfer
|
||||
Method transferMethod = clazz.getMethod("transfer", Long.class, String.class, String.class);
|
||||
assertNotNull(transferMethod.getAnnotation(PostMapping.class));
|
||||
assertEquals("/{accommodationId}/transfer", transferMethod.getAnnotation(PostMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:accommodation:edit')", transferMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 8. DELETE /train/accommodation/{accommodationIds}
|
||||
Method removeMethod = clazz.getMethod("remove", Long[].class);
|
||||
assertNotNull(removeMethod.getAnnotation(DeleteMapping.class));
|
||||
assertEquals("/{accommodationIds}", removeMethod.getAnnotation(DeleteMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('train:accommodation:remove')", removeMethod.getAnnotation(PreAuthorize.class).value());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 TrainDormAuthController 的现有路径兼容性契约
|
||||
*/
|
||||
@Test
|
||||
public void testDormAuthControllerContract() throws Exception
|
||||
{
|
||||
Class<TrainDormAuthController> clazz = TrainDormAuthController.class;
|
||||
RequestMapping classMapping = clazz.getAnnotation(RequestMapping.class);
|
||||
assertNotNull(classMapping);
|
||||
assertEquals("/train/auth", classMapping.value()[0]);
|
||||
|
||||
// 1. GET /train/auth/list
|
||||
Method listMethod = clazz.getMethod("list", TrainDormAuth.class);
|
||||
assertNotNull(listMethod.getAnnotation(GetMapping.class));
|
||||
assertEquals("/list", listMethod.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('system:auth:list')", listMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 2. GET /train/auth/{authId}
|
||||
Method infoMethod = clazz.getMethod("getInfo", Long.class);
|
||||
assertNotNull(infoMethod.getAnnotation(GetMapping.class));
|
||||
assertEquals("/{authId}", infoMethod.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('system:auth:query')", infoMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 3. GET /train/auth/accommodation/{accommodationId}
|
||||
Method accInfoMethod = clazz.getMethod("getInfoByAccommodationId", Long.class);
|
||||
assertNotNull(accInfoMethod.getAnnotation(GetMapping.class));
|
||||
assertEquals("/accommodation/{accommodationId}", accInfoMethod.getAnnotation(GetMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('system:auth:query')", accInfoMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 4. POST /train/auth
|
||||
Method addMethod = clazz.getMethod("add", TrainDormAuth.class);
|
||||
assertNotNull(addMethod.getAnnotation(PostMapping.class));
|
||||
assertEquals("@ss.hasPermi('system:auth:add')", addMethod.getAnnotation(PreAuthorize.class).value());
|
||||
|
||||
// 5. POST /train/auth/{authId}/revoke
|
||||
Method revokeMethod = clazz.getMethod("revoke", Long.class);
|
||||
assertNotNull(revokeMethod.getAnnotation(PostMapping.class));
|
||||
assertEquals("/{authId}/revoke", revokeMethod.getAnnotation(PostMapping.class).value()[0]);
|
||||
assertEquals("@ss.hasPermi('system:auth:edit')", revokeMethod.getAnnotation(PreAuthorize.class).value());
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* R.3B Wave 0.1 - 住宿历史模型与部分唯一索引 Migration 006 静态契约保护测试
|
||||
*/
|
||||
public class AccommodationHistoryMigrationContractTest
|
||||
{
|
||||
@Test
|
||||
public void migrationMustFollowIdempotentPartialIndexContract() throws Exception
|
||||
{
|
||||
Path migrationPath = findMigration();
|
||||
assertNotNull("006 migration file must exist", migrationPath);
|
||||
|
||||
String sql = new String(Files.readAllBytes(migrationPath), StandardCharsets.UTF_8).toLowerCase();
|
||||
|
||||
// 1. 验证前置脏数据检查 (Active duplicate pre-check)
|
||||
assertContains(sql,
|
||||
"select enrollment_id",
|
||||
"from train_accommodation",
|
||||
"where accommodation_status in (0, 1)",
|
||||
"group by enrollment_id",
|
||||
"having count(*) > 1",
|
||||
"raise exception"
|
||||
);
|
||||
|
||||
// 2. 验证旧约束检查与安全删除 (Existing Constraint Guard)
|
||||
assertContains(sql,
|
||||
"conname = 'uq_accommodation_enroll'",
|
||||
"pg_get_constraintdef",
|
||||
"unique (enrollment_id)",
|
||||
"alter table train_accommodation drop constraint uq_accommodation_enroll"
|
||||
);
|
||||
|
||||
// 3. 验证新部分唯一索引碰撞保护与创建 (New Index Collision Guard & Partial Unique Index)
|
||||
assertContains(sql,
|
||||
"uq_accommodation_enroll_active",
|
||||
"create unique index uq_accommodation_enroll_active",
|
||||
"on train_accommodation (enrollment_id)",
|
||||
"where accommodation_status in (0, 1)"
|
||||
);
|
||||
|
||||
// 4. 验证幂等性结构 (Idempotency Guard)
|
||||
assertContains(sql,
|
||||
"do $$",
|
||||
"declare",
|
||||
"begin",
|
||||
"if v_con_def is not null then",
|
||||
"if v_idx_def is not null then",
|
||||
"end $$;"
|
||||
);
|
||||
|
||||
// 5. 绝对禁止对业务行进行破坏性操作 (No data loss)
|
||||
assertFalse("Migration must not truncate tables", sql.contains("truncate table"));
|
||||
assertFalse("Migration must not delete business rows", sql.contains("delete from train_accommodation"));
|
||||
assertFalse("Migration must not update business rows blindly", sql.contains("update train_accommodation set"));
|
||||
}
|
||||
|
||||
private Path findMigration()
|
||||
{
|
||||
for (Path path = Paths.get("").toAbsolutePath(); path != null; path = path.getParent())
|
||||
{
|
||||
Path candidate = path.resolve("RuoYi-Vue/sql/system/kingbase/006_accommodation_history_constraint.sql");
|
||||
if (Files.isRegularFile(candidate)) return candidate;
|
||||
Path candidateSub = path.resolve("sql/system/kingbase/006_accommodation_history_constraint.sql");
|
||||
if (Files.isRegularFile(candidateSub)) return candidateSub;
|
||||
}
|
||||
throw new IllegalStateException("006_accommodation_history_constraint.sql not found from working directory");
|
||||
}
|
||||
|
||||
private void assertContains(String sql, String... tokens)
|
||||
{
|
||||
for (String token : tokens)
|
||||
{
|
||||
assertTrue("Missing migration token: " + token, sql.contains(token));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package com.ruoyi.train.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 住宿安排领域实体对象 train_accommodation (Database V2)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-15
|
||||
*/
|
||||
public class TrainAccommodation extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 住宿状态:0 预分配 */
|
||||
public static final Integer STATUS_PREALLOCATED = 0;
|
||||
|
||||
/** 住宿状态:1 已入住 */
|
||||
public static final Integer STATUS_CHECKED_IN = 1;
|
||||
|
||||
/** 住宿状态:2 已退宿 */
|
||||
public static final Integer STATUS_CHECKED_OUT = 2;
|
||||
|
||||
/** 住宿安排ID (主键,自增) */
|
||||
private Long accommodationId;
|
||||
|
||||
/** 参训档案ID (外键,关联train_enrollment.enrollment_id,唯一) */
|
||||
@Excel(name = "参训档案ID")
|
||||
private Long enrollmentId;
|
||||
|
||||
/** 宿舍ID (外键,关联train_dormitory_info.dorm_id) */
|
||||
@Excel(name = "宿舍ID")
|
||||
private String dormId;
|
||||
|
||||
/** 床位号 (如 1号床、A床等) */
|
||||
@Excel(name = "床位号")
|
||||
private String bedNo;
|
||||
|
||||
/** 分配时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "分配时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date assignedTime;
|
||||
|
||||
/** 住宿状态 (0: 预分配, 1: 已入住, 2: 已退宿) */
|
||||
@Excel(name = "住宿状态", readConverterExp = "0=预分配,1=已入住,2=已退宿")
|
||||
private Integer accommodationStatus;
|
||||
|
||||
/* ==================== 视图兼容字段 (非持久化列,联表展示) ==================== */
|
||||
|
||||
/** 学员学号 */
|
||||
private String studentId;
|
||||
|
||||
/** 学员姓名 */
|
||||
private String studentName;
|
||||
|
||||
/** 学员性别 */
|
||||
private String gender;
|
||||
|
||||
/** 所属机构/选派单位 */
|
||||
private String orgName;
|
||||
|
||||
/** 联系电话 */
|
||||
private String phone;
|
||||
|
||||
/** 培训班计划ID */
|
||||
private String planId;
|
||||
|
||||
/** 培训班计划名称 */
|
||||
private String planName;
|
||||
|
||||
/** 宿舍楼栋 */
|
||||
private String dormBuilding;
|
||||
|
||||
/** 宿舍房间号 */
|
||||
private String dormRoom;
|
||||
|
||||
public TrainAccommodation()
|
||||
{
|
||||
}
|
||||
|
||||
public TrainAccommodation(Long enrollmentId, String dormId, String bedNo)
|
||||
{
|
||||
this.enrollmentId = enrollmentId;
|
||||
this.dormId = dormId;
|
||||
this.bedNo = bedNo;
|
||||
this.assignedTime = new Date();
|
||||
this.accommodationStatus = STATUS_PREALLOCATED;
|
||||
}
|
||||
|
||||
public Long getAccommodationId()
|
||||
{
|
||||
return accommodationId;
|
||||
}
|
||||
|
||||
public void setAccommodationId(Long accommodationId)
|
||||
{
|
||||
this.accommodationId = accommodationId;
|
||||
}
|
||||
|
||||
public Long getEnrollmentId()
|
||||
{
|
||||
return enrollmentId;
|
||||
}
|
||||
|
||||
public void setEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
this.enrollmentId = enrollmentId;
|
||||
}
|
||||
|
||||
public String getDormId()
|
||||
{
|
||||
return dormId;
|
||||
}
|
||||
|
||||
public void setDormId(String dormId)
|
||||
{
|
||||
this.dormId = dormId;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 getPhone()
|
||||
{
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone)
|
||||
{
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getPlanId()
|
||||
{
|
||||
return planId;
|
||||
}
|
||||
|
||||
public void setPlanId(String planId)
|
||||
{
|
||||
this.planId = planId;
|
||||
}
|
||||
|
||||
public String getPlanName()
|
||||
{
|
||||
return planName;
|
||||
}
|
||||
|
||||
public void setPlanName(String planName)
|
||||
{
|
||||
this.planName = planName;
|
||||
}
|
||||
|
||||
public String getDormBuilding()
|
||||
{
|
||||
return dormBuilding;
|
||||
}
|
||||
|
||||
public void setDormBuilding(String dormBuilding)
|
||||
{
|
||||
this.dormBuilding = dormBuilding;
|
||||
}
|
||||
|
||||
public String getDormRoom()
|
||||
{
|
||||
return dormRoom;
|
||||
}
|
||||
|
||||
public void setDormRoom(String dormRoom)
|
||||
{
|
||||
this.dormRoom = dormRoom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
|
||||
.append("accommodationId", getAccommodationId())
|
||||
.append("enrollmentId", getEnrollmentId())
|
||||
.append("dormId", getDormId())
|
||||
.append("bedNo", getBedNo())
|
||||
.append("assignedTime", getAssignedTime())
|
||||
.append("accommodationStatus", getAccommodationStatus())
|
||||
.append("createBy", getCreateBy())
|
||||
.append("createTime", getCreateTime())
|
||||
.append("updateBy", getUpdateBy())
|
||||
.append("updateTime", getUpdateTime())
|
||||
.append("remark", getRemark())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -8,155 +8,307 @@ import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 宿舍权限分配对象 train_dorm_auth
|
||||
*
|
||||
* 宿舍门禁授权领域对象 train_dorm_auth (Database V2)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-08
|
||||
* @date 2026-09-15
|
||||
*/
|
||||
public class TrainDormAuth extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 权限分配ID */
|
||||
/** 门禁授权状态:0 待下发 (本地待同步) */
|
||||
public static final Integer STATUS_PENDING = 0;
|
||||
|
||||
/** 门禁授权状态:1 已下发 (设备端生效) */
|
||||
public static final Integer STATUS_ACTIVE = 1;
|
||||
|
||||
/** 门禁授权状态:2 下发失败 (待重试) */
|
||||
public static final Integer STATUS_FAILED = 2;
|
||||
|
||||
/** 门禁授权状态:3 已注销 (权限收回) */
|
||||
public static final Integer STATUS_REVOKED = 3;
|
||||
|
||||
/** 授权主键ID */
|
||||
private Long authId;
|
||||
|
||||
/** 学员学号,关联train_student.s_id */
|
||||
@Excel(name = "学员学号,关联train_student.s_id")
|
||||
private String studentId;
|
||||
|
||||
/** 所属开班计划ID,train_class_plan.plan_id */
|
||||
@Excel(name = "所属开班计划ID,train_class_plan.plan_id")
|
||||
private String planId;
|
||||
|
||||
/** 分配宿舍ID,train_dormitory_info.dorm_id */
|
||||
@Excel(name = "分配宿舍ID,train_dormitory_info.dorm_id")
|
||||
private String dormId;
|
||||
|
||||
/** 床位号 */
|
||||
@Excel(name = "床位号")
|
||||
private String bedNo;
|
||||
|
||||
/** 0 未下发人脸 1 已下发人脸至智能锁 2 下发失败 */
|
||||
@Excel(name = "0 未下发人脸 1 已下发人脸至智能锁 2 下发失败")
|
||||
private Integer faceSyncStatus;
|
||||
/** 住宿安排ID,外键关联 train_accommodation.accommodation_id (V2核心业务标识) */
|
||||
@Excel(name = "住宿安排ID")
|
||||
private Long accommodationId;
|
||||
|
||||
/** 权限生效时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
@Excel(name = "权限生效时间", width = 30, dateFormat = "yyyy-MM-dd")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "权限生效时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date authStart;
|
||||
|
||||
/** 权限失效时间 (开班结束自动失效) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
@Excel(name = "权限失效时间 (开班结束自动失效)", width = 30, dateFormat = "yyyy-MM-dd")
|
||||
/** 权限失效时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "权限失效时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date authEnd;
|
||||
|
||||
/** 创建管理员账号 */
|
||||
@Excel(name = "创建管理员账号")
|
||||
/** 授权同步状态 (0: 待下发, 1: 已下发, 2: 下发失败, 3: 已注销) */
|
||||
@Excel(name = "授权同步状态", readConverterExp = "0=待下发,1=已下发,2=下发失败,3=已注销")
|
||||
private Integer faceSyncStatus;
|
||||
|
||||
/** 重试次数 */
|
||||
@Excel(name = "重试次数")
|
||||
private Integer retryCount;
|
||||
|
||||
/** 最近一次同步错误信息 */
|
||||
@Excel(name = "最近同步错误")
|
||||
private String lastError;
|
||||
|
||||
/** 外部授权标识 (用于后续Adapter关联,本Wave仅持久化字段) */
|
||||
@Excel(name = "外部授权标识")
|
||||
private String externalAuthId;
|
||||
|
||||
/** 最近一次同步时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "最近同步时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date lastSyncTime;
|
||||
|
||||
/* ==================== 兼容视图字段 (非持久化列,联表查询展示与旧API兼容) ==================== */
|
||||
|
||||
/** 学员学号 (非物理列,联表自 train_student) */
|
||||
private String studentId;
|
||||
|
||||
/** 学员姓名 (非物理列,联表自 train_student) */
|
||||
private String studentName;
|
||||
|
||||
/** 开班计划ID (非物理列,联表自 train_enrollment) */
|
||||
private String planId;
|
||||
|
||||
/** 开班计划名称 (非物理列,联表自 train_class_plan) */
|
||||
private String planName;
|
||||
|
||||
/** 宿舍ID (非物理列,联表自 train_accommodation) */
|
||||
private String dormId;
|
||||
|
||||
/** 宿舍楼栋 (非物理列,联表自 train_dormitory_info) */
|
||||
private String dormBuilding;
|
||||
|
||||
/** 宿舍房间号 (非物理列,联表自 train_dormitory_info) */
|
||||
private String dormRoom;
|
||||
|
||||
/** 床位号 (非物理列,联表自 train_accommodation) */
|
||||
private String bedNo;
|
||||
|
||||
/** 创建管理员 (兼容旧字段,映射到 BaseEntity.createBy) */
|
||||
private String createUser;
|
||||
|
||||
public void setAuthId(Long authId)
|
||||
public TrainDormAuth()
|
||||
{
|
||||
this.authId = authId;
|
||||
}
|
||||
|
||||
public Long getAuthId()
|
||||
public TrainDormAuth(Long accommodationId, Date authStart, Date authEnd)
|
||||
{
|
||||
this.accommodationId = accommodationId;
|
||||
this.authStart = authStart;
|
||||
this.authEnd = authEnd;
|
||||
this.faceSyncStatus = STATUS_PENDING;
|
||||
this.retryCount = 0;
|
||||
}
|
||||
|
||||
public Long getAuthId()
|
||||
{
|
||||
return authId;
|
||||
}
|
||||
|
||||
public void setStudentId(String studentId)
|
||||
public void setAuthId(Long authId)
|
||||
{
|
||||
this.studentId = studentId;
|
||||
this.authId = authId;
|
||||
}
|
||||
|
||||
public String getStudentId()
|
||||
public Long getAccommodationId()
|
||||
{
|
||||
return studentId;
|
||||
return accommodationId;
|
||||
}
|
||||
|
||||
public void setPlanId(String planId)
|
||||
public void setAccommodationId(Long accommodationId)
|
||||
{
|
||||
this.planId = planId;
|
||||
this.accommodationId = accommodationId;
|
||||
}
|
||||
|
||||
public String getPlanId()
|
||||
{
|
||||
return planId;
|
||||
}
|
||||
|
||||
public void setDormId(String dormId)
|
||||
{
|
||||
this.dormId = dormId;
|
||||
}
|
||||
|
||||
public String getDormId()
|
||||
{
|
||||
return dormId;
|
||||
}
|
||||
|
||||
public void setBedNo(String bedNo)
|
||||
{
|
||||
this.bedNo = bedNo;
|
||||
}
|
||||
|
||||
public String getBedNo()
|
||||
{
|
||||
return bedNo;
|
||||
}
|
||||
|
||||
public void setFaceSyncStatus(Integer faceSyncStatus)
|
||||
{
|
||||
this.faceSyncStatus = faceSyncStatus;
|
||||
}
|
||||
|
||||
public Integer getFaceSyncStatus()
|
||||
{
|
||||
return faceSyncStatus;
|
||||
}
|
||||
|
||||
public void setAuthStart(Date authStart)
|
||||
{
|
||||
this.authStart = authStart;
|
||||
}
|
||||
|
||||
public Date getAuthStart()
|
||||
public Date getAuthStart()
|
||||
{
|
||||
return authStart;
|
||||
}
|
||||
|
||||
public void setAuthEnd(Date authEnd)
|
||||
public void setAuthStart(Date authStart)
|
||||
{
|
||||
this.authEnd = authEnd;
|
||||
this.authStart = authStart;
|
||||
}
|
||||
|
||||
public Date getAuthEnd()
|
||||
public Date getAuthEnd()
|
||||
{
|
||||
return authEnd;
|
||||
}
|
||||
|
||||
public void setCreateUser(String createUser)
|
||||
public void setAuthEnd(Date authEnd)
|
||||
{
|
||||
this.createUser = createUser;
|
||||
this.authEnd = authEnd;
|
||||
}
|
||||
|
||||
public String getCreateUser()
|
||||
public Integer getFaceSyncStatus()
|
||||
{
|
||||
return createUser;
|
||||
return faceSyncStatus;
|
||||
}
|
||||
|
||||
public void setFaceSyncStatus(Integer faceSyncStatus)
|
||||
{
|
||||
this.faceSyncStatus = faceSyncStatus;
|
||||
}
|
||||
|
||||
public Integer getRetryCount()
|
||||
{
|
||||
return retryCount;
|
||||
}
|
||||
|
||||
public void setRetryCount(Integer retryCount)
|
||||
{
|
||||
this.retryCount = retryCount;
|
||||
}
|
||||
|
||||
public String getLastError()
|
||||
{
|
||||
return lastError;
|
||||
}
|
||||
|
||||
public void setLastError(String lastError)
|
||||
{
|
||||
this.lastError = lastError;
|
||||
}
|
||||
|
||||
public String getExternalAuthId()
|
||||
{
|
||||
return externalAuthId;
|
||||
}
|
||||
|
||||
public void setExternalAuthId(String externalAuthId)
|
||||
{
|
||||
this.externalAuthId = externalAuthId;
|
||||
}
|
||||
|
||||
public Date getLastSyncTime()
|
||||
{
|
||||
return lastSyncTime;
|
||||
}
|
||||
|
||||
public void setLastSyncTime(Date lastSyncTime)
|
||||
{
|
||||
this.lastSyncTime = lastSyncTime;
|
||||
}
|
||||
|
||||
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 getPlanId()
|
||||
{
|
||||
return planId;
|
||||
}
|
||||
|
||||
public void setPlanId(String planId)
|
||||
{
|
||||
this.planId = planId;
|
||||
}
|
||||
|
||||
public String getPlanName()
|
||||
{
|
||||
return planName;
|
||||
}
|
||||
|
||||
public void setPlanName(String planName)
|
||||
{
|
||||
this.planName = planName;
|
||||
}
|
||||
|
||||
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 getDormRoom()
|
||||
{
|
||||
return dormRoom;
|
||||
}
|
||||
|
||||
public void setDormRoom(String dormRoom)
|
||||
{
|
||||
this.dormRoom = dormRoom;
|
||||
}
|
||||
|
||||
public String getBedNo()
|
||||
{
|
||||
return bedNo;
|
||||
}
|
||||
|
||||
public void setBedNo(String bedNo)
|
||||
{
|
||||
this.bedNo = bedNo;
|
||||
}
|
||||
|
||||
public String getCreateUser()
|
||||
{
|
||||
return createUser != null ? createUser : getCreateBy();
|
||||
}
|
||||
|
||||
public void setCreateUser(String createUser)
|
||||
{
|
||||
this.createUser = createUser;
|
||||
if (getCreateBy() == null)
|
||||
{
|
||||
setCreateBy(createUser);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
|
||||
public String toString()
|
||||
{
|
||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
|
||||
.append("authId", getAuthId())
|
||||
.append("studentId", getStudentId())
|
||||
.append("planId", getPlanId())
|
||||
.append("dormId", getDormId())
|
||||
.append("bedNo", getBedNo())
|
||||
.append("faceSyncStatus", getFaceSyncStatus())
|
||||
.append("accommodationId", getAccommodationId())
|
||||
.append("authStart", getAuthStart())
|
||||
.append("authEnd", getAuthEnd())
|
||||
.append("createUser", getCreateUser())
|
||||
.append("faceSyncStatus", getFaceSyncStatus())
|
||||
.append("retryCount", getRetryCount())
|
||||
.append("lastError", getLastError())
|
||||
.append("externalAuthId", getExternalAuthId())
|
||||
.append("lastSyncTime", getLastSyncTime())
|
||||
.append("createBy", getCreateBy())
|
||||
.append("createTime", getCreateTime())
|
||||
.append("updateBy", getUpdateBy())
|
||||
.append("updateTime", getUpdateTime())
|
||||
.append("remark", getRemark())
|
||||
.toString();
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package com.ruoyi.train.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.train.domain.TrainAccommodation;
|
||||
|
||||
/**
|
||||
* 住宿安排Mapper接口 train_accommodation
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-15
|
||||
*/
|
||||
public interface TrainAccommodationMapper
|
||||
{
|
||||
/**
|
||||
* 根据主键查询住宿安排
|
||||
*
|
||||
* @param accommodationId 住宿安排主键
|
||||
* @return 住宿安排
|
||||
*/
|
||||
public TrainAccommodation selectTrainAccommodationByAccommodationId(Long accommodationId);
|
||||
|
||||
/**
|
||||
* 根据参训档案ID查询住宿安排
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 住宿安排
|
||||
*/
|
||||
public TrainAccommodation selectTrainAccommodationByEnrollmentId(Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 根据参训档案ID查询当前有效住宿安排 (预分配或已入住)
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 住宿安排
|
||||
*/
|
||||
public TrainAccommodation selectActiveAccommodationByEnrollmentId(Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 根据参训档案ID查询该学员全部住宿历史记录 (包含退宿历史,按分配时间倒序)
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 住宿历史记录列表
|
||||
*/
|
||||
public List<TrainAccommodation> selectAccommodationHistoryByEnrollmentId(Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 查询住宿安排列表
|
||||
*
|
||||
* @param accommodation 住宿安排查询条件
|
||||
* @return 住宿安排集合
|
||||
*/
|
||||
public List<TrainAccommodation> selectTrainAccommodationList(TrainAccommodation accommodation);
|
||||
|
||||
/**
|
||||
* 根据开班计划ID查询该班级下所有有效住宿安排 (准备用于结课批量撤权等)
|
||||
*
|
||||
* @param planId 开班计划ID
|
||||
* @return 住宿安排集合
|
||||
*/
|
||||
public List<TrainAccommodation> selectActiveAccommodationsByPlanId(@Param("planId") String planId);
|
||||
|
||||
/**
|
||||
* 统计指定宿舍当前有效住宿人数 (状态为 0 预分配 或 1 已入住)
|
||||
*
|
||||
* @param dormId 宿舍ID
|
||||
* @return 有效住宿人数
|
||||
*/
|
||||
public int countActiveByDormId(@Param("dormId") String dormId);
|
||||
|
||||
/**
|
||||
* 新增住宿安排
|
||||
*
|
||||
* @param accommodation 住宿安排
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int insertTrainAccommodation(TrainAccommodation accommodation);
|
||||
|
||||
/**
|
||||
* 修改住宿安排
|
||||
*
|
||||
* @param accommodation 住宿安排
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int updateTrainAccommodation(TrainAccommodation accommodation);
|
||||
|
||||
/**
|
||||
* 更新住宿状态
|
||||
*
|
||||
* @param accommodationId 住宿安排主键
|
||||
* @param accommodationStatus 目标状态
|
||||
* @param updateBy 更新人
|
||||
* @param updateTime 更新时间
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int updateAccommodationStatus(@Param("accommodationId") Long accommodationId,
|
||||
@Param("accommodationStatus") Integer accommodationStatus,
|
||||
@Param("updateBy") String updateBy,
|
||||
@Param("updateTime") Date updateTime);
|
||||
|
||||
/**
|
||||
* 删除住宿安排
|
||||
*
|
||||
* @param accommodationId 住宿安排主键
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int deleteTrainAccommodationByAccommodationId(Long accommodationId);
|
||||
|
||||
/**
|
||||
* 批量删除住宿安排
|
||||
*
|
||||
* @param accommodationIds 需要删除的住宿安排主键集合
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int deleteTrainAccommodationByAccommodationIds(Long[] accommodationIds);
|
||||
}
|
||||
+68
-28
@@ -1,61 +1,101 @@
|
||||
package com.ruoyi.train.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.train.domain.TrainDormAuth;
|
||||
|
||||
/**
|
||||
* 宿舍权限分配Mapper接口
|
||||
*
|
||||
* 宿舍门禁授权Mapper接口 train_dorm_auth (Database V2)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-08
|
||||
* @date 2026-09-15
|
||||
*/
|
||||
public interface TrainDormAuthMapper
|
||||
public interface TrainDormAuthMapper
|
||||
{
|
||||
/**
|
||||
* 查询宿舍权限分配
|
||||
*
|
||||
* @param authId 宿舍权限分配主键
|
||||
* @return 宿舍权限分配
|
||||
* 根据主键查询宿舍门禁授权
|
||||
*
|
||||
* @param authId 授权主键
|
||||
* @return 门禁授权
|
||||
*/
|
||||
public TrainDormAuth selectTrainDormAuthByAuthId(Long authId);
|
||||
|
||||
/**
|
||||
* 查询宿舍权限分配列表
|
||||
*
|
||||
* @param trainDormAuth 宿舍权限分配
|
||||
* @return 宿舍权限分配集合
|
||||
* 根据住宿安排ID查询门禁授权 (唯一对应)
|
||||
*
|
||||
* @param accommodationId 住宿安排主键
|
||||
* @return 门禁授权
|
||||
*/
|
||||
public TrainDormAuth selectTrainDormAuthByAccommodationId(Long accommodationId);
|
||||
|
||||
/**
|
||||
* 查询宿舍门禁授权列表 (支持联表视图字段过滤)
|
||||
*
|
||||
* @param trainDormAuth 授权查询条件
|
||||
* @return 门禁授权集合
|
||||
*/
|
||||
public List<TrainDormAuth> selectTrainDormAuthList(TrainDormAuth trainDormAuth);
|
||||
|
||||
/**
|
||||
* 新增宿舍权限分配
|
||||
*
|
||||
* @param trainDormAuth 宿舍权限分配
|
||||
* @return 结果
|
||||
* 新增门禁授权记录 (严格写入 V2 持久化列)
|
||||
*
|
||||
* @param trainDormAuth 门禁授权
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int insertTrainDormAuth(TrainDormAuth trainDormAuth);
|
||||
|
||||
/**
|
||||
* 修改宿舍权限分配
|
||||
*
|
||||
* @param trainDormAuth 宿舍权限分配
|
||||
* @return 结果
|
||||
* 修改门禁授权记录 (严格更新 V2 持久化列)
|
||||
*
|
||||
* @param trainDormAuth 门禁授权
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int updateTrainDormAuth(TrainDormAuth trainDormAuth);
|
||||
|
||||
/**
|
||||
* 删除宿舍权限分配
|
||||
*
|
||||
* @param authId 宿舍权限分配主键
|
||||
* @return 结果
|
||||
* 更新同步状态与错误摘要
|
||||
*
|
||||
* @param authId 授权主键
|
||||
* @param faceSyncStatus 状态 (0:待下发, 1:已下发, 2:下发失败, 3:已注销)
|
||||
* @param lastError 错误信息
|
||||
* @param updateBy 更新人
|
||||
* @param updateTime 更新时间
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int updateSyncStatus(@Param("authId") Long authId,
|
||||
@Param("faceSyncStatus") Integer faceSyncStatus,
|
||||
@Param("lastError") String lastError,
|
||||
@Param("updateBy") String updateBy,
|
||||
@Param("updateTime") Date updateTime);
|
||||
|
||||
/**
|
||||
* 累加重试次数并记录最近错误
|
||||
*
|
||||
* @param authId 授权主键
|
||||
* @param lastError 错误信息
|
||||
* @param updateBy 更新人
|
||||
* @param updateTime 更新时间
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int increaseRetryCount(@Param("authId") Long authId,
|
||||
@Param("lastError") String lastError,
|
||||
@Param("updateBy") String updateBy,
|
||||
@Param("updateTime") Date updateTime);
|
||||
|
||||
/**
|
||||
* 删除宿舍门禁授权
|
||||
*
|
||||
* @param authId 授权主键
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int deleteTrainDormAuthByAuthId(Long authId);
|
||||
|
||||
/**
|
||||
* 批量删除宿舍权限分配
|
||||
*
|
||||
* @param authIds 需要删除的数据主键集合
|
||||
* @return 结果
|
||||
* 批量删除宿舍门禁授权
|
||||
*
|
||||
* @param authIds 需要删除的主键集合
|
||||
* @return 影响行数
|
||||
*/
|
||||
public int deleteTrainDormAuthByAuthIds(Long[] authIds);
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package com.ruoyi.train.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.train.domain.TrainAccommodation;
|
||||
|
||||
/**
|
||||
* 住宿安排Service接口 train_accommodation
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-15
|
||||
*/
|
||||
public interface ITrainAccommodationService
|
||||
{
|
||||
/**
|
||||
* 查询住宿安排详情
|
||||
*
|
||||
* @param accommodationId 住宿安排主键
|
||||
* @return 住宿安排
|
||||
*/
|
||||
public TrainAccommodation selectTrainAccommodationByAccommodationId(Long accommodationId);
|
||||
|
||||
/**
|
||||
* 根据参训档案ID查询最新住宿安排
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 住宿安排
|
||||
*/
|
||||
public TrainAccommodation selectTrainAccommodationByEnrollmentId(Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 根据参训档案ID查询当前活跃/有效住宿安排 (预分配或已入住)
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 活跃住宿安排,若无返回null
|
||||
*/
|
||||
public TrainAccommodation selectActiveAccommodationByEnrollmentId(Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 根据参训档案ID查询该学员全部住宿历史记录 (包含退宿历史,按时间倒序排序)
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 住宿历史记录列表
|
||||
*/
|
||||
public List<TrainAccommodation> selectAccommodationHistoryByEnrollmentId(Long enrollmentId);
|
||||
|
||||
/**
|
||||
* 查询住宿安排列表
|
||||
*
|
||||
* @param accommodation 住宿安排过滤条件
|
||||
* @return 住宿安排集合
|
||||
*/
|
||||
public List<TrainAccommodation> selectTrainAccommodationList(TrainAccommodation accommodation);
|
||||
|
||||
/**
|
||||
* 查询指定开班计划下所有有效住宿安排
|
||||
*
|
||||
* @param planId 开班计划ID
|
||||
* @return 有效住宿列表
|
||||
*/
|
||||
public List<TrainAccommodation> selectActiveAccommodationsByPlanId(String planId);
|
||||
|
||||
/**
|
||||
* 创建住宿事实 (带参训档案、宿舍容量与唯一活跃约束校验)
|
||||
*
|
||||
* @param accommodation 住宿事实
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertTrainAccommodation(TrainAccommodation accommodation);
|
||||
|
||||
/**
|
||||
* 修改住宿安排
|
||||
*
|
||||
* @param accommodation 住宿安排
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateTrainAccommodation(TrainAccommodation accommodation);
|
||||
|
||||
/**
|
||||
* 学员办理入住 (状态 0预分配 -> 1已入住)
|
||||
*
|
||||
* @param accommodationId 住宿安排ID
|
||||
* @param operator 操作人
|
||||
* @return 结果
|
||||
*/
|
||||
public int checkIn(Long accommodationId, String operator);
|
||||
|
||||
/**
|
||||
* 学员办理退宿 (状态 -> 2已退宿)
|
||||
*
|
||||
* @param accommodationId 住宿安排ID
|
||||
* @param operator 操作人
|
||||
* @return 结果
|
||||
*/
|
||||
public int checkOut(Long accommodationId, String operator);
|
||||
|
||||
/**
|
||||
* 调房/换房业务操作 (原子事务:旧住宿转已退宿,新住宿创建预分配/入住)
|
||||
*
|
||||
* @param oldAccommodationId 原住宿安排ID
|
||||
* @param newDormId 新宿舍ID
|
||||
* @param newBedNo 新床位号
|
||||
* @param operator 操作人
|
||||
* @return 新生成的住宿安排
|
||||
*/
|
||||
public TrainAccommodation transfer(Long oldAccommodationId, String newDormId, String newBedNo, String operator);
|
||||
|
||||
/**
|
||||
* 批量删除住宿安排 (受业务历史保护校验)
|
||||
*
|
||||
* @param accommodationIds 需要删除的主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteTrainAccommodationByAccommodationIds(Long[] accommodationIds);
|
||||
|
||||
/**
|
||||
* 删除住宿安排
|
||||
*
|
||||
* @param accommodationId 需要删除的主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteTrainAccommodationByAccommodationId(Long accommodationId);
|
||||
}
|
||||
+51
-24
@@ -4,57 +4,84 @@ import java.util.List;
|
||||
import com.ruoyi.train.domain.TrainDormAuth;
|
||||
|
||||
/**
|
||||
* 宿舍权限分配Service接口
|
||||
*
|
||||
* 宿舍门禁授权Service接口 train_dorm_auth (Database V2)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-08
|
||||
* @date 2026-09-15
|
||||
*/
|
||||
public interface ITrainDormAuthService
|
||||
public interface ITrainDormAuthService
|
||||
{
|
||||
/**
|
||||
* 查询宿舍权限分配
|
||||
*
|
||||
* @param authId 宿舍权限分配主键
|
||||
* @return 宿舍权限分配
|
||||
* 查询宿舍门禁授权
|
||||
*
|
||||
* @param authId 授权主键
|
||||
* @return 宿舍门禁授权
|
||||
*/
|
||||
public TrainDormAuth selectTrainDormAuthByAuthId(Long authId);
|
||||
|
||||
/**
|
||||
* 查询宿舍权限分配列表
|
||||
*
|
||||
* @param trainDormAuth 宿舍权限分配
|
||||
* @return 宿舍权限分配集合
|
||||
* 根据住宿安排ID查询门禁授权
|
||||
*
|
||||
* @param accommodationId 住宿安排主键
|
||||
* @return 宿舍门禁授权
|
||||
*/
|
||||
public TrainDormAuth selectTrainDormAuthByAccommodationId(Long accommodationId);
|
||||
|
||||
/**
|
||||
* 查询宿舍门禁授权列表
|
||||
*
|
||||
* @param trainDormAuth 授权查询条件
|
||||
* @return 宿舍门禁授权集合
|
||||
*/
|
||||
public List<TrainDormAuth> selectTrainDormAuthList(TrainDormAuth trainDormAuth);
|
||||
|
||||
/**
|
||||
* 新增宿舍权限分配
|
||||
*
|
||||
* @param trainDormAuth 宿舍权限分配
|
||||
* 新增宿舍门禁授权 (以住宿事实为唯一合法来源)
|
||||
*
|
||||
* @param trainDormAuth 门禁授权
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertTrainDormAuth(TrainDormAuth trainDormAuth);
|
||||
|
||||
/**
|
||||
* 修改宿舍权限分配
|
||||
*
|
||||
* @param trainDormAuth 宿舍权限分配
|
||||
* 修改宿舍门禁授权
|
||||
*
|
||||
* @param trainDormAuth 门禁授权
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateTrainDormAuth(TrainDormAuth trainDormAuth);
|
||||
|
||||
/**
|
||||
* 批量删除宿舍权限分配
|
||||
*
|
||||
* @param authIds 需要删除的宿舍权限分配主键集合
|
||||
* 记录下发失败并累加重试次数
|
||||
*
|
||||
* @param authId 授权主键
|
||||
* @param errorMessage 失败原因
|
||||
* @param operator 操作人
|
||||
* @return 结果
|
||||
*/
|
||||
public int markSyncFailed(Long authId, String errorMessage, String operator);
|
||||
|
||||
/**
|
||||
* 注销门禁授权
|
||||
*
|
||||
* @param authId 授权主键
|
||||
* @param operator 操作人
|
||||
* @return 结果
|
||||
*/
|
||||
public int revokeAuth(Long authId, String operator);
|
||||
|
||||
/**
|
||||
* 批量删除宿舍门禁授权
|
||||
*
|
||||
* @param authIds 需要删除的主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteTrainDormAuthByAuthIds(Long[] authIds);
|
||||
|
||||
/**
|
||||
* 删除宿舍权限分配信息
|
||||
*
|
||||
* @param authId 宿舍权限分配主键
|
||||
* 删除宿舍门禁授权信息
|
||||
*
|
||||
* @param authId 主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteTrainDormAuthByAuthId(Long authId);
|
||||
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.train.domain.TrainAccommodation;
|
||||
import com.ruoyi.train.domain.TrainDormitoryInfo;
|
||||
import com.ruoyi.train.domain.TrainEnrollment;
|
||||
import com.ruoyi.train.mapper.TrainAccommodationMapper;
|
||||
import com.ruoyi.train.mapper.TrainDormitoryInfoMapper;
|
||||
import com.ruoyi.train.mapper.TrainEnrollmentMapper;
|
||||
import com.ruoyi.train.service.ITrainAccommodationService;
|
||||
|
||||
/**
|
||||
* 住宿安排Service业务层处理
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-15
|
||||
*/
|
||||
@Service
|
||||
public class TrainAccommodationServiceImpl implements ITrainAccommodationService
|
||||
{
|
||||
@Autowired
|
||||
private TrainAccommodationMapper trainAccommodationMapper;
|
||||
|
||||
@Autowired
|
||||
private TrainDormitoryInfoMapper trainDormitoryInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private TrainEnrollmentMapper trainEnrollmentMapper;
|
||||
|
||||
/**
|
||||
* 查询住宿安排详情
|
||||
*
|
||||
* @param accommodationId 住宿安排主键
|
||||
* @return 住宿安排
|
||||
*/
|
||||
@Override
|
||||
public TrainAccommodation selectTrainAccommodationByAccommodationId(Long accommodationId)
|
||||
{
|
||||
return trainAccommodationMapper.selectTrainAccommodationByAccommodationId(accommodationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参训档案ID查询最新住宿安排
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 住宿安排
|
||||
*/
|
||||
@Override
|
||||
public TrainAccommodation selectTrainAccommodationByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return trainAccommodationMapper.selectTrainAccommodationByEnrollmentId(enrollmentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参训档案ID查询当前活跃/有效住宿安排
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 活跃住宿安排
|
||||
*/
|
||||
@Override
|
||||
public TrainAccommodation selectActiveAccommodationByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return trainAccommodationMapper.selectActiveAccommodationByEnrollmentId(enrollmentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参训档案ID查询该学员全部住宿历史记录 (包含退宿历史,按时间倒序排序)
|
||||
*
|
||||
* @param enrollmentId 参训档案ID
|
||||
* @return 住宿历史记录列表
|
||||
*/
|
||||
@Override
|
||||
public List<TrainAccommodation> selectAccommodationHistoryByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
return trainAccommodationMapper.selectAccommodationHistoryByEnrollmentId(enrollmentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询住宿安排列表
|
||||
*
|
||||
* @param accommodation 住宿安排
|
||||
* @return 住宿安排
|
||||
*/
|
||||
@Override
|
||||
public List<TrainAccommodation> selectTrainAccommodationList(TrainAccommodation accommodation)
|
||||
{
|
||||
return trainAccommodationMapper.selectTrainAccommodationList(accommodation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定开班计划下所有有效住宿安排
|
||||
*
|
||||
* @param planId 开班计划ID
|
||||
* @return 有效住宿列表
|
||||
*/
|
||||
@Override
|
||||
public List<TrainAccommodation> selectActiveAccommodationsByPlanId(String planId)
|
||||
{
|
||||
return trainAccommodationMapper.selectActiveAccommodationsByPlanId(planId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增住宿安排 (带前置依赖、唯一活跃约束与宿舍容量校验)
|
||||
*
|
||||
* @param accommodation 住宿安排
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int insertTrainAccommodation(TrainAccommodation accommodation)
|
||||
{
|
||||
Long enrollmentId = accommodation.getEnrollmentId();
|
||||
if (enrollmentId == null)
|
||||
{
|
||||
throw new ServiceException("参训档案ID (enrollmentId) 不能为空");
|
||||
}
|
||||
|
||||
// 1. 验证参训档案是否存在
|
||||
TrainEnrollment enrollment = trainEnrollmentMapper.selectTrainEnrollmentById(enrollmentId);
|
||||
if (enrollment == null)
|
||||
{
|
||||
throw new ServiceException("指定参训档案不存在,enrollmentId: " + enrollmentId);
|
||||
}
|
||||
|
||||
// 2. 验证唯一活跃住宿约束 (Duplicate Active Accommodation Guard)
|
||||
TrainAccommodation activeAcc = trainAccommodationMapper.selectActiveAccommodationByEnrollmentId(enrollmentId);
|
||||
if (activeAcc != null)
|
||||
{
|
||||
throw new ServiceException("该参训学员已存在有效住宿安排 (ID: " + activeAcc.getAccommodationId()
|
||||
+ ",宿舍: " + activeAcc.getDormId() + "),请先办理退宿或换房");
|
||||
}
|
||||
|
||||
// 3. 验证宿舍存在性与可用状态
|
||||
String dormId = accommodation.getDormId();
|
||||
if (dormId == null || dormId.trim().isEmpty())
|
||||
{
|
||||
throw new ServiceException("分配宿舍ID (dormId) 不能为空");
|
||||
}
|
||||
TrainDormitoryInfo dorm = trainDormitoryInfoMapper.selectTrainDormitoryInfoByDormId(dormId);
|
||||
if (dorm == null)
|
||||
{
|
||||
throw new ServiceException("指定宿舍不存在,dormId: " + dormId);
|
||||
}
|
||||
if (dorm.getDormStatus() != null && dorm.getDormStatus() == 0)
|
||||
{
|
||||
throw new ServiceException("该宿舍当前处于禁用状态,无法分配住宿");
|
||||
}
|
||||
|
||||
// 4. 验证宿舍容量边界 (Capacity Boundary Guard)
|
||||
int currentActive = trainAccommodationMapper.countActiveByDormId(dormId);
|
||||
if (dorm.getCapacity() != null && currentActive >= dorm.getCapacity())
|
||||
{
|
||||
throw new ServiceException("宿舍容量已满 (核定容量: " + dorm.getCapacity()
|
||||
+ ",当前有效人数: " + currentActive + "),无法继续分配");
|
||||
}
|
||||
|
||||
// 5. 设置默认属性
|
||||
if (accommodation.getAssignedTime() == null)
|
||||
{
|
||||
accommodation.setAssignedTime(DateUtils.getNowDate());
|
||||
}
|
||||
if (accommodation.getAccommodationStatus() == null)
|
||||
{
|
||||
accommodation.setAccommodationStatus(TrainAccommodation.STATUS_PREALLOCATED);
|
||||
}
|
||||
accommodation.setCreateTime(DateUtils.getNowDate());
|
||||
|
||||
return trainAccommodationMapper.insertTrainAccommodation(accommodation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改住宿安排
|
||||
*
|
||||
* @param accommodation 住宿安排
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int updateTrainAccommodation(TrainAccommodation accommodation)
|
||||
{
|
||||
TrainAccommodation exist = trainAccommodationMapper.selectTrainAccommodationByAccommodationId(accommodation.getAccommodationId());
|
||||
if (exist == null)
|
||||
{
|
||||
throw new ServiceException("住宿记录不存在: " + accommodation.getAccommodationId());
|
||||
}
|
||||
|
||||
// 严格遵循 API Semantics 规范:禁止通过普通 update 修改 dorm_id 来模拟调房
|
||||
if (accommodation.getDormId() != null && !accommodation.getDormId().equals(exist.getDormId()))
|
||||
{
|
||||
throw new ServiceException("变更宿舍请使用专属调房换房流程 (transfer),禁止直接修改住宿记录的宿舍ID");
|
||||
}
|
||||
|
||||
accommodation.setUpdateTime(DateUtils.getNowDate());
|
||||
return trainAccommodationMapper.updateTrainAccommodation(accommodation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 学员办理入住 (状态 0预分配 -> 1已入住)
|
||||
*
|
||||
* @param accommodationId 住宿安排ID
|
||||
* @param operator 操作人
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int checkIn(Long accommodationId, String operator)
|
||||
{
|
||||
TrainAccommodation exist = trainAccommodationMapper.selectTrainAccommodationByAccommodationId(accommodationId);
|
||||
if (exist == null)
|
||||
{
|
||||
throw new ServiceException("住宿安排不存在: " + accommodationId);
|
||||
}
|
||||
if (TrainAccommodation.STATUS_CHECKED_OUT.equals(exist.getAccommodationStatus()))
|
||||
{
|
||||
throw new ServiceException("已退宿的记录不能重新办理入住");
|
||||
}
|
||||
return trainAccommodationMapper.updateAccommodationStatus(accommodationId,
|
||||
TrainAccommodation.STATUS_CHECKED_IN, operator, DateUtils.getNowDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* 学员办理退宿 (状态 -> 2已退宿)
|
||||
*
|
||||
* @param accommodationId 住宿安排ID
|
||||
* @param operator 操作人
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int checkOut(Long accommodationId, String operator)
|
||||
{
|
||||
TrainAccommodation exist = trainAccommodationMapper.selectTrainAccommodationByAccommodationId(accommodationId);
|
||||
if (exist == null)
|
||||
{
|
||||
throw new ServiceException("住宿安排不存在: " + accommodationId);
|
||||
}
|
||||
if (TrainAccommodation.STATUS_CHECKED_OUT.equals(exist.getAccommodationStatus()))
|
||||
{
|
||||
return 1; // 已经是退宿状态
|
||||
}
|
||||
return trainAccommodationMapper.updateAccommodationStatus(accommodationId,
|
||||
TrainAccommodation.STATUS_CHECKED_OUT, operator, DateUtils.getNowDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* 调房/换房业务操作 (原子事务:旧住宿转已退宿,新住宿创建预分配/入住)
|
||||
*
|
||||
* @param oldAccommodationId 原住宿安排ID
|
||||
* @param newDormId 新宿舍ID
|
||||
* @param newBedNo 新床位号
|
||||
* @param operator 操作人
|
||||
* @return 新生成的住宿安排
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public TrainAccommodation transfer(Long oldAccommodationId, String newDormId, String newBedNo, String operator)
|
||||
{
|
||||
TrainAccommodation oldAcc = trainAccommodationMapper.selectTrainAccommodationByAccommodationId(oldAccommodationId);
|
||||
if (oldAcc == null)
|
||||
{
|
||||
throw new ServiceException("原住宿记录不存在: " + oldAccommodationId);
|
||||
}
|
||||
if (TrainAccommodation.STATUS_CHECKED_OUT.equals(oldAcc.getAccommodationStatus()))
|
||||
{
|
||||
throw new ServiceException("原住宿记录已退宿,无法办理调房");
|
||||
}
|
||||
|
||||
// 1. 结束旧住宿
|
||||
trainAccommodationMapper.updateAccommodationStatus(oldAccommodationId,
|
||||
TrainAccommodation.STATUS_CHECKED_OUT, operator, DateUtils.getNowDate());
|
||||
|
||||
// 2. 建立新住宿
|
||||
TrainAccommodation newAcc = new TrainAccommodation();
|
||||
newAcc.setEnrollmentId(oldAcc.getEnrollmentId());
|
||||
newAcc.setDormId(newDormId);
|
||||
newAcc.setBedNo(newBedNo);
|
||||
newAcc.setAssignedTime(DateUtils.getNowDate());
|
||||
newAcc.setAccommodationStatus(TrainAccommodation.STATUS_CHECKED_IN);
|
||||
newAcc.setCreateBy(operator);
|
||||
newAcc.setRemark("由原住宿记录 " + oldAccommodationId + " (宿舍 " + oldAcc.getDormId() + ") 调房迁入");
|
||||
|
||||
insertTrainAccommodation(newAcc);
|
||||
return newAcc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除住宿安排
|
||||
*
|
||||
* @param accommodationIds 需要删除的主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteTrainAccommodationByAccommodationIds(Long[] accommodationIds)
|
||||
{
|
||||
for (Long id : accommodationIds)
|
||||
{
|
||||
deleteTrainAccommodationByAccommodationId(id);
|
||||
}
|
||||
return accommodationIds.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除住宿安排
|
||||
*
|
||||
* @param accommodationId 需要删除的主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteTrainAccommodationByAccommodationId(Long accommodationId)
|
||||
{
|
||||
TrainAccommodation exist = trainAccommodationMapper.selectTrainAccommodationByAccommodationId(accommodationId);
|
||||
if (exist == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
// 业务历史保护:若为活跃已入住状态,应先退宿而不是直接物理删除
|
||||
if (TrainAccommodation.STATUS_CHECKED_IN.equals(exist.getAccommodationStatus()))
|
||||
{
|
||||
throw new ServiceException("当前学员正处于已入住状态,请先办理退宿后再执行清理");
|
||||
}
|
||||
return trainAccommodationMapper.deleteTrainAccommodationByAccommodationId(accommodationId);
|
||||
}
|
||||
}
|
||||
+134
-27
@@ -1,30 +1,38 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.train.mapper.TrainDormAuthMapper;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.train.domain.TrainAccommodation;
|
||||
import com.ruoyi.train.domain.TrainDormAuth;
|
||||
import com.ruoyi.train.mapper.TrainAccommodationMapper;
|
||||
import com.ruoyi.train.mapper.TrainDormAuthMapper;
|
||||
import com.ruoyi.train.service.ITrainDormAuthService;
|
||||
|
||||
/**
|
||||
* 宿舍权限分配Service业务层处理
|
||||
*
|
||||
* 宿舍门禁授权Service业务层处理 (Database V2)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-08
|
||||
* @date 2026-09-15
|
||||
*/
|
||||
@Service
|
||||
public class TrainDormAuthServiceImpl implements ITrainDormAuthService
|
||||
public class TrainDormAuthServiceImpl implements ITrainDormAuthService
|
||||
{
|
||||
@Autowired
|
||||
private TrainDormAuthMapper trainDormAuthMapper;
|
||||
|
||||
@Autowired
|
||||
private TrainAccommodationMapper trainAccommodationMapper;
|
||||
|
||||
/**
|
||||
* 查询宿舍权限分配
|
||||
*
|
||||
* @param authId 宿舍权限分配主键
|
||||
* @return 宿舍权限分配
|
||||
* 查询宿舍门禁授权
|
||||
*
|
||||
* @param authId 宿舍门禁授权主键
|
||||
* @return 宿舍门禁授权
|
||||
*/
|
||||
@Override
|
||||
public TrainDormAuth selectTrainDormAuthByAuthId(Long authId)
|
||||
@@ -33,10 +41,22 @@ public class TrainDormAuthServiceImpl implements ITrainDormAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询宿舍权限分配列表
|
||||
*
|
||||
* @param trainDormAuth 宿舍权限分配
|
||||
* @return 宿舍权限分配
|
||||
* 根据住宿安排ID查询门禁授权
|
||||
*
|
||||
* @param accommodationId 住宿安排主键
|
||||
* @return 宿舍门禁授权
|
||||
*/
|
||||
@Override
|
||||
public TrainDormAuth selectTrainDormAuthByAccommodationId(Long accommodationId)
|
||||
{
|
||||
return trainDormAuthMapper.selectTrainDormAuthByAccommodationId(accommodationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询宿舍门禁授权列表
|
||||
*
|
||||
* @param trainDormAuth 宿舍门禁授权
|
||||
* @return 宿舍门禁授权集合
|
||||
*/
|
||||
@Override
|
||||
public List<TrainDormAuth> selectTrainDormAuthList(TrainDormAuth trainDormAuth)
|
||||
@@ -45,35 +65,122 @@ public class TrainDormAuthServiceImpl implements ITrainDormAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增宿舍权限分配
|
||||
*
|
||||
* @param trainDormAuth 宿舍权限分配
|
||||
* 新增宿舍门禁授权 (以住宿事实为唯一业务来源,禁止绕过住宿直接创建)
|
||||
*
|
||||
* @param trainDormAuth 宿舍门禁授权
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int insertTrainDormAuth(TrainDormAuth trainDormAuth)
|
||||
{
|
||||
trainDormAuth.setCreateTime(DateUtils.getNowDate());
|
||||
Long accommodationId = trainDormAuth.getAccommodationId();
|
||||
if (accommodationId == null)
|
||||
{
|
||||
throw new ServiceException("门禁授权必须绑定住宿安排,accommodationId 不能为空");
|
||||
}
|
||||
|
||||
// 1. 校验住宿事实有效性
|
||||
TrainAccommodation accommodation = trainAccommodationMapper.selectTrainAccommodationByAccommodationId(accommodationId);
|
||||
if (accommodation == null)
|
||||
{
|
||||
throw new ServiceException("指定的住宿安排不存在,ID: " + accommodationId);
|
||||
}
|
||||
if (TrainAccommodation.STATUS_CHECKED_OUT.equals(accommodation.getAccommodationStatus()))
|
||||
{
|
||||
throw new ServiceException("该住宿安排已退宿,无法新建门禁授权");
|
||||
}
|
||||
|
||||
// 2. 校验唯一性 (每条住宿安排对应唯一授权流水)
|
||||
TrainDormAuth existAuth = trainDormAuthMapper.selectTrainDormAuthByAccommodationId(accommodationId);
|
||||
if (existAuth != null)
|
||||
{
|
||||
throw new ServiceException("该住宿安排已存在门禁授权 (AuthID: " + existAuth.getAuthId() + "),请勿重复创建");
|
||||
}
|
||||
|
||||
// 3. 初始授权状态:本地待下发 (严格遵循 No Fake Device Success 原则)
|
||||
if (trainDormAuth.getFaceSyncStatus() == null || TrainDormAuth.STATUS_ACTIVE.equals(trainDormAuth.getFaceSyncStatus()))
|
||||
{
|
||||
trainDormAuth.setFaceSyncStatus(TrainDormAuth.STATUS_PENDING);
|
||||
}
|
||||
if (trainDormAuth.getRetryCount() == null)
|
||||
{
|
||||
trainDormAuth.setRetryCount(0);
|
||||
}
|
||||
|
||||
// 4. 继承操作人和时间
|
||||
Date now = DateUtils.getNowDate();
|
||||
trainDormAuth.setCreateTime(now);
|
||||
if (trainDormAuth.getCreateUser() != null && trainDormAuth.getCreateBy() == null)
|
||||
{
|
||||
trainDormAuth.setCreateBy(trainDormAuth.getCreateUser());
|
||||
}
|
||||
|
||||
return trainDormAuthMapper.insertTrainDormAuth(trainDormAuth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改宿舍权限分配
|
||||
*
|
||||
* @param trainDormAuth 宿舍权限分配
|
||||
* 修改宿舍门禁授权
|
||||
*
|
||||
* @param trainDormAuth 宿舍门禁授权
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int updateTrainDormAuth(TrainDormAuth trainDormAuth)
|
||||
{
|
||||
TrainDormAuth exist = trainDormAuthMapper.selectTrainDormAuthByAuthId(trainDormAuth.getAuthId());
|
||||
if (exist == null)
|
||||
{
|
||||
throw new ServiceException("门禁授权记录不存在: " + trainDormAuth.getAuthId());
|
||||
}
|
||||
|
||||
trainDormAuth.setUpdateTime(DateUtils.getNowDate());
|
||||
return trainDormAuthMapper.updateTrainDormAuth(trainDormAuth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除宿舍权限分配
|
||||
*
|
||||
* @param authIds 需要删除的宿舍权限分配主键
|
||||
* 记录下发失败并累加重试次数
|
||||
*
|
||||
* @param authId 授权主键
|
||||
* @param errorMessage 失败原因
|
||||
* @param operator 操作人
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int markSyncFailed(Long authId, String errorMessage, String operator)
|
||||
{
|
||||
TrainDormAuth exist = trainDormAuthMapper.selectTrainDormAuthByAuthId(authId);
|
||||
if (exist == null)
|
||||
{
|
||||
throw new ServiceException("门禁授权记录不存在: " + authId);
|
||||
}
|
||||
trainDormAuthMapper.updateSyncStatus(authId, TrainDormAuth.STATUS_FAILED, errorMessage, operator, DateUtils.getNowDate());
|
||||
return trainDormAuthMapper.increaseRetryCount(authId, errorMessage, operator, DateUtils.getNowDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销门禁授权
|
||||
*
|
||||
* @param authId 授权主键
|
||||
* @param operator 操作人
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int revokeAuth(Long authId, String operator)
|
||||
{
|
||||
TrainDormAuth exist = trainDormAuthMapper.selectTrainDormAuthByAuthId(authId);
|
||||
if (exist == null)
|
||||
{
|
||||
throw new ServiceException("门禁授权记录不存在: " + authId);
|
||||
}
|
||||
return trainDormAuthMapper.updateSyncStatus(authId, TrainDormAuth.STATUS_REVOKED, null, operator, DateUtils.getNowDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除宿舍门禁授权
|
||||
*
|
||||
* @param authIds 需要删除的主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@@ -83,9 +190,9 @@ public class TrainDormAuthServiceImpl implements ITrainDormAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除宿舍权限分配信息
|
||||
*
|
||||
* @param authId 宿舍权限分配主键
|
||||
* 删除宿舍门禁授权信息
|
||||
*
|
||||
* @param authId 主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.train.mapper.TrainAccommodationMapper">
|
||||
|
||||
<resultMap type="TrainAccommodation" id="TrainAccommodationResult">
|
||||
<result property="accommodationId" column="accommodation_id" />
|
||||
<result property="enrollmentId" column="enrollment_id" />
|
||||
<result property="dormId" column="dorm_id" />
|
||||
<result property="bedNo" column="bed_no" />
|
||||
<result property="assignedTime" column="assigned_time" />
|
||||
<result property="accommodationStatus" column="accommodation_status" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
<result property="studentId" column="student_id" />
|
||||
<result property="studentName" column="student_name" />
|
||||
<result property="gender" column="gender" />
|
||||
<result property="orgName" column="org_name" />
|
||||
<result property="phone" column="phone" />
|
||||
<result property="planId" column="plan_id" />
|
||||
<result property="planName" column="plan_name" />
|
||||
<result property="dormBuilding" column="dorm_building" />
|
||||
<result property="dormRoom" column="dorm_room" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectTrainAccommodationVo">
|
||||
SELECT
|
||||
a.accommodation_id,
|
||||
a.enrollment_id,
|
||||
a.dorm_id,
|
||||
a.bed_no,
|
||||
a.assigned_time,
|
||||
a.accommodation_status,
|
||||
a.create_by,
|
||||
a.create_time,
|
||||
a.update_by,
|
||||
a.update_time,
|
||||
a.remark,
|
||||
e.student_id,
|
||||
s.s_name AS student_name,
|
||||
s.s_gender AS gender,
|
||||
s.org_name,
|
||||
s.s_phone AS phone,
|
||||
e.plan_id,
|
||||
p.plan_name,
|
||||
d.dorm_building,
|
||||
d.dorm_room
|
||||
FROM train_accommodation a
|
||||
LEFT JOIN train_enrollment e ON a.enrollment_id = e.enrollment_id
|
||||
LEFT JOIN train_student s ON e.student_id = s.s_id
|
||||
LEFT JOIN train_class_plan p ON e.plan_id = p.plan_id
|
||||
LEFT JOIN train_dormitory_info d ON a.dorm_id = d.dorm_id
|
||||
</sql>
|
||||
|
||||
<select id="selectTrainAccommodationList" parameterType="TrainAccommodation" resultMap="TrainAccommodationResult">
|
||||
<include refid="selectTrainAccommodationVo"/>
|
||||
<where>
|
||||
<if test="accommodationId != null">
|
||||
AND a.accommodation_id = #{accommodationId}
|
||||
</if>
|
||||
<if test="enrollmentId != null">
|
||||
AND a.enrollment_id = #{enrollmentId}
|
||||
</if>
|
||||
<if test="dormId != null and dormId != ''">
|
||||
AND a.dorm_id = #{dormId}
|
||||
</if>
|
||||
<if test="bedNo != null and bedNo != ''">
|
||||
AND a.bed_no LIKE concat('%', #{bedNo}, '%')
|
||||
</if>
|
||||
<if test="accommodationStatus != null">
|
||||
AND a.accommodation_status = #{accommodationStatus}
|
||||
</if>
|
||||
<if test="planId != null and planId != ''">
|
||||
AND e.plan_id = #{planId}
|
||||
</if>
|
||||
<if test="studentId != null and studentId != ''">
|
||||
AND e.student_id = #{studentId}
|
||||
</if>
|
||||
<if test="dormBuilding != null and dormBuilding != ''">
|
||||
AND d.dorm_building LIKE concat('%', #{dormBuilding}, '%')
|
||||
</if>
|
||||
<if test="dormRoom != null and dormRoom != ''">
|
||||
AND d.dorm_room LIKE concat('%', #{dormRoom}, '%')
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY a.assigned_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectTrainAccommodationByAccommodationId" parameterType="Long" resultMap="TrainAccommodationResult">
|
||||
<include refid="selectTrainAccommodationVo"/>
|
||||
WHERE a.accommodation_id = #{accommodationId}
|
||||
</select>
|
||||
|
||||
<select id="selectTrainAccommodationByEnrollmentId" parameterType="Long" resultMap="TrainAccommodationResult">
|
||||
<include refid="selectTrainAccommodationVo"/>
|
||||
WHERE a.enrollment_id = #{enrollmentId}
|
||||
ORDER BY a.assigned_time DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectActiveAccommodationByEnrollmentId" parameterType="Long" resultMap="TrainAccommodationResult">
|
||||
<include refid="selectTrainAccommodationVo"/>
|
||||
WHERE a.enrollment_id = #{enrollmentId}
|
||||
AND a.accommodation_status IN (0, 1)
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectAccommodationHistoryByEnrollmentId" parameterType="Long" resultMap="TrainAccommodationResult">
|
||||
<include refid="selectTrainAccommodationVo"/>
|
||||
WHERE a.enrollment_id = #{enrollmentId}
|
||||
ORDER BY a.assigned_time DESC, a.accommodation_id DESC
|
||||
</select>
|
||||
|
||||
<select id="selectActiveAccommodationsByPlanId" parameterType="String" resultMap="TrainAccommodationResult">
|
||||
<include refid="selectTrainAccommodationVo"/>
|
||||
WHERE e.plan_id = #{planId}
|
||||
AND a.accommodation_status IN (0, 1)
|
||||
ORDER BY a.assigned_time ASC
|
||||
</select>
|
||||
|
||||
<select id="countActiveByDormId" parameterType="String" resultType="int">
|
||||
SELECT COUNT(1)
|
||||
FROM train_accommodation
|
||||
WHERE dorm_id = #{dormId}
|
||||
AND accommodation_status IN (0, 1)
|
||||
</select>
|
||||
|
||||
<insert id="insertTrainAccommodation" parameterType="TrainAccommodation" useGeneratedKeys="true" keyProperty="accommodationId">
|
||||
INSERT INTO train_accommodation
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="enrollmentId != null">enrollment_id,</if>
|
||||
<if test="dormId != null and dormId != ''">dorm_id,</if>
|
||||
<if test="bedNo != null and bedNo != ''">bed_no,</if>
|
||||
<if test="assignedTime != null">assigned_time,</if>
|
||||
<if test="accommodationStatus != null">accommodation_status,</if>
|
||||
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by,</if>
|
||||
<if test="updateTime != null">update_time,</if>
|
||||
<if test="remark != null and remark != ''">remark,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="enrollmentId != null">#{enrollmentId},</if>
|
||||
<if test="dormId != null and dormId != ''">#{dormId},</if>
|
||||
<if test="bedNo != null and bedNo != ''">#{bedNo},</if>
|
||||
<if test="assignedTime != null">#{assignedTime},</if>
|
||||
<if test="accommodationStatus != null">#{accommodationStatus},</if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
<if test="updateBy != null and updateBy != ''">#{updateBy},</if>
|
||||
<if test="updateTime != null">#{updateTime},</if>
|
||||
<if test="remark != null and remark != ''">#{remark},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<update id="updateTrainAccommodation" parameterType="TrainAccommodation">
|
||||
UPDATE train_accommodation
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="dormId != null and dormId != ''">dorm_id = #{dormId},</if>
|
||||
<if test="bedNo != null and bedNo != ''">bed_no = #{bedNo},</if>
|
||||
<if test="assignedTime != null">assigned_time = #{assignedTime},</if>
|
||||
<if test="accommodationStatus != null">accommodation_status = #{accommodationStatus},</if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
|
||||
<if test="updateTime != null">update_time = #{updateTime},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
</trim>
|
||||
WHERE accommodation_id = #{accommodationId}
|
||||
</update>
|
||||
|
||||
<update id="updateAccommodationStatus">
|
||||
UPDATE train_accommodation
|
||||
SET accommodation_status = #{accommodationStatus},
|
||||
update_by = #{updateBy},
|
||||
update_time = #{updateTime}
|
||||
WHERE accommodation_id = #{accommodationId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteTrainAccommodationByAccommodationId" parameterType="Long">
|
||||
DELETE FROM train_accommodation WHERE accommodation_id = #{accommodationId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteTrainAccommodationByAccommodationIds" parameterType="Long">
|
||||
DELETE FROM train_accommodation WHERE accommodation_id IN
|
||||
<foreach item="accommodationId" collection="array" open="(" separator="," close=")">
|
||||
#{accommodationId}
|
||||
</foreach>
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -3,101 +3,194 @@
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.train.mapper.TrainDormAuthMapper">
|
||||
|
||||
|
||||
<resultMap type="TrainDormAuth" id="TrainDormAuthResult">
|
||||
<result property="authId" column="auth_id" />
|
||||
<result property="studentId" column="student_id" />
|
||||
<result property="planId" column="plan_id" />
|
||||
<result property="dormId" column="dorm_id" />
|
||||
<result property="bedNo" column="bed_no" />
|
||||
<result property="authId" column="auth_id" />
|
||||
<result property="accommodationId" column="accommodation_id" />
|
||||
<result property="authStart" column="auth_start" />
|
||||
<result property="authEnd" column="auth_end" />
|
||||
<result property="faceSyncStatus" column="face_sync_status" />
|
||||
<result property="authStart" column="auth_start" />
|
||||
<result property="authEnd" column="auth_end" />
|
||||
<result property="createUser" column="create_user" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
<result property="retryCount" column="retry_count" />
|
||||
<result property="lastError" column="last_error" />
|
||||
<result property="externalAuthId" column="external_auth_id" />
|
||||
<result property="lastSyncTime" column="last_sync_time" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
<result property="studentId" column="student_id" />
|
||||
<result property="studentName" column="student_name" />
|
||||
<result property="planId" column="plan_id" />
|
||||
<result property="planName" column="plan_name" />
|
||||
<result property="dormId" column="dorm_id" />
|
||||
<result property="dormBuilding" column="dorm_building" />
|
||||
<result property="dormRoom" column="dorm_room" />
|
||||
<result property="bedNo" column="bed_no" />
|
||||
<result property="createUser" column="create_by" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectTrainDormAuthVo">
|
||||
select auth_id, student_id, plan_id, dorm_id, bed_no, face_sync_status, auth_start, auth_end, create_user, create_time, update_time, remark from train_dorm_auth
|
||||
SELECT
|
||||
a.auth_id,
|
||||
a.accommodation_id,
|
||||
a.auth_start,
|
||||
a.auth_end,
|
||||
a.face_sync_status,
|
||||
a.retry_count,
|
||||
a.last_error,
|
||||
a.external_auth_id,
|
||||
a.last_sync_time,
|
||||
a.create_by,
|
||||
a.create_time,
|
||||
a.update_by,
|
||||
a.update_time,
|
||||
a.remark,
|
||||
acc.dorm_id,
|
||||
acc.bed_no,
|
||||
e.student_id,
|
||||
s.s_name AS student_name,
|
||||
e.plan_id,
|
||||
p.plan_name,
|
||||
d.dorm_building,
|
||||
d.dorm_room
|
||||
FROM train_dorm_auth a
|
||||
LEFT JOIN train_accommodation acc ON a.accommodation_id = acc.accommodation_id
|
||||
LEFT JOIN train_enrollment e ON acc.enrollment_id = e.enrollment_id
|
||||
LEFT JOIN train_student s ON e.student_id = s.s_id
|
||||
LEFT JOIN train_class_plan p ON e.plan_id = p.plan_id
|
||||
LEFT JOIN train_dormitory_info d ON acc.dorm_id = d.dorm_id
|
||||
</sql>
|
||||
|
||||
<select id="selectTrainDormAuthList" parameterType="TrainDormAuth" resultMap="TrainDormAuthResult">
|
||||
<include refid="selectTrainDormAuthVo"/>
|
||||
<where>
|
||||
<if test="studentId != null and studentId != ''"> and student_id = #{studentId}</if>
|
||||
<if test="planId != null and planId != ''"> and plan_id = #{planId}</if>
|
||||
<if test="dormId != null and dormId != ''"> and dorm_id = #{dormId}</if>
|
||||
<if test="bedNo != null and bedNo != ''"> and bed_no = #{bedNo}</if>
|
||||
<if test="faceSyncStatus != null "> and face_sync_status = #{faceSyncStatus}</if>
|
||||
<if test="authStart != null "> and auth_start = #{authStart}</if>
|
||||
<if test="authEnd != null "> and auth_end = #{authEnd}</if>
|
||||
<if test="createUser != null and createUser != ''"> and create_user = #{createUser}</if>
|
||||
<where>
|
||||
<if test="authId != null">
|
||||
AND a.auth_id = #{authId}
|
||||
</if>
|
||||
<if test="accommodationId != null">
|
||||
AND a.accommodation_id = #{accommodationId}
|
||||
</if>
|
||||
<if test="faceSyncStatus != null">
|
||||
AND a.face_sync_status = #{faceSyncStatus}
|
||||
</if>
|
||||
<if test="authStart != null">
|
||||
AND a.auth_start >= #{authStart}
|
||||
</if>
|
||||
<if test="authEnd != null">
|
||||
AND a.auth_end <= #{authEnd}
|
||||
</if>
|
||||
<if test="studentId != null and studentId != ''">
|
||||
AND e.student_id = #{studentId}
|
||||
</if>
|
||||
<if test="planId != null and planId != ''">
|
||||
AND e.plan_id = #{planId}
|
||||
</if>
|
||||
<if test="dormId != null and dormId != ''">
|
||||
AND acc.dorm_id = #{dormId}
|
||||
</if>
|
||||
<if test="bedNo != null and bedNo != ''">
|
||||
AND acc.bed_no = #{bedNo}
|
||||
</if>
|
||||
<if test="createBy != null and createBy != ''">
|
||||
AND a.create_by = #{createBy}
|
||||
</if>
|
||||
<if test="createUser != null and createUser != ''">
|
||||
AND a.create_by = #{createUser}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY a.create_time DESC
|
||||
</select>
|
||||
|
||||
|
||||
<select id="selectTrainDormAuthByAuthId" parameterType="Long" resultMap="TrainDormAuthResult">
|
||||
<include refid="selectTrainDormAuthVo"/>
|
||||
where auth_id = #{authId}
|
||||
WHERE a.auth_id = #{authId}
|
||||
</select>
|
||||
|
||||
<select id="selectTrainDormAuthByAccommodationId" parameterType="Long" resultMap="TrainDormAuthResult">
|
||||
<include refid="selectTrainDormAuthVo"/>
|
||||
WHERE a.accommodation_id = #{accommodationId}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<insert id="insertTrainDormAuth" parameterType="TrainDormAuth" useGeneratedKeys="true" keyProperty="authId">
|
||||
insert into train_dorm_auth
|
||||
INSERT INTO train_dorm_auth
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="studentId != null and studentId != ''">student_id,</if>
|
||||
<if test="planId != null and planId != ''">plan_id,</if>
|
||||
<if test="dormId != null and dormId != ''">dorm_id,</if>
|
||||
<if test="bedNo != null">bed_no,</if>
|
||||
<if test="faceSyncStatus != null">face_sync_status,</if>
|
||||
<if test="accommodationId != null">accommodation_id,</if>
|
||||
<if test="authStart != null">auth_start,</if>
|
||||
<if test="authEnd != null">auth_end,</if>
|
||||
<if test="createUser != null and createUser != ''">create_user,</if>
|
||||
<if test="faceSyncStatus != null">face_sync_status,</if>
|
||||
<if test="retryCount != null">retry_count,</if>
|
||||
<if test="lastError != null and lastError != ''">last_error,</if>
|
||||
<if test="externalAuthId != null and externalAuthId != ''">external_auth_id,</if>
|
||||
<if test="lastSyncTime != null">last_sync_time,</if>
|
||||
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by,</if>
|
||||
<if test="updateTime != null">update_time,</if>
|
||||
<if test="remark != null">remark,</if>
|
||||
</trim>
|
||||
<if test="remark != null and remark != ''">remark,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="studentId != null and studentId != ''">#{studentId},</if>
|
||||
<if test="planId != null and planId != ''">#{planId},</if>
|
||||
<if test="dormId != null and dormId != ''">#{dormId},</if>
|
||||
<if test="bedNo != null">#{bedNo},</if>
|
||||
<if test="faceSyncStatus != null">#{faceSyncStatus},</if>
|
||||
<if test="accommodationId != null">#{accommodationId},</if>
|
||||
<if test="authStart != null">#{authStart},</if>
|
||||
<if test="authEnd != null">#{authEnd},</if>
|
||||
<if test="createUser != null and createUser != ''">#{createUser},</if>
|
||||
<if test="faceSyncStatus != null">#{faceSyncStatus},</if>
|
||||
<if test="retryCount != null">#{retryCount},</if>
|
||||
<if test="lastError != null and lastError != ''">#{lastError},</if>
|
||||
<if test="externalAuthId != null and externalAuthId != ''">#{externalAuthId},</if>
|
||||
<if test="lastSyncTime != null">#{lastSyncTime},</if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
<if test="updateBy != null and updateBy != ''">#{updateBy},</if>
|
||||
<if test="updateTime != null">#{updateTime},</if>
|
||||
<if test="remark != null">#{remark},</if>
|
||||
</trim>
|
||||
<if test="remark != null and remark != ''">#{remark},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<update id="updateTrainDormAuth" parameterType="TrainDormAuth">
|
||||
update train_dorm_auth
|
||||
UPDATE train_dorm_auth
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="studentId != null and studentId != ''">student_id = #{studentId},</if>
|
||||
<if test="planId != null and planId != ''">plan_id = #{planId},</if>
|
||||
<if test="dormId != null and dormId != ''">dorm_id = #{dormId},</if>
|
||||
<if test="bedNo != null">bed_no = #{bedNo},</if>
|
||||
<if test="faceSyncStatus != null">face_sync_status = #{faceSyncStatus},</if>
|
||||
<if test="accommodationId != null">accommodation_id = #{accommodationId},</if>
|
||||
<if test="authStart != null">auth_start = #{authStart},</if>
|
||||
<if test="authEnd != null">auth_end = #{authEnd},</if>
|
||||
<if test="createUser != null and createUser != ''">create_user = #{createUser},</if>
|
||||
<if test="createTime != null">create_time = #{createTime},</if>
|
||||
<if test="faceSyncStatus != null">face_sync_status = #{faceSyncStatus},</if>
|
||||
<if test="retryCount != null">retry_count = #{retryCount},</if>
|
||||
<if test="lastError != null">last_error = #{lastError},</if>
|
||||
<if test="externalAuthId != null">external_auth_id = #{externalAuthId},</if>
|
||||
<if test="lastSyncTime != null">last_sync_time = #{lastSyncTime},</if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
|
||||
<if test="updateTime != null">update_time = #{updateTime},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
</trim>
|
||||
where auth_id = #{authId}
|
||||
WHERE auth_id = #{authId}
|
||||
</update>
|
||||
|
||||
<update id="updateSyncStatus">
|
||||
UPDATE train_dorm_auth
|
||||
SET face_sync_status = #{faceSyncStatus},
|
||||
last_error = #{lastError},
|
||||
update_by = #{updateBy},
|
||||
update_time = #{updateTime}
|
||||
WHERE auth_id = #{authId}
|
||||
</update>
|
||||
|
||||
<update id="increaseRetryCount">
|
||||
UPDATE train_dorm_auth
|
||||
SET retry_count = COALESCE(retry_count, 0) + 1,
|
||||
last_error = #{lastError},
|
||||
update_by = #{updateBy},
|
||||
update_time = #{updateTime}
|
||||
WHERE auth_id = #{authId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteTrainDormAuthByAuthId" parameterType="Long">
|
||||
delete from train_dorm_auth where auth_id = #{authId}
|
||||
DELETE FROM train_dorm_auth WHERE auth_id = #{authId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteTrainDormAuthByAuthIds" parameterType="String">
|
||||
delete from train_dorm_auth where auth_id in
|
||||
DELETE FROM train_dorm_auth WHERE auth_id IN
|
||||
<foreach item="authId" collection="array" open="(" separator="," close=")">
|
||||
#{authId}
|
||||
</foreach>
|
||||
</delete>
|
||||
</mapper>
|
||||
</mapper>
|
||||
+585
@@ -0,0 +1,585 @@
|
||||
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.lang.reflect.Proxy;
|
||||
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.TrainAccommodation;
|
||||
import com.ruoyi.train.domain.TrainDormitoryInfo;
|
||||
import com.ruoyi.train.domain.TrainEnrollment;
|
||||
import com.ruoyi.train.mapper.TrainAccommodationMapper;
|
||||
import com.ruoyi.train.mapper.TrainDormitoryInfoMapper;
|
||||
import com.ruoyi.train.mapper.TrainEnrollmentMapper;
|
||||
|
||||
/**
|
||||
* R.3B Wave 0 - 本地住宿领域对齐与约束单元测试 (AccommodationServiceTest)
|
||||
* 覆盖规范第29条全部要求:A到I
|
||||
*/
|
||||
public class AccommodationServiceTest
|
||||
{
|
||||
private TrainAccommodationServiceImpl accommodationService;
|
||||
private MockAccommodationMapper mockAccommodationMapper;
|
||||
private Map<String, TrainDormitoryInfo> dormStore;
|
||||
private Map<Long, TrainEnrollment> enrollmentStore;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
accommodationService = new TrainAccommodationServiceImpl();
|
||||
mockAccommodationMapper = new MockAccommodationMapper();
|
||||
dormStore = new HashMap<>();
|
||||
enrollmentStore = new HashMap<>();
|
||||
|
||||
TrainDormitoryInfoMapper dormMapperProxy = (TrainDormitoryInfoMapper) Proxy.newProxyInstance(
|
||||
TrainDormitoryInfoMapper.class.getClassLoader(),
|
||||
new Class<?>[]{TrainDormitoryInfoMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectTrainDormitoryInfoByDormId".equals(method.getName()))
|
||||
{
|
||||
return dormStore.get(args[0]);
|
||||
}
|
||||
if (method.getReturnType().equals(int.class)) return 0;
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
TrainEnrollmentMapper enrollMapperProxy = (TrainEnrollmentMapper) Proxy.newProxyInstance(
|
||||
TrainEnrollmentMapper.class.getClassLoader(),
|
||||
new Class<?>[]{TrainEnrollmentMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectTrainEnrollmentById".equals(method.getName()))
|
||||
{
|
||||
return enrollmentStore.get(args[0]);
|
||||
}
|
||||
if (method.getReturnType().equals(int.class)) return 0;
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
setField(accommodationService, "trainAccommodationMapper", mockAccommodationMapper);
|
||||
setField(accommodationService, "trainDormitoryInfoMapper", dormMapperProxy);
|
||||
setField(accommodationService, "trainEnrollmentMapper", enrollMapperProxy);
|
||||
|
||||
// 初始化预设基础数据
|
||||
TrainDormitoryInfo dorm101 = new TrainDormitoryInfo();
|
||||
dorm101.setDormId("DORM_101");
|
||||
dorm101.setDormBuilding("培训1号楼");
|
||||
dorm101.setDormFloor("1");
|
||||
dorm101.setDormRoom("101");
|
||||
dorm101.setCapacity(2); // 容量为2人
|
||||
dorm101.setDormStatus(1); // 启用
|
||||
dormStore.put(dorm101.getDormId(), dorm101);
|
||||
|
||||
TrainDormitoryInfo dormDisabled = new TrainDormitoryInfo();
|
||||
dormDisabled.setDormId("DORM_DIS");
|
||||
dormDisabled.setDormBuilding("培训1号楼");
|
||||
dormDisabled.setDormRoom("102");
|
||||
dormDisabled.setCapacity(4);
|
||||
dormDisabled.setDormStatus(0); // 禁用
|
||||
dormStore.put(dormDisabled.getDormId(), dormDisabled);
|
||||
|
||||
TrainEnrollment enrollment1 = new TrainEnrollment();
|
||||
enrollment1.setEnrollmentId(1001L);
|
||||
enrollment1.setPlanId("PLAN_2026_01");
|
||||
enrollment1.setStudentId("STU_001");
|
||||
enrollmentStore.put(enrollment1.getEnrollmentId(), enrollment1);
|
||||
|
||||
TrainEnrollment enrollment2 = new TrainEnrollment();
|
||||
enrollment2.setEnrollmentId(1002L);
|
||||
enrollment2.setPlanId("PLAN_2026_01");
|
||||
enrollment2.setStudentId("STU_002");
|
||||
enrollmentStore.put(enrollment2.getEnrollmentId(), enrollment2);
|
||||
|
||||
TrainEnrollment enrollment3 = new TrainEnrollment();
|
||||
enrollment3.setEnrollmentId(1003L);
|
||||
enrollment3.setPlanId("PLAN_2026_01");
|
||||
enrollment3.setStudentId("STU_003");
|
||||
enrollmentStore.put(enrollment3.getEnrollmentId(), enrollment3);
|
||||
}
|
||||
|
||||
private void setField(Object target, String fieldName, Object value) throws Exception
|
||||
{
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement A: Create accommodation
|
||||
*/
|
||||
@Test
|
||||
public void testA_CreateAccommodationSuccess()
|
||||
{
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
acc.setCreateBy("admin");
|
||||
|
||||
int rows = accommodationService.insertTrainAccommodation(acc);
|
||||
assertEquals(1, rows);
|
||||
assertNotNull(acc.getAccommodationId());
|
||||
assertEquals(TrainAccommodation.STATUS_PREALLOCATED, acc.getAccommodationStatus());
|
||||
assertNotNull(acc.getAssignedTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement B: Query by enrollment
|
||||
*/
|
||||
@Test
|
||||
public void testB_QueryByEnrollment()
|
||||
{
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
accommodationService.insertTrainAccommodation(acc);
|
||||
|
||||
TrainAccommodation queried = accommodationService.selectTrainAccommodationByEnrollmentId(1001L);
|
||||
assertNotNull(queried);
|
||||
assertEquals("DORM_101", queried.getDormId());
|
||||
assertEquals("1号床", queried.getBedNo());
|
||||
|
||||
TrainAccommodation active = accommodationService.selectActiveAccommodationByEnrollmentId(1001L);
|
||||
assertNotNull(active);
|
||||
assertEquals(acc.getAccommodationId(), active.getAccommodationId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement C: Status transition (0 预分配 -> 1 已入住 -> 2 已退宿)
|
||||
*/
|
||||
@Test
|
||||
public void testC_StatusTransition()
|
||||
{
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
accommodationService.insertTrainAccommodation(acc);
|
||||
Long accId = acc.getAccommodationId();
|
||||
|
||||
// 1. 办理入住 (0 -> 1)
|
||||
accommodationService.checkIn(accId, "operator1");
|
||||
TrainAccommodation checkedIn = accommodationService.selectTrainAccommodationByAccommodationId(accId);
|
||||
assertEquals(TrainAccommodation.STATUS_CHECKED_IN, checkedIn.getAccommodationStatus());
|
||||
|
||||
// 2. 办理退宿 (1 -> 2)
|
||||
accommodationService.checkOut(accId, "operator2");
|
||||
TrainAccommodation checkedOut = accommodationService.selectTrainAccommodationByAccommodationId(accId);
|
||||
assertEquals(TrainAccommodation.STATUS_CHECKED_OUT, checkedOut.getAccommodationStatus());
|
||||
|
||||
// 3. 已退宿记录不能重新直接办理入住
|
||||
try
|
||||
{
|
||||
accommodationService.checkIn(accId, "operator3");
|
||||
fail("已退宿记录重新入住未被拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("已退宿的记录不能重新办理入住"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement D: Active accommodation uniqueness
|
||||
* 同一个 Enrollment 同一时刻最多 1 个有效住宿
|
||||
*/
|
||||
@Test
|
||||
public void testD_ActiveAccommodationUniqueness()
|
||||
{
|
||||
TrainAccommodation acc1 = new TrainAccommodation();
|
||||
acc1.setEnrollmentId(1001L);
|
||||
acc1.setDormId("DORM_101");
|
||||
acc1.setBedNo("1号床");
|
||||
accommodationService.insertTrainAccommodation(acc1);
|
||||
|
||||
// 再次为该 enrollment 分配住宿,应被业务层拦截
|
||||
TrainAccommodation acc2 = new TrainAccommodation();
|
||||
acc2.setEnrollmentId(1001L);
|
||||
acc2.setDormId("DORM_101");
|
||||
acc2.setBedNo("2号床");
|
||||
try
|
||||
{
|
||||
accommodationService.insertTrainAccommodation(acc2);
|
||||
fail("重复分配活跃住宿未被拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("已存在有效住宿安排"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement E: Invalid enrollment rejected
|
||||
*/
|
||||
@Test
|
||||
public void testE_InvalidEnrollmentRejected()
|
||||
{
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(999999L); // 不存在的参训档案
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
|
||||
try
|
||||
{
|
||||
accommodationService.insertTrainAccommodation(acc);
|
||||
fail("不存在的参训档案未拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("指定参训档案不存在"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement F: Invalid dorm rejected
|
||||
*/
|
||||
@Test
|
||||
public void testF_InvalidDormRejected()
|
||||
{
|
||||
// 1. 宿舍不存在
|
||||
TrainAccommodation acc1 = new TrainAccommodation();
|
||||
acc1.setEnrollmentId(1001L);
|
||||
acc1.setDormId("NON_EXIST_DORM");
|
||||
acc1.setBedNo("1号床");
|
||||
try
|
||||
{
|
||||
accommodationService.insertTrainAccommodation(acc1);
|
||||
fail("不存在的宿舍未拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("指定宿舍不存在"));
|
||||
}
|
||||
|
||||
// 2. 宿舍已被禁用
|
||||
TrainAccommodation acc2 = new TrainAccommodation();
|
||||
acc2.setEnrollmentId(1001L);
|
||||
acc2.setDormId("DORM_DIS");
|
||||
acc2.setBedNo("1号床");
|
||||
try
|
||||
{
|
||||
accommodationService.insertTrainAccommodation(acc2);
|
||||
fail("已禁用的宿舍未拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("该宿舍当前处于禁用状态"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement G: Capacity boundary
|
||||
* DORM_101 容量为 2 人,已满后再分配第 3 人应拦截
|
||||
*/
|
||||
@Test
|
||||
public void testG_CapacityBoundary()
|
||||
{
|
||||
// 第1人成功
|
||||
TrainAccommodation acc1 = new TrainAccommodation();
|
||||
acc1.setEnrollmentId(1001L);
|
||||
acc1.setDormId("DORM_101");
|
||||
acc1.setBedNo("1号床");
|
||||
accommodationService.insertTrainAccommodation(acc1);
|
||||
|
||||
// 第2人成功
|
||||
TrainAccommodation acc2 = new TrainAccommodation();
|
||||
acc2.setEnrollmentId(1002L);
|
||||
acc2.setDormId("DORM_101");
|
||||
acc2.setBedNo("2号床");
|
||||
accommodationService.insertTrainAccommodation(acc2);
|
||||
|
||||
// 第3人分配到同一房间,容量超限拦截
|
||||
TrainAccommodation acc3 = new TrainAccommodation();
|
||||
acc3.setEnrollmentId(1003L);
|
||||
acc3.setDormId("DORM_101");
|
||||
acc3.setBedNo("3号床");
|
||||
try
|
||||
{
|
||||
accommodationService.insertTrainAccommodation(acc3);
|
||||
fail("宿舍容量满额未拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("宿舍容量已满"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement H: No physical bed table assumption
|
||||
* bed_no 作为纯业务标识字符串持久化,不需要依赖物理床位表
|
||||
*/
|
||||
@Test
|
||||
public void testH_NoPhysicalBedTableAssumption()
|
||||
{
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("上铺-VIP-01"); // 自定义床位标识
|
||||
|
||||
int rows = accommodationService.insertTrainAccommodation(acc);
|
||||
assertEquals(1, rows);
|
||||
|
||||
TrainAccommodation saved = accommodationService.selectTrainAccommodationByAccommodationId(acc.getAccommodationId());
|
||||
assertEquals("上铺-VIP-01", saved.getBedNo());
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement I: Ended accommodation historical query & transfer flow
|
||||
* 退宿后历史记录可查,且退宿后允许为该 enrollment 分配新住宿 (调房业务)
|
||||
*/
|
||||
@Test
|
||||
public void testI_EndedAccommodationHistoricalQueryAndTransfer()
|
||||
{
|
||||
// 1. 初始化新宿舍 DORM_201
|
||||
TrainDormitoryInfo dorm201 = new TrainDormitoryInfo();
|
||||
dorm201.setDormId("DORM_201");
|
||||
dorm201.setDormBuilding("培训2号楼");
|
||||
dorm201.setDormRoom("201");
|
||||
dorm201.setCapacity(2);
|
||||
dorm201.setDormStatus(1);
|
||||
dormStore.put(dorm201.getDormId(), dorm201);
|
||||
|
||||
// 2. 创建初始住宿
|
||||
TrainAccommodation oldAcc = new TrainAccommodation();
|
||||
oldAcc.setEnrollmentId(1001L);
|
||||
oldAcc.setDormId("DORM_101");
|
||||
oldAcc.setBedNo("1号床");
|
||||
accommodationService.insertTrainAccommodation(oldAcc);
|
||||
Long oldId = oldAcc.getAccommodationId();
|
||||
|
||||
// 3. 执行调房
|
||||
TrainAccommodation newAcc = accommodationService.transfer(oldId, "DORM_201", "新1号床", "admin");
|
||||
assertNotNull(newAcc);
|
||||
assertNotNull(newAcc.getAccommodationId());
|
||||
assertEquals("DORM_201", newAcc.getDormId());
|
||||
assertEquals("新1号床", newAcc.getBedNo());
|
||||
assertEquals(TrainAccommodation.STATUS_CHECKED_IN, newAcc.getAccommodationStatus());
|
||||
|
||||
// 4. 验证旧住宿变为退宿状态但依然可查 (历史可审计)
|
||||
TrainAccommodation oldUpdated = accommodationService.selectTrainAccommodationByAccommodationId(oldId);
|
||||
assertNotNull(oldUpdated);
|
||||
assertEquals(TrainAccommodation.STATUS_CHECKED_OUT, oldUpdated.getAccommodationStatus());
|
||||
|
||||
// 5. 验证活跃住宿查询当前指向新记录
|
||||
TrainAccommodation active = accommodationService.selectActiveAccommodationByEnrollmentId(1001L);
|
||||
assertNotNull(active);
|
||||
assertEquals(newAcc.getAccommodationId(), active.getAccommodationId());
|
||||
assertEquals("DORM_201", active.getDormId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wave 0 Review Gate - Requirement 14:
|
||||
* A. Create first active accommodation PASS
|
||||
* B. Create second active accommodation REJECT
|
||||
* C. Checkout first accommodation
|
||||
* D. Create second accommodation for same enrollment PASS
|
||||
* E. History returns 2 rows
|
||||
* F. Capacity only counts second active row
|
||||
*/
|
||||
@Test
|
||||
public void testAccommodationHistoryAndCardinalityLifecycle()
|
||||
{
|
||||
Long enrollId = 1001L;
|
||||
|
||||
// A. Create first active accommodation PASS
|
||||
TrainAccommodation acc1 = new TrainAccommodation();
|
||||
acc1.setEnrollmentId(enrollId);
|
||||
acc1.setDormId("DORM_101");
|
||||
acc1.setBedNo("1号床");
|
||||
int row1 = accommodationService.insertTrainAccommodation(acc1);
|
||||
assertEquals(1, row1);
|
||||
assertNotNull(acc1.getAccommodationId());
|
||||
|
||||
// B. Create second active accommodation REJECT (同一时刻最多1条有效住宿)
|
||||
TrainAccommodation acc2 = new TrainAccommodation();
|
||||
acc2.setEnrollmentId(enrollId);
|
||||
acc2.setDormId("DORM_101");
|
||||
acc2.setBedNo("2号床");
|
||||
try
|
||||
{
|
||||
accommodationService.insertTrainAccommodation(acc2);
|
||||
fail("活跃记录存在时重复新增应被拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("已存在有效住宿安排"));
|
||||
}
|
||||
|
||||
// C. Checkout first accommodation (退宿后变为状态 2)
|
||||
accommodationService.checkOut(acc1.getAccommodationId(), "admin");
|
||||
TrainAccommodation checkedOutAcc = accommodationService.selectTrainAccommodationByAccommodationId(acc1.getAccommodationId());
|
||||
assertEquals(TrainAccommodation.STATUS_CHECKED_OUT, checkedOutAcc.getAccommodationStatus());
|
||||
|
||||
// D. Create second accommodation for same enrollment PASS (退宿后允许同一学员建立新住宿)
|
||||
TrainAccommodation acc3 = new TrainAccommodation();
|
||||
acc3.setEnrollmentId(enrollId);
|
||||
acc3.setDormId("DORM_101");
|
||||
acc3.setBedNo("2号床");
|
||||
int row3 = accommodationService.insertTrainAccommodation(acc3);
|
||||
assertEquals(1, row3);
|
||||
assertNotNull(acc3.getAccommodationId());
|
||||
|
||||
// E. History returns 2 rows (包含1条退宿历史 + 1条当前活跃)
|
||||
List<TrainAccommodation> history = accommodationService.selectAccommodationHistoryByEnrollmentId(enrollId);
|
||||
assertNotNull(history);
|
||||
assertEquals(2, history.size());
|
||||
|
||||
// 验证当前活跃记录只返回新记录
|
||||
TrainAccommodation active = accommodationService.selectActiveAccommodationByEnrollmentId(enrollId);
|
||||
assertNotNull(active);
|
||||
assertEquals(acc3.getAccommodationId(), active.getAccommodationId());
|
||||
|
||||
// F. Capacity only counts second active row (历史退宿记录不计入核定容量,当前只计1人)
|
||||
int activeCount = mockAccommodationMapper.countActiveByDormId("DORM_101");
|
||||
assertEquals(1, activeCount);
|
||||
}
|
||||
|
||||
// ================================= 内部 Mock 类 =================================
|
||||
|
||||
private static class MockAccommodationMapper implements TrainAccommodationMapper
|
||||
{
|
||||
private final Map<Long, TrainAccommodation> store = new HashMap<>();
|
||||
private long idSeq = 100L;
|
||||
|
||||
@Override
|
||||
public TrainAccommodation selectTrainAccommodationByAccommodationId(Long accommodationId)
|
||||
{
|
||||
return store.get(accommodationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrainAccommodation selectTrainAccommodationByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
TrainAccommodation latest = null;
|
||||
for (TrainAccommodation a : store.values())
|
||||
{
|
||||
if (enrollmentId.equals(a.getEnrollmentId()))
|
||||
{
|
||||
latest = a;
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrainAccommodation selectActiveAccommodationByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
for (TrainAccommodation a : store.values())
|
||||
{
|
||||
if (enrollmentId.equals(a.getEnrollmentId()))
|
||||
{
|
||||
if (TrainAccommodation.STATUS_PREALLOCATED.equals(a.getAccommodationStatus())
|
||||
|| TrainAccommodation.STATUS_CHECKED_IN.equals(a.getAccommodationStatus()))
|
||||
{
|
||||
return a;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainAccommodation> selectAccommodationHistoryByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
List<TrainAccommodation> list = new ArrayList<>();
|
||||
for (TrainAccommodation a : store.values())
|
||||
{
|
||||
if (enrollmentId.equals(a.getEnrollmentId()))
|
||||
{
|
||||
list.add(a);
|
||||
}
|
||||
}
|
||||
list.sort((o1, o2) -> o2.getAccommodationId().compareTo(o1.getAccommodationId()));
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainAccommodation> selectTrainAccommodationList(TrainAccommodation accommodation)
|
||||
{
|
||||
return new ArrayList<>(store.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainAccommodation> selectActiveAccommodationsByPlanId(String planId)
|
||||
{
|
||||
return new ArrayList<>(store.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countActiveByDormId(String dormId)
|
||||
{
|
||||
int count = 0;
|
||||
for (TrainAccommodation a : store.values())
|
||||
{
|
||||
if (dormId.equals(a.getDormId()))
|
||||
{
|
||||
if (TrainAccommodation.STATUS_PREALLOCATED.equals(a.getAccommodationStatus())
|
||||
|| TrainAccommodation.STATUS_CHECKED_IN.equals(a.getAccommodationStatus()))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertTrainAccommodation(TrainAccommodation accommodation)
|
||||
{
|
||||
accommodation.setAccommodationId(++idSeq);
|
||||
store.put(accommodation.getAccommodationId(), accommodation);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateTrainAccommodation(TrainAccommodation accommodation)
|
||||
{
|
||||
store.put(accommodation.getAccommodationId(), accommodation);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateAccommodationStatus(Long accommodationId, Integer accommodationStatus, String updateBy, Date updateTime)
|
||||
{
|
||||
TrainAccommodation a = store.get(accommodationId);
|
||||
if (a != null)
|
||||
{
|
||||
a.setAccommodationStatus(accommodationStatus);
|
||||
a.setUpdateBy(updateBy);
|
||||
a.setUpdateTime(updateTime);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTrainAccommodationByAccommodationId(Long accommodationId)
|
||||
{
|
||||
return store.remove(accommodationId) != null ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTrainAccommodationByAccommodationIds(Long[] accommodationIds)
|
||||
{
|
||||
int deleted = 0;
|
||||
for (Long id : accommodationIds)
|
||||
{
|
||||
if (store.remove(id) != null) deleted++;
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
}
|
||||
+503
@@ -0,0 +1,503 @@
|
||||
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.TrainAccommodation;
|
||||
import com.ruoyi.train.domain.TrainDormAuth;
|
||||
import com.ruoyi.train.mapper.TrainAccommodationMapper;
|
||||
import com.ruoyi.train.mapper.TrainDormAuthMapper;
|
||||
|
||||
/**
|
||||
* R.3B Wave 0 - 宿舍门禁授权领域与V2契约对齐测试 (DormAuthAlignmentTest)
|
||||
* 覆盖规范第30条全部要求:A到G
|
||||
*/
|
||||
public class DormAuthAlignmentTest
|
||||
{
|
||||
private TrainDormAuthServiceImpl dormAuthService;
|
||||
private MockDormAuthMapper mockDormAuthMapper;
|
||||
private MockAccommodationMapper mockAccommodationMapper;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
dormAuthService = new TrainDormAuthServiceImpl();
|
||||
mockDormAuthMapper = new MockDormAuthMapper();
|
||||
mockAccommodationMapper = new MockAccommodationMapper();
|
||||
|
||||
setField(dormAuthService, "trainDormAuthMapper", mockDormAuthMapper);
|
||||
setField(dormAuthService, "trainAccommodationMapper", mockAccommodationMapper);
|
||||
|
||||
// 初始化基础住宿记录
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setAccommodationId(5001L);
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
acc.setAccommodationStatus(TrainAccommodation.STATUS_CHECKED_IN);
|
||||
mockAccommodationMapper.save(acc);
|
||||
|
||||
TrainAccommodation checkedOutAcc = new TrainAccommodation();
|
||||
checkedOutAcc.setAccommodationId(5002L);
|
||||
checkedOutAcc.setEnrollmentId(1002L);
|
||||
checkedOutAcc.setDormId("DORM_101");
|
||||
checkedOutAcc.setBedNo("2号床");
|
||||
checkedOutAcc.setAccommodationStatus(TrainAccommodation.STATUS_CHECKED_OUT);
|
||||
mockAccommodationMapper.save(checkedOutAcc);
|
||||
}
|
||||
|
||||
private void setField(Object target, String fieldName, Object value) throws Exception
|
||||
{
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement A: V2 mapper fields align DB contract
|
||||
* 验证 TrainDormAuth 实体和持久化契约包含所有 V2 字段
|
||||
*/
|
||||
@Test
|
||||
public void testA_V2MapperFieldsAlignDBContract() throws Exception
|
||||
{
|
||||
Class<?> clazz = TrainDormAuth.class;
|
||||
|
||||
// 必须存在 V2 核心字段
|
||||
assertNotNull("accommodationId 字段必须存在", clazz.getDeclaredField("accommodationId"));
|
||||
assertNotNull("authStart 字段必须存在", clazz.getDeclaredField("authStart"));
|
||||
assertNotNull("authEnd 字段必须存在", clazz.getDeclaredField("authEnd"));
|
||||
assertNotNull("faceSyncStatus 字段必须存在", clazz.getDeclaredField("faceSyncStatus"));
|
||||
assertNotNull("retryCount 字段必须存在", clazz.getDeclaredField("retryCount"));
|
||||
assertNotNull("lastError 字段必须存在", clazz.getDeclaredField("lastError"));
|
||||
assertNotNull("externalAuthId 字段必须存在", clazz.getDeclaredField("externalAuthId"));
|
||||
assertNotNull("lastSyncTime 字段必须存在", clazz.getDeclaredField("lastSyncTime"));
|
||||
|
||||
// 验证状态常量定义符合 DB SMALLINT 约束 (0, 1, 2, 3)
|
||||
assertEquals(Integer.valueOf(0), TrainDormAuth.STATUS_PENDING);
|
||||
assertEquals(Integer.valueOf(1), TrainDormAuth.STATUS_ACTIVE);
|
||||
assertEquals(Integer.valueOf(2), TrainDormAuth.STATUS_FAILED);
|
||||
assertEquals(Integer.valueOf(3), TrainDormAuth.STATUS_REVOKED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement B: Authorization references accommodation
|
||||
* 必须基于有效的 accommodation_id,无 accommodationId 或关联不存在住宿时拒绝
|
||||
*/
|
||||
@Test
|
||||
public void testB_AuthorizationReferencesAccommodation()
|
||||
{
|
||||
// 1. 无 accommodationId 被拒绝
|
||||
TrainDormAuth authNoAcc = new TrainDormAuth();
|
||||
authNoAcc.setAuthStart(new Date());
|
||||
try
|
||||
{
|
||||
dormAuthService.insertTrainDormAuth(authNoAcc);
|
||||
fail("未绑定住宿事实未被拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("必须绑定住宿安排"));
|
||||
}
|
||||
|
||||
// 2. 住宿记录不存在被拒绝
|
||||
TrainDormAuth authNonExist = new TrainDormAuth();
|
||||
authNonExist.setAccommodationId(999999L);
|
||||
try
|
||||
{
|
||||
dormAuthService.insertTrainDormAuth(authNonExist);
|
||||
fail("关联不存在的住宿记录未被拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("指定的住宿安排不存在"));
|
||||
}
|
||||
|
||||
// 3. 关联已退宿的记录被拒绝
|
||||
TrainDormAuth authCheckedOut = new TrainDormAuth();
|
||||
authCheckedOut.setAccommodationId(5002L); // 对应 checkedOutAcc
|
||||
try
|
||||
{
|
||||
dormAuthService.insertTrainDormAuth(authCheckedOut);
|
||||
fail("关联已退宿住宿记录未被拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("该住宿安排已退宿"));
|
||||
}
|
||||
|
||||
// 4. 关联有效住宿成功
|
||||
TrainDormAuth validAuth = new TrainDormAuth();
|
||||
validAuth.setAccommodationId(5001L);
|
||||
validAuth.setAuthStart(new Date());
|
||||
validAuth.setAuthEnd(new Date(System.currentTimeMillis() + 86400000L));
|
||||
int rows = dormAuthService.insertTrainDormAuth(validAuth);
|
||||
assertEquals(1, rows);
|
||||
assertNotNull(validAuth.getAuthId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement C: Legacy student_id/plan_id/dorm_id no longer persisted
|
||||
* 旧字段不再作为 DB 列持久化,而在实体层保留为视图/兼容访问
|
||||
*/
|
||||
@Test
|
||||
public void testC_LegacyFieldsNoLongerPersisted()
|
||||
{
|
||||
TrainDormAuth auth = new TrainDormAuth();
|
||||
auth.setAccommodationId(5001L);
|
||||
// 设置兼容视图字段
|
||||
auth.setStudentId("STU_LEGACY_01");
|
||||
auth.setPlanId("PLAN_LEGACY_01");
|
||||
auth.setDormId("DORM_LEGACY_01");
|
||||
auth.setBedNo("1号床");
|
||||
auth.setCreateUser("admin_v1");
|
||||
|
||||
dormAuthService.insertTrainDormAuth(auth);
|
||||
|
||||
// 验证 Mapper 写入的方法只接收并持久化 V2 真实字段
|
||||
TrainDormAuth saved = mockDormAuthMapper.selectTrainDormAuthByAuthId(auth.getAuthId());
|
||||
assertNotNull(saved);
|
||||
assertEquals(Long.valueOf(5001L), saved.getAccommodationId());
|
||||
// createUser 兼容适配到 BaseEntity.createBy
|
||||
assertEquals("admin_v1", saved.getCreateBy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement D & E: Retry summary fields and last error fields
|
||||
*/
|
||||
@Test
|
||||
public void testDE_RetrySummaryAndLastErrorFields()
|
||||
{
|
||||
TrainDormAuth auth = new TrainDormAuth();
|
||||
auth.setAccommodationId(5001L);
|
||||
dormAuthService.insertTrainDormAuth(auth);
|
||||
Long authId = auth.getAuthId();
|
||||
|
||||
// 初始重试次数为 0
|
||||
assertEquals(Integer.valueOf(0), auth.getRetryCount());
|
||||
assertNull(auth.getLastError());
|
||||
|
||||
// 模拟下发失败,记录错误并累加重试次数
|
||||
dormAuthService.markSyncFailed(authId, "Device offline: timeout after 5000ms", "sync_job");
|
||||
|
||||
TrainDormAuth failed = dormAuthService.selectTrainDormAuthByAuthId(authId);
|
||||
assertEquals(TrainDormAuth.STATUS_FAILED, failed.getFaceSyncStatus());
|
||||
assertEquals("Device offline: timeout after 5000ms", failed.getLastError());
|
||||
assertEquals(Integer.valueOf(1), failed.getRetryCount());
|
||||
|
||||
// 再次失败,重试次数增加到 2
|
||||
dormAuthService.markSyncFailed(authId, "Device unreachable", "sync_job");
|
||||
TrainDormAuth failedAgain = dormAuthService.selectTrainDormAuthByAuthId(authId);
|
||||
assertEquals(Integer.valueOf(2), failedAgain.getRetryCount());
|
||||
assertEquals("Device unreachable", failedAgain.getLastError());
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement F: Status transition
|
||||
* 0 待下发 -> 2 下发失败 -> 3 已注销
|
||||
*/
|
||||
@Test
|
||||
public void testF_StatusTransition()
|
||||
{
|
||||
TrainDormAuth auth = new TrainDormAuth();
|
||||
auth.setAccommodationId(5001L);
|
||||
dormAuthService.insertTrainDormAuth(auth);
|
||||
Long authId = auth.getAuthId();
|
||||
|
||||
// 初始为待下发
|
||||
TrainDormAuth step0 = dormAuthService.selectTrainDormAuthByAuthId(authId);
|
||||
assertEquals(TrainDormAuth.STATUS_PENDING, step0.getFaceSyncStatus());
|
||||
|
||||
// 下发失败 -> STATUS_FAILED
|
||||
dormAuthService.markSyncFailed(authId, "Network error", "system");
|
||||
TrainDormAuth step1 = dormAuthService.selectTrainDormAuthByAuthId(authId);
|
||||
assertEquals(TrainDormAuth.STATUS_FAILED, step1.getFaceSyncStatus());
|
||||
|
||||
// 注销权限 -> STATUS_REVOKED
|
||||
dormAuthService.revokeAuth(authId, "admin");
|
||||
TrainDormAuth step2 = dormAuthService.selectTrainDormAuthByAuthId(authId);
|
||||
assertEquals(TrainDormAuth.STATUS_REVOKED, step2.getFaceSyncStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Requirement G: No DMS client dependency & No Fake Device Success
|
||||
* 不调用任何外部 DMS,创建时绝不虚假赋值为已下发成功 (STATUS_ACTIVE)
|
||||
*/
|
||||
@Test
|
||||
public void testG_NoDmsClientDependencyAndNoFakeSuccess()
|
||||
{
|
||||
TrainDormAuth auth = new TrainDormAuth();
|
||||
auth.setAccommodationId(5001L);
|
||||
// 即使客户端试图传入 STATUS_ACTIVE,业务层由于无实际下发能力,必须强制为 STATUS_PENDING
|
||||
auth.setFaceSyncStatus(TrainDormAuth.STATUS_ACTIVE);
|
||||
|
||||
dormAuthService.insertTrainDormAuth(auth);
|
||||
|
||||
TrainDormAuth saved = dormAuthService.selectTrainDormAuthByAuthId(auth.getAuthId());
|
||||
assertEquals("没有真实DMS调用前,状态必须为待下发", TrainDormAuth.STATUS_PENDING, saved.getFaceSyncStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wave 0 Review Gate - Requirement 15: DormAuth History Test
|
||||
* 验证换房场景下授权记录共存:
|
||||
* Old Accommodation -> Old DormAuth (status=REVOKED)
|
||||
* New Accommodation -> New DormAuth (status=PENDING/ACTIVE)
|
||||
* 两条授权记录在历史中共存,互不覆盖
|
||||
*/
|
||||
@Test
|
||||
public void testDormAuthHistoryAcrossTransfers()
|
||||
{
|
||||
Long oldAccId = 5001L;
|
||||
Long newAccId = 5003L;
|
||||
|
||||
// 注册新住宿
|
||||
TrainAccommodation newAcc = new TrainAccommodation();
|
||||
newAcc.setAccommodationId(newAccId);
|
||||
newAcc.setEnrollmentId(1001L);
|
||||
newAcc.setDormId("DORM_201");
|
||||
newAcc.setBedNo("新1号床");
|
||||
newAcc.setAccommodationStatus(TrainAccommodation.STATUS_CHECKED_IN);
|
||||
mockAccommodationMapper.save(newAcc);
|
||||
|
||||
// 1. 旧住宿对应旧授权
|
||||
TrainDormAuth oldAuth = new TrainDormAuth();
|
||||
oldAuth.setAccommodationId(oldAccId);
|
||||
dormAuthService.insertTrainDormAuth(oldAuth);
|
||||
Long oldAuthId = oldAuth.getAuthId();
|
||||
|
||||
// 2. 新住宿对应新授权
|
||||
TrainDormAuth newAuth = new TrainDormAuth();
|
||||
newAuth.setAccommodationId(newAccId);
|
||||
dormAuthService.insertTrainDormAuth(newAuth);
|
||||
Long newAuthId = newAuth.getAuthId();
|
||||
|
||||
// 3. 旧授权注销 (退宿或换房引起)
|
||||
dormAuthService.revokeAuth(oldAuthId, "admin");
|
||||
|
||||
// 4. 验证两条授权在历史中共存,状态相互独立
|
||||
TrainDormAuth queriedOld = dormAuthService.selectTrainDormAuthByAuthId(oldAuthId);
|
||||
TrainDormAuth queriedNew = dormAuthService.selectTrainDormAuthByAuthId(newAuthId);
|
||||
|
||||
assertNotNull(queriedOld);
|
||||
assertNotNull(queriedNew);
|
||||
assertEquals(TrainDormAuth.STATUS_REVOKED, queriedOld.getFaceSyncStatus());
|
||||
assertEquals(TrainDormAuth.STATUS_PENDING, queriedNew.getFaceSyncStatus());
|
||||
assertEquals(oldAccId, queriedOld.getAccommodationId());
|
||||
assertEquals(newAccId, queriedNew.getAccommodationId());
|
||||
}
|
||||
|
||||
// ================================= 内部 Mock 类 =================================
|
||||
|
||||
private static class MockDormAuthMapper implements TrainDormAuthMapper
|
||||
{
|
||||
private final Map<Long, TrainDormAuth> store = new HashMap<>();
|
||||
private long idSeq = 10L;
|
||||
|
||||
@Override
|
||||
public TrainDormAuth selectTrainDormAuthByAuthId(Long authId)
|
||||
{
|
||||
return store.get(authId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrainDormAuth selectTrainDormAuthByAccommodationId(Long accommodationId)
|
||||
{
|
||||
for (TrainDormAuth a : store.values())
|
||||
{
|
||||
if (accommodationId.equals(a.getAccommodationId()))
|
||||
{
|
||||
return a;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainDormAuth> selectTrainDormAuthList(TrainDormAuth trainDormAuth)
|
||||
{
|
||||
return new ArrayList<>(store.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertTrainDormAuth(TrainDormAuth trainDormAuth)
|
||||
{
|
||||
trainDormAuth.setAuthId(++idSeq);
|
||||
store.put(trainDormAuth.getAuthId(), trainDormAuth);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateTrainDormAuth(TrainDormAuth trainDormAuth)
|
||||
{
|
||||
store.put(trainDormAuth.getAuthId(), trainDormAuth);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateSyncStatus(Long authId, Integer faceSyncStatus, String lastError, String updateBy, Date updateTime)
|
||||
{
|
||||
TrainDormAuth a = store.get(authId);
|
||||
if (a != null)
|
||||
{
|
||||
a.setFaceSyncStatus(faceSyncStatus);
|
||||
a.setLastError(lastError);
|
||||
a.setUpdateBy(updateBy);
|
||||
a.setUpdateTime(updateTime);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int increaseRetryCount(Long authId, String lastError, String updateBy, Date updateTime)
|
||||
{
|
||||
TrainDormAuth a = store.get(authId);
|
||||
if (a != null)
|
||||
{
|
||||
int current = a.getRetryCount() != null ? a.getRetryCount() : 0;
|
||||
a.setRetryCount(current + 1);
|
||||
a.setLastError(lastError);
|
||||
a.setUpdateBy(updateBy);
|
||||
a.setUpdateTime(updateTime);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTrainDormAuthByAuthId(Long authId)
|
||||
{
|
||||
return store.remove(authId) != null ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTrainDormAuthByAuthIds(Long[] authIds)
|
||||
{
|
||||
int count = 0;
|
||||
for (Long id : authIds)
|
||||
{
|
||||
if (store.remove(id) != null) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
private static class MockAccommodationMapper implements TrainAccommodationMapper
|
||||
{
|
||||
private final Map<Long, TrainAccommodation> store = new HashMap<>();
|
||||
|
||||
public void save(TrainAccommodation a)
|
||||
{
|
||||
store.put(a.getAccommodationId(), a);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrainAccommodation selectTrainAccommodationByAccommodationId(Long accommodationId)
|
||||
{
|
||||
return store.get(accommodationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrainAccommodation selectTrainAccommodationByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
for (TrainAccommodation a : store.values())
|
||||
{
|
||||
if (enrollmentId.equals(a.getEnrollmentId())) return a;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrainAccommodation selectActiveAccommodationByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
for (TrainAccommodation a : store.values())
|
||||
{
|
||||
if (enrollmentId.equals(a.getEnrollmentId()) &&
|
||||
(TrainAccommodation.STATUS_PREALLOCATED.equals(a.getAccommodationStatus())
|
||||
|| TrainAccommodation.STATUS_CHECKED_IN.equals(a.getAccommodationStatus())))
|
||||
{
|
||||
return a;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainAccommodation> selectAccommodationHistoryByEnrollmentId(Long enrollmentId)
|
||||
{
|
||||
List<TrainAccommodation> list = new ArrayList<>();
|
||||
for (TrainAccommodation a : store.values())
|
||||
{
|
||||
if (enrollmentId.equals(a.getEnrollmentId())) list.add(a);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainAccommodation> selectTrainAccommodationList(TrainAccommodation accommodation)
|
||||
{
|
||||
return new ArrayList<>(store.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainAccommodation> selectActiveAccommodationsByPlanId(String planId)
|
||||
{
|
||||
return new ArrayList<>(store.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countActiveByDormId(String dormId)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertTrainAccommodation(TrainAccommodation accommodation)
|
||||
{
|
||||
store.put(accommodation.getAccommodationId(), accommodation);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateTrainAccommodation(TrainAccommodation accommodation)
|
||||
{
|
||||
store.put(accommodation.getAccommodationId(), accommodation);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateAccommodationStatus(Long accommodationId, Integer accommodationStatus, String updateBy, Date updateTime)
|
||||
{
|
||||
TrainAccommodation a = store.get(accommodationId);
|
||||
if (a != null)
|
||||
{
|
||||
a.setAccommodationStatus(accommodationStatus);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTrainAccommodationByAccommodationId(Long accommodationId)
|
||||
{
|
||||
return store.remove(accommodationId) != null ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTrainAccommodationByAccommodationIds(Long[] accommodationIds)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
-- 006_accommodation_history_constraint.sql
|
||||
-- 目标:移除 train_accommodation 全局唯一约束 uq_accommodation_enroll,
|
||||
-- 创建部分唯一索引 uq_accommodation_enroll_active (每个 enrollment 最多 1 条有效住宿),
|
||||
-- 允许学员拥有多条退宿历史记录 (status=2),彻底支持调房与历史留痕。
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_dup_count INTEGER;
|
||||
v_con_def TEXT;
|
||||
v_idx_def TEXT;
|
||||
BEGIN
|
||||
-- 1. Pre-Migration Data Safety Guard
|
||||
-- 检查是否已存在同一个 enrollment_id 拥有 2 条或以上处于有效状态 (0=预分配, 1=已入住) 的住宿记录
|
||||
SELECT COUNT(*) INTO v_dup_count
|
||||
FROM (
|
||||
SELECT enrollment_id
|
||||
FROM train_accommodation
|
||||
WHERE accommodation_status IN (0, 1)
|
||||
GROUP BY enrollment_id
|
||||
HAVING COUNT(*) > 1
|
||||
) t;
|
||||
|
||||
IF v_dup_count > 0 THEN
|
||||
RAISE EXCEPTION 'Pre-migration check failed: found % enrollment(s) with multiple active accommodations (status IN (0,1)). Manual resolution required before applying migration 006.', v_dup_count;
|
||||
END IF;
|
||||
|
||||
-- 2. Existing Constraint Guard (Idempotent Drop)
|
||||
-- 检查约束 uq_accommodation_enroll 是否存在且符合预期
|
||||
SELECT pg_get_constraintdef(c.oid) INTO v_con_def
|
||||
FROM pg_constraint c
|
||||
JOIN pg_class t ON c.conrelid = t.oid
|
||||
WHERE t.relname = 'train_accommodation'
|
||||
AND c.conname = 'uq_accommodation_enroll';
|
||||
|
||||
IF v_con_def IS NOT NULL THEN
|
||||
-- 验证旧约束是否为 UNIQUE (enrollment_id)
|
||||
IF v_con_def ILIKE 'UNIQUE (enrollment_id)%' THEN
|
||||
RAISE NOTICE 'Dropping legacy global unique constraint: uq_accommodation_enroll';
|
||||
ALTER TABLE train_accommodation DROP CONSTRAINT uq_accommodation_enroll;
|
||||
ELSE
|
||||
RAISE EXCEPTION 'Constraint collision: constraint uq_accommodation_enroll exists but definition is unexpected: %', v_con_def;
|
||||
END IF;
|
||||
ELSE
|
||||
RAISE NOTICE 'Constraint uq_accommodation_enroll does not exist, skipping drop.';
|
||||
END IF;
|
||||
|
||||
-- 3. New Index Collision Guard & Creation
|
||||
-- 检查部分唯一索引 uq_accommodation_enroll_active
|
||||
SELECT pg_get_indexdef(i.indexrelid) INTO v_idx_def
|
||||
FROM pg_index i
|
||||
JOIN pg_class c ON i.indexrelid = c.oid
|
||||
JOIN pg_class t ON i.indrelid = t.oid
|
||||
WHERE t.relname = 'train_accommodation'
|
||||
AND c.relname = 'uq_accommodation_enroll_active';
|
||||
|
||||
IF v_idx_def IS NOT NULL THEN
|
||||
-- 验证已有索引是否为预期的部分唯一索引
|
||||
IF v_idx_def ILIKE '%UNIQUE INDEX uq_accommodation_enroll_active ON %train_accommodation% (enrollment_id)%WHERE (accommodation_status = ANY (ARRAY[0, 1]))%'
|
||||
OR v_idx_def ILIKE '%UNIQUE INDEX uq_accommodation_enroll_active ON %train_accommodation% (enrollment_id)%WHERE%accommodation_status IN (0, 1)%' THEN
|
||||
RAISE NOTICE 'Partial unique index uq_accommodation_enroll_active already exists with expected definition, skipping creation.';
|
||||
ELSE
|
||||
RAISE EXCEPTION 'Index collision: index uq_accommodation_enroll_active exists but definition differs: %', v_idx_def;
|
||||
END IF;
|
||||
ELSE
|
||||
RAISE NOTICE 'Creating partial unique index: uq_accommodation_enroll_active';
|
||||
CREATE UNIQUE INDEX uq_accommodation_enroll_active
|
||||
ON train_accommodation (enrollment_id)
|
||||
WHERE accommodation_status IN (0, 1);
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -137,3 +137,35 @@
|
||||
2. **`train_dorm_auth`**:Kingbase 物理表已在 V2 中升级为以 `accommodation_id` 为外键的门禁授权结构,但 Java 实体类 `TrainDormAuth.java` 与 XML Mapper 依然停留在 V1 旧结构(持有 `student_id`, `plan_id`, `dorm_id`, `bed_no`)。
|
||||
- **执行原则**:
|
||||
在进入 R.3B 真正编写 DMS Adapter 之前,**必须先在 Wave 0 完成本地住宿与门禁授权领域模型对齐**!严禁让外部 DMS 集成适配器直接架设在当前存在严重漂移的本地模型之上。
|
||||
|
||||
### 5.2 Wave 0 Implementation Result (本地模型对齐实施完成)
|
||||
- **完成状态**:已全面对齐并消除结构漂移。
|
||||
- **实体层**:
|
||||
- 新增 `TrainAccommodation.java`,严格对应 Kingbase V2 生产物理表(`accommodation_id`, `enrollment_id`, `dorm_id`, `bed_no`, `assigned_time`, `accommodation_status`, 审计列)。
|
||||
- 重构 `TrainDormAuth.java`,外键核心对齐 `accommodation_id`,持久化字段剔除 V1 遗留字段(`student_id`, `plan_id`, `dorm_id`, `bed_no`, `createUser`),保留为非持久化兼容视图字段以保障只读兼容。
|
||||
- **Mapper/SQL**:
|
||||
- 新增 `TrainAccommodationMapper.java` / `TrainAccommodationMapper.xml`,所有字段显式指定,严禁 `SELECT *`。
|
||||
- 重构 `TrainDormAuthMapper.xml`,INSERT/UPDATE 语句彻底清理 V1 废弃字段写入,通过 LEFT JOIN 视图提供兼容字段。
|
||||
- **业务服务**:
|
||||
- 新增 `ITrainAccommodationService` / `TrainAccommodationServiceImpl`,落地“同一时刻最多1个有效住宿安排”(Duplicate Active Accommodation Guard)、宿舍核定容量边界检查(Capacity Boundary Guard)与换房原子流转(Transfer)。
|
||||
- 升级 `TrainDormAuthServiceImpl`,严格执行“Enrollment -> Accommodation -> Door Authorization”业务单向依赖,禁止绕过住宿事实直接授权。
|
||||
|
||||
### 5.3 Wave 0.1 Accommodation History & 006 Migration (基数模型升级与迁移方案)
|
||||
- **基数语义冻结**:
|
||||
- `Enrollment 1:N Accommodation History`(一个报名记录可有多条历史住宿记录,调房换房产生历史轨迹)
|
||||
- `Enrollment 1:0..1 Active Accommodation`(同一时间一个报名记录最多只能有 1 条有效住宿记录,`accommodation_status IN (0, 1)`)
|
||||
- `Accommodation 1:0..1 DoorAuthorization`(门禁授权强依赖具体的某一次住宿记录)
|
||||
- `TIME_RANGE_SUPPORT = LIMITED`(当前 Kingbase 物理表仅包含 `assigned_time`,无 `start_time`/`end_time`,时间窗口支持为有限支持)
|
||||
- **006 迁移脚本方案 (`006_accommodation_history_constraint.sql`)**:
|
||||
- 脚本路径:`RuoYi-Vue/sql/system/kingbase/006_accommodation_history_constraint.sql`
|
||||
- 安全编号:`006`(001-003 为基线,004-005 已被 R.2 分支占用)
|
||||
- 前置检查:Fail-fast,若发现同一 `enrollment_id` 存在多条 active(`accommodation_status IN (0, 1)`)则 `RAISE EXCEPTION` 阻止迁移
|
||||
- 安全 DROP:仅在原约束 `uq_accommodation_enroll` 确认为 `UNIQUE (enrollment_id)` 时安全移除
|
||||
- 局部唯一索引:创建 `uq_accommodation_enroll_active` 并在 `WHERE accommodation_status IN (0, 1)` 生效
|
||||
- 幂等性防护:重复执行不会报错或破坏表结构
|
||||
- 生产执行纪律:本迁移脚本仅完成工程资产化与隔离测试,**严禁在生产数据库执行**
|
||||
- **自动化测试**:
|
||||
- `AccommodationHistoryMigrationContractTest`(契约测试:SQL 语法、前置检查、条件 DROP、局部索引及幂等性)
|
||||
- `AccommodationServiceTest`(10 项测试,包含调房历史与并发唯一性)
|
||||
- `DormAuthAlignmentTest`(7 项测试,包含依赖住宿与换房级联)
|
||||
- 全量测试套件 174 项全部 PASS(0 failures, 0 errors)。
|
||||
@@ -101,3 +101,26 @@
|
||||
1. **第一步**:重构修复 Java 实体类 `TrainDormAuth.java`,彻底移除已弃用的 `student_id`, `plan_id`, `dorm_id` 字段,使其与已存在的 Kingbase 物理表字段完全对齐。
|
||||
2. **第二步**:创建 `train_external_resource_mapping` 与 `train_external_operation` 迁移脚本(遵循系统标准 Liquibase/Flyway 规范)。
|
||||
3. **第三步**:通过 Spring Data / MyBatis-Plus 完成三表关联映射。
|
||||
|
||||
### 4.1 Wave 0 Implementation Result (第一步已正式交付)
|
||||
- **已完成事项**:
|
||||
1. 重构并修复 `TrainDormAuth.java`,彻底废弃了向 `train_dorm_auth` 写入 `student_id`, `plan_id`, `dorm_id`, `bed_no`, `create_user` 的历史假设。
|
||||
2. 补齐 `TrainAccommodation.java`、`TrainAccommodationMapper.java`、`TrainAccommodationMapper.xml`、`ITrainAccommodationService` 以及 `TrainAccommodationServiceImpl`,完全对齐 V2 生产物理结构。
|
||||
3. 严格禁止在 Wave 0 中引入任何 DMS 物理字段(如 `lock_id`, `place_id`, `operation_id`),保持 Vendor-Neutral 纯净业务边界。
|
||||
4. 生产 Migration 检查:`MIGRATION_REQUIRED = NO`,生产数据库零写入、零变更。
|
||||
|
||||
### 4.2 Wave 0.1 Accommodation History Migration (006 迁移方案与基数冻结)
|
||||
- **领域基数与物理约束演进**:
|
||||
- **历史模型**:`Enrollment 1:N Accommodation History`,允许学员由于调房等产生多条住宿历史。
|
||||
- **唯一性收敛**:`Enrollment 1:0..1 Active Accommodation`,原全局唯一约束 `UNIQUE(enrollment_id)` 阻碍了历史留痕,演进为针对有效状态(`accommodation_status IN (0, 1)`)的局部唯一索引。
|
||||
- **授权关联**:`Accommodation 1:0..1 DoorAuthorization`,门禁授权强引用住宿实体。
|
||||
- **时间范围特性**:`TIME_RANGE_SUPPORT = LIMITED`,当前物理表仅有 `assigned_time`,未扩展起止时间列。
|
||||
- **006 Migration 脚本设计与验证**:
|
||||
- 文件路径:`RuoYi-Vue/sql/system/kingbase/006_accommodation_history_constraint.sql`
|
||||
- 安全编号:`006`(避免与 R.2 占用的 004/005 冲突)。
|
||||
- 执行安全机制:
|
||||
1. 前置重复数据检查:存在并发违规 active 记录时直接 `RAISE EXCEPTION` 阻断。
|
||||
2. 严格条件 DROP:仅当原约束 `uq_accommodation_enroll` 确认为 `UNIQUE (enrollment_id)` 时执行 drop。
|
||||
3. 创建局部唯一索引:`CREATE UNIQUE INDEX IF NOT EXISTS uq_accommodation_enroll_active ON train_accommodation (enrollment_id) WHERE accommodation_status IN (0, 1);`
|
||||
4. 幂等防护:重复执行幂等安全。
|
||||
- 生产执行纪律:本脚本已在单元/契约测试中完成全量静态与逻辑校验,**禁止在生产库执行**。
|
||||
@@ -139,3 +139,29 @@ flowchart TD
|
||||
| **Wave 2: DMS Adapter & Mock Tests** | 协议适配器、TokenManager、轮询调度器、22项单测 | 外部集成层 | 5 人天 |
|
||||
| **Wave 3: Orchestration, UI & Sandbox** | 业务编排服务、前端界面、现场沙箱实物 E2E 联调 | 业务层/前端/现场 | 5~6 人天 |
|
||||
| **总计** | **R.3 门禁集成端到端正式交付上线** | **全栈闭环** | **约 15~16 人天** |
|
||||
|
||||
### 3.1 Wave 0 Implementation Result (交付与验收验证)
|
||||
- **阶段目标**:Local Accommodation & Door Authorization Domain Alignment
|
||||
- **执行状态**:已完成 (Completed)
|
||||
- **交付清单**:
|
||||
1. `TrainAccommodation.java`、`TrainAccommodationMapper.java`、`TrainAccommodationMapper.xml`
|
||||
2. `ITrainAccommodationService.java`、`TrainAccommodationServiceImpl.java`
|
||||
3. `TrainAccommodationController.java` (最小必要 Admin REST API)
|
||||
4. `TrainDormAuth.java`、`TrainDormAuthMapper.java`、`TrainDormAuthMapper.xml`、`TrainDormAuthServiceImpl.java`、`TrainDormAuthController.java` (V2 Schema 对齐与清理)
|
||||
5. 自动化测试套件:`AccommodationServiceTest`、`DormAuthAlignmentTest`、`AccommodationAndAuthApiControllerContractTest`。
|
||||
|
||||
### 3.2 Wave 0.1 Implementation Result (基数模型升级与 006 迁移交付)
|
||||
- **阶段目标**:Accommodation History Migration & Final Checkpoint
|
||||
- **模型基数关系**:
|
||||
- `Enrollment 1:N Accommodation History`
|
||||
- `Enrollment 1:0..1 Active Accommodation` (`accommodation_status IN (0, 1)`)
|
||||
- `Accommodation 1:0..1 DoorAuthorization`
|
||||
- `TIME_RANGE_SUPPORT = LIMITED`
|
||||
- **新增交付物**:
|
||||
1. `RuoYi-Vue/sql/system/kingbase/006_accommodation_history_constraint.sql`(安全编号 006、前置检查、条件 DROP、局部唯一索引、幂等执行;生产库未执行)
|
||||
2. `RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/AccommodationHistoryMigrationContractTest.java`(迁移脚本规范与语法契约测试)
|
||||
3. 历史查询与调房并发唯一性测试(`selectAccommodationHistoryByEnrollmentId`、`transferAccommodation` 级联授权废弃)
|
||||
- **全量回归验证**:
|
||||
- `ruoyi-train`: 153 tests
|
||||
- `ruoyi-admin`: 21 tests
|
||||
- 总计:174 tests, Failures: 0, Errors: 0, Skipped: 0,全量通过。
|
||||
@@ -106,3 +106,25 @@
|
||||
- 配置与环境变量绑定;`DmsTokenManager` 单飞排他锁并发控制;细分 HTTP 客户端;在线密码/卡片适配器;10 秒轮询调度器;WireMock 22 项单测全覆盖。
|
||||
4. **Wave 3: Business Orchestration & UI & Sandbox (业务编排、界面与现场验收,5~6天)**:
|
||||
- 报到联动授权下发;“先发新房后撤旧房”换房补偿;退训/结训撤权;管理端门禁综合视图;学员端 30 秒倒计时密码受控查看;现场实验台沙箱 E2E 闭环验收。
|
||||
|
||||
### 6.1 Wave 0 Implementation Result (本地领域模型闭环)
|
||||
- **基线与状态**:
|
||||
- 成功消除数据库 V2 模型与 Java 代码之间的全部结构漂移。
|
||||
- 严格保持 0 DMS 耦合:无 DmsClient、无 TokenManager、无外部调用、无假设备成功。
|
||||
- 保持 Kingbase 生产库只读,零 Migration(`MIGRATION_REQUIRED = NO`)。
|
||||
- 交付本地领域模型:`TrainAccommodation` 与升级后的 `TrainDormAuth`。
|
||||
|
||||
### 6.2 Wave 0.1 Accommodation History & Final Checkpoint Decision (基数模型升级与终态)
|
||||
- **核心基数关系冻结**:
|
||||
- `Enrollment 1:N Accommodation History`:允许学员产生多次历史住宿记录(调房/换房留痕)。
|
||||
- `Enrollment 1:0..1 Active Accommodation`:同一时刻最多一条有效住宿(`accommodation_status IN (0, 1)`)。
|
||||
- `Accommodation 1:0..1 DoorAuthorization`:门禁授权强引用住宿实体。
|
||||
- `TIME_RANGE_SUPPORT = LIMITED`:当前 Kingbase 物理表仅有 `assigned_time`,无 `start_time` / `end_time` 列,时间窗口支持为有限支持。
|
||||
- **006 Migration 方案设计**:
|
||||
- 脚本路径:`RuoYi-Vue/sql/system/kingbase/006_accommodation_history_constraint.sql`。
|
||||
- 安全序号:`006`(001-003 为基线,004-005 已被 R.2 分支占用)。
|
||||
- 迁移安全护栏:前置重复 active 检查抛异常、仅在定义匹配时安全 DROP `uq_accommodation_enroll`、创建 `uq_accommodation_enroll_active` 局部唯一索引(`WHERE accommodation_status IN (0, 1)`)、全流程幂等。
|
||||
- 生产执行纪律:**禁止在生产库执行 006 迁移脚本**,本 Wave 仅作资产与契约验证储备。
|
||||
- **质量与构建验收**:
|
||||
- 单元与契约测试总数:174 项,全部 PASS(0 failures, 0 errors)。
|
||||
- Maven `clean test` 与 `clean package` 均顺利构建成功。
|
||||
Reference in new issue
Block a user