新增培训:实现教师主数据P0能力

This commit is contained in:
wangzhibiao committed 2026-09-14 10:43:03 +08:00
1 parent 7fcaea29b8
commit 043c08823a
9 files changed
+586 -293

No files matched your search

@@ -1,13 +1,12 @@
package com.ruoyi.web.controller.train;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import java.util.regex.Pattern;
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;
@@ -15,90 +14,108 @@ 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.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.train.domain.TrainTeacher;
import com.ruoyi.train.service.ITrainTeacherService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
/**
* 教师信息Controller
*
*
* @author ruoyi
* @date 2026-09-08
* @date 2026-09-14
*/
@RestController
@RequestMapping("/train/teacher")
public class TrainTeacherController extends BaseController
{
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Autowired
private ITrainTeacherService trainTeacherService;
/**
* 查询教师信息列表
* 查询教师信息列表 (GET /train/teacher/list)
*/
@PreAuthorize("@ss.hasPermi('system:teacher:list')")
@PreAuthorize("@ss.hasPermi('train:teacher:list')")
@GetMapping("/list")
public TableDataInfo list(TrainTeacher trainTeacher)
{
PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageSize = pageDomain.getPageSize();
if (pageSize != null && pageSize > 100)
{
throw new ServiceException("分页大小不能超过100");
}
startPage();
List<TrainTeacher> list = trainTeacherService.selectTrainTeacherList(trainTeacher);
return getDataTable(list);
}
/**
* 导出教师信息列表
* 获取教师信息详细信息 (GET /train/teacher/{tId})
*/
@PreAuthorize("@ss.hasPermi('system:teacher:export')")
@Log(title = "教师信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, TrainTeacher trainTeacher)
{
List<TrainTeacher> list = trainTeacherService.selectTrainTeacherList(trainTeacher);
ExcelUtil<TrainTeacher> util = new ExcelUtil<TrainTeacher>(TrainTeacher.class);
util.exportExcel(response, list, "教师信息数据");
}
/**
* 获取教师信息详细信息
*/
@PreAuthorize("@ss.hasPermi('system:teacher:query')")
@PreAuthorize("@ss.hasPermi('train:teacher:query')")
@GetMapping(value = "/{tId}")
public AjaxResult getInfo(@PathVariable("tId") String tId)
{
return success(trainTeacherService.selectTrainTeacherByTId(tId));
TrainTeacher teacher = trainTeacherService.selectTrainTeacherByTId(tId);
if (teacher == null)
{
return AjaxResult.error(40401, "教师不存在");
}
return success(teacher);
}
/**
* 新增教师信息
* 新增教师信息 (POST /train/teacher)
*/
@PreAuthorize("@ss.hasPermi('system:teacher:add')")
@Log(title = "教师信息", businessType = BusinessType.INSERT)
@PreAuthorize("@ss.hasPermi('train:teacher:add')")
@Log(title = "教师管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody TrainTeacher trainTeacher)
{
if (StringUtils.isEmpty(trainTeacher.gettName()))
{
return AjaxResult.error("教师姓名不能为空");
}
if (trainTeacher.gettName().trim().length() > 50)
{
return AjaxResult.error("教师姓名长度不能超过50个字符");
}
if (StringUtils.isNotEmpty(trainTeacher.gettPhone()) && !isValidPhone(trainTeacher.gettPhone()))
{
return AjaxResult.error("手机号码格式不正确");
}
return toAjax(trainTeacherService.insertTrainTeacher(trainTeacher));
}
/**
* 修改教师信息
* 修改教师信息 (PUT /train/teacher/{tId})
*/
@PreAuthorize("@ss.hasPermi('system:teacher:edit')")
@Log(title = "教师信息", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody TrainTeacher trainTeacher)
@PreAuthorize("@ss.hasPermi('train:teacher:edit')")
@Log(title = "教师管理", businessType = BusinessType.UPDATE)
@PutMapping(value = "/{tId}")
public AjaxResult edit(@PathVariable("tId") String tId, @RequestBody TrainTeacher trainTeacher)
{
trainTeacher.settId(tId);
if (StringUtils.isNotEmpty(trainTeacher.gettName()) && trainTeacher.gettName().trim().length() > 50)
{
return AjaxResult.error("教师姓名长度不能超过50个字符");
}
if (StringUtils.isNotEmpty(trainTeacher.gettPhone()) && !isValidPhone(trainTeacher.gettPhone()))
{
return AjaxResult.error("手机号码格式不正确");
}
return toAjax(trainTeacherService.updateTrainTeacher(trainTeacher));
}
/**
* 删除教师信息
*/
@PreAuthorize("@ss.hasPermi('system:teacher:remove')")
@Log(title = "教师信息", businessType = BusinessType.DELETE)
@DeleteMapping("/{tIds}")
public AjaxResult remove(@PathVariable String[] tIds)
private boolean isValidPhone(String phone)
{
return toAjax(trainTeacherService.deleteTrainTeacherByTIds(tIds));
return PHONE_PATTERN.matcher(phone).matches();
}
}
@@ -9,15 +9,15 @@ import com.ruoyi.common.core.domain.BaseEntity;
/**
* 教师信息对象 train_teacher
*
*
* @author ruoyi
* @date 2026-09-08
* @date 2026-09-14
*/
public class TrainTeacher extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 教师工号 */
/** 教师工号/标识 */
private String tId;
/** 教师姓名 */
@@ -25,7 +25,7 @@ public class TrainTeacher extends BaseEntity
private String tName;
/** 性别:男 / 女 */
@Excel(name = "性别:男 / 女")
@Excel(name = "性别")
private String tGender;
/** 出生日期 */
@@ -33,13 +33,16 @@ public class TrainTeacher extends BaseEntity
@Excel(name = "出生日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date tBirth;
/** 所属院系 */
@Excel(name = "所属院系")
private String tDepartment;
/** sys_user登录账号逻辑关联ID */
private Long userId;
/** 职称:讲师 / 副教授 / 教授 */
@Excel(name = "职称:讲师 / 副教授 / 教授")
private String tTitle;
/** 所属单位/部门名称 (由旧 t_department 迁移) */
@Excel(name = "所属单位")
private String orgName;
/** 职务/职称 (由旧 t_title 迁移) */
@Excel(name = "职务/职称")
private String jobTitle;
/** 联系电话 */
@Excel(name = "联系电话")
@@ -49,98 +52,121 @@ public class TrainTeacher extends BaseEntity
@Excel(name = "邮箱")
private String tEmail;
/** 创建人 (管理员 / 教师) */
@Excel(name = "创建人 (管理员 / 教师)")
private String createUser;
/** 状态 (1:正常, 0:停用) */
@Excel(name = "状态", readConverterExp = "1=正常,0=停用")
private Integer status;
public void settId(String tId)
/** 删除标志 (0:存在, 2:删除) */
private String delFlag;
public void settId(String tId)
{
this.tId = tId;
}
public String gettId()
public String gettId()
{
return tId;
}
public void settName(String tName)
public void settName(String tName)
{
this.tName = tName;
}
public String gettName()
public String gettName()
{
return tName;
}
public void settGender(String tGender)
public void settGender(String tGender)
{
this.tGender = tGender;
}
public String gettGender()
public String gettGender()
{
return tGender;
}
public void settBirth(Date tBirth)
public void settBirth(Date tBirth)
{
this.tBirth = tBirth;
}
public Date gettBirth()
public Date gettBirth()
{
return tBirth;
}
public void settDepartment(String tDepartment)
public void setUserId(Long userId)
{
this.tDepartment = tDepartment;
this.userId = userId;
}
public String gettDepartment()
public Long getUserId()
{
return tDepartment;
return userId;
}
public void settTitle(String tTitle)
public void setOrgName(String orgName)
{
this.tTitle = tTitle;
this.orgName = orgName;
}
public String gettTitle()
public String getOrgName()
{
return tTitle;
return orgName;
}
public void settPhone(String tPhone)
public void setJobTitle(String jobTitle)
{
this.jobTitle = jobTitle;
}
public String getJobTitle()
{
return jobTitle;
}
public void settPhone(String tPhone)
{
this.tPhone = tPhone;
}
public String gettPhone()
public String gettPhone()
{
return tPhone;
}
public void settEmail(String tEmail)
public void settEmail(String tEmail)
{
this.tEmail = tEmail;
}
public String gettEmail()
public String gettEmail()
{
return tEmail;
}
public void setCreateUser(String createUser)
public void setStatus(Integer status)
{
this.createUser = createUser;
this.status = status;
}
public String getCreateUser()
public Integer getStatus()
{
return createUser;
return status;
}
public void setDelFlag(String delFlag)
{
this.delFlag = delFlag;
}
public String getDelFlag()
{
return delFlag;
}
@Override
@@ -150,12 +176,16 @@ public class TrainTeacher extends BaseEntity
.append("tName", gettName())
.append("tGender", gettGender())
.append("tBirth", gettBirth())
.append("tDepartment", gettDepartment())
.append("tTitle", gettTitle())
.append("userId", getUserId())
.append("orgName", getOrgName())
.append("jobTitle", getJobTitle())
.append("tPhone", gettPhone())
.append("tEmail", gettEmail())
.append("createUser", getCreateUser())
.append("status", getStatus())
.append("delFlag", getDelFlag())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.toString();
@@ -5,15 +5,15 @@ import com.ruoyi.train.domain.TrainTeacher;
/**
* 教师信息Mapper接口
*
*
* @author ruoyi
* @date 2026-09-08
*/
public interface TrainTeacherMapper
public interface TrainTeacherMapper
{
/**
* 查询教师信息
*
*
* @param tId 教师信息主键
* @return 教师信息
*/
@@ -21,7 +21,7 @@ public interface TrainTeacherMapper
/**
* 查询教师信息列表
*
*
* @param trainTeacher 教师信息
* @return 教师信息集合
*/
@@ -29,7 +29,7 @@ public interface TrainTeacherMapper
/**
* 新增教师信息
*
*
* @param trainTeacher 教师信息
* @return 结果
*/
@@ -37,15 +37,23 @@ public interface TrainTeacherMapper
/**
* 修改教师信息
*
*
* @param trainTeacher 教师信息
* @return 结果
*/
public int updateTrainTeacher(TrainTeacher trainTeacher);
/**
* 根据手机号码查询教师(用于全局查重)
*
* @param tPhone 手机号码
* @return 教师信息
*/
public TrainTeacher checkPhoneUnique(String tPhone);
/**
* 删除教师信息
*
*
* @param tId 教师信息主键
* @return 结果
*/
@@ -53,7 +61,7 @@ public interface TrainTeacherMapper
/**
* 批量删除教师信息
*
*
* @param tIds 需要删除的数据主键集合
* @return 结果
*/
@@ -5,15 +5,15 @@ import com.ruoyi.train.domain.TrainTeacher;
/**
* 教师信息Service接口
*
*
* @author ruoyi
* @date 2026-09-08
*/
public interface ITrainTeacherService
public interface ITrainTeacherService
{
/**
* 查询教师信息
*
*
* @param tId 教师信息主键
* @return 教师信息
*/
@@ -21,7 +21,7 @@ public interface ITrainTeacherService
/**
* 查询教师信息列表
*
*
* @param trainTeacher 教师信息
* @return 教师信息集合
*/
@@ -29,7 +29,7 @@ public interface ITrainTeacherService
/**
* 新增教师信息
*
*
* @param trainTeacher 教师信息
* @return 结果
*/
@@ -37,15 +37,23 @@ public interface ITrainTeacherService
/**
* 修改教师信息
*
*
* @param trainTeacher 教师信息
* @return 结果
*/
public int updateTrainTeacher(TrainTeacher trainTeacher);
/**
* 校验教师手机号码是否唯一
*
* @param trainTeacher 教师信息
* @return 结果
*/
public boolean checkPhoneUnique(TrainTeacher trainTeacher);
/**
* 批量删除教师信息
*
*
* @param tIds 需要删除的教师信息主键集合
* @return 结果
*/
@@ -53,7 +61,7 @@ public interface ITrainTeacherService
/**
* 删除教师信息信息
*
*
* @param tId 教师信息主键
* @return 结果
*/
@@ -1,7 +1,11 @@
package com.ruoyi.train.service.impl;
import java.util.List;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.train.mapper.TrainTeacherMapper;
@@ -10,19 +14,19 @@ import com.ruoyi.train.service.ITrainTeacherService;
/**
* 教师信息Service业务层处理
*
*
* @author ruoyi
* @date 2026-09-08
* @date 2026-09-14
*/
@Service
public class TrainTeacherServiceImpl implements ITrainTeacherService
public class TrainTeacherServiceImpl implements ITrainTeacherService
{
@Autowired
private TrainTeacherMapper trainTeacherMapper;
/**
* 查询教师信息
*
*
* @param tId 教师信息主键
* @return 教师信息
*/
@@ -34,7 +38,7 @@ public class TrainTeacherServiceImpl implements ITrainTeacherService
/**
* 查询教师信息列表
*
*
* @param trainTeacher 教师信息
* @return 教师信息
*/
@@ -44,35 +48,125 @@ public class TrainTeacherServiceImpl implements ITrainTeacherService
return trainTeacherMapper.selectTrainTeacherList(trainTeacher);
}
/**
* 校验教师手机号码是否唯一
*
* @param trainTeacher 教师信息
* @return true 唯一,false 重复
*/
@Override
public boolean checkPhoneUnique(TrainTeacher trainTeacher)
{
if (StringUtils.isEmpty(trainTeacher.gettPhone()))
{
return true;
}
TrainTeacher info = trainTeacherMapper.checkPhoneUnique(trainTeacher.gettPhone());
if (StringUtils.isNotNull(info) && !StringUtils.equals(info.gettId(), trainTeacher.gettId()))
{
return false;
}
return true;
}
/**
* 新增教师信息
*
*
* @param trainTeacher 教师信息
* @return 结果
*/
@Override
public int insertTrainTeacher(TrainTeacher trainTeacher)
{
// 1. 姓名校验
if (StringUtils.isEmpty(trainTeacher.gettName()))
{
throw new ServiceException("教师姓名不能为空");
}
if (trainTeacher.gettName().trim().length() > 50)
{
throw new ServiceException("教师姓名长度不能超过50个字符");
}
// 2. 手机号系统全局查重 (AC-02: 40001 TEACHER_PHONE_DUPLICATED)
if (StringUtils.isNotEmpty(trainTeacher.gettPhone()) && !checkPhoneUnique(trainTeacher))
{
throw new ServiceException("教师手机号码已存在", 40001);
}
// 3. 主键生成:支持自定义工号或自动生成32位UUID
if (StringUtils.isEmpty(trainTeacher.gettId()))
{
trainTeacher.settId(IdUtils.fastSimpleUUID());
}
// 4. 默认值与审计字段
if (trainTeacher.getStatus() == null)
{
trainTeacher.setStatus(1);
}
trainTeacher.setDelFlag("0");
trainTeacher.setCreateTime(DateUtils.getNowDate());
try
{
trainTeacher.setCreateBy(SecurityUtils.getUsername());
}
catch (Exception e)
{
// 非登录上下文防崩溃
}
return trainTeacherMapper.insertTrainTeacher(trainTeacher);
}
/**
* 修改教师信息
*
*
* @param trainTeacher 教师信息
* @return 结果
*/
@Override
public int updateTrainTeacher(TrainTeacher trainTeacher)
{
// 1. 检查教师是否存在 (40401 TEACHER_NOT_FOUND)
if (StringUtils.isEmpty(trainTeacher.gettId()))
{
throw new ServiceException("教师工号不能为空");
}
TrainTeacher existing = trainTeacherMapper.selectTrainTeacherByTId(trainTeacher.gettId());
if (existing == null)
{
throw new ServiceException("教师不存在", 40401);
}
// 2. 姓名校验
if (StringUtils.isNotEmpty(trainTeacher.gettName()) && trainTeacher.gettName().trim().length() > 50)
{
throw new ServiceException("教师姓名长度不能超过50个字符");
}
// 3. 手机号查重 (AC-02: 40001 TEACHER_PHONE_DUPLICATED)
if (StringUtils.isNotEmpty(trainTeacher.gettPhone()) && !checkPhoneUnique(trainTeacher))
{
throw new ServiceException("教师手机号码已存在", 40001);
}
trainTeacher.setUpdateTime(DateUtils.getNowDate());
try
{
trainTeacher.setUpdateBy(SecurityUtils.getUsername());
}
catch (Exception e)
{
// 非登录上下文防崩溃
}
return trainTeacherMapper.updateTrainTeacher(trainTeacher);
}
/**
* 批量删除教师信息
*
* 批量删除教师信息(逻辑删除)
*
* @param tIds 需要删除的教师信息主键
* @return 结果
*/
@@ -83,8 +177,8 @@ public class TrainTeacherServiceImpl implements ITrainTeacherService
}
/**
* 删除教师信息信息
*
* 删除教师信息(逻辑删除)
*
* @param tId 教师信息主键
* @return 结果
*/
@@ -3,43 +3,54 @@
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.train.mapper.TrainTeacherMapper">
<resultMap type="TrainTeacher" id="TrainTeacherResult">
<result property="tId" column="t_id" />
<result property="tName" column="t_name" />
<result property="tGender" column="t_gender" />
<result property="tBirth" column="t_birth" />
<result property="tDepartment" column="t_department" />
<result property="tTitle" column="t_title" />
<result property="userId" column="user_id" />
<result property="orgName" column="org_name" />
<result property="jobTitle" column="job_title" />
<result property="tPhone" column="t_phone" />
<result property="tEmail" column="t_email" />
<result property="createUser" column="create_user" />
<result property="status" column="status" />
<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="selectTrainTeacherVo">
select t_id, t_name, t_gender, t_birth, t_department, t_title, t_phone, t_email, create_user, create_time, update_time, remark from train_teacher
select t_id, t_name, t_gender, t_birth, user_id, org_name, job_title, t_phone, t_email, status, del_flag, create_by, create_time, update_by, update_time, remark from train_teacher
</sql>
<select id="selectTrainTeacherList" parameterType="TrainTeacher" resultMap="TrainTeacherResult">
<include refid="selectTrainTeacherVo"/>
<where>
<where>
and del_flag = '0'
<if test="tName != null and tName != ''"> and t_name like concat('%', #{tName}, '%')</if>
<if test="tGender != null and tGender != ''"> and t_gender = #{tGender}</if>
<if test="tBirth != null "> and t_birth = #{tBirth}</if>
<if test="tDepartment != null and tDepartment != ''"> and t_department = #{tDepartment}</if>
<if test="tTitle != null and tTitle != ''"> and t_title = #{tTitle}</if>
<if test="tPhone != null and tPhone != ''"> and t_phone = #{tPhone}</if>
<if test="orgName != null and orgName != ''"> and org_name like concat('%', #{orgName}, '%')</if>
<if test="jobTitle != null and jobTitle != ''"> and job_title like concat('%', #{jobTitle}, '%')</if>
<if test="tPhone != null and tPhone != ''"> and t_phone like concat('%', #{tPhone}, '%')</if>
<if test="tEmail != null and tEmail != ''"> and t_email = #{tEmail}</if>
<if test="createUser != null and createUser != ''"> and create_user = #{createUser}</if>
<if test="status != null"> and status = #{status}</if>
</where>
order by create_time desc
</select>
<select id="selectTrainTeacherByTId" parameterType="String" resultMap="TrainTeacherResult">
<include refid="selectTrainTeacherVo"/>
where t_id = #{tId}
where t_id = #{tId} and del_flag = '0'
</select>
<select id="checkPhoneUnique" parameterType="String" resultMap="TrainTeacherResult">
<include refid="selectTrainTeacherVo"/>
where t_phone = #{tPhone} and del_flag = '0' limit 1
</select>
<insert id="insertTrainTeacher" parameterType="TrainTeacher">
@@ -49,12 +60,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="tName != null and tName != ''">t_name,</if>
<if test="tGender != null">t_gender,</if>
<if test="tBirth != null">t_birth,</if>
<if test="tDepartment != null and tDepartment != ''">t_department,</if>
<if test="tTitle != null">t_title,</if>
<if test="tPhone != null">t_phone,</if>
<if test="userId != null">user_id,</if>
<if test="orgName != null and orgName != ''">org_name,</if>
<if test="jobTitle != null and jobTitle != ''">job_title,</if>
<if test="tPhone != null and tPhone != ''">t_phone,</if>
<if test="tEmail != null">t_email,</if>
<if test="createUser != null and createUser != ''">create_user,</if>
<if test="status != null">status,</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>
@@ -63,12 +78,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="tName != null and tName != ''">#{tName},</if>
<if test="tGender != null">#{tGender},</if>
<if test="tBirth != null">#{tBirth},</if>
<if test="tDepartment != null and tDepartment != ''">#{tDepartment},</if>
<if test="tTitle != null">#{tTitle},</if>
<if test="tPhone != null">#{tPhone},</if>
<if test="userId != null">#{userId},</if>
<if test="orgName != null and orgName != ''">#{orgName},</if>
<if test="jobTitle != null and jobTitle != ''">#{jobTitle},</if>
<if test="tPhone != null and tPhone != ''">#{tPhone},</if>
<if test="tEmail != null">#{tEmail},</if>
<if test="createUser != null and createUser != ''">#{createUser},</if>
<if test="status != null">#{status},</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>
@@ -80,26 +99,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="tName != null and tName != ''">t_name = #{tName},</if>
<if test="tGender != null">t_gender = #{tGender},</if>
<if test="tBirth != null">t_birth = #{tBirth},</if>
<if test="tDepartment != null and tDepartment != ''">t_department = #{tDepartment},</if>
<if test="tTitle != null">t_title = #{tTitle},</if>
<if test="userId != null">user_id = #{userId},</if>
<if test="orgName != null">org_name = #{orgName},</if>
<if test="jobTitle != null">job_title = #{jobTitle},</if>
<if test="tPhone != null">t_phone = #{tPhone},</if>
<if test="tEmail != null">t_email = #{tEmail},</if>
<if test="createUser != null and createUser != ''">create_user = #{createUser},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="status != null">status = #{status},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
</trim>
where t_id = #{tId}
where t_id = #{tId} and del_flag = '0'
</update>
<delete id="deleteTrainTeacherByTId" parameterType="String">
delete from train_teacher where t_id = #{tId}
</delete>
<!-- 逻辑删除:将 del_flag 置为 '2' -->
<update id="deleteTrainTeacherByTId" parameterType="String">
update train_teacher set del_flag = '2' where t_id = #{tId}
</update>
<delete id="deleteTrainTeacherByTIds" parameterType="String">
delete from train_teacher where t_id in
<!-- 批量逻辑删除:将 del_flag 置为 '2' -->
<update id="deleteTrainTeacherByTIds" parameterType="String">
update train_teacher set del_flag = '2' where t_id in
<foreach item="tId" collection="array" open="(" separator="," close=")">
#{tId}
</foreach>
</delete>
</mapper>
</update>
</mapper>
+1 -9
View File
@@ -29,16 +29,8 @@ export function addTeacher(data) {
// 修改教师信息
export function updateTeacher(data) {
return request({
url: '/train/teacher',
url: '/train/teacher/' + (data.tId || ''),
method: 'put',
data: data
})
}
// 删除教师信息
export function delTeacher(tId) {
return request({
url: '/train/teacher/' + tId,
method: 'delete'
})
}
+81 -145
View File
@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="68px">
<el-form-item label="教师姓名" prop="tName">
<el-input
v-model="queryParams.tName"
@@ -9,34 +9,18 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="性别:男 / 女" prop="tGender">
<el-form-item label="所属单位" prop="orgName">
<el-input
v-model="queryParams.tGender"
placeholder="请输入性别:男 / 女"
v-model="queryParams.orgName"
placeholder="请输入所属单位"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="出生日期" prop="tBirth">
<el-date-picker clearable
v-model="queryParams.tBirth"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择出生日期">
</el-date-picker>
</el-form-item>
<el-form-item label="所属院系" prop="tDepartment">
<el-form-item label="职务职称" prop="jobTitle">
<el-input
v-model="queryParams.tDepartment"
placeholder="请输入所属院系"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="职称:讲师 / 副教授 / 教授" prop="tTitle">
<el-input
v-model="queryParams.tTitle"
placeholder="请输入职称:讲师 / 副教授 / 教授"
v-model="queryParams.jobTitle"
placeholder="请输入职务/职称"
clearable
@keyup.enter.native="handleQuery"
/>
@@ -49,21 +33,11 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="邮箱" prop="tEmail">
<el-input
v-model="queryParams.tEmail"
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 label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="教师状态" clearable>
<el-option label="正常" :value="1" />
<el-option label="停用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@@ -74,81 +48,59 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
v-hasPermi="['train:teacher:add']"
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['train:teacher:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-hasPermi="['train:teacher:edit']"
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['train:teacher: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:teacher: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:teacher: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="teacherList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="教师工号" align="center" prop="tId" />
<el-table-column label="教师姓名" align="center" prop="tName" />
<el-table-column label="性别:男 / 女" align="center" prop="tGender" />
<el-table-column label="出生日期" align="center" prop="tBirth" width="180">
<el-table-column label="性别" align="center" prop="tGender" width="80" />
<el-table-column label="出生日期" align="center" prop="tBirth" width="120">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.tBirth, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="所属院系" align="center" prop="tDepartment" />
<el-table-column label="职称:讲师 / 副教授 / 教授" align="center" prop="tTitle" />
<el-table-column label="联系电话" align="center" prop="tPhone" />
<el-table-column label="邮箱" align="center" prop="tEmail" />
<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="orgName" :show-overflow-tooltip="true" />
<el-table-column label="职务/职称" align="center" prop="jobTitle" :show-overflow-tooltip="true" />
<el-table-column label="联系电话" align="center" prop="tPhone" width="130" />
<el-table-column label="邮箱" align="center" prop="tEmail" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" prop="status" width="90">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
<template slot-scope="scope">
<el-button
v-hasPermi="['train:teacher:edit']"
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['train:teacher:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['train:teacher:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -162,39 +114,46 @@
/>
<!-- 添加或修改教师信息对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-dialog :title="title" :visible.sync="open" width="550px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="90px">
<el-form-item label="教师姓名" prop="tName">
<el-input v-model="form.tName" placeholder="请输入教师姓名" />
<el-input v-model="form.tName" placeholder="请输入教师姓名" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="性别:男 / 女" prop="tGender">
<el-input v-model="form.tGender" placeholder="请输入性别:男 / 女" />
<el-form-item label="性别" prop="tGender">
<el-radio-group v-model="form.tGender">
<el-radio label="男">男</el-radio>
<el-radio label="女">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="出生日期" prop="tBirth">
<el-date-picker clearable
<el-date-picker
v-model="form.tBirth"
clearable
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择出生日期">
</el-date-picker>
placeholder="请选择出生日期"
/>
</el-form-item>
<el-form-item label="所属院系" prop="tDepartment">
<el-input v-model="form.tDepartment" placeholder="请输入所属院系" />
<el-form-item label="所属单位" prop="orgName">
<el-input v-model="form.orgName" placeholder="请输入所属单位/部门" maxlength="100" />
</el-form-item>
<el-form-item label="职称:讲师 / 副教授 / 教授" prop="tTitle">
<el-input v-model="form.tTitle" placeholder="请输入职称:讲师 / 副教授 / 教授" />
<el-form-item label="职务/职称" prop="jobTitle">
<el-input v-model="form.jobTitle" placeholder="请输入职务/职称" maxlength="50" />
</el-form-item>
<el-form-item label="联系电话" prop="tPhone">
<el-input v-model="form.tPhone" placeholder="请输入联系电话" />
<el-input v-model="form.tPhone" placeholder="请输入11位手机号码" maxlength="20" />
</el-form-item>
<el-form-item label="邮箱" prop="tEmail">
<el-input v-model="form.tEmail" placeholder="请输入邮箱" />
<el-input v-model="form.tEmail" placeholder="请输入电子邮箱" maxlength="100" />
</el-form-item>
<el-form-item label="创建人 (管理员 / 教师)" prop="createUser">
<el-input v-model="form.createUser" placeholder="请输入创建人 (管理员 / 教师)" />
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注信息" maxlength="500" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@@ -206,10 +165,10 @@
</template>
<script>
import { listTeacher, getTeacher, delTeacher, addTeacher, updateTeacher } from "@/api/train/teacher"
import { listTeacher, getTeacher, addTeacher, updateTeacher } from '@/api/train/teacher'
export default {
name: "Teacher",
name: 'Teacher',
data() {
return {
// 遮罩层
@@ -218,8 +177,6 @@ export default {
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
@@ -227,7 +184,7 @@ export default {
// 教师信息表格数据
teacherList: [],
// 弹出层标题
title: "",
title: '',
// 是否显示弹出层
open: false,
// 查询参数
@@ -235,27 +192,25 @@ export default {
pageNum: 1,
pageSize: 10,
tName: null,
tGender: null,
tBirth: null,
tDepartment: null,
tTitle: null,
orgName: null,
jobTitle: null,
tPhone: null,
tEmail: null,
createUser: null,
status: null
},
// 表单参数
form: {},
// 表单校验
rules: {
tName: [
{ required: true, message: "教师姓名不能为空", trigger: "blur" }
{ required: true, message: '教师姓名不能为空', trigger: 'blur' },
{ max: 50, message: '教师姓名长度不能超过50个字符', trigger: 'blur' }
],
tDepartment: [
{ required: true, message: "所属院系不能为空", trigger: "blur" }
],
createUser: [
{ required: true, message: "创建人 (管理员 / 教师)不能为空", trigger: "blur" }
tPhone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: 'blur' }
],
tEmail: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
]
}
}
},
@@ -282,18 +237,16 @@ export default {
this.form = {
tId: null,
tName: null,
tGender: null,
tGender: '男',
tBirth: null,
tDepartment: null,
tTitle: null,
orgName: null,
jobTitle: null,
tPhone: null,
tEmail: null,
createUser: null,
createTime: null,
updateTime: null,
status: 1,
remark: null
}
this.resetForm("form")
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
@@ -302,66 +255,49 @@ export default {
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.resetForm('queryForm')
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.tId)
this.single = selection.length!==1
this.multiple = !selection.length
this.single = selection.length !== 1
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = "添加教师信息"
this.title = '添加教师信息'
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const tId = row.tId || this.ids
const tId = row.tId || this.ids[0]
getTeacher(tId).then(response => {
this.form = response.data
this.open = true
this.title = "修改教师信息"
this.title = '修改教师信息'
})
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.tId != null) {
updateTeacher(this.form).then(response => {
this.$modal.msgSuccess("修改成功")
this.$modal.msgSuccess('修改成功')
this.open = false
this.getList()
})
} else {
addTeacher(this.form).then(response => {
this.$modal.msgSuccess("新增成功")
this.$modal.msgSuccess('新增成功')
this.open = false
this.getList()
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const tIds = row.tId || this.ids
this.$modal.confirm('是否确认删除教师信息编号为"' + tIds + '"的数据项?').then(function() {
return delTeacher(tIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('train/teacher/export', {
...this.queryParams
}, `teacher_${new Date().getTime()}.xlsx`)
}
}
}
+186
View File
@@ -0,0 +1,186 @@
# ISSUE-01-01:师资主数据管理能力开发与验收报告
> **文档定位:** 本文档记录 RailwaySystem P0 阶段第一个正式 Issue(`ISSUE-01-01 师资主数据管理能力`)兼 Generator 正式 Pilot 的完整实施与验收结果。
> **实施分支:** `feat/issue-01-01-teacher`
> **基线 Commit:** `7fcaea29b8810546fd972ba0c9d366c71a357d63` (`Tag: issues-v1.0`)
---
## 1. Issue 目标
1. **业务目标:** 为排课选师资、计划主讲教师提供完备的教师档案检索、详情查看、建档与维护能力;
2. **功能范围:** 教师姓名、单位、性别、出生日期、职务/职称、联系电话、邮箱维护;分页检索;手机号系统全局查重;RESTful 详情与更新;逻辑删除;
3. **技术目标:** 完成 Generator V2 正式业务表 Pilot 验证,通过只读/隔离预览,比对并最小化修改现有实现,坚决落实逻辑删除与 API Contract V1.0.1 契约规范。
---
## 2. 原实现盘点 (Current Teacher Implementation Inventory)
在开发启动前,对系统已有教师模块进行了细致盘点:
| 资产层级 | 现有路径 / 标识 | 旧实现状态 | 与 API Contract V1.0.1 / Database V2 差异 |
| :--- | :--- | :--- | :--- |
| **Entity** | `com.ruoyi.train.domain.TrainTeacher` | 包含 `tDepartment`, `tTitle`, `createUser` | 数据库 V2 已删除上述旧列,新增 `userId`, `orgName`, `jobTitle`, `status`, `delFlag`,旧实体无法匹配 V2 |
| **Mapper XML** | `ruoyi-train/.../TrainTeacherMapper.xml` | 包含旧列查询;使用物理 `DELETE FROM` | 字段查询在 V2 会报错;物理删除破坏了系统的逻辑删除语义 |
| **Mapper 接口** | `com.ruoyi.train.mapper.TrainTeacherMapper` | 标准 CRUD 接口 | 缺少手机号全局唯一性查重方法接口 |
| **Service** | `TrainTeacherServiceImpl` | 简单直通 Mapper | 缺少手机号全局查重、40001/40401 错误码处理,主键策略未适配 varchar(32) |
| **Controller** | `TrainTeacherController` | 权限使用 `system:teacher:*`;`PUT /train/teacher` | 权限违背契约;PUT 路径未遵循 `PUT /train/teacher/{tId}`;缺少 `pageSize <= 100` 校验 |
| **前端 API** | `build_ui/src/api/train/teacher.js` | `updateTeacher` 为 `PUT /train/teacher` | 未对齐 RESTful `PUT /train/teacher/{tId}` |
| **前端视图** | `build_ui/src/views/train/teacher/index.vue` | 表单使用院系、职称、创建人 | 未对齐单位 `orgName`、职务 `jobTitle`、状态 `status`;缺少手机号格式前端校验 |
---
## 3. Generator Pilot 结果
依据授权范围,本轮仅针对 `train_teacher` 表开放 Generator Import 与 Preview:
1. **导入范围约束:** 仅且仅导入 `train_teacher`,其余业务表严禁导入;导入前 `gen_table` 为 0,导入后 `gen_table` 精确为 1 行;
2. **表/列元数据审查:**
- 候选查询及预览中,历史备份表 `bak_v1_*` 完全不可见;
- `t_id`:`isPk = 1`, `isIncrement = 0`, `javaType = String`, `columnType = character varying(32)`(严格核验不是自增);
- 数据类型映射验证:
- `character varying` -> `String`
- `character(1)` -> `String`
- `timestamp` -> `Date`
- `date` -> `Date`
- `smallint` -> `Long/Integer`
3. **隔离生成与三方 Diff:**
- 全部 Generator 输出至隔离临时目录 `/tmp/railwaysystem-generator/issue-01-01/`,**严禁且未直接覆盖任何工程源码**;
- **三方比对结论:**
- Generator 生成的包名为 `com.ruoyi.system.*`,权限为 `system:teacher:*`,删除为物理 `DELETE`,表单包含迁移注释;
- 现有项目包名为 `com.ruoyi.train.*`,契约权限要求为 `train:teacher:*`,删除要求为基于 `del_flag` 的逻辑删除;
- 采取**最小化手工合并重构**,摒弃机械覆盖。
---
## 4. 保留 / 修改 / 替换文件清单
| 文件路径 | 策略 | 变更重点说明 |
| :--- | :---: | :--- |
| `RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/TrainTeacher.java` | **MODIFY** | 移除旧字段 `tDepartment`, `tTitle`, `createUser`;新增 `userId`, `orgName`, `jobTitle`, `status`, `delFlag` 及对应 getter/setter |
| `RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainTeacherMapper.xml` | **MODIFY** | 映射 V2 新列;所有查询增加 `del_flag = '0'` 过滤;删除改为 `UPDATE ... SET del_flag = '2'`;增加 `checkPhoneUnique` SQL |
| `RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainTeacherMapper.java` | **MODIFY** | 增加 `checkPhoneUnique(String tPhone)` 方法 |
| `RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainTeacherService.java` | **MODIFY** | 增加 `checkPhoneUnique(TrainTeacher trainTeacher)` 业务契约 |
| `RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainTeacherServiceImpl.java` | **MODIFY** | 落实核心业务逻辑:新增/修改手机号系统全局查重抛出 40001;教师不存在抛出 40401;UUID 32位主键生成;默认值与审计赋值;逻辑删除 |
| `RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainTeacherController.java` | **MODIFY** | 仅公开暴露 4 个 P0 Endpoint;权限统一为 `train:teacher:*`;对齐契约 RESTful 路径 `PUT /train/teacher/{tId}`;校验 `pageSize <= 100`、姓名必填与手机号格式;移除所有 Contract 外端点(DELETE/export) |
| `build_ui/src/api/train/teacher.js` | **MODIFY** | `updateTeacher` 请求路径对齐为 `/train/teacher/{tId}`;移除未在契约中定义的 `delTeacher` |
| `build_ui/src/views/train/teacher/index.vue` | **MODIFY** | 替换旧字段为 `orgName`, `jobTitle`, `status`;增加手机号正则校验;权限统一为 `train:teacher:*`;移除删除、导出按钮及未使用的处理方法;通过 eslint 零 warning/error 校验 |
---
## 5. API Contract 映射与落实
按 API Contract V1.0.1 冻结契约(`docs/api-v2/02_module_a_base_resource_api.md`),Teacher 模块正式公开的 HTTP API 严格收敛为 **4 个 P0 API**:
### 5.1 正式公开交付的 P0 API (4 个)
| 契约 API | HTTP Method | 路径 | 权限标识 | 核心实现与校验 |
| :--- | :---: | :--- | :--- | :--- |
| **分页查询教师列表** | `GET` | `/train/teacher/list` | `train:teacher:list` | 支持姓名、单位、职务、电话、状态多条件筛选;强制 `pageSize <= 100`;默认过滤 `del_flag = '0'` |
| **获取教师详情** | `GET` | `/train/teacher/{tId}` | `train:teacher:query` | 教师 ID 存在性检查;若不存在返回 HTTP 200 + 业务状态码 `40401` (`TEACHER_NOT_FOUND`) |
| **新增教师** | `POST` | `/train/teacher` | `train:teacher:add` | 姓名必填 (<=50字符);手机号格式正则校验;Service 层手机号重复预检查(重复抛出 `40001` `TEACHER_PHONE_DUPLICATED`);生成 32 位 UUID 主键 |
| **修改教师** | `PUT` | `/train/teacher/{tId}` | `train:teacher:edit` | 路径参数与实体 ID 绑定;校验存在性 (不存在抛出 40401);Service 层手机号查重 (排除自身);更新审计时间与审计人 |
### 5.2 状态接口说明 (P1 范围)
- **启用/停用教师状态** (`PUT /train/teacher/{tId}/status`):属于冻结契约规划的 P1 接口。本 Issue (P0) 不提前公开该端点,教师状态可通过 P0 `PUT /train/teacher/{tId}` 进行维护。
### 5.3 Contract 外 HTTP Endpoint 治理
根据技术负责人审查意见,**HTTP API Contract V1.0.1 中不存在公开的 Teacher 删除与导出端点**:
- `DELETE /train/teacher/{tIds}` 与 `POST /train/teacher/export` 已从 `TrainTeacherController` 中彻底删除映射,不再公开暴露任何 Contract 外 HTTP Endpoint;
- 前端 `teacher.js` 与 `index.vue` 同步移除删除与导出调用及界面按钮,消除死代码。
---
## 6. 权限设计说明
1. **代码层完全落实:**
- 后端全部采用 `@PreAuthorize("@ss.hasPermi('train:teacher:*')")`;
- 前端操作按钮全部绑定 `v-hasPermi="['train:teacher:*']"`;
- 彻底废除旧有的 `system:teacher:*`;
2. **零数据库写权限:**
- 严格遵循指令规则,本 Issue **绝对不向 `sys_menu` 插入或更新任何菜单与按钮数据**;
- 菜单数据的入库统一留待 `ISSUE-11-01` 集中处理。
---
## 7. 手机号唯一性与持久层能力设计
1. **手机号唯一性实现现状与边界:**
- **数据库现状:** 经只读系统目录核查,当前 Database V2 的 `train_teacher` 表仅包含主键索引 `train_teacher_pkey` 以及普通索引 `idx_teacher_user_id`、`idx_teacher_name`,**不存在 `UNIQUE(t_phone)` 约束或唯一索引**;
- **实现机制:** 当前仅在 **Service层进行手机号重复预检查**;
- **边界说明:** **并发强唯一性不由当前数据库约束保证**。本 Issue 严格遵守禁令,未修改 Database V2 结构增加约束;已登记为 **Database Change Review candidate** 候选议题;
- **错误码合规性:** 业务查重异常返回的错误码 `40001`(`TEACHER_PHONE_DUPLICATED`)直接引自冻结契约 `docs/api-v2/10_permission_and_error_codes.md` 第 104 行,不存在自行发明。
2. **底层持久化删除能力设计:**
- **架构原则:** `HTTP Contract ≠ 内部 Persistence Capability`;
- **逻辑删除能力保留在 Persistence 层**:Mapper 与 Service 层保留基于 `del_flag` 的软删除方法(`del_flag = '2'`),以支持系统内部业务或级联运维调度使用;
- **一期 V1.0.1 未对外暴露 Teacher 删除 HTTP API**,Controller 层无公开删除映射。
---
## 8. 后端验证结果
通过独立的后端测试用例套件 (`TestTeacherBackendService`),全链路覆盖所有场景:
| 测试用例编号 | 场景描述 | 期望结果 | 实测结果 | 状态 |
| :---: | :--- | :--- | :--- | :---: |
| **TC-01** | 基线唯一历史教师查询 | 查出 1 行数据,字段正确映射 | 行数精确为 1,`orgName` 等字段完全一致 | **PASS** |
| **TC-02** | AC-01: 正常新增教师 | 成功入库,UUID 32位主键,`del_flag='0'` | 影响行数 1,主键格式正确,查回一致 | **PASS** |
| **TC-03** | AC-02: 手机号重复新增 | 抛出 40001 `TEACHER_PHONE_DUPLICATED` | 成功捕获 40001 业务异常,阻断新增 | **PASS** |
| **TC-04** | 参数校验 (姓名为空 / 超长) | 拦截非法请求并提示对应错误信息 | 姓名空/超长均被严格拦截 | **PASS** |
| **TC-05** | 查询与更新不存在的 ID | 查询返回空,更新抛出 40401 错误码 | 捕获 40401 `TEACHER_NOT_FOUND` | **PASS** |
| **TC-06** | 修改已有教师资料 | 成功更新单位、职务、职称等字段 | 影响行数 1,再次查回字段与修改值完全一致 | **PASS** |
| **TC-07** | 逻辑删除语义与物理恢复 | 软删除置 `del_flag='2'`,恢复基线 1 行 | 数据库物理验证通过,测试数据已自动复原 | **PASS** |
后端构建结果:
```text
mvn compile -DskipTests -f RuoYi-Vue/pom.xml
[INFO] BUILD SUCCESS
```
---
## 9. 前端验证结果
1. **代码规范与 Lint 验证:**
- 执行命令:`npx eslint --no-ignore src/api/train/teacher.js src/views/train/teacher/index.vue`
- 验证结果:`0 problems (0 errors, 0 warnings)`;
2. **UI 功能与服务验证:**
- 前端页面仅保留新增、修改操作及搜索过滤,删除了删除与导出入口,界面整洁规范;
- 表单严格执行姓名必填、手机号正则、邮箱格式等前端校验。
---
## 10. Database 影响与事实审计
1. **业务数据 DML 事实:**
- **YES,执行过受控测试业务 DML。测试完成后已清理。**
- 验收期间通过单元测试受控插入过一条测试教师记录(手机号 `13800000001`)用于 AC-01 与 AC-02 验证,随后在 TC-07 中彻底物理清理 (`DELETE`);
- **当前只读核验:**
- `SELECT COUNT(*) FROM train_teacher;` -> 精确为 **1**(恢复正式基线);
- `SELECT COUNT(*) FROM train_teacher WHERE t_phone = '13800000001';` -> 精确为 **0**;
- 测试生成的临时记录全部清零,未泄露或残留任何测试数据。
2. **Database V2 结构影响:** 21 张正式业务表结构未发生任何 DDL 变动,保持基线一致;
3. **Generator 基础设施兼容性处理:**
- Generator Pilot 首次真实 Import 暴露了国产 Kingbase 数据库下 Generator 元数据列(`table_id` / `column_id`)自增兼容性问题;
- 该问题已确认属于 **Generator Infrastructure Patch**,已转交独立基础设施补丁任务处理;
- **本 Teacher 分支未包含该 DDL 脚本,未将基础设施改动混入业务 Commit**;
4. **权限/字典表影响:** 未执行 `007_dicts.sql`,未修改 `sys_menu` 与 `sys_dict`。
---
## 11. 已知限制与后续演进
1. **菜单权限数据:** 页面按钮声明了 `train:teacher:*` 权限标识,由于未向 `sys_menu` 插入数据,在非 admin 超级管理员角色下需要待 `ISSUE-11-01` 菜单数据初始化后方可在生产环境分配角色权限;
2. **公开 HTTP 接口收敛:** 一期 V1.0.1 严格对外暴露 4 个 P0 接口,删除与状态启停用独立端点留待后续版本根据需求统一规划;
3. **并发强唯一性:** 当前手机号查重依赖 Service 层校验,若后续业务高并发场景需要强唯一性,由 Database Change Review 统一评估增加数据库约束。
---
## 12. Acceptance Criteria 逐项结果
| AC 编号 | Acceptance Criteria 描述 | 验证结果 |
| :---: | :--- | :---: |
| **AC-01** | Given 教师手机号 `13800000001` 不存在,When 提交新增教师请求,Then 成功插入 `train_teacher` 并返回 200。 | **PASS** |
| **AC-02** | Given 教师手机号 `13800000001` 已存在,When 再次提交新增,Then 抛出错误码 40001 (`TEACHER_PHONE_DUPLICATED`),不产生新增记录。 | **PASS** |