新增培训:实现开班计划P0能力
This commit is contained in:
1 parent
7fcaea29b8
commit
eff048874d
21 files changed
+1561
-526
No files matched your search
-104
@@ -1,104 +0,0 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.service.ITrainClassPlanService;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
|
||||
/**
|
||||
* 开班计划Controller
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-08
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/train/plan")
|
||||
public class TrainClassPlanController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ITrainClassPlanService trainClassPlanService;
|
||||
|
||||
/**
|
||||
* 查询开班计划列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:plan:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(TrainClassPlan trainClassPlan)
|
||||
{
|
||||
startPage();
|
||||
List<TrainClassPlan> list = trainClassPlanService.selectTrainClassPlanList(trainClassPlan);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出开班计划列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:plan:export')")
|
||||
@Log(title = "开班计划", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, TrainClassPlan trainClassPlan)
|
||||
{
|
||||
List<TrainClassPlan> list = trainClassPlanService.selectTrainClassPlanList(trainClassPlan);
|
||||
ExcelUtil<TrainClassPlan> util = new ExcelUtil<TrainClassPlan>(TrainClassPlan.class);
|
||||
util.exportExcel(response, list, "开班计划数据");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取开班计划详细信息
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:plan:query')")
|
||||
@GetMapping(value = "/{planId}")
|
||||
public AjaxResult getInfo(@PathVariable("planId") String planId)
|
||||
{
|
||||
return success(trainClassPlanService.selectTrainClassPlanByPlanId(planId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增开班计划
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:plan:add')")
|
||||
@Log(title = "开班计划", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody TrainClassPlan trainClassPlan)
|
||||
{
|
||||
return toAjax(trainClassPlanService.insertTrainClassPlan(trainClassPlan));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改开班计划
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:plan:edit')")
|
||||
@Log(title = "开班计划", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody TrainClassPlan trainClassPlan)
|
||||
{
|
||||
return toAjax(trainClassPlanService.updateTrainClassPlan(trainClassPlan));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除开班计划
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:plan:remove')")
|
||||
@Log(title = "开班计划", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{planIds}")
|
||||
public AjaxResult remove(@PathVariable String[] planIds)
|
||||
{
|
||||
return toAjax(trainClassPlanService.deleteTrainClassPlanByPlanIds(planIds));
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
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.validation.annotation.Validated;
|
||||
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.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.dto.PlanCreateDTO;
|
||||
import com.ruoyi.train.domain.dto.PlanUpdateDTO;
|
||||
import com.ruoyi.train.domain.query.PlanQuery;
|
||||
import com.ruoyi.train.domain.vo.PlanItemVO;
|
||||
import com.ruoyi.train.service.PlanAppService;
|
||||
|
||||
/** 培训计划生命周期 API。 */
|
||||
@RestController
|
||||
@RequestMapping("/train/plans")
|
||||
public class TrainPlanController extends BaseController
|
||||
{
|
||||
private final PlanAppService planAppService;
|
||||
|
||||
@Autowired
|
||||
public TrainPlanController(PlanAppService planAppService)
|
||||
{
|
||||
this.planAppService = planAppService;
|
||||
}
|
||||
|
||||
/** 分页查询培训计划列表。 */
|
||||
@PreAuthorize("@ss.hasPermi('train:plan:list')")
|
||||
@GetMapping
|
||||
public TableDataInfo list(PlanQuery query)
|
||||
{
|
||||
startPage();
|
||||
List<PlanItemVO> list = planAppService.listPlans(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/** 获取培训计划基本信息。 */
|
||||
@PreAuthorize("@ss.hasPermi('train:plan:query')")
|
||||
@GetMapping("/{planId}")
|
||||
public AjaxResult getInfo(@PathVariable String planId)
|
||||
{
|
||||
return success(planAppService.getPlan(planId));
|
||||
}
|
||||
|
||||
/** 创建培训计划。 */
|
||||
@PreAuthorize("@ss.hasPermi('train:plan:add')")
|
||||
@Log(title = "培训计划", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@Validated @RequestBody PlanCreateDTO dto)
|
||||
{
|
||||
return success(planAppService.createPlan(dto, SecurityUtils.getUsername()));
|
||||
}
|
||||
|
||||
/** 编辑培训计划。 */
|
||||
@PreAuthorize("@ss.hasPermi('train:plan:edit')")
|
||||
@Log(title = "培训计划", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{planId}")
|
||||
public AjaxResult edit(@PathVariable String planId, @Validated @RequestBody PlanUpdateDTO dto)
|
||||
{
|
||||
planAppService.updatePlan(planId, dto, SecurityUtils.getUsername());
|
||||
return success();
|
||||
}
|
||||
|
||||
/** 结束培训计划。 */
|
||||
@PreAuthorize("@ss.hasPermi('train:plan:complete')")
|
||||
@Log(title = "培训计划结训", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/{planId}/complete")
|
||||
public AjaxResult complete(@PathVariable String planId)
|
||||
{
|
||||
planAppService.completePlan(planId, SecurityUtils.getUsername());
|
||||
return success();
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,13 @@
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.train.constant;
|
||||
|
||||
/** 培训计划正式业务错误码(API Contract V1.0.1)。 */
|
||||
public final class PlanErrorCode
|
||||
{
|
||||
public static final int PLAN_TIME_INVALID = 40010;
|
||||
public static final int PLAN_NAME_DUPLICATED = 40011;
|
||||
public static final int PLAN_STATUS_LOCKED = 40012;
|
||||
public static final int PLAN_LESSON_OUT_OF_BOUNDS = 40013;
|
||||
public static final int PLAN_ALREADY_COMPLETED = 40014;
|
||||
public static final int PLAN_NOT_FOUND = 40410;
|
||||
|
||||
public static final String PLAN_TIME_INVALID_MESSAGE = "培训计划起止时间不合法 (结束时间必须晚于开始时间)";
|
||||
public static final String PLAN_NAME_DUPLICATED_MESSAGE = "同名培训计划已存在";
|
||||
public static final String PLAN_STATUS_LOCKED_MESSAGE = "当前培训计划已结训或取消,不允许修改";
|
||||
public static final String PLAN_LESSON_OUT_OF_BOUNDS_MESSAGE = "调整培训时间导致现有已排课次超出计划周期";
|
||||
public static final String PLAN_ALREADY_COMPLETED_MESSAGE = "培训计划已经结训,不可重复执行结训";
|
||||
public static final String PLAN_NOT_FOUND_MESSAGE = "培训计划不存在";
|
||||
|
||||
private PlanErrorCode()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -24,28 +24,25 @@ public class TrainClassPlan extends BaseEntity
|
||||
@Excel(name = "开班名称")
|
||||
private String planName;
|
||||
|
||||
/** 所属课程编号,关联train_course.c_id */
|
||||
@Excel(name = "所属课程编号,关联train_course.c_id")
|
||||
private String cId;
|
||||
|
||||
/** 开班时间 */
|
||||
@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 startTime;
|
||||
|
||||
/** 结束时间 */
|
||||
@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 endTime;
|
||||
|
||||
/** 报到截止时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "报到截止时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date reportDeadline;
|
||||
|
||||
/** 培训地点 */
|
||||
@Excel(name = "培训地点")
|
||||
private String trainAddress;
|
||||
|
||||
/** 授课教师工号,关联train_teacher.t_id */
|
||||
@Excel(name = "授课教师工号,关联train_teacher.t_id")
|
||||
private String tId;
|
||||
|
||||
/** 招生人数 */
|
||||
@Excel(name = "招生人数")
|
||||
private Long recruitNum;
|
||||
@@ -58,9 +55,8 @@ public class TrainClassPlan extends BaseEntity
|
||||
@Excel(name = "0 未开始 1 进行中 2 已结束 3 已取消")
|
||||
private Integer planStatus;
|
||||
|
||||
/** 创建管理员账号 */
|
||||
@Excel(name = "创建管理员账号")
|
||||
private String createUser;
|
||||
/** 删除标志(0代表存在 2代表删除) */
|
||||
private String delFlag;
|
||||
|
||||
public void setPlanId(String planId)
|
||||
{
|
||||
@@ -82,16 +78,6 @@ public class TrainClassPlan extends BaseEntity
|
||||
return planName;
|
||||
}
|
||||
|
||||
public void setcId(String cId)
|
||||
{
|
||||
this.cId = cId;
|
||||
}
|
||||
|
||||
public String getcId()
|
||||
{
|
||||
return cId;
|
||||
}
|
||||
|
||||
public void setStartTime(Date startTime)
|
||||
{
|
||||
this.startTime = startTime;
|
||||
@@ -112,26 +98,26 @@ public class TrainClassPlan extends BaseEntity
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setTrainAddress(String trainAddress)
|
||||
public void setReportDeadline(Date reportDeadline)
|
||||
{
|
||||
this.reportDeadline = reportDeadline;
|
||||
}
|
||||
|
||||
public Date getReportDeadline()
|
||||
{
|
||||
return reportDeadline;
|
||||
}
|
||||
|
||||
public void setTrainAddress(String trainAddress)
|
||||
{
|
||||
this.trainAddress = trainAddress;
|
||||
}
|
||||
|
||||
public String getTrainAddress()
|
||||
public String getTrainAddress()
|
||||
{
|
||||
return trainAddress;
|
||||
}
|
||||
|
||||
public void settId(String tId)
|
||||
{
|
||||
this.tId = tId;
|
||||
}
|
||||
|
||||
public String gettId()
|
||||
{
|
||||
return tId;
|
||||
}
|
||||
|
||||
public void setRecruitNum(Long recruitNum)
|
||||
{
|
||||
this.recruitNum = recruitNum;
|
||||
@@ -162,14 +148,14 @@ public class TrainClassPlan extends BaseEntity
|
||||
return planStatus;
|
||||
}
|
||||
|
||||
public void setCreateUser(String createUser)
|
||||
public void setDelFlag(String delFlag)
|
||||
{
|
||||
this.createUser = createUser;
|
||||
this.delFlag = delFlag;
|
||||
}
|
||||
|
||||
public String getCreateUser()
|
||||
public String getDelFlag()
|
||||
{
|
||||
return createUser;
|
||||
return delFlag;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -177,16 +163,17 @@ public class TrainClassPlan extends BaseEntity
|
||||
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
|
||||
.append("planId", getPlanId())
|
||||
.append("planName", getPlanName())
|
||||
.append("cId", getcId())
|
||||
.append("startTime", getStartTime())
|
||||
.append("endTime", getEndTime())
|
||||
.append("reportDeadline", getReportDeadline())
|
||||
.append("trainAddress", getTrainAddress())
|
||||
.append("tId", gettId())
|
||||
.append("recruitNum", getRecruitNum())
|
||||
.append("applyCondition", getApplyCondition())
|
||||
.append("planStatus", getPlanStatus())
|
||||
.append("createUser", getCreateUser())
|
||||
.append("delFlag", getDelFlag())
|
||||
.append("createBy", getCreateBy())
|
||||
.append("createTime", getCreateTime())
|
||||
.append("updateBy", getUpdateBy())
|
||||
.append("updateTime", getUpdateTime())
|
||||
.append("remark", getRemark())
|
||||
.toString();
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ruoyi.train.domain.dto;
|
||||
|
||||
import java.util.Date;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/** 创建培训计划请求。 */
|
||||
public class PlanCreateDTO
|
||||
{
|
||||
@NotBlank(message = "培训班名称不能为空")
|
||||
@Size(max = 150, message = "培训班名称不能超过150个字符")
|
||||
private String planName;
|
||||
|
||||
@NotNull(message = "开班时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
|
||||
@NotNull(message = "结束时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
|
||||
@NotNull(message = "报到截止时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date reportDeadline;
|
||||
|
||||
@NotNull(message = "招生人数不能为空")
|
||||
@Min(value = 1, message = "招生人数必须大于0")
|
||||
private Long recruitNum;
|
||||
|
||||
@Size(max = 200, message = "培训地点不能超过200个字符")
|
||||
private String trainAddress;
|
||||
|
||||
private String applyCondition;
|
||||
|
||||
@Size(max = 500, message = "备注不能超过500个字符")
|
||||
private String remark;
|
||||
|
||||
public String getPlanName() { return planName; }
|
||||
public void setPlanName(String planName) { this.planName = planName; }
|
||||
public Date getStartTime() { return startTime; }
|
||||
public void setStartTime(Date startTime) { this.startTime = startTime; }
|
||||
public Date getEndTime() { return endTime; }
|
||||
public void setEndTime(Date endTime) { this.endTime = endTime; }
|
||||
public Date getReportDeadline() { return reportDeadline; }
|
||||
public void setReportDeadline(Date reportDeadline) { this.reportDeadline = reportDeadline; }
|
||||
public Long getRecruitNum() { return recruitNum; }
|
||||
public void setRecruitNum(Long recruitNum) { this.recruitNum = recruitNum; }
|
||||
public String getTrainAddress() { return trainAddress; }
|
||||
public void setTrainAddress(String trainAddress) { this.trainAddress = trainAddress; }
|
||||
public String getApplyCondition() { return applyCondition; }
|
||||
public void setApplyCondition(String applyCondition) { this.applyCondition = applyCondition; }
|
||||
public String getRemark() { return remark; }
|
||||
public void setRemark(String remark) { this.remark = remark; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ruoyi.train.domain.dto;
|
||||
|
||||
import java.util.Date;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/** 编辑培训计划请求。生命周期状态不接受客户端直接修改。 */
|
||||
public class PlanUpdateDTO
|
||||
{
|
||||
@NotBlank(message = "培训班名称不能为空")
|
||||
@Size(max = 150, message = "培训班名称不能超过150个字符")
|
||||
private String planName;
|
||||
|
||||
@NotNull(message = "开班时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
|
||||
@NotNull(message = "结束时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
|
||||
@NotNull(message = "报到截止时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date reportDeadline;
|
||||
|
||||
@NotNull(message = "招生人数不能为空")
|
||||
@Min(value = 1, message = "招生人数必须大于0")
|
||||
private Long recruitNum;
|
||||
|
||||
@Size(max = 200, message = "培训地点不能超过200个字符")
|
||||
private String trainAddress;
|
||||
|
||||
private String applyCondition;
|
||||
|
||||
@Size(max = 500, message = "备注不能超过500个字符")
|
||||
private String remark;
|
||||
|
||||
public String getPlanName() { return planName; }
|
||||
public void setPlanName(String planName) { this.planName = planName; }
|
||||
public Date getStartTime() { return startTime; }
|
||||
public void setStartTime(Date startTime) { this.startTime = startTime; }
|
||||
public Date getEndTime() { return endTime; }
|
||||
public void setEndTime(Date endTime) { this.endTime = endTime; }
|
||||
public Date getReportDeadline() { return reportDeadline; }
|
||||
public void setReportDeadline(Date reportDeadline) { this.reportDeadline = reportDeadline; }
|
||||
public Long getRecruitNum() { return recruitNum; }
|
||||
public void setRecruitNum(Long recruitNum) { this.recruitNum = recruitNum; }
|
||||
public String getTrainAddress() { return trainAddress; }
|
||||
public void setTrainAddress(String trainAddress) { this.trainAddress = trainAddress; }
|
||||
public String getApplyCondition() { return applyCondition; }
|
||||
public void setApplyCondition(String applyCondition) { this.applyCondition = applyCondition; }
|
||||
public String getRemark() { return remark; }
|
||||
public void setRemark(String remark) { this.remark = remark; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ruoyi.train.domain.query;
|
||||
|
||||
import java.util.Date;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
/** 培训计划分页筛选条件。分页参数由 RuoYi BaseController 接管。 */
|
||||
public class PlanQuery
|
||||
{
|
||||
private String planName;
|
||||
private Integer planStatus;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTimeBegin;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTimeEnd;
|
||||
|
||||
public String getPlanName() { return planName; }
|
||||
public void setPlanName(String planName) { this.planName = planName; }
|
||||
public Integer getPlanStatus() { return planStatus; }
|
||||
public void setPlanStatus(Integer planStatus) { this.planStatus = planStatus; }
|
||||
public Date getStartTimeBegin() { return startTimeBegin; }
|
||||
public void setStartTimeBegin(Date startTimeBegin) { this.startTimeBegin = startTimeBegin; }
|
||||
public Date getStartTimeEnd() { return startTimeEnd; }
|
||||
public void setStartTimeEnd(Date startTimeEnd) { this.startTimeEnd = startTimeEnd; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/** 培训计划基本信息视图。 */
|
||||
public class PlanDetailVO
|
||||
{
|
||||
private String planId;
|
||||
private String planName;
|
||||
private Integer planStatus;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date reportDeadline;
|
||||
private String trainAddress;
|
||||
private Long recruitNum;
|
||||
private String applyCondition;
|
||||
private String remark;
|
||||
private String createBy;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
private String updateBy;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
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 Integer getPlanStatus() { return planStatus; }
|
||||
public void setPlanStatus(Integer planStatus) { this.planStatus = planStatus; }
|
||||
public Date getStartTime() { return startTime; }
|
||||
public void setStartTime(Date startTime) { this.startTime = startTime; }
|
||||
public Date getEndTime() { return endTime; }
|
||||
public void setEndTime(Date endTime) { this.endTime = endTime; }
|
||||
public Date getReportDeadline() { return reportDeadline; }
|
||||
public void setReportDeadline(Date reportDeadline) { this.reportDeadline = reportDeadline; }
|
||||
public String getTrainAddress() { return trainAddress; }
|
||||
public void setTrainAddress(String trainAddress) { this.trainAddress = trainAddress; }
|
||||
public Long getRecruitNum() { return recruitNum; }
|
||||
public void setRecruitNum(Long recruitNum) { this.recruitNum = recruitNum; }
|
||||
public String getApplyCondition() { return applyCondition; }
|
||||
public void setApplyCondition(String applyCondition) { this.applyCondition = applyCondition; }
|
||||
public String getRemark() { return remark; }
|
||||
public void setRemark(String remark) { this.remark = remark; }
|
||||
public String getCreateBy() { return createBy; }
|
||||
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
||||
public Date getCreateTime() { return createTime; }
|
||||
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
||||
public String getUpdateBy() { return updateBy; }
|
||||
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
||||
public Date getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/** 培训计划列表项。 */
|
||||
public class PlanItemVO
|
||||
{
|
||||
private String planId;
|
||||
private String planName;
|
||||
private Integer planStatus;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date reportDeadline;
|
||||
private String trainAddress;
|
||||
private Long recruitNum;
|
||||
private Long courseCount;
|
||||
private Long enrolledCount;
|
||||
|
||||
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 Integer getPlanStatus() { return planStatus; }
|
||||
public void setPlanStatus(Integer planStatus) { this.planStatus = planStatus; }
|
||||
public Date getStartTime() { return startTime; }
|
||||
public void setStartTime(Date startTime) { this.startTime = startTime; }
|
||||
public Date getEndTime() { return endTime; }
|
||||
public void setEndTime(Date endTime) { this.endTime = endTime; }
|
||||
public Date getReportDeadline() { return reportDeadline; }
|
||||
public void setReportDeadline(Date reportDeadline) { this.reportDeadline = reportDeadline; }
|
||||
public String getTrainAddress() { return trainAddress; }
|
||||
public void setTrainAddress(String trainAddress) { this.trainAddress = trainAddress; }
|
||||
public Long getRecruitNum() { return recruitNum; }
|
||||
public void setRecruitNum(Long recruitNum) { this.recruitNum = recruitNum; }
|
||||
public Long getCourseCount() { return courseCount; }
|
||||
public void setCourseCount(Long courseCount) { this.courseCount = courseCount; }
|
||||
public Long getEnrolledCount() { return enrolledCount; }
|
||||
public void setEnrolledCount(Long enrolledCount) { this.enrolledCount = enrolledCount; }
|
||||
}
|
||||
+18
-15
@@ -2,6 +2,9 @@ package com.ruoyi.train.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.domain.query.PlanQuery;
|
||||
import com.ruoyi.train.domain.vo.PlanItemVO;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 开班计划Mapper接口
|
||||
@@ -19,13 +22,24 @@ public interface TrainClassPlanMapper
|
||||
*/
|
||||
public TrainClassPlan selectTrainClassPlanByPlanId(String planId);
|
||||
|
||||
/**
|
||||
* 查询并锁定培训计划,供生命周期动作使用。
|
||||
*/
|
||||
public TrainClassPlan selectTrainClassPlanByPlanIdForUpdate(String planId);
|
||||
|
||||
/**
|
||||
* 查询开班计划列表
|
||||
*
|
||||
* @param trainClassPlan 开班计划
|
||||
* @param query 培训计划筛选条件
|
||||
* @return 开班计划集合
|
||||
*/
|
||||
public List<TrainClassPlan> selectTrainClassPlanList(TrainClassPlan trainClassPlan);
|
||||
public List<PlanItemVO> selectTrainClassPlanList(PlanQuery query);
|
||||
|
||||
/**
|
||||
* 按规范化名称查询重复计划。
|
||||
*/
|
||||
public int countTrainClassPlanByName(@Param("planName") String planName,
|
||||
@Param("excludePlanId") String excludePlanId);
|
||||
|
||||
/**
|
||||
* 新增开班计划
|
||||
@@ -44,18 +58,7 @@ public interface TrainClassPlanMapper
|
||||
public int updateTrainClassPlan(TrainClassPlan trainClassPlan);
|
||||
|
||||
/**
|
||||
* 删除开班计划
|
||||
*
|
||||
* @param planId 开班计划主键
|
||||
* @return 结果
|
||||
* 更新生命周期状态。
|
||||
*/
|
||||
public int deleteTrainClassPlanByPlanId(String planId);
|
||||
|
||||
/**
|
||||
* 批量删除开班计划
|
||||
*
|
||||
* @param planIds 需要删除的数据主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteTrainClassPlanByPlanIds(String[] planIds);
|
||||
public int updateTrainClassPlanStatus(TrainClassPlan trainClassPlan);
|
||||
}
|
||||
+9
-17
@@ -2,6 +2,8 @@ package com.ruoyi.train.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.domain.query.PlanQuery;
|
||||
import com.ruoyi.train.domain.vo.PlanItemVO;
|
||||
|
||||
/**
|
||||
* 开班计划Service接口
|
||||
@@ -19,13 +21,17 @@ public interface ITrainClassPlanService
|
||||
*/
|
||||
public TrainClassPlan selectTrainClassPlanByPlanId(String planId);
|
||||
|
||||
public TrainClassPlan selectTrainClassPlanByPlanIdForUpdate(String planId);
|
||||
|
||||
/**
|
||||
* 查询开班计划列表
|
||||
*
|
||||
* @param trainClassPlan 开班计划
|
||||
* @param query 培训计划筛选条件
|
||||
* @return 开班计划集合
|
||||
*/
|
||||
public List<TrainClassPlan> selectTrainClassPlanList(TrainClassPlan trainClassPlan);
|
||||
public List<PlanItemVO> selectTrainClassPlanList(PlanQuery query);
|
||||
|
||||
public boolean existsByPlanName(String planName, String excludePlanId);
|
||||
|
||||
/**
|
||||
* 新增开班计划
|
||||
@@ -43,19 +49,5 @@ public interface ITrainClassPlanService
|
||||
*/
|
||||
public int updateTrainClassPlan(TrainClassPlan trainClassPlan);
|
||||
|
||||
/**
|
||||
* 批量删除开班计划
|
||||
*
|
||||
* @param planIds 需要删除的开班计划主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteTrainClassPlanByPlanIds(String[] planIds);
|
||||
|
||||
/**
|
||||
* 删除开班计划信息
|
||||
*
|
||||
* @param planId 开班计划主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteTrainClassPlanByPlanId(String planId);
|
||||
public int updateTrainClassPlanStatus(TrainClassPlan trainClassPlan);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.train.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.train.domain.dto.PlanCreateDTO;
|
||||
import com.ruoyi.train.domain.dto.PlanUpdateDTO;
|
||||
import com.ruoyi.train.domain.query.PlanQuery;
|
||||
import com.ruoyi.train.domain.vo.PlanDetailVO;
|
||||
import com.ruoyi.train.domain.vo.PlanItemVO;
|
||||
|
||||
/** 培训计划生命周期应用服务。 */
|
||||
public interface PlanAppService
|
||||
{
|
||||
List<PlanItemVO> listPlans(PlanQuery query);
|
||||
|
||||
PlanDetailVO getPlan(String planId);
|
||||
|
||||
String createPlan(PlanCreateDTO dto, String operator);
|
||||
|
||||
void updatePlan(String planId, PlanUpdateDTO dto, String operator);
|
||||
|
||||
void completePlan(String planId, String operator);
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
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.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.train.constant.PlanErrorCode;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.domain.dto.PlanCreateDTO;
|
||||
import com.ruoyi.train.domain.dto.PlanUpdateDTO;
|
||||
import com.ruoyi.train.domain.query.PlanQuery;
|
||||
import com.ruoyi.train.domain.vo.PlanDetailVO;
|
||||
import com.ruoyi.train.domain.vo.PlanItemVO;
|
||||
import com.ruoyi.train.service.ITrainClassPlanService;
|
||||
import com.ruoyi.train.service.PlanAppService;
|
||||
import com.ruoyi.train.service.port.PlanEnrollmentCompletionPort;
|
||||
|
||||
/** 培训计划生命周期应用服务实现。 */
|
||||
@Service
|
||||
public class PlanAppServiceImpl implements PlanAppService
|
||||
{
|
||||
private static final int STATUS_NOT_STARTED = 0;
|
||||
private static final int STATUS_COMPLETED = 2;
|
||||
private static final int STATUS_CANCELLED = 3;
|
||||
|
||||
private final ITrainClassPlanService planService;
|
||||
private final List<PlanEnrollmentCompletionPort> enrollmentCompletionPorts;
|
||||
|
||||
@Autowired
|
||||
public PlanAppServiceImpl(ITrainClassPlanService planService,
|
||||
ObjectProvider<PlanEnrollmentCompletionPort> enrollmentCompletionPortProvider)
|
||||
{
|
||||
this(planService, enrollmentCompletionPortProvider.orderedStream()
|
||||
.collect(java.util.stream.Collectors.toList()));
|
||||
}
|
||||
|
||||
/** Test seam; production has no fake/no-op Enrollment adapter before ISSUE-03-01. */
|
||||
PlanAppServiceImpl(ITrainClassPlanService planService,
|
||||
List<PlanEnrollmentCompletionPort> enrollmentCompletionPorts)
|
||||
{
|
||||
this.planService = planService;
|
||||
this.enrollmentCompletionPorts = enrollmentCompletionPorts == null
|
||||
? Collections.emptyList() : enrollmentCompletionPorts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PlanItemVO> listPlans(PlanQuery query)
|
||||
{
|
||||
return planService.selectTrainClassPlanList(query == null ? new PlanQuery() : query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlanDetailVO getPlan(String planId)
|
||||
{
|
||||
TrainClassPlan plan = requirePlan(planService.selectTrainClassPlanByPlanId(planId));
|
||||
return toDetailVO(plan);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String createPlan(PlanCreateDTO dto, String operator)
|
||||
{
|
||||
validatePlan(dto.getStartTime(), dto.getEndTime(), dto.getReportDeadline(), dto.getRecruitNum());
|
||||
String planName = normalizePlanName(dto.getPlanName());
|
||||
ensureUniqueName(planName, null);
|
||||
|
||||
Date now = DateUtils.getNowDate();
|
||||
TrainClassPlan plan = new TrainClassPlan();
|
||||
plan.setPlanId(IdUtils.fastSimpleUUID());
|
||||
plan.setPlanName(planName);
|
||||
plan.setStartTime(dto.getStartTime());
|
||||
plan.setEndTime(dto.getEndTime());
|
||||
plan.setReportDeadline(dto.getReportDeadline());
|
||||
plan.setTrainAddress(dto.getTrainAddress());
|
||||
plan.setRecruitNum(dto.getRecruitNum());
|
||||
plan.setApplyCondition(dto.getApplyCondition());
|
||||
plan.setPlanStatus(STATUS_NOT_STARTED);
|
||||
plan.setDelFlag("0");
|
||||
plan.setCreateBy(operator);
|
||||
plan.setCreateTime(now);
|
||||
plan.setRemark(dto.getRemark());
|
||||
planService.insertTrainClassPlan(plan);
|
||||
return plan.getPlanId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updatePlan(String planId, PlanUpdateDTO dto, String operator)
|
||||
{
|
||||
TrainClassPlan existing = requirePlan(planService.selectTrainClassPlanByPlanIdForUpdate(planId));
|
||||
if (isLocked(existing.getPlanStatus()))
|
||||
{
|
||||
throw error(PlanErrorCode.PLAN_STATUS_LOCKED_MESSAGE, PlanErrorCode.PLAN_STATUS_LOCKED);
|
||||
}
|
||||
validatePlan(dto.getStartTime(), dto.getEndTime(), dto.getReportDeadline(), dto.getRecruitNum());
|
||||
String planName = normalizePlanName(dto.getPlanName());
|
||||
ensureUniqueName(planName, planId);
|
||||
|
||||
TrainClassPlan plan = new TrainClassPlan();
|
||||
plan.setPlanId(planId);
|
||||
plan.setPlanName(planName);
|
||||
plan.setStartTime(dto.getStartTime());
|
||||
plan.setEndTime(dto.getEndTime());
|
||||
plan.setReportDeadline(dto.getReportDeadline());
|
||||
plan.setTrainAddress(dto.getTrainAddress());
|
||||
plan.setRecruitNum(dto.getRecruitNum());
|
||||
plan.setApplyCondition(dto.getApplyCondition());
|
||||
plan.setUpdateBy(operator);
|
||||
plan.setUpdateTime(DateUtils.getNowDate());
|
||||
plan.setRemark(dto.getRemark());
|
||||
planService.updateTrainClassPlan(plan);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void completePlan(String planId, String operator)
|
||||
{
|
||||
List<PlanEnrollmentCompletionPort> completionPorts = requireEnrollmentCompletionPorts();
|
||||
TrainClassPlan plan = requirePlan(planService.selectTrainClassPlanByPlanIdForUpdate(planId));
|
||||
if (Integer.valueOf(STATUS_COMPLETED).equals(plan.getPlanStatus()))
|
||||
{
|
||||
throw error(PlanErrorCode.PLAN_ALREADY_COMPLETED_MESSAGE,
|
||||
PlanErrorCode.PLAN_ALREADY_COMPLETED);
|
||||
}
|
||||
if (Integer.valueOf(STATUS_CANCELLED).equals(plan.getPlanStatus()))
|
||||
{
|
||||
throw error(PlanErrorCode.PLAN_STATUS_LOCKED_MESSAGE, PlanErrorCode.PLAN_STATUS_LOCKED);
|
||||
}
|
||||
|
||||
Date completeTime = DateUtils.getNowDate();
|
||||
TrainClassPlan statusUpdate = new TrainClassPlan();
|
||||
statusUpdate.setPlanId(planId);
|
||||
statusUpdate.setPlanStatus(STATUS_COMPLETED);
|
||||
statusUpdate.setUpdateBy(operator);
|
||||
statusUpdate.setUpdateTime(completeTime);
|
||||
planService.updateTrainClassPlanStatus(statusUpdate);
|
||||
|
||||
for (PlanEnrollmentCompletionPort port : completionPorts)
|
||||
{
|
||||
port.completeEnrollments(planId, operator, completeTime);
|
||||
}
|
||||
}
|
||||
|
||||
private List<PlanEnrollmentCompletionPort> requireEnrollmentCompletionPorts()
|
||||
{
|
||||
if (enrollmentCompletionPorts.isEmpty())
|
||||
{
|
||||
throw new IllegalStateException(
|
||||
"PlanEnrollmentCompletionPort is not configured; plan completion is disabled");
|
||||
}
|
||||
return enrollmentCompletionPorts;
|
||||
}
|
||||
|
||||
private TrainClassPlan requirePlan(TrainClassPlan plan)
|
||||
{
|
||||
if (plan == null)
|
||||
{
|
||||
throw error(PlanErrorCode.PLAN_NOT_FOUND_MESSAGE, PlanErrorCode.PLAN_NOT_FOUND);
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
private void ensureUniqueName(String planName, String excludePlanId)
|
||||
{
|
||||
if (planService.existsByPlanName(planName, excludePlanId))
|
||||
{
|
||||
throw error(PlanErrorCode.PLAN_NAME_DUPLICATED_MESSAGE,
|
||||
PlanErrorCode.PLAN_NAME_DUPLICATED);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizePlanName(String planName)
|
||||
{
|
||||
return StringUtils.trim(planName);
|
||||
}
|
||||
|
||||
private void validatePlan(Date startTime, Date endTime, Date reportDeadline, Long recruitNum)
|
||||
{
|
||||
if (startTime == null || endTime == null || !startTime.before(endTime))
|
||||
{
|
||||
throw error(PlanErrorCode.PLAN_TIME_INVALID_MESSAGE, PlanErrorCode.PLAN_TIME_INVALID);
|
||||
}
|
||||
if (reportDeadline == null || reportDeadline.after(startTime))
|
||||
{
|
||||
throw error("报到截止时间不得晚于开班时间", PlanErrorCode.PLAN_TIME_INVALID);
|
||||
}
|
||||
if (recruitNum == null || recruitNum.longValue() <= 0)
|
||||
{
|
||||
throw new ServiceException("计划招生人数必须大于0");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLocked(Integer status)
|
||||
{
|
||||
return Integer.valueOf(STATUS_COMPLETED).equals(status)
|
||||
|| Integer.valueOf(STATUS_CANCELLED).equals(status);
|
||||
}
|
||||
|
||||
private PlanDetailVO toDetailVO(TrainClassPlan plan)
|
||||
{
|
||||
PlanDetailVO vo = new PlanDetailVO();
|
||||
vo.setPlanId(plan.getPlanId());
|
||||
vo.setPlanName(plan.getPlanName());
|
||||
vo.setPlanStatus(plan.getPlanStatus());
|
||||
vo.setStartTime(plan.getStartTime());
|
||||
vo.setEndTime(plan.getEndTime());
|
||||
vo.setReportDeadline(plan.getReportDeadline());
|
||||
vo.setTrainAddress(plan.getTrainAddress());
|
||||
vo.setRecruitNum(plan.getRecruitNum());
|
||||
vo.setApplyCondition(plan.getApplyCondition());
|
||||
vo.setRemark(plan.getRemark());
|
||||
vo.setCreateBy(plan.getCreateBy());
|
||||
vo.setCreateTime(plan.getCreateTime());
|
||||
vo.setUpdateBy(plan.getUpdateBy());
|
||||
vo.setUpdateTime(plan.getUpdateTime());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private ServiceException error(String message, int code)
|
||||
{
|
||||
return new ServiceException(message, code);
|
||||
}
|
||||
}
|
||||
+19
-23
@@ -6,6 +6,8 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.train.mapper.TrainClassPlanMapper;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.domain.query.PlanQuery;
|
||||
import com.ruoyi.train.domain.vo.PlanItemVO;
|
||||
import com.ruoyi.train.service.ITrainClassPlanService;
|
||||
|
||||
/**
|
||||
@@ -32,16 +34,28 @@ public class TrainClassPlanServiceImpl implements ITrainClassPlanService
|
||||
return trainClassPlanMapper.selectTrainClassPlanByPlanId(planId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrainClassPlan selectTrainClassPlanByPlanIdForUpdate(String planId)
|
||||
{
|
||||
return trainClassPlanMapper.selectTrainClassPlanByPlanIdForUpdate(planId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询开班计划列表
|
||||
*
|
||||
* @param trainClassPlan 开班计划
|
||||
* @param query 培训计划筛选条件
|
||||
* @return 开班计划
|
||||
*/
|
||||
@Override
|
||||
public List<TrainClassPlan> selectTrainClassPlanList(TrainClassPlan trainClassPlan)
|
||||
public List<PlanItemVO> selectTrainClassPlanList(PlanQuery query)
|
||||
{
|
||||
return trainClassPlanMapper.selectTrainClassPlanList(trainClassPlan);
|
||||
return trainClassPlanMapper.selectTrainClassPlanList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByPlanName(String planName, String excludePlanId)
|
||||
{
|
||||
return trainClassPlanMapper.countTrainClassPlanByName(planName, excludePlanId) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,27 +84,9 @@ public class TrainClassPlanServiceImpl implements ITrainClassPlanService
|
||||
return trainClassPlanMapper.updateTrainClassPlan(trainClassPlan);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除开班计划
|
||||
*
|
||||
* @param planIds 需要删除的开班计划主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteTrainClassPlanByPlanIds(String[] planIds)
|
||||
public int updateTrainClassPlanStatus(TrainClassPlan trainClassPlan)
|
||||
{
|
||||
return trainClassPlanMapper.deleteTrainClassPlanByPlanIds(planIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除开班计划信息
|
||||
*
|
||||
* @param planId 开班计划主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteTrainClassPlanByPlanId(String planId)
|
||||
{
|
||||
return trainClassPlanMapper.deleteTrainClassPlanByPlanId(planId);
|
||||
return trainClassPlanMapper.updateTrainClassPlanStatus(trainClassPlan);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.ruoyi.train.service.port;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 计划结训时的参训档案归档边界。
|
||||
* ISSUE-03-01 实现该端口后,调用将与计划状态变更处于同一事务。
|
||||
*/
|
||||
public interface PlanEnrollmentCompletionPort
|
||||
{
|
||||
void completeEnrollments(String planId, String operator, Date completeTime);
|
||||
}
|
||||
@@ -7,43 +7,101 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<resultMap type="TrainClassPlan" id="TrainClassPlanResult">
|
||||
<result property="planId" column="plan_id" />
|
||||
<result property="planName" column="plan_name" />
|
||||
<result property="cId" column="c_id" />
|
||||
<result property="startTime" column="start_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
<result property="reportDeadline" column="report_deadline" />
|
||||
<result property="trainAddress" column="train_address" />
|
||||
<result property="tId" column="t_id" />
|
||||
<result property="recruitNum" column="recruit_num" />
|
||||
<result property="applyCondition" column="apply_condition" />
|
||||
<result property="planStatus" column="plan_status" />
|
||||
<result property="createUser" column="create_user" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
<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" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectTrainClassPlanVo">
|
||||
select plan_id, plan_name, c_id, start_time, end_time, train_address, t_id, recruit_num, apply_condition, plan_status, create_user, create_time, update_time, remark from train_class_plan
|
||||
<resultMap type="PlanItemVO" id="PlanItemResult">
|
||||
<result property="planId" column="plan_id" />
|
||||
<result property="planName" column="plan_name" />
|
||||
<result property="startTime" column="start_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
<result property="reportDeadline" column="report_deadline" />
|
||||
<result property="trainAddress" column="train_address" />
|
||||
<result property="recruitNum" column="recruit_num" />
|
||||
<result property="planStatus" column="effective_plan_status" />
|
||||
<result property="courseCount" column="course_count" />
|
||||
<result property="enrolledCount" column="enrolled_count" />
|
||||
</resultMap>
|
||||
|
||||
<!-- 有效状态的唯一生产规则:持久化状态 0 在开始时间到达后只读呈现为 1。 -->
|
||||
<sql id="effectivePlanStatusPolicy">
|
||||
case when plan_status = 0 and start_time <= CURRENT_TIMESTAMP then 1
|
||||
else plan_status end
|
||||
</sql>
|
||||
|
||||
<select id="selectTrainClassPlanList" parameterType="TrainClassPlan" resultMap="TrainClassPlanResult">
|
||||
<include refid="selectTrainClassPlanVo"/>
|
||||
<where>
|
||||
<if test="planName != null and planName != ''"> and plan_name like concat('%', #{planName}, '%')</if>
|
||||
<if test="cId != null and cId != ''"> and c_id = #{cId}</if>
|
||||
<if test="startTime != null "> and start_time = #{startTime}</if>
|
||||
<if test="endTime != null "> and end_time = #{endTime}</if>
|
||||
<if test="trainAddress != null and trainAddress != ''"> and train_address = #{trainAddress}</if>
|
||||
<if test="tId != null and tId != ''"> and t_id = #{tId}</if>
|
||||
<if test="recruitNum != null "> and recruit_num = #{recruitNum}</if>
|
||||
<if test="applyCondition != null and applyCondition != ''"> and apply_condition = #{applyCondition}</if>
|
||||
<if test="planStatus != null "> and plan_status = #{planStatus}</if>
|
||||
<if test="createUser != null and createUser != ''"> and create_user = #{createUser}</if>
|
||||
<sql id="selectTrainClassPlanVo">
|
||||
select plan_id, plan_name, start_time, end_time, report_deadline, train_address,
|
||||
recruit_num, apply_condition,
|
||||
<include refid="effectivePlanStatusPolicy"/> as plan_status,
|
||||
del_flag, create_by, create_time,
|
||||
update_by, update_time, remark
|
||||
from train_class_plan
|
||||
</sql>
|
||||
|
||||
<select id="selectTrainClassPlanList" parameterType="PlanQuery" resultMap="PlanItemResult">
|
||||
select p.plan_id,
|
||||
p.plan_name,
|
||||
p.start_time,
|
||||
p.end_time,
|
||||
p.report_deadline,
|
||||
p.train_address,
|
||||
p.recruit_num,
|
||||
p.plan_status as effective_plan_status,
|
||||
(select count(*) from train_plan_course pc where pc.plan_id = p.plan_id) as course_count,
|
||||
(select count(*) from train_enrollment e
|
||||
where e.plan_id = p.plan_id and e.enrollment_status != 3) as enrolled_count
|
||||
from (
|
||||
<include refid="selectTrainClassPlanVo"/>
|
||||
) p
|
||||
<where>
|
||||
p.del_flag = '0'
|
||||
<if test="planName != null and planName != ''">
|
||||
and p.plan_name like concat('%', #{planName}, '%')
|
||||
</if>
|
||||
<if test="planStatus != null">
|
||||
and p.plan_status = #{planStatus}
|
||||
</if>
|
||||
<if test="startTimeBegin != null">
|
||||
and p.start_time >= #{startTimeBegin}
|
||||
</if>
|
||||
<if test="startTimeEnd != null">
|
||||
and p.start_time <= #{startTimeEnd}
|
||||
</if>
|
||||
</where>
|
||||
order by p.start_time desc, p.create_time desc
|
||||
</select>
|
||||
|
||||
<select id="selectTrainClassPlanByPlanId" parameterType="String" resultMap="TrainClassPlanResult">
|
||||
<include refid="selectTrainClassPlanVo"/>
|
||||
where plan_id = #{planId}
|
||||
where plan_id = #{planId} and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectTrainClassPlanByPlanIdForUpdate" parameterType="String" resultMap="TrainClassPlanResult">
|
||||
<include refid="selectTrainClassPlanVo"/>
|
||||
where plan_id = #{planId} and del_flag = '0'
|
||||
for update
|
||||
</select>
|
||||
|
||||
<select id="countTrainClassPlanByName" resultType="int">
|
||||
select count(*)
|
||||
from train_class_plan
|
||||
where del_flag = '0'
|
||||
and lower(trim(plan_name)) = lower(trim(#{planName}))
|
||||
<if test="excludePlanId != null and excludePlanId != ''">
|
||||
and plan_id != #{excludePlanId}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<insert id="insertTrainClassPlan" parameterType="TrainClassPlan">
|
||||
@@ -51,32 +109,34 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="planId != null">plan_id,</if>
|
||||
<if test="planName != null and planName != ''">plan_name,</if>
|
||||
<if test="cId != null and cId != ''">c_id,</if>
|
||||
<if test="startTime != null">start_time,</if>
|
||||
<if test="endTime != null">end_time,</if>
|
||||
<if test="reportDeadline != null">report_deadline,</if>
|
||||
<if test="trainAddress != null">train_address,</if>
|
||||
<if test="tId != null and tId != ''">t_id,</if>
|
||||
<if test="recruitNum != null">recruit_num,</if>
|
||||
<if test="applyCondition != null">apply_condition,</if>
|
||||
<if test="planStatus != null">plan_status,</if>
|
||||
<if test="createUser != null and createUser != ''">create_user,</if>
|
||||
<if test="delFlag != null and delFlag != ''">del_flag,</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>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="planId != null">#{planId},</if>
|
||||
<if test="planName != null and planName != ''">#{planName},</if>
|
||||
<if test="cId != null and cId != ''">#{cId},</if>
|
||||
<if test="startTime != null">#{startTime},</if>
|
||||
<if test="endTime != null">#{endTime},</if>
|
||||
<if test="reportDeadline != null">#{reportDeadline},</if>
|
||||
<if test="trainAddress != null">#{trainAddress},</if>
|
||||
<if test="tId != null and tId != ''">#{tId},</if>
|
||||
<if test="recruitNum != null">#{recruitNum},</if>
|
||||
<if test="applyCondition != null">#{applyCondition},</if>
|
||||
<if test="planStatus != null">#{planStatus},</if>
|
||||
<if test="createUser != null and createUser != ''">#{createUser},</if>
|
||||
<if test="delFlag != null and delFlag != ''">#{delFlag},</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>
|
||||
@@ -84,32 +144,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<update id="updateTrainClassPlan" parameterType="TrainClassPlan">
|
||||
update train_class_plan
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="planName != null and planName != ''">plan_name = #{planName},</if>
|
||||
<if test="cId != null and cId != ''">c_id = #{cId},</if>
|
||||
<if test="startTime != null">start_time = #{startTime},</if>
|
||||
<if test="endTime != null">end_time = #{endTime},</if>
|
||||
<if test="trainAddress != null">train_address = #{trainAddress},</if>
|
||||
<if test="tId != null and tId != ''">t_id = #{tId},</if>
|
||||
<if test="recruitNum != null">recruit_num = #{recruitNum},</if>
|
||||
<if test="applyCondition != null">apply_condition = #{applyCondition},</if>
|
||||
<if test="planStatus != null">plan_status = #{planStatus},</if>
|
||||
<if test="createUser != null and createUser != ''">create_user = #{createUser},</if>
|
||||
<if test="createTime != null">create_time = #{createTime},</if>
|
||||
<if test="updateTime != null">update_time = #{updateTime},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
</trim>
|
||||
where plan_id = #{planId}
|
||||
set plan_name = #{planName},
|
||||
start_time = #{startTime},
|
||||
end_time = #{endTime},
|
||||
report_deadline = #{reportDeadline},
|
||||
train_address = #{trainAddress},
|
||||
recruit_num = #{recruitNum},
|
||||
apply_condition = #{applyCondition},
|
||||
update_by = #{updateBy},
|
||||
update_time = #{updateTime},
|
||||
remark = #{remark}
|
||||
where plan_id = #{planId} and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<delete id="deleteTrainClassPlanByPlanId" parameterType="String">
|
||||
delete from train_class_plan where plan_id = #{planId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteTrainClassPlanByPlanIds" parameterType="String">
|
||||
delete from train_class_plan where plan_id in
|
||||
<foreach item="planId" collection="array" open="(" separator="," close=")">
|
||||
#{planId}
|
||||
</foreach>
|
||||
</delete>
|
||||
</mapper>
|
||||
<update id="updateTrainClassPlanStatus" parameterType="TrainClassPlan">
|
||||
update train_class_plan
|
||||
set plan_status = #{planStatus},
|
||||
update_by = #{updateBy},
|
||||
update_time = #{updateTime}
|
||||
where plan_id = #{planId} and del_flag = '0'
|
||||
</update>
|
||||
</mapper>
|
||||
+429
@@ -0,0 +1,429 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.train.constant.PlanErrorCode;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.domain.dto.PlanCreateDTO;
|
||||
import com.ruoyi.train.domain.dto.PlanUpdateDTO;
|
||||
import com.ruoyi.train.domain.query.PlanQuery;
|
||||
import com.ruoyi.train.domain.vo.PlanDetailVO;
|
||||
import com.ruoyi.train.domain.vo.PlanItemVO;
|
||||
import com.ruoyi.train.service.ITrainClassPlanService;
|
||||
import com.ruoyi.train.service.port.PlanEnrollmentCompletionPort;
|
||||
|
||||
/** ISSUE-02-01 培训计划生命周期单元测试。 */
|
||||
public class PlanAppServiceImplTest
|
||||
{
|
||||
private InMemoryPlanService repository;
|
||||
private RecordingEnrollmentPort enrollmentPort;
|
||||
private PlanAppServiceImpl appService;
|
||||
|
||||
@Before
|
||||
public void setUp()
|
||||
{
|
||||
repository = new InMemoryPlanService();
|
||||
enrollmentPort = new RecordingEnrollmentPort();
|
||||
appService = new PlanAppServiceImpl(repository, Collections.singletonList(enrollmentPort));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createPlanShouldGenerateIdAndForceInitialStatus()
|
||||
{
|
||||
String planId = appService.createPlan(validCreate(" 2026年信号培训班 "), "admin");
|
||||
|
||||
TrainClassPlan saved = repository.selectTrainClassPlanByPlanId(planId);
|
||||
assertNotNull(planId);
|
||||
assertEquals(32, planId.length());
|
||||
assertEquals("2026年信号培训班", saved.getPlanName());
|
||||
assertEquals(Integer.valueOf(0), saved.getPlanStatus());
|
||||
assertEquals("admin", saved.getCreateBy());
|
||||
for (Field field : TrainClassPlan.class.getDeclaredFields())
|
||||
{
|
||||
assertTrue("Plan Domain 不得恢复单课程/单教师字段",
|
||||
!"cId".equals(field.getName()) && !"tId".equals(field.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createPlanShouldRejectReversedOrEqualTime()
|
||||
{
|
||||
PlanCreateDTO reversed = validCreate("倒序计划");
|
||||
reversed.setStartTime(date("2026-09-15 08:00:00"));
|
||||
reversed.setEndTime(date("2026-09-10 08:00:00"));
|
||||
assertError(PlanErrorCode.PLAN_TIME_INVALID, () -> appService.createPlan(reversed, "admin"));
|
||||
|
||||
PlanCreateDTO equal = validCreate("同一时刻计划");
|
||||
equal.setEndTime(equal.getStartTime());
|
||||
assertError(PlanErrorCode.PLAN_TIME_INVALID, () -> appService.createPlan(equal, "admin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createPlanShouldRejectReportDeadlineAfterStart()
|
||||
{
|
||||
PlanCreateDTO dto = validCreate("超时报到计划");
|
||||
dto.setReportDeadline(date("2030-10-10 09:00:00"));
|
||||
assertError(PlanErrorCode.PLAN_TIME_INVALID, () -> appService.createPlan(dto, "admin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createPlanShouldRejectDuplicatedName()
|
||||
{
|
||||
appService.createPlan(validCreate("重复计划"), "admin");
|
||||
assertError(PlanErrorCode.PLAN_NAME_DUPLICATED,
|
||||
() -> appService.createPlan(validCreate(" 重复计划 "), "admin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPlanShouldReturnDetailAndRejectMissingPlan()
|
||||
{
|
||||
String planId = appService.createPlan(validCreate("查询计划"), "admin");
|
||||
PlanDetailVO detail = appService.getPlan(planId);
|
||||
assertEquals(planId, detail.getPlanId());
|
||||
assertEquals("查询计划", detail.getPlanName());
|
||||
|
||||
assertError(PlanErrorCode.PLAN_NOT_FOUND, () -> appService.getPlan("missing"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPlanShouldExposeInProgressStatusAfterStartTime()
|
||||
{
|
||||
PlanCreateDTO dto = validCreate("已开班计划");
|
||||
dto.setReportDeadline(date("2020-01-01 08:00:00"));
|
||||
dto.setStartTime(date("2020-01-02 08:00:00"));
|
||||
dto.setEndTime(date("2030-01-03 08:00:00"));
|
||||
String planId = appService.createPlan(dto, "admin");
|
||||
|
||||
assertEquals(Integer.valueOf(1), appService.getPlan(planId).getPlanStatus());
|
||||
assertEquals("有效状态查询不得回写持久化状态", Integer.valueOf(0),
|
||||
repository.store.get(planId).getPlanStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updatePlanShouldUpdateEditablePlan()
|
||||
{
|
||||
String planId = appService.createPlan(validCreate("原计划"), "admin");
|
||||
PlanUpdateDTO update = validUpdate("新计划");
|
||||
update.setTrainAddress("新基地");
|
||||
|
||||
appService.updatePlan(planId, update, "editor");
|
||||
|
||||
TrainClassPlan saved = repository.selectTrainClassPlanByPlanId(planId);
|
||||
assertEquals("新计划", saved.getPlanName());
|
||||
assertEquals("新基地", saved.getTrainAddress());
|
||||
assertEquals("editor", saved.getUpdateBy());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updatePlanShouldRejectCompletedOrCancelledPlan()
|
||||
{
|
||||
String completedId = appService.createPlan(validCreate("已结束计划"), "admin");
|
||||
repository.store.get(completedId).setPlanStatus(2);
|
||||
assertError(PlanErrorCode.PLAN_STATUS_LOCKED,
|
||||
() -> appService.updatePlan(completedId, validUpdate("修改已结束"), "admin"));
|
||||
|
||||
String cancelledId = appService.createPlan(validCreate("已取消计划"), "admin");
|
||||
repository.store.get(cancelledId).setPlanStatus(3);
|
||||
assertError(PlanErrorCode.PLAN_STATUS_LOCKED,
|
||||
() -> appService.updatePlan(cancelledId, validUpdate("修改已取消"), "admin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completePlanShouldUpdateStatusAndInvokeEnrollmentBoundary() throws NoSuchMethodException
|
||||
{
|
||||
String planId = appService.createPlan(validCreate("待结训计划"), "admin");
|
||||
|
||||
appService.completePlan(planId, "operator");
|
||||
|
||||
assertEquals(Integer.valueOf(2), repository.store.get(planId).getPlanStatus());
|
||||
assertEquals(1, enrollmentPort.invocationCount);
|
||||
assertEquals(planId, enrollmentPort.lastPlanId);
|
||||
assertEquals("operator", enrollmentPort.lastOperator);
|
||||
assertNotNull(enrollmentPort.lastCompleteTime);
|
||||
Transactional transaction = PlanAppServiceImpl.class
|
||||
.getMethod("completePlan", String.class, String.class)
|
||||
.getAnnotation(Transactional.class);
|
||||
assertNotNull(transaction);
|
||||
assertEquals(Exception.class, transaction.rollbackFor()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completePlanShouldPropagateEnrollmentPortFailure()
|
||||
{
|
||||
InMemoryPlanService failingRepository = new InMemoryPlanService();
|
||||
RuntimeException boundaryFailure = new IllegalStateException("Enrollment completion failed");
|
||||
PlanEnrollmentCompletionPort failingPort = (id, user, time) ->
|
||||
{
|
||||
throw boundaryFailure;
|
||||
};
|
||||
PlanAppServiceImpl failingService = new PlanAppServiceImpl(failingRepository,
|
||||
Collections.singletonList(failingPort));
|
||||
String failingPlanId = failingService.createPlan(validCreate("事务异常计划"), "admin");
|
||||
try
|
||||
{
|
||||
failingService.completePlan(failingPlanId, "operator");
|
||||
fail("Enrollment 端口异常必须向事务代理传播");
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
assertSame(boundaryFailure, e);
|
||||
}
|
||||
assertEquals(1, failingRepository.statusUpdateInvocationCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completePlanShouldFailClosedWhenEnrollmentPortMissing()
|
||||
{
|
||||
InMemoryPlanService noPortRepository = new InMemoryPlanService();
|
||||
PlanAppServiceImpl noPortService = new PlanAppServiceImpl(noPortRepository,
|
||||
Collections.emptyList());
|
||||
String planId = noPortService.createPlan(validCreate("缺少联动适配器计划"), "admin");
|
||||
|
||||
try
|
||||
{
|
||||
noPortService.completePlan(planId, "operator");
|
||||
fail("缺少 Enrollment Completion Port 时必须 fail closed");
|
||||
}
|
||||
catch (IllegalStateException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("PlanEnrollmentCompletionPort"));
|
||||
}
|
||||
assertEquals(0, noPortRepository.statusUpdateInvocationCount);
|
||||
assertEquals(Integer.valueOf(0), noPortRepository.store.get(planId).getPlanStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completePlanShouldRejectRepeatedCompletion()
|
||||
{
|
||||
String planId = appService.createPlan(validCreate("幂等计划"), "admin");
|
||||
appService.completePlan(planId, "admin");
|
||||
|
||||
assertError(PlanErrorCode.PLAN_ALREADY_COMPLETED,
|
||||
() -> appService.completePlan(planId, "admin"));
|
||||
assertEquals(1, enrollmentPort.invocationCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completePlanShouldRejectMissingOrCancelledPlan()
|
||||
{
|
||||
assertError(PlanErrorCode.PLAN_NOT_FOUND,
|
||||
() -> appService.completePlan("missing", "admin"));
|
||||
|
||||
String planId = appService.createPlan(validCreate("取消计划"), "admin");
|
||||
repository.store.get(planId).setPlanStatus(3);
|
||||
assertError(PlanErrorCode.PLAN_STATUS_LOCKED,
|
||||
() -> appService.completePlan(planId, "admin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listPlansShouldDelegatePlanQuery()
|
||||
{
|
||||
PlanItemVO item = new PlanItemVO();
|
||||
item.setPlanId("P001");
|
||||
repository.listResult = Collections.singletonList(item);
|
||||
|
||||
List<PlanItemVO> result = appService.listPlans(new PlanQuery());
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("P001", result.get(0).getPlanId());
|
||||
}
|
||||
|
||||
private PlanCreateDTO validCreate(String name)
|
||||
{
|
||||
PlanCreateDTO dto = new PlanCreateDTO();
|
||||
dto.setPlanName(name);
|
||||
dto.setReportDeadline(date("2030-10-09 18:00:00"));
|
||||
dto.setStartTime(date("2030-10-10 08:30:00"));
|
||||
dto.setEndTime(date("2030-10-24 17:30:00"));
|
||||
dto.setTrainAddress("培训基地");
|
||||
dto.setRecruitNum(40L);
|
||||
dto.setApplyCondition("在岗职工");
|
||||
dto.setRemark("测试计划");
|
||||
return dto;
|
||||
}
|
||||
|
||||
private PlanUpdateDTO validUpdate(String name)
|
||||
{
|
||||
PlanUpdateDTO dto = new PlanUpdateDTO();
|
||||
dto.setPlanName(name);
|
||||
dto.setReportDeadline(date("2030-10-09 18:00:00"));
|
||||
dto.setStartTime(date("2030-10-10 08:30:00"));
|
||||
dto.setEndTime(date("2030-10-24 17:30:00"));
|
||||
dto.setTrainAddress("培训基地");
|
||||
dto.setRecruitNum(40L);
|
||||
dto.setApplyCondition("在岗职工");
|
||||
dto.setRemark("更新测试");
|
||||
return dto;
|
||||
}
|
||||
|
||||
private Date date(String value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(value);
|
||||
}
|
||||
catch (ParseException e)
|
||||
{
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertError(int code, ThrowingAction action)
|
||||
{
|
||||
try
|
||||
{
|
||||
action.run();
|
||||
fail("Expected ServiceException code " + code);
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertEquals(Integer.valueOf(code), e.getCode());
|
||||
assertTrue(e.getMessage() != null && !e.getMessage().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
private interface ThrowingAction
|
||||
{
|
||||
void run();
|
||||
}
|
||||
|
||||
private static class RecordingEnrollmentPort implements PlanEnrollmentCompletionPort
|
||||
{
|
||||
private int invocationCount;
|
||||
private String lastPlanId;
|
||||
private String lastOperator;
|
||||
private Date lastCompleteTime;
|
||||
|
||||
@Override
|
||||
public void completeEnrollments(String planId, String operator, Date completeTime)
|
||||
{
|
||||
invocationCount++;
|
||||
lastPlanId = planId;
|
||||
lastOperator = operator;
|
||||
lastCompleteTime = completeTime;
|
||||
}
|
||||
}
|
||||
|
||||
private static class InMemoryPlanService implements ITrainClassPlanService
|
||||
{
|
||||
private final Map<String, TrainClassPlan> store = new LinkedHashMap<>();
|
||||
private List<PlanItemVO> listResult = new ArrayList<>();
|
||||
private int statusUpdateInvocationCount;
|
||||
|
||||
@Override
|
||||
public TrainClassPlan selectTrainClassPlanByPlanId(String planId)
|
||||
{
|
||||
return effectiveView(store.get(planId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrainClassPlan selectTrainClassPlanByPlanIdForUpdate(String planId)
|
||||
{
|
||||
return effectiveView(store.get(planId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PlanItemVO> selectTrainClassPlanList(PlanQuery query)
|
||||
{
|
||||
return listResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByPlanName(String planName, String excludePlanId)
|
||||
{
|
||||
for (TrainClassPlan plan : store.values())
|
||||
{
|
||||
if (plan.getPlanName().trim().equalsIgnoreCase(planName.trim())
|
||||
&& !plan.getPlanId().equals(excludePlanId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertTrainClassPlan(TrainClassPlan plan)
|
||||
{
|
||||
store.put(plan.getPlanId(), plan);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateTrainClassPlan(TrainClassPlan update)
|
||||
{
|
||||
TrainClassPlan existing = store.get(update.getPlanId());
|
||||
existing.setPlanName(update.getPlanName());
|
||||
existing.setStartTime(update.getStartTime());
|
||||
existing.setEndTime(update.getEndTime());
|
||||
existing.setReportDeadline(update.getReportDeadline());
|
||||
existing.setTrainAddress(update.getTrainAddress());
|
||||
existing.setRecruitNum(update.getRecruitNum());
|
||||
existing.setApplyCondition(update.getApplyCondition());
|
||||
existing.setUpdateBy(update.getUpdateBy());
|
||||
existing.setUpdateTime(update.getUpdateTime());
|
||||
existing.setRemark(update.getRemark());
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateTrainClassPlanStatus(TrainClassPlan update)
|
||||
{
|
||||
statusUpdateInvocationCount++;
|
||||
TrainClassPlan existing = store.get(update.getPlanId());
|
||||
existing.setPlanStatus(update.getPlanStatus());
|
||||
existing.setUpdateBy(update.getUpdateBy());
|
||||
existing.setUpdateTime(update.getUpdateTime());
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** Mirrors the mapper's read-only effectivePlanStatusPolicy for this JVM fake. */
|
||||
private TrainClassPlan effectiveView(TrainClassPlan source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
TrainClassPlan view = new TrainClassPlan();
|
||||
view.setPlanId(source.getPlanId());
|
||||
view.setPlanName(source.getPlanName());
|
||||
view.setStartTime(source.getStartTime());
|
||||
view.setEndTime(source.getEndTime());
|
||||
view.setReportDeadline(source.getReportDeadline());
|
||||
view.setTrainAddress(source.getTrainAddress());
|
||||
view.setRecruitNum(source.getRecruitNum());
|
||||
view.setApplyCondition(source.getApplyCondition());
|
||||
view.setPlanStatus(source.getPlanStatus());
|
||||
view.setDelFlag(source.getDelFlag());
|
||||
view.setCreateBy(source.getCreateBy());
|
||||
view.setCreateTime(source.getCreateTime());
|
||||
view.setUpdateBy(source.getUpdateBy());
|
||||
view.setUpdateTime(source.getUpdateTime());
|
||||
view.setRemark(source.getRemark());
|
||||
if (Integer.valueOf(0).equals(source.getPlanStatus())
|
||||
&& source.getStartTime() != null
|
||||
&& !source.getStartTime().after(new Date()))
|
||||
{
|
||||
view.setPlanStatus(1);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import request from '@/utils/request'
|
||||
// 查询开班计划列表
|
||||
export function listPlan(query) {
|
||||
return request({
|
||||
url: '/train/plan/list',
|
||||
url: '/train/plans',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
@@ -12,7 +12,7 @@ export function listPlan(query) {
|
||||
// 查询开班计划详细
|
||||
export function getPlan(planId) {
|
||||
return request({
|
||||
url: '/train/plan/' + planId,
|
||||
url: '/train/plans/' + planId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -20,25 +20,25 @@ export function getPlan(planId) {
|
||||
// 新增开班计划
|
||||
export function addPlan(data) {
|
||||
return request({
|
||||
url: '/train/plan',
|
||||
url: '/train/plans',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改开班计划
|
||||
export function updatePlan(data) {
|
||||
export function updatePlan(planId, data) {
|
||||
return request({
|
||||
url: '/train/plan',
|
||||
url: '/train/plans/' + planId,
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除开班计划
|
||||
export function delPlan(planId) {
|
||||
// 结束培训计划
|
||||
export function completePlan(planId) {
|
||||
return request({
|
||||
url: '/train/plan/' + planId,
|
||||
method: 'delete'
|
||||
url: '/train/plans/' + planId + '/complete',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
@@ -1,68 +1,30 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="开班名称" prop="planName">
|
||||
<el-input
|
||||
v-model="queryParams.planName"
|
||||
placeholder="请输入开班名称"
|
||||
<el-form
|
||||
v-show="showSearch"
|
||||
ref="queryForm"
|
||||
:model="queryParams"
|
||||
size="small"
|
||||
:inline="true"
|
||||
label-width="88px"
|
||||
>
|
||||
<el-form-item label="培训班名称" prop="planName">
|
||||
<el-input v-model="queryParams.planName" placeholder="请输入培训班名称" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="计划状态" prop="planStatus">
|
||||
<el-select v-model="queryParams.planStatus" placeholder="全部状态" clearable>
|
||||
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开班时间">
|
||||
<el-date-picker
|
||||
v-model="startTimeRange"
|
||||
type="datetimerange"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属课程编号,关联train_course.c_id" prop="cId">
|
||||
<el-input
|
||||
v-model="queryParams.cId"
|
||||
placeholder="请输入所属课程编号,关联train_course.c_id"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="开班时间" prop="startTime">
|
||||
<el-date-picker clearable
|
||||
v-model="queryParams.startTime"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="请选择开班时间">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" prop="endTime">
|
||||
<el-date-picker clearable
|
||||
v-model="queryParams.endTime"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="请选择结束时间">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="培训地点" prop="trainAddress">
|
||||
<el-input
|
||||
v-model="queryParams.trainAddress"
|
||||
placeholder="请输入培训地点"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="授课教师工号,关联train_teacher.t_id" prop="tId">
|
||||
<el-input
|
||||
v-model="queryParams.tId"
|
||||
placeholder="请输入授课教师工号,关联train_teacher.t_id"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="招生人数" prop="recruitNum">
|
||||
<el-input
|
||||
v-model="queryParams.recruitNum"
|
||||
placeholder="请输入招生人数"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建管理员账号" prop="createUser">
|
||||
<el-input
|
||||
v-model="queryParams.createUser"
|
||||
placeholder="请输入创建管理员账号"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
@@ -74,141 +36,99 @@
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPermi="['train:plan:add']"
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
v-hasPermi="['train:plan:add']"
|
||||
>新增</el-button>
|
||||
>新增计划</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
:disabled="single"
|
||||
@click="handleUpdate"
|
||||
v-hasPermi="['train:plan:edit']"
|
||||
>修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete"
|
||||
v-hasPermi="['train:plan:remove']"
|
||||
>删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="el-icon-download"
|
||||
size="mini"
|
||||
@click="handleExport"
|
||||
v-hasPermi="['train:plan:export']"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
<right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="planList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="开班计划ID主键" align="center" prop="planId" />
|
||||
<el-table-column label="开班名称" align="center" prop="planName" />
|
||||
<el-table-column label="所属课程编号,关联train_course.c_id" align="center" prop="cId" />
|
||||
<el-table-column label="开班时间" align="center" prop="startTime" width="180">
|
||||
<el-table v-loading="loading" :data="planList">
|
||||
<el-table-column label="计划编号" align="center" prop="planId" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="培训班名称" align="left" prop="planName" min-width="230" show-overflow-tooltip />
|
||||
<el-table-column label="状态" align="center" prop="planStatus" width="100">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
|
||||
<el-tag :type="statusTagType(scope.row.planStatus)">{{ statusLabel(scope.row.planStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" align="center" prop="endTime" width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="培训地点" align="center" prop="trainAddress" />
|
||||
<el-table-column label="授课教师工号,关联train_teacher.t_id" align="center" prop="tId" />
|
||||
<el-table-column label="招生人数" align="center" prop="recruitNum" />
|
||||
<el-table-column label="报名条件" align="center" prop="applyCondition" />
|
||||
<el-table-column label="0 未开始 1 进行中 2 已结束 3 已取消" align="center" prop="planStatus" />
|
||||
<el-table-column label="创建管理员账号" align="center" prop="createUser" />
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<el-table-column label="开班时间" align="center" prop="startTime" width="165" />
|
||||
<el-table-column label="结束时间" align="center" prop="endTime" width="165" />
|
||||
<el-table-column label="报到截止" align="center" prop="reportDeadline" width="165" />
|
||||
<el-table-column label="培训地点" align="left" prop="trainAddress" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="计划人数" align="center" prop="recruitNum" width="90" />
|
||||
<el-table-column label="课程数" align="center" prop="courseCount" width="80" />
|
||||
<el-table-column label="参训人数" align="center" prop="enrolledCount" width="90" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="170" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:plan:edit']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
:disabled="isLocked(scope.row.planStatus)"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['train:plan:edit']"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:plan:complete']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['train:plan:remove']"
|
||||
>删除</el-button>
|
||||
icon="el-icon-finished"
|
||||
:disabled="isLocked(scope.row.planStatus)"
|
||||
@click="handleComplete(scope.row)"
|
||||
>结束计划</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改开班计划对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="开班名称" prop="planName">
|
||||
<el-input v-model="form.planName" placeholder="请输入开班名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属课程编号,关联train_course.c_id" prop="cId">
|
||||
<el-input v-model="form.cId" placeholder="请输入所属课程编号,关联train_course.c_id" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开班时间" prop="startTime">
|
||||
<el-date-picker clearable
|
||||
v-model="form.startTime"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="请选择开班时间">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" prop="endTime">
|
||||
<el-date-picker clearable
|
||||
v-model="form.endTime"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="请选择结束时间">
|
||||
</el-date-picker>
|
||||
<el-dialog :title="title" :visible.sync="open" width="660px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="培训班名称" prop="planName">
|
||||
<el-input v-model="form.planName" maxlength="150" show-word-limit placeholder="请输入培训班名称" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开班时间" prop="startTime">
|
||||
<el-date-picker v-model="form.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择开班时间" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="结束时间" prop="endTime">
|
||||
<el-date-picker v-model="form.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择结束时间" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="报到截止" prop="reportDeadline">
|
||||
<el-date-picker v-model="form.reportDeadline" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择报到截止时间" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="计划人数" prop="recruitNum">
|
||||
<el-input-number v-model="form.recruitNum" :min="1" :precision="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="培训地点" prop="trainAddress">
|
||||
<el-input v-model="form.trainAddress" placeholder="请输入培训地点" />
|
||||
</el-form-item>
|
||||
<el-form-item label="授课教师工号,关联train_teacher.t_id" prop="tId">
|
||||
<el-input v-model="form.tId" placeholder="请输入授课教师工号,关联train_teacher.t_id" />
|
||||
</el-form-item>
|
||||
<el-form-item label="招生人数" prop="recruitNum">
|
||||
<el-input v-model="form.recruitNum" placeholder="请输入招生人数" />
|
||||
<el-input v-model="form.trainAddress" maxlength="200" show-word-limit placeholder="请输入培训地点" />
|
||||
</el-form-item>
|
||||
<el-form-item label="报名条件" prop="applyCondition">
|
||||
<el-input v-model="form.applyCondition" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="创建管理员账号" prop="createUser">
|
||||
<el-input v-model="form.createUser" placeholder="请输入创建管理员账号" />
|
||||
<el-input v-model="form.applyCondition" type="textarea" :rows="3" placeholder="请输入报名条件" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注" />
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" show-word-limit placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
@@ -220,67 +140,38 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPlan, getPlan, delPlan, addPlan, updatePlan } from "@/api/train/plan"
|
||||
import { addPlan, completePlan, getPlan, listPlan, updatePlan } from '@/api/train/plan'
|
||||
|
||||
export default {
|
||||
name: "Plan",
|
||||
name: 'Plan',
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
total: 0,
|
||||
// 开班计划表格数据
|
||||
planList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
startTimeRange: [],
|
||||
title: '',
|
||||
open: false,
|
||||
// 查询参数
|
||||
statusOptions: [
|
||||
{ value: 0, label: '未开始' },
|
||||
{ value: 1, label: '进行中' },
|
||||
{ value: 2, label: '已结束' },
|
||||
{ value: 3, label: '已取消' }
|
||||
],
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
planName: null,
|
||||
cId: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
trainAddress: null,
|
||||
tId: null,
|
||||
recruitNum: null,
|
||||
applyCondition: null,
|
||||
planStatus: null,
|
||||
createUser: null,
|
||||
planStatus: null
|
||||
},
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 表单校验
|
||||
rules: {
|
||||
planName: [
|
||||
{ required: true, message: "开班名称不能为空", trigger: "blur" }
|
||||
],
|
||||
cId: [
|
||||
{ required: true, message: "所属课程编号,关联train_course.c_id不能为空", trigger: "blur" }
|
||||
],
|
||||
startTime: [
|
||||
{ required: true, message: "开班时间不能为空", trigger: "blur" }
|
||||
],
|
||||
endTime: [
|
||||
{ required: true, message: "结束时间不能为空", trigger: "blur" }
|
||||
],
|
||||
tId: [
|
||||
{ required: true, message: "授课教师工号,关联train_teacher.t_id不能为空", trigger: "blur" }
|
||||
],
|
||||
createUser: [
|
||||
{ required: true, message: "创建管理员账号不能为空", trigger: "blur" }
|
||||
],
|
||||
planName: [{ required: true, message: '培训班名称不能为空', trigger: 'blur' }],
|
||||
startTime: [{ required: true, message: '开班时间不能为空', trigger: 'change' }],
|
||||
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }],
|
||||
reportDeadline: [{ required: true, message: '报到截止时间不能为空', trigger: 'change' }],
|
||||
recruitNum: [{ required: true, message: '计划人数不能为空', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -288,107 +179,110 @@ export default {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
/** 查询开班计划列表 */
|
||||
getList() {
|
||||
this.loading = true
|
||||
listPlan(this.queryParams).then(response => {
|
||||
const query = { ...this.queryParams }
|
||||
if (this.startTimeRange && this.startTimeRange.length === 2) {
|
||||
query.startTimeBegin = this.startTimeRange[0]
|
||||
query.startTimeEnd = this.startTimeRange[1]
|
||||
}
|
||||
listPlan(query).then(response => {
|
||||
this.planList = response.rows
|
||||
this.total = response.total
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
// 取消按钮
|
||||
statusLabel(status) {
|
||||
const option = this.statusOptions.find(item => item.value === status)
|
||||
return option ? option.label : '未知'
|
||||
},
|
||||
statusTagType(status) {
|
||||
return { 0: 'info', 1: 'primary', 2: 'success', 3: 'danger' }[status] || 'info'
|
||||
},
|
||||
isLocked(status) {
|
||||
return status === 2 || status === 3
|
||||
},
|
||||
cancel() {
|
||||
this.open = false
|
||||
this.reset()
|
||||
},
|
||||
// 表单重置
|
||||
reset() {
|
||||
this.form = {
|
||||
planId: null,
|
||||
planName: null,
|
||||
cId: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
reportDeadline: null,
|
||||
trainAddress: null,
|
||||
tId: null,
|
||||
recruitNum: null,
|
||||
recruitNum: 1,
|
||||
applyCondition: null,
|
||||
planStatus: null,
|
||||
createUser: null,
|
||||
createTime: null,
|
||||
updateTime: null,
|
||||
remark: null
|
||||
}
|
||||
this.resetForm("form")
|
||||
this.resetForm('form')
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery() {
|
||||
this.resetForm("queryForm")
|
||||
this.startTimeRange = []
|
||||
this.resetForm('queryForm')
|
||||
this.handleQuery()
|
||||
},
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.planId)
|
||||
this.single = selection.length!==1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd() {
|
||||
this.reset()
|
||||
this.open = true
|
||||
this.title = "添加开班计划"
|
||||
this.title = '创建培训计划'
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
this.reset()
|
||||
const planId = row.planId || this.ids
|
||||
getPlan(planId).then(response => {
|
||||
getPlan(row.planId).then(response => {
|
||||
this.form = response.data
|
||||
this.open = true
|
||||
this.title = "修改开班计划"
|
||||
this.title = '编辑培训计划'
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.planId != null) {
|
||||
updatePlan(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addPlan(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
}
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return
|
||||
if (this.form.startTime >= this.form.endTime) {
|
||||
this.$modal.msgError('结束时间必须晚于开班时间')
|
||||
return
|
||||
}
|
||||
if (this.form.reportDeadline > this.form.startTime) {
|
||||
this.$modal.msgError('报到截止时间不得晚于开班时间')
|
||||
return
|
||||
}
|
||||
const request = this.form.planId
|
||||
? updatePlan(this.form.planId, this.planPayload())
|
||||
: addPlan(this.planPayload())
|
||||
request.then(() => {
|
||||
this.$modal.msgSuccess(this.form.planId ? '修改成功' : '创建成功')
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
})
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const planIds = row.planId || this.ids
|
||||
this.$modal.confirm('是否确认删除开班计划编号为"' + planIds + '"的数据项?').then(function() {
|
||||
return delPlan(planIds)
|
||||
}).then(() => {
|
||||
this.getList()
|
||||
this.$modal.msgSuccess("删除成功")
|
||||
}).catch(() => {})
|
||||
planPayload() {
|
||||
return {
|
||||
planName: this.form.planName,
|
||||
startTime: this.form.startTime,
|
||||
endTime: this.form.endTime,
|
||||
reportDeadline: this.form.reportDeadline,
|
||||
trainAddress: this.form.trainAddress,
|
||||
recruitNum: this.form.recruitNum,
|
||||
applyCondition: this.form.applyCondition,
|
||||
remark: this.form.remark
|
||||
}
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport() {
|
||||
this.download('train/plan/export', {
|
||||
...this.queryParams
|
||||
}, `plan_${new Date().getTime()}.xlsx`)
|
||||
handleComplete(row) {
|
||||
this.$modal.confirm('结束计划后将锁定计划信息并保留全部历史记录,是否继续?').then(() => {
|
||||
return completePlan(row.planId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('计划已结束')
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
# ISSUE-02-01 培训计划生命周期与基础管理开发验收
|
||||
|
||||
## 1. 基线与范围
|
||||
|
||||
- 分支:`feat/issue-02-01-plan`
|
||||
- 起始 HEAD:`7fcaea29b8810546fd972ba0c9d366c71a357d63`
|
||||
- Issue 基线:`issues-v1.0`
|
||||
- API 基线:`api-v2-contract-v1.0.1`(`4873e5f555ecfe7d1d21d313007c2c8f6f65d6b6`)
|
||||
- Generator 策略为 `PARTIAL`,本轮未执行 Generator Import。
|
||||
- 本 Issue 只实现 Plan 自身的创建、编辑、列表、详情、结束和稳定上下文;未实现 PlanCourse、Enrollment、Lesson、Accommodation、Attendance、Score、Study、Statistics。
|
||||
|
||||
## 2. 旧实现审计
|
||||
|
||||
| 旧文件 | 判定 | 处理 |
|
||||
|---|---|---|
|
||||
| `TrainClassPlan.java` | MODIFY | 按 Database V2 删除 `cId/tId/createUser`,增加 `reportDeadline/delFlag`,使用标准审计字段。 |
|
||||
| `TrainClassPlanMapper.java` | MODIFY | 移除物理删除能力,增加名称预查重、生命周期行锁读取和状态更新。 |
|
||||
| `TrainClassPlanMapper.xml` | REPLACE | 旧单课程/单教师 SQL 替换为 V2 字段、有效状态、列表筛选、概览只读计数及行锁 SQL。 |
|
||||
| `ITrainClassPlanService.java` | MODIFY | 保留为基础持久化边界,不承载生命周期编排。 |
|
||||
| `TrainClassPlanServiceImpl.java` | MODIFY | 只封装 Mapper。 |
|
||||
| `TrainClassPlanController.java` | DELETE | 删除旧 `/train/plan` CRUD、删除和导出端点。 |
|
||||
| `TrainPlanController.java` | REPLACE | 使用正式 `/train/plans` Contract,只依赖 `PlanAppService`。 |
|
||||
| `build_ui/src/api/train/plan.js` | MODIFY | 使用正式复数路径,删除 delete API,增加 complete Command。 |
|
||||
| `build_ui/src/views/train/plan/index.vue` | REPLACE | 仅呈现和编辑 Plan 自身,不绑定单课程或单教师。 |
|
||||
|
||||
旧模型的 `Plan = 单课程 + 单教师` 已从 Domain、Mapper、Controller DTO/VO、前端 API 和页面中消除。课程属于 ISSUE-02-02 的 `PlanCourse`,教师属于具体 `Lesson`;本 Issue 未提前实现这些写能力。
|
||||
|
||||
## 3. 正式 P0 API 与 Controller 契约
|
||||
|
||||
| Priority | Method | Path | Permission | Request | Response |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | GET | `/train/plans` | `train:plan:list` | `PlanQuery`:`pageNum/pageSize` 由分页框架接管,业务字段为 `planName/planStatus/startTimeBegin/startTimeEnd` | `TableDataInfo<PlanItemVO>` |
|
||||
| P0 | POST | `/train/plans` | `train:plan:add` | `PlanCreateDTO` | Contract 未命名专用响应 VO;当前为标准 `AjaxResult`,`data` 为 `plan_id` |
|
||||
| P0 | GET | `/train/plans/{planId}` | `train:plan:query` | Path `planId` | `AjaxResult<PlanDetailVO>` |
|
||||
| P0 | PUT | `/train/plans/{planId}` | `train:plan:edit` | Path `planId` + `PlanUpdateDTO` | 标准空 `AjaxResult` |
|
||||
| P0 | POST | `/train/plans/{planId}/complete` | `train:plan:complete` | Path `planId`,无 Request Body | 标准空 `AjaxResult` |
|
||||
|
||||
`TrainPlanController` 没有 Contract 外 Endpoint;旧 `TrainClassPlanController` 已删除,不存在双 Controller 路由。Controller 未接收或返回 `TrainClassPlan` Persistence Domain。
|
||||
|
||||
### 3.1 实际 DTO/VO 清单
|
||||
|
||||
- `PlanCreateDTO`:`planName/startTime/endTime/reportDeadline/recruitNum/trainAddress/applyCondition/remark`。来源以 Module B 1.2 的具体 JSON 和校验规则为准。
|
||||
- `PlanUpdateDTO`:`planName/startTime/endTime/reportDeadline/recruitNum/trainAddress/applyCondition/remark`,不接受 `planStatus`。Module B 1.4 明列名称、地址、人数、报到截止和备注;Issue 业务目标及 Service Boundary 同时要求起止时间维护与课次边界检查,因此保留起止时间。`applyCondition` 的更新字段在目录中未逐项展开,登记为 Contract 表述缺口,不修改冻结文档。
|
||||
- `PlanQuery`:`planName/planStatus/startTimeBegin/startTimeEnd`;分页字段由 RuoYi 接管。
|
||||
- `PlanItemVO`:`planId/planName/planStatus/startTime/endTime/reportDeadline/trainAddress/recruitNum/courseCount/enrolledCount`。
|
||||
- `PlanDetailVO`:`planId/planName/planStatus/startTime/endTime/reportDeadline/trainAddress/recruitNum/applyCondition/remark/createBy/createTime/updateBy/updateTime`。
|
||||
|
||||
`docs/api-v2/06_dto_vo_catalog.md` 的 `PlanCreateDTO` 摘要未列 `applyCondition`,但 `docs/api-v2/03_module_b_plan_enrollment_schedule_api.md` 1.2 的确认 JSON 明确包含它;实现采用更具体的 Module B 定义,此差异作为 Contract Gap 记录。
|
||||
|
||||
## 4. PlanAppService 边界
|
||||
|
||||
- `listPlans`:读取分页概览,不做写操作。
|
||||
- `getPlan`:读取 `PlanDetailVO`,不存在时抛出 Plan not found。
|
||||
- `createPlan`:名称规范化与友好查重、时间/人数校验、生成技术主键、强制初始状态 0。
|
||||
- `updatePlan`:行锁读取、有效状态校验、名称和字段校验后更新;客户端不能直接改状态。
|
||||
- `completePlan`:先确认至少一个真实 Enrollment Completion Port 已接线,再行锁读取、校验并更新 Plan 状态,随后在同一同步事务调用 Port。
|
||||
|
||||
Controller 只处理 HTTP、Bean Validation、权限、日志和标准响应,不调用 Mapper 或编排多步业务。
|
||||
|
||||
## 5. 生命周期与 effective status
|
||||
|
||||
- 创建只写 `plan_status=0`。
|
||||
- DB 不会因时间到达而将 0 写成 1;`TrainClassPlanMapper.xml#effectivePlanStatusPolicy` 是唯一生产规则源:持久化状态为 0 且 `start_time <= CURRENT_TIMESTAMP` 时,只读结果呈现为 1。
|
||||
- 列表输出、`planStatus` 筛选、详情、编辑的 `FOR UPDATE` 读取、complete 的 `FOR UPDATE` 读取均复用该 SQL fragment。Application Service 不再重复时间判断。
|
||||
- 查询不会写数据库,也不会根据 `end_time` 自动结束;结束只能由正式 complete Command 写状态 2。
|
||||
- 状态 0 与派生状态 1 均允许 complete;Contract 未要求 `now >= end_time`,实现未增加该条件。
|
||||
- 状态 2 重复 complete 返回 `40014`;状态 3 complete 返回 `40012`。
|
||||
- 状态 2/3 禁止编辑。状态 3 仅作历史只读兼容;没有 cancel Endpoint,前端没有取消按钮。
|
||||
- 后续业务若需判断 Plan 状态,应通过 `ITrainClassPlanService` 的正式读取/锁读取取得已应用 effective status 的 Plan,而不是自行复制时间判断。
|
||||
|
||||
## 6. Enrollment Completion Port 审计
|
||||
|
||||
- 接口:`PlanEnrollmentCompletionPort`。
|
||||
- Main/生产实现类:无。
|
||||
- `@Configuration/@Bean` 绑定:无。
|
||||
- 构造注入:`PlanAppServiceImpl` 使用 `ObjectProvider<PlanEnrollmentCompletionPort>` 收集已注册 adapter;Bean 缺失不阻断应用启动,但执行 complete 时会 fail closed。
|
||||
- 测试实现:测试类内部 `RecordingEnrollmentPort`,另有 lambda throwing fake 用于异常传播;都不进入生产 runtime。
|
||||
- 分类:**Case C — 只有接口与测试 Fake,生产尚未接线**。
|
||||
|
||||
没有注册 silent no-op 实现,也没有伪造 Enrollment UPDATE。当前没有生产 adapter 时,`completePlan` 在任何 Plan 查询或状态更新前抛出无业务错误码的内部 `IllegalStateException`;请求失败,Plan 不会产生单边 `status=2`。ISSUE-03-01 必须实现真实 adapter,并以集成测试验证 `enrollment_status=4`、`complete_time` 以及任一步失败时的跨表回滚。
|
||||
|
||||
### AC-03 拆分
|
||||
|
||||
- **AC-03a — UNIT_PASS**:单元测试验证 Plan 状态更新、Completion Port 恰好调用一次、缺失 Port 时 fail closed、端口异常不被吞掉;同时确认 `completePlan` 使用 `@Transactional(rollbackFor=Exception.class)` 且端口在方法内同步调用。
|
||||
- **AC-03b — INTEGRATION_PENDING (ISSUE-03-01)**:真实 Enrollment 批量 `status=4 + complete_time` 及跨表提交/回滚尚未验证。
|
||||
- **Integration Safety — UNIT_PASS**:Port missing 时在 Plan update 前失败,状态更新调用次数为 0,Plan 保持原状态。
|
||||
|
||||
当前证据只能表述为“transaction orchestration seam 已验证”;不能表述为完整跨表事务或 Enrollment runtime 已通过。
|
||||
|
||||
## 7. 错误码映射与 Contract Gap
|
||||
|
||||
| 常量 | 值 | 冻结语义与来源 | 当前触发位置 |
|
||||
|---|---:|---|---|
|
||||
| `PLAN_TIME_INVALID` | 40010 | API 10 错误码表;Module B 1.2;ISSUE-02-01 | create/update 的 `start >= end`;`reportDeadline > startTime` 同属计划时间参数校验 |
|
||||
| `PLAN_NAME_DUPLICATED` | 40011 | API 10;Module B 1.2;ISSUE-02-01 | create/update 的 Service 友好名称预查重 |
|
||||
| `PLAN_STATUS_LOCKED` | 40012 | API 10;ISSUE-02-01 | 状态 2/3 编辑;状态 3 complete |
|
||||
| `PLAN_LESSON_OUT_OF_BOUNDS` | 40013 | API 10(UC-02) | 本 Issue 不实现 Lesson,当前无触发点;待 Scheduling 边界接入,未伪造校验 |
|
||||
| `PLAN_ALREADY_COMPLETED` | 40014 | API 10(UC-29);ISSUE-02-01 | 状态 2 重复 complete |
|
||||
| `PLAN_NOT_FOUND` | 40410 | 冻结 Issue Backlog 的多个 Plan 依赖用例 | detail/update/complete 的计划不存在 |
|
||||
|
||||
API 10 的全局错误码表未列 40410、Module B 1.3 也未为详情逐项列 not-found code,但冻结 Issue Backlog 已使用 `PLAN_NOT_FOUND (40410)`。实现未发明新码;此目录间缺口仅登记,不修改冻结文档。招生人数由 DTO Bean Validation 处理,Service 防御校验使用普通 `ServiceException`,不再错误复用 40010,也未新增业务错误码。
|
||||
|
||||
## 8. 名称唯一与技术主键
|
||||
|
||||
- Database V2 没有 `UNIQUE(plan_name)`;当前只有 Service 层忽略首尾空格与大小写的友好重复预检查,并发强唯一性不保证。本 Issue 不新增 UNIQUE。
|
||||
- “自动编号”仅指 `plan_id = IdUtils.fastSimpleUUID()` 生成的 32 位 UUID 技术主键。没有、也未发明 `plan_code` 或 `plan_no` 物理字段。
|
||||
|
||||
## 9. 测试依赖与 14 个测试方法
|
||||
|
||||
`ruoyi-train/pom.xml` 仅新增:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
没有新增 Mockito、Spring Boot Test、H2 或生产 runtime 依赖。全部 14 个测试使用 JVM `InMemoryPlanService` Fake 和测试 Port,不 Mock JDBC、不连接也不写真实数据库:
|
||||
|
||||
| # | `@Test` 方法 | 验证规则 |
|
||||
|---:|---|---|
|
||||
| 1 | `createPlanShouldGenerateIdAndForceInitialStatus` | 32 位 `plan_id`、名称 trim、强制状态 0、审计人,并通过反射确认 Domain 无 `cId/tId` |
|
||||
| 2 | `createPlanShouldRejectReversedOrEqualTime` | `start > end` 与 `start == end` 均返回 40010 |
|
||||
| 3 | `createPlanShouldRejectReportDeadlineAfterStart` | 报到截止晚于开班返回 40010 |
|
||||
| 4 | `createPlanShouldRejectDuplicatedName` | 名称 trim/忽略大小写预查重返回 40011 |
|
||||
| 5 | `getPlanShouldReturnDetailAndRejectMissingPlan` | detail VO 映射及不存在返回 40410 |
|
||||
| 6 | `getPlanShouldExposeInProgressStatusAfterStartTime` | 0→1 有效状态只读呈现,且持久化 Fake 中仍为 0,证明查询不回写 |
|
||||
| 7 | `updatePlanShouldUpdateEditablePlan` | 状态 0/1 可编辑与审计更新 |
|
||||
| 8 | `updatePlanShouldRejectCompletedOrCancelledPlan` | 状态 2、状态 3 分别禁止 edit,均返回 40012 |
|
||||
| 9 | `completePlanShouldUpdateStatusAndInvokeEnrollmentBoundary` | Plan→2、Port 参数/次数及事务注解 |
|
||||
| 10 | `completePlanShouldPropagateEnrollmentPortFailure` | Port 运行时异常保持原异常向事务代理传播;Fake 记录更新发生在 Port 调用前 |
|
||||
| 11 | `completePlanShouldFailClosedWhenEnrollmentPortMissing` | 无 Port 时抛内部异常、状态更新调用为 0、Plan 保持状态 0 |
|
||||
| 12 | `completePlanShouldRejectRepeatedCompletion` | 重复 complete 返回 40014,Port 不重复调用 |
|
||||
| 13 | `completePlanShouldRejectMissingOrCancelledPlan` | 不存在返回 40410;状态 3 complete 返回 40012 |
|
||||
| 14 | `listPlansShouldDelegatePlanQuery` | list 委托并返回 `PlanItemVO` |
|
||||
|
||||
该单元测试不能证明真实 Spring 事务回滚或 Enrollment SQL;这些证据属于 AC-03b 集成门禁。
|
||||
|
||||
## 10. Database 与权限影响
|
||||
|
||||
- 未执行 Generator Import;无 DDL、迁移、ALTER/CREATE/DROP。
|
||||
- 未修改 `007_dicts.sql`、`sys_menu`、`sys_dict`、`sys_role`、`sys_role_menu`。
|
||||
- 未写任何测试/正式业务数据;测试均为 JVM 内存数据。
|
||||
- 后端权限严格为 `train:plan:list/query/add/edit/complete`;前端按钮只使用 `train:plan:add/edit/complete`,无 `system:plan:*`。
|
||||
|
||||
## 11. Acceptance Criteria
|
||||
|
||||
| AC | 状态 | 证据与边界 |
|
||||
|---|---|---|
|
||||
| AC-01 逆序日期返回 40010 | UNIT_PASS | 逆序和相等边界均覆盖。 |
|
||||
| AC-02 状态 2 修改返回 40012 | UNIT_PASS | 同时覆盖历史状态 3。 |
|
||||
| AC-03a Plan complete orchestration seam | UNIT_PASS | 状态更新、Port 一次调用、缺失 Port fail closed、异常传播及事务注解已验证。 |
|
||||
| AC-03b 30 名 Enrollment 真实联动 | INTEGRATION_PENDING (ISSUE-03-01) | 未实现 Enrollment adapter/SQL,后续必须验证 status=4、complete_time 与跨表回滚。 |
|
||||
| Integration Safety | UNIT_PASS | Port missing 时 update 调用为 0,Plan 不会单边持久化为 2。 |
|
||||
|
||||
## 12. 验证记录
|
||||
|
||||
- `mvn test -pl ruoyi-train -am -f RuoYi-Vue/pom.xml`:14 tests,0 failures,0 errors,0 skipped,`BUILD SUCCESS`。
|
||||
- `mvn compile -DskipTests -f RuoYi-Vue/pom.xml`:8 个 Reactor 模块全部成功,`BUILD SUCCESS`。
|
||||
- 评审指定 ESLint(主目录已安装 executable + `--resolve-plugins-relative-to`,工作目录仍为本 Issue worktree):退出码 0,无 error/warning。
|
||||
- `npm run build:prod`:build complete;仅有仓库既有 `src/views/graph/antv/index.vue` 的 `@antv/x6 Addon` warning,与本 Issue 文件无关。
|
||||
- Final Review 本轮未修改任何前端文件;沿用上一轮 ESLint/build 结果。
|
||||
- 最终 `git diff --check`、`git status --short`、`git diff --stat` 结果以交付回复为准。
|
||||
- 未执行 commit/merge/rebase,等待技术负责人复审。
|
||||
Reference in new issue
Block a user