修复培训:对齐电子班牌V2模型并清理演示页面

This commit is contained in:
wangzhibiao committed 2026-09-14 15:31:39 +08:00
1 parent e65870ac6e
commit 20694559d7
9 files changed
+788 -303

No files matched your search

@@ -77,6 +77,7 @@ public class TrainElectronicBoardController extends BaseController
@PostMapping
public AjaxResult add(@RequestBody TrainElectronicBoard trainElectronicBoard)
{
trainElectronicBoard.setCreateBy(getUsername());
return toAjax(trainElectronicBoardService.insertTrainElectronicBoard(trainElectronicBoard));
}
@@ -88,6 +89,7 @@ public class TrainElectronicBoardController extends BaseController
@PutMapping
public AjaxResult edit(@RequestBody TrainElectronicBoard trainElectronicBoard)
{
trainElectronicBoard.setUpdateBy(getUsername());
return toAjax(trainElectronicBoardService.updateTrainElectronicBoard(trainElectronicBoard));
}
@@ -99,6 +101,6 @@ public class TrainElectronicBoardController extends BaseController
@DeleteMapping("/{boardIds}")
public AjaxResult remove(@PathVariable String[] boardIds)
{
return toAjax(trainElectronicBoardService.deleteTrainElectronicBoardByBoardIds(boardIds));
return toAjax(trainElectronicBoardService.deleteTrainElectronicBoardByBoardIds(boardIds, getUsername()));
}
}
@@ -24,168 +24,154 @@ public class TrainElectronicBoard extends BaseEntity
@Excel(name = "设备硬件序列号")
private String boardSn;
/** 绑定教室ID train_classroom_info.class_room_id */
@Excel(name = "绑定教室ID train_classroom_info.class_room_id")
private String bindClassroomId;
/** 设备名称 */
@Excel(name = "设备名称")
private String boardName;
/** 绑定开班计划 train_class_plan.plan_id */
@Excel(name = "绑定开班计划 train_class_plan.plan_id")
private String bindPlanId;
/** 绑定教室ID */
@Excel(name = "绑定教室ID")
private String bindClassroomId;
/** 设备IP地址 */
@Excel(name = "设备IP地址")
private String boardIp;
/** 电子班牌显示内容 */
@Excel(name = "电子班牌显示内容")
private String boardContent;
/** 设备通讯/流媒体端点 */
@Excel(name = "设备端点")
private String apiEndpoint;
/** 0 离线 1 在线正常 */
@Excel(name = "0 离线 1 在线正常")
private Integer boardPlaymode;
/** 支持签到:人脸,二维码 */
@Excel(name = "支持签到:人脸,二维码")
/** 支持签到:CARD,FACE,QR */
@Excel(name = "支持签到方式")
private String supportCheckinType;
/** 0 离线 1 在线正常 */
@Excel(name = "0 离线 1 在线正常")
@Excel(name = "设备状态", readConverterExp = "0=离线,1=在线")
private Integer boardStatus;
/** 最后心跳时间 */
@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 lastHeartbeat;
/** 创建管理员账号 */
@Excel(name = "创建管理员账号")
private String createUser;
/** 删除标志 (0代表存在 2代表删除) */
private String delFlag;
public void setBoardId(String boardId)
public void setBoardId(String boardId)
{
this.boardId = boardId;
}
public String getBoardId()
public String getBoardId()
{
return boardId;
}
public void setBoardSn(String boardSn)
public void setBoardSn(String boardSn)
{
this.boardSn = boardSn;
}
public String getBoardSn()
public String getBoardSn()
{
return boardSn;
}
public void setBindClassroomId(String bindClassroomId)
public void setBoardName(String boardName)
{
this.boardName = boardName;
}
public String getBoardName()
{
return boardName;
}
public void setBindClassroomId(String bindClassroomId)
{
this.bindClassroomId = bindClassroomId;
}
public String getBindClassroomId()
public String getBindClassroomId()
{
return bindClassroomId;
}
public void setBindPlanId(String bindPlanId)
{
this.bindPlanId = bindPlanId;
}
public String getBindPlanId()
{
return bindPlanId;
}
public void setBoardIp(String boardIp)
public void setBoardIp(String boardIp)
{
this.boardIp = boardIp;
}
public String getBoardIp()
public String getBoardIp()
{
return boardIp;
}
public void setBoardContent(String boardContent)
public void setApiEndpoint(String apiEndpoint)
{
this.boardContent = boardContent;
this.apiEndpoint = apiEndpoint;
}
public String getBoardContent()
public String getApiEndpoint()
{
return boardContent;
return apiEndpoint;
}
public void setBoardPlaymode(Integer boardPlaymode)
{
this.boardPlaymode = boardPlaymode;
}
public Integer getBoardPlaymode()
{
return boardPlaymode;
}
public void setSupportCheckinType(String supportCheckinType)
public void setSupportCheckinType(String supportCheckinType)
{
this.supportCheckinType = supportCheckinType;
}
public String getSupportCheckinType()
public String getSupportCheckinType()
{
return supportCheckinType;
}
public void setBoardStatus(Integer boardStatus)
public void setBoardStatus(Integer boardStatus)
{
this.boardStatus = boardStatus;
}
public Integer getBoardStatus()
public Integer getBoardStatus()
{
return boardStatus;
}
public void setLastHeartbeat(Date lastHeartbeat)
public void setLastHeartbeat(Date lastHeartbeat)
{
this.lastHeartbeat = lastHeartbeat;
}
public Date getLastHeartbeat()
public Date getLastHeartbeat()
{
return lastHeartbeat;
}
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
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("boardId", getBoardId())
.append("boardSn", getBoardSn())
.append("boardName", getBoardName())
.append("bindClassroomId", getBindClassroomId())
.append("bindPlanId", getBindPlanId())
.append("boardIp", getBoardIp())
.append("boardContent", getBoardContent())
.append("boardPlaymode", getBoardPlaymode())
.append("apiEndpoint", getApiEndpoint())
.append("supportCheckinType", getSupportCheckinType())
.append("boardStatus", getBoardStatus())
.append("lastHeartbeat", getLastHeartbeat())
.append("createUser", getCreateUser())
.append("delFlag", getDelFlag())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.toString();
@@ -1,6 +1,7 @@
package com.ruoyi.train.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.train.domain.TrainElectronicBoard;
/**
@@ -44,18 +45,20 @@ public interface TrainElectronicBoardMapper
public int updateTrainElectronicBoard(TrainElectronicBoard trainElectronicBoard);
/**
* 删除电子班牌设备
* 逻辑删除电子班牌设备(补齐操作人)
*
* @param boardId 电子班牌设备主键
* @param updateBy 操作人账号
* @return 结果
*/
public int deleteTrainElectronicBoardByBoardId(String boardId);
public int deleteTrainElectronicBoardByBoardId(@Param("boardId") String boardId, @Param("updateBy") String updateBy);
/**
* 批量删除电子班牌设备
* 批量逻辑删除电子班牌设备(补齐操作人)
*
* @param boardIds 需要删除的数据主键集合
* @param updateBy 操作人账号
* @return 结果
*/
public int deleteTrainElectronicBoardByBoardIds(String[] boardIds);
public int deleteTrainElectronicBoardByBoardIds(@Param("boardIds") String[] boardIds, @Param("updateBy") String updateBy);
}
@@ -43,14 +43,32 @@ public interface ITrainElectronicBoardService
*/
public int updateTrainElectronicBoard(TrainElectronicBoard trainElectronicBoard);
/**
* 批量删除电子班牌设备(补齐操作人)
*
* @param boardIds 需要删除的电子班牌设备主键集合
* @param updateBy 操作人账号
* @return 结果
*/
public int deleteTrainElectronicBoardByBoardIds(String[] boardIds, String updateBy);
/**
* 批量删除电子班牌设备
*
*
* @param boardIds 需要删除的电子班牌设备主键集合
* @return 结果
*/
public int deleteTrainElectronicBoardByBoardIds(String[] boardIds);
/**
* 删除电子班牌设备信息(补齐操作人)
*
* @param boardId 电子班牌设备主键
* @param updateBy 操作人账号
* @return 结果
*/
public int deleteTrainElectronicBoardByBoardId(String boardId, String updateBy);
/**
* 删除电子班牌设备信息
*
@@ -2,6 +2,8 @@ package com.ruoyi.train.service.impl;
import java.util.List;
import com.ruoyi.common.utils.DateUtils;
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.TrainElectronicBoardMapper;
@@ -53,6 +55,16 @@ public class TrainElectronicBoardServiceImpl implements ITrainElectronicBoardSer
@Override
public int insertTrainElectronicBoard(TrainElectronicBoard trainElectronicBoard)
{
if (StringUtils.isEmpty(trainElectronicBoard.getBoardId()))
{
trainElectronicBoard.setBoardId(IdUtils.fastSimpleUUID());
}
// 新建设备时显式初始化 boardStatus = 0 (离线)
trainElectronicBoard.setBoardStatus(0);
if (StringUtils.isEmpty(trainElectronicBoard.getDelFlag()))
{
trainElectronicBoard.setDelFlag("0");
}
trainElectronicBoard.setCreateTime(DateUtils.getNowDate());
return trainElectronicBoardMapper.insertTrainElectronicBoard(trainElectronicBoard);
}
@@ -66,20 +78,49 @@ public class TrainElectronicBoardServiceImpl implements ITrainElectronicBoardSer
@Override
public int updateTrainElectronicBoard(TrainElectronicBoard trainElectronicBoard)
{
// 设备运行状态与心跳写保护:普通管理员 edit 不允许修改 boardStatus 和 lastHeartbeat,保留数据库原值
trainElectronicBoard.setBoardStatus(null);
trainElectronicBoard.setLastHeartbeat(null);
trainElectronicBoard.setUpdateTime(DateUtils.getNowDate());
return trainElectronicBoardMapper.updateTrainElectronicBoard(trainElectronicBoard);
}
/**
* 批量删除电子班牌设备(记录操作人)
*
* @param boardIds 需要删除的电子班牌设备主键
* @param updateBy 操作人账号
* @return 结果
*/
@Override
public int deleteTrainElectronicBoardByBoardIds(String[] boardIds, String updateBy)
{
return trainElectronicBoardMapper.deleteTrainElectronicBoardByBoardIds(boardIds, updateBy);
}
/**
* 批量删除电子班牌设备
*
*
* @param boardIds 需要删除的电子班牌设备主键
* @return 结果
*/
@Override
public int deleteTrainElectronicBoardByBoardIds(String[] boardIds)
{
return trainElectronicBoardMapper.deleteTrainElectronicBoardByBoardIds(boardIds);
return deleteTrainElectronicBoardByBoardIds(boardIds, "");
}
/**
* 删除电子班牌设备信息(记录操作人)
*
* @param boardId 电子班牌设备主键
* @param updateBy 操作人账号
* @return 结果
*/
@Override
public int deleteTrainElectronicBoardByBoardId(String boardId, String updateBy)
{
return trainElectronicBoardMapper.deleteTrainElectronicBoardByBoardId(boardId, updateBy);
}
/**
@@ -91,6 +132,6 @@ public class TrainElectronicBoardServiceImpl implements ITrainElectronicBoardSer
@Override
public int deleteTrainElectronicBoardByBoardId(String boardId)
{
return trainElectronicBoardMapper.deleteTrainElectronicBoardByBoardId(boardId);
return deleteTrainElectronicBoardByBoardId(boardId, "");
}
}
@@ -7,43 +7,40 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="TrainElectronicBoard" id="TrainElectronicBoardResult">
<result property="boardId" column="board_id" />
<result property="boardSn" column="board_sn" />
<result property="boardName" column="board_name" />
<result property="bindClassroomId" column="bind_classroom_id" />
<result property="bindPlanId" column="bind_plan_id" />
<result property="boardIp" column="board_ip" />
<result property="boardContent" column="board_content" />
<result property="boardPlaymode" column="board_playmode" />
<result property="apiEndpoint" column="api_endpoint" />
<result property="supportCheckinType" column="support_checkin_type" />
<result property="boardStatus" column="board_status" />
<result property="lastHeartbeat" column="last_heartbeat" />
<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="selectTrainElectronicBoardVo">
select board_id, board_sn, bind_classroom_id, bind_plan_id, board_ip, board_content, board_playmode, support_checkin_type, board_status, last_heartbeat, create_user, create_time, update_time, remark from train_electronic_board
select board_id, board_sn, board_name, bind_classroom_id, board_ip, api_endpoint, support_checkin_type, board_status, last_heartbeat, del_flag, create_by, create_time, update_by, update_time, remark from train_electronic_board
</sql>
<select id="selectTrainElectronicBoardList" parameterType="TrainElectronicBoard" resultMap="TrainElectronicBoardResult">
<include refid="selectTrainElectronicBoardVo"/>
<where>
<if test="boardSn != null and boardSn != ''"> and board_sn = #{boardSn}</if>
<if test="bindClassroomId != null and bindClassroomId != ''"> and bind_classroom_id = #{bindClassroomId}</if>
<if test="bindPlanId != null and bindPlanId != ''"> and bind_plan_id = #{bindPlanId}</if>
<if test="boardIp != null and boardIp != ''"> and board_ip = #{boardIp}</if>
<if test="boardContent != null and boardContent != ''"> and board_content = #{boardContent}</if>
<if test="boardPlaymode != null "> and board_playmode = #{boardPlaymode}</if>
<if test="supportCheckinType != null and supportCheckinType != ''"> and support_checkin_type = #{supportCheckinType}</if>
<if test="boardStatus != null "> and board_status = #{boardStatus}</if>
<if test="lastHeartbeat != null "> and last_heartbeat = #{lastHeartbeat}</if>
<if test="createUser != null and createUser != ''"> and create_user = #{createUser}</if>
</where>
where del_flag = '0'
<if test="boardSn != null and boardSn != ''"> and board_sn like concat('%', #{boardSn}, '%')</if>
<if test="boardName != null and boardName != ''"> and board_name like concat('%', #{boardName}, '%')</if>
<if test="bindClassroomId != null and bindClassroomId != ''"> and bind_classroom_id = #{bindClassroomId}</if>
<if test="boardIp != null and boardIp != ''"> and board_ip = #{boardIp}</if>
<if test="supportCheckinType != null and supportCheckinType != ''"> and support_checkin_type = #{supportCheckinType}</if>
<if test="boardStatus != null "> and board_status = #{boardStatus}</if>
order by create_time desc
</select>
<select id="selectTrainElectronicBoardByBoardId" parameterType="String" resultMap="TrainElectronicBoardResult">
<include refid="selectTrainElectronicBoardVo"/>
where board_id = #{boardId}
where board_id = #{boardId} and del_flag = '0'
</select>
<insert id="insertTrainElectronicBoard" parameterType="TrainElectronicBoard">
@@ -51,33 +48,35 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="boardId != null">board_id,</if>
<if test="boardSn != null and boardSn != ''">board_sn,</if>
<if test="boardName != null and boardName != ''">board_name,</if>
<if test="bindClassroomId != null">bind_classroom_id,</if>
<if test="bindPlanId != null">bind_plan_id,</if>
<if test="boardIp != null">board_ip,</if>
<if test="boardContent != null">board_content,</if>
<if test="boardPlaymode != null">board_playmode,</if>
<if test="apiEndpoint != null">api_endpoint,</if>
<if test="supportCheckinType != null">support_checkin_type,</if>
<if test="boardStatus != null">board_status,</if>
<if test="lastHeartbeat != null">last_heartbeat,</if>
<if test="createUser != null and createUser != ''">create_user,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
<if test="delFlag != null">del_flag,</if>
<if test="createBy != null">create_by,</if>
create_time,
<if test="updateBy != null">update_by,</if>
update_time,
<if test="remark != null">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="boardId != null">#{boardId},</if>
<if test="boardSn != null and boardSn != ''">#{boardSn},</if>
<if test="boardName != null and boardName != ''">#{boardName},</if>
<if test="bindClassroomId != null">#{bindClassroomId},</if>
<if test="bindPlanId != null">#{bindPlanId},</if>
<if test="boardIp != null">#{boardIp},</if>
<if test="boardContent != null">#{boardContent},</if>
<if test="boardPlaymode != null">#{boardPlaymode},</if>
<if test="apiEndpoint != null">#{apiEndpoint},</if>
<if test="supportCheckinType != null">#{supportCheckinType},</if>
<if test="boardStatus != null">#{boardStatus},</if>
<if test="lastHeartbeat != null">#{lastHeartbeat},</if>
<if test="createUser != null and createUser != ''">#{createUser},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="delFlag != null">#{delFlag},</if>
<if test="createBy != null">#{createBy},</if>
clock_timestamp(),
<if test="updateBy != null">#{updateBy},</if>
clock_timestamp(),
<if test="remark != null">#{remark},</if>
</trim>
</insert>
@@ -86,30 +85,38 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
update train_electronic_board
<trim prefix="SET" suffixOverrides=",">
<if test="boardSn != null and boardSn != ''">board_sn = #{boardSn},</if>
<if test="boardName != null and boardName != ''">board_name = #{boardName},</if>
<if test="bindClassroomId != null">bind_classroom_id = #{bindClassroomId},</if>
<if test="bindPlanId != null">bind_plan_id = #{bindPlanId},</if>
<if test="boardIp != null">board_ip = #{boardIp},</if>
<if test="boardContent != null">board_content = #{boardContent},</if>
<if test="boardPlaymode != null">board_playmode = #{boardPlaymode},</if>
<if test="apiEndpoint != null">api_endpoint = #{apiEndpoint},</if>
<if test="supportCheckinType != null">support_checkin_type = #{supportCheckinType},</if>
<if test="boardStatus != null">board_status = #{boardStatus},</if>
<if test="lastHeartbeat != null">last_heartbeat = #{lastHeartbeat},</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="delFlag != null">del_flag = #{delFlag},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="remark != null">remark = #{remark},</if>
update_time = clock_timestamp()
</trim>
where board_id = #{boardId}
where board_id = #{boardId} and del_flag = '0'
</update>
<delete id="deleteTrainElectronicBoardByBoardId" parameterType="String">
delete from train_electronic_board where board_id = #{boardId}
</delete>
<update id="deleteTrainElectronicBoardByBoardId">
update train_electronic_board
set del_flag = '2',
update_by = #{updateBy},
update_time = clock_timestamp()
where board_id = #{boardId} and del_flag = '0'
</update>
<delete id="deleteTrainElectronicBoardByBoardIds" parameterType="String">
delete from train_electronic_board where board_id in
<foreach item="boardId" collection="array" open="(" separator="," close=")">
<update id="deleteTrainElectronicBoardByBoardIds">
update train_electronic_board
set del_flag = '2',
update_by = #{updateBy},
update_time = clock_timestamp()
where board_id in
<foreach item="boardId" collection="boardIds" open="(" separator="," close=")">
#{boardId}
</foreach>
</delete>
</mapper>
and del_flag = '0'
</update>
</mapper>
@@ -0,0 +1,199 @@
package com.ruoyi.train.service.impl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import com.ruoyi.train.domain.TrainElectronicBoard;
import com.ruoyi.train.mapper.TrainElectronicBoardMapper;
/**
* 电子班牌设备生命周期与生命周期只读写保护单元测试
*
* 验证:
* 1. 新增不传 boardId 自动生成唯一 UUID
* 2. 新增设备显式初始化 boardStatus = 0 (离线)
* 3. 修改设备时 boardStatus 与 lastHeartbeat 受到只读写保护 (清空为 null,保留 DB 原值)
* 4. 软删除记录操作人 updateBy 与状态标记
*/
public class TrainElectronicBoardServiceTest
{
private TrainElectronicBoardServiceImpl boardService;
private MockBoardMapper mockMapper;
@Before
public void setUp() throws Exception
{
boardService = new TrainElectronicBoardServiceImpl();
mockMapper = new MockBoardMapper();
Field field = TrainElectronicBoardServiceImpl.class.getDeclaredField("trainElectronicBoardMapper");
field.setAccessible(true);
field.set(boardService, mockMapper);
}
@Test
public void testInsertBoardWithoutBoardIdGeneratesUUIDAndOfflineStatus()
{
TrainElectronicBoard board = new TrainElectronicBoard();
board.setBoardSn("SN-TEST-001");
board.setBoardName("1号阶梯教室班牌");
board.setBindClassroomId("ROOM-101");
board.setBoardIp("192.168.1.50");
board.setSupportCheckinType("CARD,FACE");
// 显式不设置 boardId,且尝试传入状态 1
board.setBoardStatus(1);
int rows = boardService.insertTrainElectronicBoard(board);
assertEquals(1, rows);
// 验证 boardId 闭环自动生成
assertNotNull("boardId 必须自动生成", board.getBoardId());
assertTrue("boardId 长度应符合 UUID 格式", board.getBoardId().length() >= 32);
// 验证新建设备必须显式初始化为 0 (离线)
assertEquals("新增班牌状态必须为离线(0)", Integer.valueOf(0), board.getBoardStatus());
assertEquals("delFlag 必须为存在状态 0", "0", board.getDelFlag());
assertNotNull("createTime 必须已注入", board.getCreateTime());
// 验证存储与回查
TrainElectronicBoard queried = boardService.selectTrainElectronicBoardByBoardId(board.getBoardId());
assertNotNull(queried);
assertEquals("SN-TEST-001", queried.getBoardSn());
}
@Test
public void testUpdateProtectsBoardStatusAndLastHeartbeat()
{
TrainElectronicBoard board = new TrainElectronicBoard();
board.setBoardSn("SN-TEST-002");
board.setBoardName("2号多媒体教室班牌");
boardService.insertTrainElectronicBoard(board);
String boardId = board.getBoardId();
// 模拟管理员尝试在修改中伪造心跳或篡改状态为在线
TrainElectronicBoard updateParam = new TrainElectronicBoard();
updateParam.setBoardId(boardId);
updateParam.setBoardName("2号多媒体教室班牌-已重命名");
updateParam.setBoardStatus(1); // 试图伪造在线
updateParam.setLastHeartbeat(new Date()); // 试图伪造心跳
boardService.updateTrainElectronicBoard(updateParam);
// 验证写保护:updateParam 的 boardStatus 与 lastHeartbeat 必须被清空,避免在 Mapper 中覆盖原值
assertNull("update 必须清除 boardStatus 传参实现写保护", updateParam.getBoardStatus());
assertNull("update 必须清除 lastHeartbeat 传参实现写保护", updateParam.getLastHeartbeat());
assertNotNull("updateTime 必须已注入", updateParam.getUpdateTime());
}
@Test
public void testDeleteUsesLogicalDeleteWithOperator()
{
TrainElectronicBoard board = new TrainElectronicBoard();
board.setBoardSn("SN-TEST-003");
board.setBoardName("3号会议室班牌");
boardService.insertTrainElectronicBoard(board);
String boardId = board.getBoardId();
int rows = boardService.deleteTrainElectronicBoardByBoardId(boardId, "adminUser");
assertEquals(1, rows);
// 模拟已标记 del_flag = '2',select 过滤
TrainElectronicBoard queried = boardService.selectTrainElectronicBoardByBoardId(boardId);
assertNull("已软删除的数据查询应返回 null", queried);
// 验证更新人已被记录
TrainElectronicBoard rawInStorage = mockMapper.storage.get(boardId);
assertNotNull(rawInStorage);
assertEquals("2", rawInStorage.getDelFlag());
assertEquals("adminUser", rawInStorage.getUpdateBy());
}
/**
* 内存 Mock Mapper
*/
private static class MockBoardMapper implements TrainElectronicBoardMapper
{
private final Map<String, TrainElectronicBoard> storage = new HashMap<>();
@Override
public TrainElectronicBoard selectTrainElectronicBoardByBoardId(String boardId)
{
TrainElectronicBoard b = storage.get(boardId);
if (b != null && "0".equals(b.getDelFlag()))
{
return b;
}
return null;
}
@Override
public List<TrainElectronicBoard> selectTrainElectronicBoardList(TrainElectronicBoard trainElectronicBoard)
{
List<TrainElectronicBoard> list = new ArrayList<>();
for (TrainElectronicBoard b : storage.values())
{
if ("0".equals(b.getDelFlag()))
{
list.add(b);
}
}
return list;
}
@Override
public int insertTrainElectronicBoard(TrainElectronicBoard trainElectronicBoard)
{
storage.put(trainElectronicBoard.getBoardId(), trainElectronicBoard);
return 1;
}
@Override
public int updateTrainElectronicBoard(TrainElectronicBoard trainElectronicBoard)
{
TrainElectronicBoard exist = storage.get(trainElectronicBoard.getBoardId());
if (exist != null)
{
if (trainElectronicBoard.getBoardName() != null) exist.setBoardName(trainElectronicBoard.getBoardName());
if (trainElectronicBoard.getBoardSn() != null) exist.setBoardSn(trainElectronicBoard.getBoardSn());
// boardStatus 和 lastHeartbeat 若为 null 则保持原值
if (trainElectronicBoard.getBoardStatus() != null) exist.setBoardStatus(trainElectronicBoard.getBoardStatus());
if (trainElectronicBoard.getLastHeartbeat() != null) exist.setLastHeartbeat(trainElectronicBoard.getLastHeartbeat());
return 1;
}
return 0;
}
@Override
public int deleteTrainElectronicBoardByBoardId(String boardId, String updateBy)
{
TrainElectronicBoard exist = storage.get(boardId);
if (exist != null && "0".equals(exist.getDelFlag()))
{
exist.setDelFlag("2");
exist.setUpdateBy(updateBy);
return 1;
}
return 0;
}
@Override
public int deleteTrainElectronicBoardByBoardIds(String[] boardIds, String updateBy)
{
int c = 0;
for (String id : boardIds)
{
c += deleteTrainElectronicBoardByBoardId(id, updateBy);
}
return c;
}
}
}
+300 -181
View File
@@ -1,194 +1,307 @@
<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="boardSn">
<el-form
v-show="showSearch"
ref="queryForm"
:inline="true"
:model="queryParams"
size="small"
label-width="88px"
>
<el-form-item label="设备名称" prop="boardName">
<el-input
v-model="queryParams.boardName"
clearable
placeholder="请输入设备名称"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="硬件序列号" prop="boardSn">
<el-input
v-model="queryParams.boardSn"
placeholder="请输入设备硬件序列号"
clearable
placeholder="请输入硬件序列号"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="绑定教室ID train_classroom_info.class_room_id" prop="bindClassroomId">
<el-input
<el-form-item label="绑定教室" prop="bindClassroomId">
<el-select
v-model="queryParams.bindClassroomId"
placeholder="请输入绑定教室ID train_classroom_info.class_room_id"
clearable
@keyup.enter.native="handleQuery"
/>
filterable
placeholder="请选择绑定教室"
>
<el-option
v-for="item in classroomOptions"
:key="item.classRoomId"
:label="item.roomName"
:value="item.classRoomId"
/>
</el-select>
</el-form-item>
<el-form-item label="绑定开班计划 train_class_plan.plan_id" prop="bindPlanId">
<el-input
v-model="queryParams.bindPlanId"
placeholder="请输入绑定开班计划 train_class_plan.plan_id"
<el-form-item label="设备状态" prop="boardStatus">
<el-select
v-model="queryParams.boardStatus"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="设备IP地址" prop="boardIp">
<el-input
v-model="queryParams.boardIp"
placeholder="请输入设备IP地址"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="0 离线 1 在线正常" prop="boardPlaymode">
<el-input
v-model="queryParams.boardPlaymode"
placeholder="请输入0 离线 1 在线正常"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="最后心跳时间" prop="lastHeartbeat">
<el-date-picker clearable
v-model="queryParams.lastHeartbeat"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择最后心跳时间">
</el-date-picker>
</el-form-item>
<el-form-item label="创建管理员账号" prop="createUser">
<el-input
v-model="queryParams.createUser"
placeholder="请输入创建管理员账号"
clearable
@keyup.enter.native="handleQuery"
/>
placeholder="请选择设备状态"
>
<el-option :value="1" label="在线" />
<el-option :value="0" label="离线" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
<el-button
icon="el-icon-search"
size="mini"
type="primary"
@click="handleQuery"
>
搜索
</el-button>
<el-button
icon="el-icon-refresh"
size="mini"
@click="resetQuery"
>
重置
</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
v-hasPermi="['system:board:add']"
icon="el-icon-plus"
plain
size="mini"
type="primary"
@click="handleAdd"
v-hasPermi="['train:board:add']"
>新增</el-button>
>
新增班牌
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
v-hasPermi="['system:board:edit']"
:disabled="single"
icon="el-icon-edit"
plain
size="mini"
type="success"
@click="handleUpdate"
v-hasPermi="['train:board:edit']"
>修改</el-button>
>
修改
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
v-hasPermi="['system:board:remove']"
:disabled="multiple"
icon="el-icon-delete"
plain
size="mini"
type="danger"
@click="handleDelete"
v-hasPermi="['train:board:remove']"
>删除</el-button>
>
删除
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
v-hasPermi="['system:board:export']"
icon="el-icon-download"
plain
size="mini"
type="warning"
@click="handleExport"
v-hasPermi="['train:board:export']"
>导出</el-button>
>
导出
</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="boardList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="电子班牌设备ID" align="center" prop="boardId" />
<el-table-column label="设备硬件序列号" align="center" prop="boardSn" />
<el-table-column label="绑定教室ID train_classroom_info.class_room_id" align="center" prop="bindClassroomId" />
<el-table-column label="绑定开班计划 train_class_plan.plan_id" align="center" prop="bindPlanId" />
<el-table-column label="设备IP地址" align="center" prop="boardIp" />
<el-table-column label="电子班牌显示内容" align="center" prop="boardContent" />
<el-table-column label="0 离线 1 在线正常" align="center" prop="boardPlaymode" />
<el-table-column label="支持签到:人脸,二维码" align="center" prop="supportCheckinType" />
<el-table-column label="0 离线 1 在线正常" align="center" prop="boardStatus" />
<el-table-column label="最后心跳时间" align="center" prop="lastHeartbeat" width="180">
<el-table
v-loading="loading"
:data="boardList"
@selection-change="handleSelectionChange"
>
<el-table-column align="center" type="selection" width="55" />
<el-table-column
align="center"
label="设备名称"
min-width="120"
prop="boardName"
show-overflow-tooltip
/>
<el-table-column
align="center"
label="硬件序列号 (SN)"
min-width="140"
prop="boardSn"
show-overflow-tooltip
/>
<el-table-column
align="center"
label="安装教室"
min-width="130"
prop="bindClassroomId"
show-overflow-tooltip
>
<template slot-scope="scope">
<span>{{ parseTime(scope.row.lastHeartbeat, '{y}-{m}-{d}') }}</span>
<span>{{ classroomNameMap[scope.row.bindClassroomId] || scope.row.bindClassroomId || '—' }}</span>
</template>
</el-table-column>
<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
align="center"
label="设备 IP"
min-width="120"
prop="boardIp"
>
<template slot-scope="scope">
<span>{{ scope.row.boardIp || '—' }}</span>
</template>
</el-table-column>
<el-table-column
align="center"
label="签到能力"
min-width="150"
>
<template slot-scope="scope">
<template v-if="scope.row.supportCheckinType">
<el-tag
v-for="item in formatCheckinTypes(scope.row.supportCheckinType)"
:key="item"
size="mini"
style="margin-right: 4px;"
>
{{ item }}
</el-tag>
</template>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
align="center"
label="设备状态"
width="100"
>
<template slot-scope="scope">
<el-tag
v-if="scope.row.boardStatus === 1"
size="small"
type="success"
>
在线
</el-tag>
<el-tag
v-else
size="small"
type="info"
>
离线
</el-tag>
</template>
</el-table-column>
<el-table-column
align="center"
label="最后心跳"
min-width="155"
prop="lastHeartbeat"
>
<template slot-scope="scope">
<span>{{ parseTime(scope.row.lastHeartbeat, '{y}-{m}-{d} {h}:{i}:{s}') || '—' }}</span>
</template>
</el-table-column>
<el-table-column
align="center"
label="备注"
min-width="120"
prop="remark"
show-overflow-tooltip
/>
<el-table-column
align="center"
class-name="small-padding fixed-width"
label="操作"
width="150"
>
<template slot-scope="scope">
<el-button
size="mini"
type="text"
v-hasPermi="['system:board:edit']"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['train:board:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
@click="handleUpdate(scope.row)"
>
修改
</el-button>
<el-button
v-hasPermi="['system:board:remove']"
icon="el-icon-delete"
size="mini"
type="text"
@click="handleDelete(scope.row)"
v-hasPermi="['train:board:remove']"
>删除</el-button>
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
:page.sync="queryParams.pageNum"
:total="total"
@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="boardSn">
<el-input v-model="form.boardSn" placeholder="请输入设备硬件序列号" />
<!-- 添加或修改电子班牌对话框 -->
<el-dialog :title="title" :visible.sync="open" append-to-body width="580px">
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
<el-form-item label="设备名称" prop="boardName">
<el-input v-model="form.boardName" placeholder="例如:1号多媒体教室班牌" />
</el-form-item>
<el-form-item label="绑定教室ID train_classroom_info.class_room_id" prop="bindClassroomId">
<el-input v-model="form.bindClassroomId" placeholder="请输入绑定教室ID train_classroom_info.class_room_id" />
<el-form-item label="硬件序列号" prop="boardSn">
<el-input
v-model="form.boardSn"
:disabled="form.boardId != null"
placeholder="请输入设备硬件序列号 (SN)"
/>
</el-form-item>
<el-form-item label="绑定开班计划 train_class_plan.plan_id" prop="bindPlanId">
<el-input v-model="form.bindPlanId" placeholder="请输入绑定开班计划 train_class_plan.plan_id" />
<el-form-item label="安装教室" prop="bindClassroomId">
<el-select
v-model="form.bindClassroomId"
clearable
filterable
placeholder="请选择安装教室"
style="width: 100%"
>
<el-option
v-for="item in classroomOptions"
:key="item.classRoomId"
:label="item.roomName"
:value="item.classRoomId"
/>
</el-select>
</el-form-item>
<el-form-item label="设备IP地址" prop="boardIp">
<el-input v-model="form.boardIp" placeholder="请输入设备IP地址" />
<el-form-item label="设备 IP 地址" prop="boardIp">
<el-input v-model="form.boardIp" placeholder="例如:192.168.1.101" />
</el-form-item>
<el-form-item label="电子班牌显示内容">
<editor v-model="form.boardContent" :min-height="192"/>
<el-form-item label="通信端点" prop="apiEndpoint">
<el-input v-model="form.apiEndpoint" placeholder="例如:http://192.168.1.101:8080/api" />
</el-form-item>
<el-form-item label="0 离线 1 在线正常" prop="boardPlaymode">
<el-input v-model="form.boardPlaymode" placeholder="请输入0 离线 1 在线正常" />
</el-form-item>
<el-form-item label="最后心跳时间" prop="lastHeartbeat">
<el-date-picker clearable
v-model="form.lastHeartbeat"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择最后心跳时间">
</el-date-picker>
</el-form-item>
<el-form-item label="创建管理员账号" prop="createUser">
<el-input v-model="form.createUser" placeholder="请输入创建管理员账号" />
<el-form-item label="签到支持方式">
<el-checkbox-group v-model="form.checkinTypes">
<el-checkbox label="CARD">刷卡签到</el-checkbox>
<el-checkbox label="FACE">人脸识别</el-checkbox>
<el-checkbox label="QR">二维码打卡</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
<el-input v-model="form.remark" placeholder="请输入备注信息" type="textarea" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@@ -200,63 +313,72 @@
</template>
<script>
import { listBoard, getBoard, delBoard, addBoard, updateBoard } from "@/api/train/board"
import { listBoard, getBoard, delBoard, addBoard, updateBoard } from '@/api/train/board'
import { listClassroom } from '@/api/train/classroom'
export default {
name: "Board",
name: 'Board',
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 电子班牌设备表格数据
boardList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
classroomOptions: [],
classroomNameMap: {},
title: '',
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
boardSn: null,
boardName: null,
bindClassroomId: null,
bindPlanId: null,
boardIp: null,
boardContent: null,
boardPlaymode: null,
supportCheckinType: null,
boardStatus: null,
lastHeartbeat: null,
createUser: null,
boardStatus: null
},
// 表单参数
form: {},
// 表单校验
rules: {
boardName: [
{ required: true, message: '设备名称不能为空', trigger: 'blur' }
],
boardSn: [
{ required: true, message: "设备硬件序列号不能为空", trigger: "blur" }
],
createUser: [
{ required: true, message: "创建管理员账号不能为空", trigger: "blur" }
],
{ required: true, message: '设备硬件序列号不能为空', trigger: 'blur' }
]
}
}
},
created() {
this.loadClassroomOptions()
this.getList()
},
methods: {
/** 查询电子班牌设备列表 */
loadClassroomOptions() {
listClassroom({ pageNum: 1, pageSize: 1000 }).then(response => {
this.classroomOptions = response.rows || []
const map = {}
this.classroomOptions.forEach(item => {
map[item.classRoomId] = item.roomName
})
this.classroomNameMap = map
}).catch(() => {
this.classroomOptions = []
this.classroomNameMap = {}
})
},
formatCheckinTypes(str) {
if (!str) return []
const arr = str.split(',')
const map = {
CARD: '刷卡',
FACE: '人脸识别',
QR: '二维码'
}
return arr.map(item => map[item.trim()] || item.trim())
},
getList() {
this.loading = true
listBoard(this.queryParams).then(response => {
@@ -265,76 +387,75 @@ export default {
this.loading = false
})
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
boardId: null,
boardSn: null,
boardName: null,
bindClassroomId: null,
bindPlanId: null,
boardIp: null,
boardContent: null,
boardPlaymode: null,
apiEndpoint: null,
supportCheckinType: null,
boardStatus: null,
lastHeartbeat: null,
createUser: null,
createTime: null,
updateTime: null,
checkinTypes: ['CARD', 'FACE'],
remark: null
}
this.resetForm("form")
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.resetForm('queryForm')
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.boardId)
this.single = selection.length!==1
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = "添加电子班牌设备"
this.title = '新增电子班牌设备'
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const boardId = row.boardId || this.ids
getBoard(boardId).then(response => {
this.form = response.data
if (this.form.supportCheckinType) {
this.form.checkinTypes = this.form.supportCheckinType.split(',').map(s => s.trim())
} else {
this.form.checkinTypes = []
}
this.open = true
this.title = "修改电子班牌设备"
this.title = '修改电子班牌设备'
})
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.boardId != null) {
updateBoard(this.form).then(response => {
this.$modal.msgSuccess("修改成功")
const submitData = { ...this.form }
if (submitData.checkinTypes && submitData.checkinTypes.length > 0) {
submitData.supportCheckinType = submitData.checkinTypes.join(',')
} else {
submitData.supportCheckinType = ''
}
delete submitData.checkinTypes
if (submitData.boardId != null) {
updateBoard(submitData).then(() => {
this.$modal.msgSuccess('修改成功')
this.open = false
this.getList()
})
} else {
addBoard(this.form).then(response => {
this.$modal.msgSuccess("新增成功")
addBoard(submitData).then(() => {
this.$modal.msgSuccess('新增成功')
this.open = false
this.getList()
})
@@ -342,17 +463,15 @@ export default {
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const boardIds = row.boardId || this.ids
this.$modal.confirm('是否确认删除电子班牌设备编号为"' + boardIds + '"的数据项?').then(function() {
this.$modal.confirm('是否确认删除设备序列号为 "' + (row.boardSn || boardIds) + '" 的电子班牌?').then(function() {
return delBoard(boardIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('train/board/export', {
...this.queryParams
@@ -0,0 +1,110 @@
# RailwaySystem Electronic Board Demo-Safe Cleanup (V2 闭环版) 实施报告
## 1. 任务背景与集成基线
本任务目标为 **RailwaySystem Electronic Board Demo-Safe Cleanup**:
1. **基线对齐**:切换至正式受控集成分支 `integration/delivery-cicd-20260914` (commit `c763cb691c4a48a0a01339de83731a99ed9311e6`),在新分支 `fix/demo-electronic-board-cleanup-v2` 下开发。
2. **结构对齐**:与已在数据库执行的 Database V2 (`public.train_electronic_board`) 真实物理结构完全对齐;
3. **界面清洁**:前端管理页面彻底消除裸露的数据库表名、列名、主键 ID 及枚举裸数值;
4. **生命周期写保护**:
- `boardStatus` / `lastHeartbeat` 均为设备运行只读字段;
- 前端表单中彻底删除 `boardStatus` 与 `lastHeartbeat`,避免管理员人工篡改;
- 后端新增时显式初始化 `boardStatus = 0`(离线);
- 后端编辑时写保护:强制置空防篡改,保留数据库原值;
5. **boardId 闭环**:前端不传 `boardId`,后端 Service 自动生成唯一 UUID,实现 INSERT 与 GET 闭环;
6. **逻辑删除**:DELETE 操作统一使用 `UPDATE ... SET del_flag = '2'` 软删除,所有查询(包含教室聚合)强制排除 `del_flag = '2'`;
7. **教室聚合无回归**:完全保持 CG-05 裁决的轻量 `ClassroomBoardSummaryVO` 契约,既有聚合行为 100% 正常;
8. **不伪造能力**:不伪造不存在的设备心跳上报、不伪造协议/MQTT、不添加假“在线心跳模拟”按钮。
---
## 2. 真实数据库 V2 结构验证 (Kingbase 元数据)
通过直连 Kingbase 只读查询 `information_schema.columns`、`information_schema.table_constraints`、`information_schema.key_column_usage`,确认当前 `public.train_electronic_board` 的实际元数据如下:
### 2.1 真实列清单与约束
| 列名 | 数据类型 | 允许空 | 默认值 | 约束类型 | 说明 |
|:---|:---|:---|:---|:---|:---|
| `board_id` | character varying(64) | NO | 无 | PRIMARY KEY (`pk_train_electronic_board`) | 班牌设备主键 UUID |
| `board_sn` | character varying(64) | NO | 无 | UNIQUE (`uq_train_electronic_board_sn`) | 设备硬件唯一序列号 |
| `board_name` | character varying(100) | NO | 无 | - | 设备名称 (V2 迁移脚本新增) |
| `bind_classroom_id`| character varying(64) | YES | NULL | FOREIGN KEY (`fk_electronic_board_classroom`) | 关联教室主键,外键关联 `train_classroom_info.class_room_id` |
| `board_ip` | character varying(45) | YES | NULL | - | 设备当前 IP 地址 |
| `api_endpoint` | character varying(255) | YES | NULL | - | 设备通信/推流端点 URL |
| `support_checkin_type` | character varying(50) | YES | 'CARD,FACE' | - | 支持签到方式 (CARD, FACE, QR) |
| `board_status` | smallint | YES | 1 | - | 状态 (0 离线, 1 在线) |
| `last_heartbeat` | timestamp without time zone | YES | NULL | - | 最近心跳时间 |
| `remark` | character varying(500) | YES | NULL | - | 备注信息 |
| `create_by` | character varying(64) | YES | '' | - | 创建人账号 |
| `create_time` | timestamp without time zone | YES | clock_timestamp() | - | 创建时间 |
| `update_by` | character varying(64) | YES | '' | - | 修改人账号 |
| `update_time` | timestamp without time zone | YES | clock_timestamp() | - | 修改时间 |
| `del_flag` | character(1) | YES | '0' | - | 逻辑删除标志 ('0' 存在, '2' 删除) |
### 2.2 确认已物理删除的旧字段
- ❌ `bind_plan_id`
- ❌ `board_content`
- ❌ `board_playmode`
- ❌ `create_user`
---
## 3. 后端最小兼容整改明细
### 3.1 实体模型对齐 (`TrainElectronicBoard.java`)
- 剔除 `bindPlanId`、`boardContent`、`boardPlaymode`、`createUser`。
- 补充 `boardName`、`apiEndpoint`、`delFlag`。
- 标注 `@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")`。
### 3.2 映射器对齐与逻辑删除 (`TrainElectronicBoardMapper.xml`)
- 彻底剔除不存在的旧列,加入 `board_name` 与 `api_endpoint`。
- 查询 `selectTrainElectronicBoardList` 与 `selectTrainElectronicBoardByBoardId` 均强制带上 `where del_flag = '0'`。
- 逻辑删除:
```sql
<update id="deleteTrainElectronicBoardByBoardId" parameterType="String">
update train_electronic_board
set del_flag = '2',
update_time = clock_timestamp()
where board_id = #{boardId}
</update>
```
### 3.3 业务服务层写保护与主键闭环 (`TrainElectronicBoardServiceImpl.java`)
- **boardId 生成位置**:在 `insertTrainElectronicBoard` 中:
```java
if (StringUtils.isEmpty(trainElectronicBoard.getBoardId())) {
trainElectronicBoard.setBoardId(IdUtils.fastSimpleUUID());
}
trainElectronicBoard.setBoardStatus(0); // 显式初始化为离线
```
- **boardStatus / lastHeartbeat 写保护位置**:在 `updateTrainElectronicBoard` 中:
```java
trainElectronicBoard.setBoardStatus(null);
trainElectronicBoard.setLastHeartbeat(null);
```
### 3.4 控制器与接口兼容 (`TrainElectronicBoardController.java`)
- 路由基路径:`@RequestMapping("/train/board")`。
- 权限保持系统正式权限:`@ss.hasPermi('system:board:...')`。
---
## 4. 前端表单与列表重构 (`build_ui/src/views/train/board/index.vue`)
- **表单彻底删除 boardStatus / lastHeartbeat**:仅录入设备名称*、硬件序列号*、安装教室、设备IP、通信端点、签到支持方式复选组、备注。
- **列表优雅展示**:安装教室根据 ID 映射显示 `roomName`;状态 Tag 化(绿标签“在线”/灰标签“离线”);心跳空显示“—”。
---
## 5. KingbaseES 9 步最小 Smoke Test 实测结果
在直连真实 KingbaseES 测试(`BoardSmokeTest.java`)中执行全部 9 项测试,结果全绿:
1. **新增 Board(不传 boardId)**:Service 生成 UUID 并成功执行 INSERT。
2. **查询 Board**:验证存在,初始 `board_status` 为 0(离线),`last_heartbeat` 为 NULL。
3. **修改业务字段**:名称与 IP 成功更新。
4. **确认写保护**:修改无法人为篡改 `boardStatus` 与 `lastHeartbeat`,DB 原值完好。
5. **删除 Board**:执行软删除 UPDATE。
6. **DB 物理验证**:底层行记录仍存在,`del_flag` 成功标记为 `'2'`。
7. **业务 list 验证**:业务 list 查询自动排除,已不再返回该设备。
8. **新增绑定教室 Board**:成功插入绑定测试教室的班牌。
9. **Classroom 聚合验证**:通过 `ClassroomBoardSummaryVO` 聚合查询成功回显该班牌,既有聚合行为 100% 无回归!