Bläddra i källkod

Merge branch 'master' of http://121.36.46.135:3000/mes-ljz/mescloud

sunxianglong 3 månader sedan
förälder
incheckning
80befcd7dd

+ 23 - 1
src/main/java/com/jeesite/modules/mes/entity/MesMaterialLl.java

@@ -9,7 +9,9 @@ import com.fasterxml.jackson.annotation.JsonFormat;
 import com.jeesite.common.entity.DataEntity;
 import com.jeesite.common.mybatis.annotation.Column;
 import com.jeesite.common.mybatis.annotation.Table;
-import com.jeesite.common.mybatis.mapper.query.QueryType;
+import com.jeesite.common.utils.excel.annotation.ExcelField;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
+import com.jeesite.common.utils.excel.annotation.ExcelFields;
 
 /**
  * 生产领料单Entity
@@ -42,6 +44,26 @@ import com.jeesite.common.mybatis.mapper.query.QueryType;
 public class MesMaterialLl extends DataEntity<MesMaterialLl> {
 
 	private static final long serialVersionUID = 1L;
+
+	@ExcelFields({
+		@ExcelField(title="第三方数据标识", attrName="otherId", align=Align.CENTER, sort=10),
+		@ExcelField(title="单据日期", attrName="businessDate", align=Align.CENTER, sort=20, dataFormat="yyyy-MM-dd hh:mm"),
+		@ExcelField(title="生产订单号", attrName="moDocNo", align=Align.CENTER, sort=30),
+		@ExcelField(title="领料部门", attrName="deptCode", align=Align.CENTER, sort=40),
+		@ExcelField(title="领料人员", attrName="employeeCode", align=Align.CENTER, sort=50),
+		@ExcelField(title="是否无备料领料", attrName="isNoPickLine", align=Align.CENTER, sort=60),
+		@ExcelField(title="存储地点", attrName="whCode", align=Align.CENTER, sort=70),
+		@ExcelField(title="库位", attrName="binCode", align=Align.CENTER, sort=80),
+		@ExcelField(title="料品编码", attrName="itemCode", align=Align.CENTER, sort=90),
+		@ExcelField(title="批号编码", attrName="lotCode", align=Align.CENTER, sort=100),
+		@ExcelField(title="项目号", attrName="projectCode", align=Align.CENTER, sort=110),
+		@ExcelField(title="领料数量", attrName="issuedQty", align=Align.CENTER, sort=120),
+		@ExcelField(title="生产领料单号", attrName="issueDocNo", align=Align.CENTER, sort=130),
+		@ExcelField(title="生产订单备料行", attrName="mopickList", align=Align.CENTER, sort=140),
+		@ExcelField(title="状态", attrName="state", align=Align.CENTER, sort=150),
+		@ExcelField(title="更新时间", attrName="updateDate", align=Align.CENTER, sort=160, dataFormat="yyyy-MM-dd hh:mm"),
+	})
+
 	private String otherId;		// 第三方数据标识
 	private Date businessDate;		// 单据日期
 	private String moDocNo;		// 生产订单号

+ 41 - 0
src/main/java/com/jeesite/modules/mes/service/MesMaterialLlService.java

@@ -1,5 +1,6 @@
 package com.jeesite.modules.mes.service;
 
+import java.math.BigDecimal;
 import java.util.Date;
 import java.util.List;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -190,6 +191,46 @@ public class MesMaterialLlService extends CrudService<MesMaterialLlDao, MesMater
 		mesMaterialLl.setIsNewRecord(false);
 		mesMaterialLl.setState("1");
 		super.update(mesMaterialLl);
+		// ERP成功后按「订单号+料号」重算备料已领数量
+		refreshBlNumsYl(mesMaterialLl.getMoDocNo(), mesMaterialLl.getItemCode());
+	}
+
+	/**
+	 * 按生产订单号+料号,将备料已领数量更新为已成功同步ERP的领料数量之和
+	 * (issue_doc_no 非空视为ERP成功)
+	 */
+	@Transactional
+	public void refreshBlNumsYl(String moDocNo, String itemCode) {
+		if (StringUtils.isBlank(moDocNo) || StringUtils.isBlank(itemCode)) {
+			return;
+		}
+		MesMaterialLl llQuery = new MesMaterialLl();
+		llQuery.setMoDocNo(moDocNo);
+		llQuery.setItemCode(itemCode);
+		List<MesMaterialLl> llList = findList(llQuery);
+		BigDecimal sum = BigDecimal.ZERO;
+		if (ListUtils.isNotEmpty(llList)) {
+			for (MesMaterialLl ll : llList) {
+				if (StringUtils.isBlank(ll.getIssueDocNo()) || ll.getIssuedQty() == null) {
+					continue;
+				}
+				sum = sum.add(BigDecimal.valueOf(ll.getIssuedQty()));
+			}
+		}
+		String numsYl = sum.stripTrailingZeros().toPlainString();
+
+		MesMaterialBl blQuery = new MesMaterialBl();
+		blQuery.setOrderId(moDocNo);
+		blQuery.setErpSn(itemCode);
+		List<MesMaterialBl> blList = mesMaterialBlService.findList(blQuery);
+		if (ListUtils.isEmpty(blList)) {
+			return;
+		}
+		for (MesMaterialBl bl : blList) {
+			bl.setNumsYl(numsYl);
+			bl.setIsNewRecord(false);
+			mesMaterialBlService.update(bl);
+		}
 	}
 
 }

+ 15 - 1
src/main/java/com/jeesite/modules/mes/web/MesMaterialLlController.java

@@ -4,7 +4,6 @@ import java.util.List;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import com.jeesite.modules.erp.entity.MesProductOrder;
 import com.jeesite.modules.mes.resp.CommonResp;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -18,7 +17,9 @@ import org.springframework.web.bind.annotation.ResponseBody;
 
 import com.jeesite.common.config.Global;
 import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.DateUtils;
 import com.jeesite.common.service.ServiceException;
+import com.jeesite.common.utils.excel.ExcelExport;
 import com.jeesite.common.web.BaseController;
 import com.jeesite.modules.mes.entity.MesMaterialLl;
 import com.jeesite.modules.mes.service.MesMaterialLlService;
@@ -66,6 +67,19 @@ public class MesMaterialLlController extends BaseController {
 	}
 
 	/**
+	 * 导出数据
+	 */
+	@RequiresPermissions("mes:mesMaterialLl:view")
+	@RequestMapping(value = "exportData")
+	public void exportData(MesMaterialLl mesMaterialLl, HttpServletResponse response) {
+		List<MesMaterialLl> list = mesMaterialLlService.findList(mesMaterialLl);
+		String fileName = "生产领料单" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+		try (ExcelExport ee = new ExcelExport("生产领料单", MesMaterialLl.class)) {
+			ee.setDataList(list).write(response, fileName);
+		}
+	}
+
+	/**
 	 * 查看编辑表单
 	 */
 	@RequiresPermissions("mes:mesMaterialLl:view")

+ 276 - 0
src/main/java/com/jeesite/modules/scheduling/TianhongBackupScheduling.java

@@ -0,0 +1,276 @@
+package com.jeesite.modules.scheduling;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.modules.tianHong.dao.MachineAttrHistoryDao;
+import com.jeesite.modules.tianHong.dao.MachineAttrStateDao;
+import com.jeesite.modules.tianHong.entity.MachineAttrHistory;
+import com.jeesite.modules.tianHong.entity.MachineAttrState;
+
+/**
+ * 天虹浇筑历史备份:每10分钟整点(00/10/20/30/40/50)
+ * 备份上一完整窗口 [T-10min, T) 到主库 mes_cloud_ljr.tianhong_backup
+ * 曲线字段:秒级 600 槽,值用 # 分隔,缺秒留空
+ */
+@Configuration
+@EnableScheduling
+public class TianhongBackupScheduling {
+
+	private static final Logger log = LoggerFactory.getLogger(TianhongBackupScheduling.class);
+
+	private static final int POINT_COUNT = 600;
+
+	private static final String UPSERT_SQL =
+			"INSERT INTO tianhong_backup ("
+			+ "eqp_id, window_start, window_end,"
+			+ "cru_temp, alu_temp, mould_info,"
+			+ "theory_pressure, real_pressure, pressure_level, ele_consume,"
+			+ "alu_val, pressurize_time, cooling_time, pre_pres, hold_pressure, mold_filling_pre,"
+			+ "point_count, create_date, update_date"
+			+ ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(),NOW()) "
+			+ "ON DUPLICATE KEY UPDATE "
+			+ "window_end=VALUES(window_end),"
+			+ "cru_temp=VALUES(cru_temp), alu_temp=VALUES(alu_temp), mould_info=VALUES(mould_info),"
+			+ "theory_pressure=VALUES(theory_pressure), real_pressure=VALUES(real_pressure),"
+			+ "pressure_level=VALUES(pressure_level), ele_consume=VALUES(ele_consume),"
+			+ "alu_val=VALUES(alu_val), pressurize_time=VALUES(pressurize_time),"
+			+ "cooling_time=VALUES(cooling_time), pre_pres=VALUES(pre_pres),"
+			+ "hold_pressure=VALUES(hold_pressure), mold_filling_pre=VALUES(mold_filling_pre),"
+			+ "point_count=VALUES(point_count), update_date=NOW()";
+
+	@Autowired
+	private MachineAttrHistoryDao machineAttrHistoryDao;
+
+	@Autowired
+	private MachineAttrStateDao machineAttrStateDao;
+
+	@Autowired
+	private DataSource dataSource;
+
+	/**
+	 * 每10分钟整点执行:备份刚结束的窗口
+	 * 例:14:10:00 执行时备份 [14:00:00, 14:10:00)
+	 */
+	@Scheduled(cron = "0 0/10 * * * ?")
+	public void backupLastClosedWindow() {
+		Date end = floorTo10Minutes(new Date());
+		Date start = addMinutes(end, -10);
+		log.info("tianhong_backup start, window=[{}, {})", format(start), format(end));
+		List<MachineAttrState> devices;
+		try {
+			devices = machineAttrStateDao.findList(new MachineAttrState());
+		} catch (Exception e) {
+			log.error("tianhong_backup list devices failed", e);
+			return;
+		}
+		if (devices == null || devices.isEmpty()) {
+			log.warn("tianhong_backup no devices");
+			return;
+		}
+		int ok = 0;
+		for (MachineAttrState device : devices) {
+			if (device == null || StringUtils.isBlank(device.getEqpId())) {
+				continue;
+			}
+			try {
+				backupOne(device.getEqpId(), start, end);
+				ok++;
+			} catch (Exception e) {
+				log.error("tianhong_backup failed eqpId={}", device.getEqpId(), e);
+			}
+		}
+		log.info("tianhong_backup done, devices={}, ok={}", devices.size(), ok);
+	}
+
+	private void backupOne(String eqpId, Date start, Date end) throws Exception {
+		String cru = toSeries(machineAttrHistoryDao.findCruInRange(eqpId, start, end), start, "cru");
+		String alu = toSeries(machineAttrHistoryDao.findAluInRange(eqpId, start, end), start, "alu");
+		String mould = toMouldSeries(machineAttrHistoryDao.findMouldInRange(eqpId, start, end), start);
+		List<MachineAttrHistory> pressureList = machineAttrHistoryDao.findPressureInRange(eqpId, start, end);
+		String theory = toSeries(pressureList, start, "theory");
+		String real = toSeries(pressureList, start, "real");
+		String level = toSeries(pressureList, start, "level");
+		String ele = toSeries(machineAttrHistoryDao.findEleInRange(eqpId, start, end), start, "ele");
+
+		String aluVal = toAttrSeries(eqpId, "alu_val", start, end);
+		String pressurize = toAttrSeries(eqpId, "pressurizeTime", start, end);
+		String cooling = toAttrSeries(eqpId, "coolingTime", start, end);
+		String prePres = toAttrSeries(eqpId, "PrePres", start, end);
+		String hold = toAttrSeries(eqpId, "HoldPressure", start, end);
+		String moldPre = toAttrSeries(eqpId, "moldFillingPre", start, end);
+
+		try (Connection conn = dataSource.getConnection()) {
+			conn.setAutoCommit(true);
+			try (PreparedStatement ps = conn.prepareStatement(UPSERT_SQL)) {
+				int i = 1;
+				ps.setString(i++, eqpId);
+				ps.setTimestamp(i++, new Timestamp(start.getTime()));
+				ps.setTimestamp(i++, new Timestamp(end.getTime()));
+				ps.setString(i++, cru);
+				ps.setString(i++, alu);
+				ps.setString(i++, mould);
+				ps.setString(i++, theory);
+				ps.setString(i++, real);
+				ps.setString(i++, level);
+				ps.setString(i++, ele);
+				ps.setString(i++, aluVal);
+				ps.setString(i++, pressurize);
+				ps.setString(i++, cooling);
+				ps.setString(i++, prePres);
+				ps.setString(i++, hold);
+				ps.setString(i++, moldPre);
+				ps.setInt(i++, POINT_COUNT);
+				ps.executeUpdate();
+			}
+		}
+	}
+
+	private String toAttrSeries(String eqpId, String attr, Date start, Date end) {
+		return toSeries(machineAttrHistoryDao.findAttrInRange(eqpId, attr, start, end), start, "attr");
+	}
+
+	private String toSeries(List<MachineAttrHistory> list, Date windowStart, String type) {
+		String[] slots = emptySlots();
+		if (list == null || list.isEmpty()) {
+			return join(slots);
+		}
+		for (MachineAttrHistory row : list) {
+			if (row == null || row.getEventTime() == null) {
+				continue;
+			}
+			int idx = secIndex(row.getEventTime(), windowStart);
+			if (idx < 0 || idx >= POINT_COUNT) {
+				continue;
+			}
+			String val;
+			if ("cru".equals(type)) {
+				val = row.getCruTemp();
+			} else if ("alu".equals(type)) {
+				val = row.getAluTemp();
+			} else if ("ele".equals(type)) {
+				val = row.getEleConsume();
+			} else if ("theory".equals(type)) {
+				val = row.getTheoryPressure();
+			} else if ("real".equals(type)) {
+				val = row.getRealPressure();
+			} else if ("level".equals(type)) {
+				val = row.getPressureLevel();
+			} else {
+				val = row.getVal();
+			}
+			slots[idx] = val == null ? "" : String.valueOf(val);
+		}
+		return join(slots);
+	}
+
+	private String toMouldSeries(List<MachineAttrHistory> list, Date windowStart) {
+		String[] slots = emptySlots();
+		if (list == null || list.isEmpty()) {
+			return join(slots);
+		}
+		Map<Integer, LinkedHashMap<String, String>> bySec = new LinkedHashMap<>();
+		for (MachineAttrHistory row : list) {
+			if (row == null || row.getEventTime() == null) {
+				continue;
+			}
+			int idx = secIndex(row.getEventTime(), windowStart);
+			if (idx < 0 || idx >= POINT_COUNT) {
+				continue;
+			}
+			LinkedHashMap<String, String> parts = bySec.get(idx);
+			if (parts == null) {
+				parts = new LinkedHashMap<>();
+				bySec.put(idx, parts);
+			}
+			String mouldId = row.getMouldInfo();
+			if (StringUtils.isBlank(mouldId)) {
+				continue;
+			}
+			parts.put(mouldId, row.getVal() == null ? "" : String.valueOf(row.getVal()));
+		}
+		for (Map.Entry<Integer, LinkedHashMap<String, String>> e : bySec.entrySet()) {
+			StringBuilder sb = new StringBuilder();
+			for (Map.Entry<String, String> p : e.getValue().entrySet()) {
+				if (sb.length() > 0) {
+					sb.append(',');
+				}
+				sb.append(p.getKey()).append(':').append(p.getValue());
+			}
+			slots[e.getKey()] = sb.toString();
+		}
+		return join(slots);
+	}
+
+	private static String[] emptySlots() {
+		String[] slots = new String[POINT_COUNT];
+		for (int i = 0; i < POINT_COUNT; i++) {
+			slots[i] = "";
+		}
+		return slots;
+	}
+
+	private static String join(String[] slots) {
+		StringBuilder sb = new StringBuilder();
+		for (int i = 0; i < slots.length; i++) {
+			if (i > 0) {
+				sb.append('#');
+			}
+			sb.append(slots[i] == null ? "" : slots[i]);
+		}
+		return sb.toString();
+	}
+
+	private static int secIndex(Date eventTime, Date windowStart) {
+		long ms = eventTime.getTime() - windowStart.getTime();
+		if (ms < 0) {
+			return -1;
+		}
+		return (int) (ms / 1000L);
+	}
+
+	private static Date floorTo10Minutes(Date date) {
+		Calendar cal = Calendar.getInstance();
+		cal.setTime(date);
+		cal.set(Calendar.SECOND, 0);
+		cal.set(Calendar.MILLISECOND, 0);
+		int minute = cal.get(Calendar.MINUTE);
+		cal.set(Calendar.MINUTE, minute - (minute % 10));
+		return cal.getTime();
+	}
+
+	private static Date addMinutes(Date date, int minutes) {
+		Calendar cal = Calendar.getInstance();
+		cal.setTime(date);
+		cal.add(Calendar.MINUTE, minutes);
+		return cal.getTime();
+	}
+
+	private static String format(Date date) {
+		Calendar cal = Calendar.getInstance();
+		cal.setTime(date);
+		return String.format("%04d-%02d-%02d %02d:%02d:%02d",
+				cal.get(Calendar.YEAR),
+				cal.get(Calendar.MONTH) + 1,
+				cal.get(Calendar.DAY_OF_MONTH),
+				cal.get(Calendar.HOUR_OF_DAY),
+				cal.get(Calendar.MINUTE),
+				cal.get(Calendar.SECOND));
+	}
+}

+ 72 - 0
src/main/java/com/jeesite/modules/tianHong/dao/MachineAttrHistoryDao.java

@@ -0,0 +1,72 @@
+package com.jeesite.modules.tianHong.dao;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.ibatis.annotations.Param;
+
+import com.jeesite.common.dao.CrudDao;
+import com.jeesite.common.mybatis.annotation.MyBatisDao;
+import com.jeesite.modules.tianHong.entity.MachineAttrHistory;
+
+/**
+ * 设备历史属性DAO接口
+ * 所属包路径 com.jeesite.modules.tianHong.dao 自动映射到 tianHong 数据源(bzh 库)
+ * @author ljr
+ * @version 2026-07-07
+ */
+@MyBatisDao
+public interface MachineAttrHistoryDao extends CrudDao<MachineAttrHistory> {
+
+	/**
+	 * 按设备+时间点分页列表(仅主表属性;关联数据由 Service 按秒补齐)
+	 */
+	@Override
+	List<MachineAttrHistory> findList(MachineAttrHistory machineAttrHistory);
+
+	/**
+	 * 该设备时间点总数(distinct event_time)
+	 */
+	Long findHistoryCount(MachineAttrHistory machineAttrHistory);
+
+	List<MachineAttrHistory> findCruBySecList(@Param("eqpId") String eqpId, @Param("secList") List<Date> secList);
+
+	List<MachineAttrHistory> findAluBySecList(@Param("eqpId") String eqpId, @Param("secList") List<Date> secList);
+
+	List<MachineAttrHistory> findMouldBySecList(@Param("eqpId") String eqpId, @Param("secList") List<Date> secList);
+
+	List<MachineAttrHistory> findPressureBySecList(@Param("eqpId") String eqpId, @Param("secList") List<Date> secList);
+
+	List<MachineAttrHistory> findEleBySecList(@Param("eqpId") String eqpId, @Param("secList") List<Date> secList);
+
+	/**
+	 * 查询指定设备的所有属性明细(用于详情展示)
+	 */
+	List<MachineAttrHistory> findAttrList(MachineAttrHistory machineAttrHistory);
+
+	/** 10分钟备份:时间窗内炉膛温度 */
+	List<MachineAttrHistory> findCruInRange(@Param("eqpId") String eqpId,
+			@Param("beginTime") Date beginTime, @Param("endTime") Date endTime);
+
+	/** 10分钟备份:时间窗内铝水温度 */
+	List<MachineAttrHistory> findAluInRange(@Param("eqpId") String eqpId,
+			@Param("beginTime") Date beginTime, @Param("endTime") Date endTime);
+
+	/** 10分钟备份:时间窗内模具温度 */
+	List<MachineAttrHistory> findMouldInRange(@Param("eqpId") String eqpId,
+			@Param("beginTime") Date beginTime, @Param("endTime") Date endTime);
+
+	/** 10分钟备份:时间窗内压力 */
+	List<MachineAttrHistory> findPressureInRange(@Param("eqpId") String eqpId,
+			@Param("beginTime") Date beginTime, @Param("endTime") Date endTime);
+
+	/** 10分钟备份:时间窗内电量 */
+	List<MachineAttrHistory> findEleInRange(@Param("eqpId") String eqpId,
+			@Param("beginTime") Date beginTime, @Param("endTime") Date endTime);
+
+	/** 10分钟备份:时间窗内指定属性 */
+	List<MachineAttrHistory> findAttrInRange(@Param("eqpId") String eqpId,
+			@Param("attr") String attr,
+			@Param("beginTime") Date beginTime, @Param("endTime") Date endTime);
+
+}

+ 308 - 0
src/main/java/com/jeesite/modules/tianHong/entity/MachineAttrHistory.java

@@ -0,0 +1,308 @@
+package com.jeesite.modules.tianHong.entity;
+
+import java.util.Date;
+import java.util.List;
+
+import com.jeesite.common.entity.DataEntity;
+
+/**
+ * 设备历史属性Entity
+ * 对应 bzh 库 machine_attr_history 表,关联 crutemphistory、alutemphistory、mouldtemphistory、pressurehistory、EleConsumeHistory
+ * @author ljr
+ * @version 2026-07-07
+ */
+public class MachineAttrHistory extends DataEntity<MachineAttrHistory> {
+
+	private static final long serialVersionUID = 1L;
+
+	// ===== machine_attr_history 主表字段(按 eqp_id 聚合,行转列取关键属性)=====
+	private String eqpId;				// 设备号
+	private String aluVal;				// 剩余模次(attr='alu_val')
+	private String pressurizeTime;		// 保压时间(attr='pressurizeTime')
+	private String coolingTime;			// 冷却时间(attr='coolingTime')
+	private String prePres;				// 预压压力设定(attr='PrePres')
+	private String holdPressure;		// 保压开始信号(attr='HoldPressure')
+	private String moldFillingPre;		// 充型压力(attr='moldFillingPre')
+	private Date attrEventTime;			// machine_attr_history 最后更新时间
+
+	// ===== 关联表:crutemphistory 炉膛温度 =====
+	private String cruTemp;				// 炉膛温度
+	private Date cruTempTime;			// 炉膛温度时间
+
+	// ===== 关联表:alutemphistory 铝水温度 =====
+	private String aluTemp;				// 铝水温度
+	private Date aluTempTime;			// 铝水温度时间
+
+	// ===== 关联表:mouldtemphistory 模具温度(一个设备可能多个测温点,拼接展示)=====
+	private String mouldInfo;			// 模具温度信息(如 "M1:725.5, M2:730.2")
+	private Date mouldTempTime;			// 模具温度时间
+
+	// ===== 关联表:pressurehistory 压力(理论压力与实际压力)=====
+	private String theoryPressure;		// 理论压力
+	private String realPressure;		// 实际压力
+	private String pressureLevel;		// 压力段位
+	private Date pressureTime;			// 压力时间
+
+	// ===== 关联表:EleConsumeHistory 电量(总计)=====
+	private String eleConsume;			// 电量
+	private Date eleConsumeTime;		// 电量时间
+
+	// ===== 查询参数:时间范围 =====
+	private Date beginTime;
+	private Date endTime;
+
+	// ===== 手工分页(避免拦截器对复杂 SQL 二次 LIMIT)=====
+	private Integer limit;
+	private Integer offset;
+
+	// ===== 详情:该设备的所有属性记录列表 =====
+	private List<MachineAttrHistory> attrList;
+
+	// ===== 行转列详情字段(findAttrList 查询时使用)=====
+	private String attr;
+	private String val;
+	private Date eventTime;
+	/** 列表行快照时间键(DATE_FORMAT event_time '%Y%m%d%H%i%s%f'),用于精确查明细 */
+	private String eventTimeKey;
+
+	public MachineAttrHistory() {
+		this(null);
+	}
+
+	public MachineAttrHistory(String id) {
+		super(id);
+	}
+
+	public String getEqpId() {
+		return eqpId;
+	}
+
+	public void setEqpId(String eqpId) {
+		this.eqpId = eqpId;
+	}
+
+	public String getAluVal() {
+		return aluVal;
+	}
+
+	public void setAluVal(String aluVal) {
+		this.aluVal = aluVal;
+	}
+
+	public String getPressurizeTime() {
+		return pressurizeTime;
+	}
+
+	public void setPressurizeTime(String pressurizeTime) {
+		this.pressurizeTime = pressurizeTime;
+	}
+
+	public String getCoolingTime() {
+		return coolingTime;
+	}
+
+	public void setCoolingTime(String coolingTime) {
+		this.coolingTime = coolingTime;
+	}
+
+	public String getPrePres() {
+		return prePres;
+	}
+
+	public void setPrePres(String prePres) {
+		this.prePres = prePres;
+	}
+
+	public String getHoldPressure() {
+		return holdPressure;
+	}
+
+	public void setHoldPressure(String holdPressure) {
+		this.holdPressure = holdPressure;
+	}
+
+	public String getMoldFillingPre() {
+		return moldFillingPre;
+	}
+
+	public void setMoldFillingPre(String moldFillingPre) {
+		this.moldFillingPre = moldFillingPre;
+	}
+
+	public Date getAttrEventTime() {
+		return attrEventTime;
+	}
+
+	public void setAttrEventTime(Date attrEventTime) {
+		this.attrEventTime = attrEventTime;
+	}
+
+	public String getCruTemp() {
+		return cruTemp;
+	}
+
+	public void setCruTemp(String cruTemp) {
+		this.cruTemp = cruTemp;
+	}
+
+	public Date getCruTempTime() {
+		return cruTempTime;
+	}
+
+	public void setCruTempTime(Date cruTempTime) {
+		this.cruTempTime = cruTempTime;
+	}
+
+	public String getAluTemp() {
+		return aluTemp;
+	}
+
+	public void setAluTemp(String aluTemp) {
+		this.aluTemp = aluTemp;
+	}
+
+	public Date getAluTempTime() {
+		return aluTempTime;
+	}
+
+	public void setAluTempTime(Date aluTempTime) {
+		this.aluTempTime = aluTempTime;
+	}
+
+	public String getMouldInfo() {
+		return mouldInfo;
+	}
+
+	public void setMouldInfo(String mouldInfo) {
+		this.mouldInfo = mouldInfo;
+	}
+
+	public Date getMouldTempTime() {
+		return mouldTempTime;
+	}
+
+	public void setMouldTempTime(Date mouldTempTime) {
+		this.mouldTempTime = mouldTempTime;
+	}
+
+	public String getTheoryPressure() {
+		return theoryPressure;
+	}
+
+	public void setTheoryPressure(String theoryPressure) {
+		this.theoryPressure = theoryPressure;
+	}
+
+	public String getRealPressure() {
+		return realPressure;
+	}
+
+	public void setRealPressure(String realPressure) {
+		this.realPressure = realPressure;
+	}
+
+	public String getPressureLevel() {
+		return pressureLevel;
+	}
+
+	public void setPressureLevel(String pressureLevel) {
+		this.pressureLevel = pressureLevel;
+	}
+
+	public Date getPressureTime() {
+		return pressureTime;
+	}
+
+	public void setPressureTime(Date pressureTime) {
+		this.pressureTime = pressureTime;
+	}
+
+	public String getEleConsume() {
+		return eleConsume;
+	}
+
+	public void setEleConsume(String eleConsume) {
+		this.eleConsume = eleConsume;
+	}
+
+	public Date getEleConsumeTime() {
+		return eleConsumeTime;
+	}
+
+	public void setEleConsumeTime(Date eleConsumeTime) {
+		this.eleConsumeTime = eleConsumeTime;
+	}
+
+	public Date getBeginTime() {
+		return beginTime;
+	}
+
+	public void setBeginTime(Date beginTime) {
+		this.beginTime = beginTime;
+	}
+
+	public Date getEndTime() {
+		return endTime;
+	}
+
+	public void setEndTime(Date endTime) {
+		this.endTime = endTime;
+	}
+
+	public Integer getLimit() {
+		return limit;
+	}
+
+	public void setLimit(Integer limit) {
+		this.limit = limit;
+	}
+
+	public Integer getOffset() {
+		return offset;
+	}
+
+	public void setOffset(Integer offset) {
+		this.offset = offset;
+	}
+
+	public List<MachineAttrHistory> getAttrList() {
+		return attrList;
+	}
+
+	public void setAttrList(List<MachineAttrHistory> attrList) {
+		this.attrList = attrList;
+	}
+
+	public String getAttr() {
+		return attr;
+	}
+
+	public void setAttr(String attr) {
+		this.attr = attr;
+	}
+
+	public String getVal() {
+		return val;
+	}
+
+	public void setVal(String val) {
+		this.val = val;
+	}
+
+	public Date getEventTime() {
+		return eventTime;
+	}
+
+	public void setEventTime(Date eventTime) {
+		this.eventTime = eventTime;
+	}
+
+	public String getEventTimeKey() {
+		return eventTimeKey;
+	}
+
+	public void setEventTimeKey(String eventTimeKey) {
+		this.eventTimeKey = eventTimeKey;
+	}
+
+}

+ 267 - 0
src/main/java/com/jeesite/modules/tianHong/service/MachineAttrHistoryService.java

@@ -0,0 +1,267 @@
+package com.jeesite.modules.tianHong.service;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.tianHong.dao.MachineAttrHistoryDao;
+import com.jeesite.modules.tianHong.entity.MachineAttrHistory;
+
+/**
+ * 设备历史属性Service
+ * @author ljr
+ * @version 2026-07-07
+ */
+@Service
+@Transactional(readOnly = true)
+public class MachineAttrHistoryService extends CrudService<MachineAttrHistoryDao, MachineAttrHistory> {
+
+	/**
+	 * 分页:必须先选设备;按 event_time 倒序,每页默认 20;关联表按「秒」补齐
+	 */
+	@Override
+	public Page<MachineAttrHistory> findPage(MachineAttrHistory machineAttrHistory) {
+		Page<MachineAttrHistory> page = machineAttrHistory.getPage();
+		if (page == null) {
+			page = new Page<>();
+			machineAttrHistory.setPage(page);
+		}
+		if (StringUtils.isBlank(machineAttrHistory.getEqpId())) {
+			page.setList(Collections.emptyList());
+			page.setCount(0);
+			return page;
+		}
+		int pageSize = page.getPageSize() > 0 ? page.getPageSize() : 20;
+		int pageNo = page.getPageNo() > 0 ? page.getPageNo() : 1;
+		if (pageSize > 100) {
+			pageSize = 100;
+		}
+		page.setPageSize(pageSize);
+		page.setPageNo(pageNo);
+
+		machineAttrHistory.setPage(null);
+		Long count = dao.findHistoryCount(machineAttrHistory);
+		page.setCount(count == null ? 0L : count);
+
+		machineAttrHistory.setLimit(pageSize);
+		machineAttrHistory.setOffset((pageNo - 1) * pageSize);
+		List<MachineAttrHistory> list = dao.findList(machineAttrHistory);
+		fillRelatedBySecond(machineAttrHistory.getEqpId(), list);
+		page.setList(list);
+		return page;
+	}
+
+	/**
+	 * 查询列表(必须带设备号)
+	 */
+	@Override
+	public List<MachineAttrHistory> findList(MachineAttrHistory machineAttrHistory) {
+		if (StringUtils.isBlank(machineAttrHistory.getEqpId())) {
+			return Collections.emptyList();
+		}
+		if (machineAttrHistory.getLimit() == null) {
+			machineAttrHistory.setLimit(20);
+		}
+		if (machineAttrHistory.getOffset() == null) {
+			machineAttrHistory.setOffset(0);
+		}
+		List<MachineAttrHistory> list = dao.findList(machineAttrHistory);
+		fillRelatedBySecond(machineAttrHistory.getEqpId(), list);
+		return list;
+	}
+
+	/**
+	 * 用本页时间点截断到秒,按索引范围补齐温压电量(避免大表按秒 JOIN)
+	 */
+	private void fillRelatedBySecond(String eqpId, List<MachineAttrHistory> list) {
+		if (StringUtils.isBlank(eqpId) || list == null || list.isEmpty()) {
+			return;
+		}
+		Set<Date> secSet = new LinkedHashSet<>();
+		for (MachineAttrHistory row : list) {
+			if (row.getAttrEventTime() != null) {
+				secSet.add(truncateToSecond(row.getAttrEventTime()));
+			}
+		}
+		if (secSet.isEmpty()) {
+			return;
+		}
+		List<Date> secList = new ArrayList<>(secSet);
+
+		Map<Long, MachineAttrHistory> cruMap = latestBySec(dao.findCruBySecList(eqpId, secList));
+		Map<Long, MachineAttrHistory> aluMap = latestBySec(dao.findAluBySecList(eqpId, secList));
+		Map<Long, MachineAttrHistory> mouldMap = latestBySec(dao.findMouldBySecList(eqpId, secList));
+		Map<Long, MachineAttrHistory> pressMap = latestBySec(dao.findPressureBySecList(eqpId, secList));
+		Map<Long, MachineAttrHistory> eleMap = latestBySec(dao.findEleBySecList(eqpId, secList));
+
+		for (MachineAttrHistory row : list) {
+			if (row.getAttrEventTime() == null) {
+				continue;
+			}
+			Long key = secKey(row.getAttrEventTime());
+			MachineAttrHistory cru = cruMap.get(key);
+			if (cru != null) {
+				row.setCruTemp(cru.getCruTemp());
+				row.setCruTempTime(cru.getCruTempTime());
+			}
+			MachineAttrHistory alu = aluMap.get(key);
+			if (alu != null) {
+				row.setAluTemp(alu.getAluTemp());
+				row.setAluTempTime(alu.getAluTempTime());
+			}
+			MachineAttrHistory mould = mouldMap.get(key);
+			if (mould != null) {
+				row.setMouldInfo(mould.getMouldInfo());
+				row.setMouldTempTime(mould.getMouldTempTime());
+			}
+			MachineAttrHistory press = pressMap.get(key);
+			if (press != null) {
+				row.setTheoryPressure(press.getTheoryPressure());
+				row.setRealPressure(press.getRealPressure());
+				row.setPressureLevel(press.getPressureLevel());
+				row.setPressureTime(press.getPressureTime());
+			}
+			MachineAttrHistory ele = eleMap.get(key);
+			if (ele != null) {
+				row.setEleConsume(ele.getEleConsume());
+				row.setEleConsumeTime(ele.getEleConsumeTime());
+			}
+		}
+	}
+
+	/** 同一秒多条时保留 EventTime 最新的一条 */
+	private Map<Long, MachineAttrHistory> latestBySec(List<MachineAttrHistory> rows) {
+		Map<Long, MachineAttrHistory> map = new HashMap<>();
+		if (rows == null) {
+			return map;
+		}
+		for (MachineAttrHistory row : rows) {
+			Date t = row.getEventTime();
+			if (t == null) {
+				t = row.getCruTempTime();
+			}
+			if (t == null) {
+				t = row.getAluTempTime();
+			}
+			if (t == null) {
+				t = row.getMouldTempTime();
+			}
+			if (t == null) {
+				t = row.getPressureTime();
+			}
+			if (t == null) {
+				t = row.getEleConsumeTime();
+			}
+			if (t == null) {
+				continue;
+			}
+			Long key = secKey(t);
+			MachineAttrHistory old = map.get(key);
+			if (old == null || compareTime(t, resolveTime(old)) > 0) {
+				if (row.getEventTime() == null) {
+					row.setEventTime(t);
+				}
+				map.put(key, row);
+			}
+		}
+		return map;
+	}
+
+	private Date resolveTime(MachineAttrHistory row) {
+		if (row.getEventTime() != null) {
+			return row.getEventTime();
+		}
+		if (row.getCruTempTime() != null) {
+			return row.getCruTempTime();
+		}
+		if (row.getAluTempTime() != null) {
+			return row.getAluTempTime();
+		}
+		if (row.getMouldTempTime() != null) {
+			return row.getMouldTempTime();
+		}
+		if (row.getPressureTime() != null) {
+			return row.getPressureTime();
+		}
+		return row.getEleConsumeTime();
+	}
+
+	private int compareTime(Date a, Date b) {
+		if (a == null && b == null) {
+			return 0;
+		}
+		if (a == null) {
+			return -1;
+		}
+		if (b == null) {
+			return 1;
+		}
+		return a.compareTo(b);
+	}
+
+	private Date truncateToSecond(Date date) {
+		Calendar cal = Calendar.getInstance();
+		cal.setTime(date);
+		cal.set(Calendar.MILLISECOND, 0);
+		return cal.getTime();
+	}
+
+	/** 用 epoch 秒做 map key,避免 Date 毫秒差异 */
+	private Long secKey(Date date) {
+		return date.getTime() / 1000L;
+	}
+
+	/**
+	 * 查询指定设备的所有属性明细(用于详情展示)
+	 */
+	public List<MachineAttrHistory> findAttrList(MachineAttrHistory machineAttrHistory) {
+		if (StringUtils.isBlank(machineAttrHistory.getEqpId())) {
+			return Collections.emptyList();
+		}
+		return dao.findAttrList(machineAttrHistory);
+	}
+
+	/**
+	 * 获取单条数据(按 eqp_id + 快照时间查询属性明细)
+	 * 须带 eventTimeKey / eventTime / beginTime~endTime,禁止无时间条件全量加载
+	 */
+	@Override
+	public MachineAttrHistory get(MachineAttrHistory machineAttrHistory) {
+		if (StringUtils.isBlank(machineAttrHistory.getEqpId())) {
+			return null;
+		}
+		MachineAttrHistory entity = new MachineAttrHistory();
+		entity.setEqpId(machineAttrHistory.getEqpId());
+		entity.setAttrEventTime(machineAttrHistory.getEventTime());
+		entity.setEventTimeKey(machineAttrHistory.getEventTimeKey());
+		boolean hasTime = StringUtils.isNotBlank(machineAttrHistory.getEventTimeKey())
+				|| machineAttrHistory.getEventTime() != null
+				|| machineAttrHistory.getBeginTime() != null
+				|| machineAttrHistory.getEndTime() != null;
+		if (!hasTime) {
+			entity.setAttrList(Collections.emptyList());
+			return entity;
+		}
+		MachineAttrHistory queryParam = new MachineAttrHistory();
+		queryParam.setEqpId(machineAttrHistory.getEqpId());
+		queryParam.setEventTimeKey(machineAttrHistory.getEventTimeKey());
+		queryParam.setEventTime(machineAttrHistory.getEventTime());
+		queryParam.setBeginTime(machineAttrHistory.getBeginTime());
+		queryParam.setEndTime(machineAttrHistory.getEndTime());
+		entity.setAttrList(dao.findAttrList(queryParam));
+		return entity;
+	}
+
+}

+ 132 - 0
src/main/java/com/jeesite/modules/tianHong/web/MachineAttrHistoryController.java

@@ -0,0 +1,132 @@
+package com.jeesite.modules.tianHong.web;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.tianHong.entity.MachineAttrHistory;
+import com.jeesite.modules.tianHong.service.MachineAttrHistoryService;
+
+/**
+ * 设备历史属性Controller
+ * 对应 bzh 库 machine_attr_history 表,关联温度/压力/电量等历史表
+ * @author ljr
+ * @version 2026-07-07
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/tianHong/machineAttrHistory")
+public class MachineAttrHistoryController extends BaseController {
+
+	@Autowired
+	private MachineAttrHistoryService machineAttrHistoryService;
+
+	/**
+	 * 获取数据
+	 * @param id 设备号(eqp_id),仅 form 表单请求时传入
+	 * @param eventTimeKey 列表行 id 中的时间键(精确快照)
+	 * @param eventTime 兼容旧参数(按秒范围,可能含同秒多快照)
+	 */
+	@ModelAttribute
+	public MachineAttrHistory get(String id, String eventTimeKey, String eventTime, boolean isNewRecord) {
+		// listData 等列表请求不传 id,直接返回空对象,避免在 @ModelAttribute 阶段执行不必要的查询
+		if (isNewRecord || id == null || id.isEmpty()) {
+			return new MachineAttrHistory();
+		}
+		MachineAttrHistory query = new MachineAttrHistory();
+		query.setEqpId(id);
+		query.setEventTimeKey(StringUtils.trim(eventTimeKey));
+		query.setEventTime(parseEventTime(eventTime));
+		MachineAttrHistory entity = machineAttrHistoryService.get(query);
+		if (entity == null) {
+			entity = new MachineAttrHistory();
+			entity.setEqpId(id);
+		}
+		return entity;
+	}
+
+	private Date parseEventTime(String eventTime) {
+		if (StringUtils.isBlank(eventTime)) {
+			return null;
+		}
+		String[] patterns = {
+				"yyyy-MM-dd HH:mm:ss.SSS",
+				"yyyy-MM-dd HH:mm:ss",
+				"yyyy-MM-dd'T'HH:mm:ss.SSS",
+				"yyyy-MM-dd'T'HH:mm:ss"
+		};
+		for (String pattern : patterns) {
+			try {
+				return new SimpleDateFormat(pattern).parse(eventTime.trim());
+			} catch (ParseException ignored) {
+				// try next
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * 查询列表页面
+	 */
+	@RequiresPermissions("tianHong:machineAttrHistory:view")
+	@RequestMapping(value = { "list", "" })
+	public String list(MachineAttrHistory machineAttrHistory, Model model) {
+		model.addAttribute("machineAttrHistory", machineAttrHistory);
+		return "modules/tianHong/machineAttrHistoryList";
+	}
+
+	/**
+	 * 查询列表数据:须传设备号;按 event_time 倒序分页(默认每页 20)
+	 */
+	@RequiresPermissions("tianHong:machineAttrHistory:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MachineAttrHistory> listData(MachineAttrHistory machineAttrHistory, HttpServletRequest request,
+			HttpServletResponse response) {
+		machineAttrHistory.setPage(new Page<>(request, response));
+		return machineAttrHistoryService.findPage(machineAttrHistory);
+	}
+
+	/**
+	 * 查询指定设备的所有属性明细(用于详情模态框)
+	 */
+	@RequiresPermissions("tianHong:machineAttrHistory:view")
+	@RequestMapping(value = "attrListData")
+	@ResponseBody
+	public List<MachineAttrHistory> attrListData(String eqpId, Date beginTime, Date endTime) {
+		if (StringUtils.isBlank(eqpId)) {
+			return Collections.emptyList();
+		}
+		MachineAttrHistory query = new MachineAttrHistory();
+		query.setEqpId(eqpId);
+		query.setBeginTime(beginTime);
+		query.setEndTime(endTime);
+		return machineAttrHistoryService.findAttrList(query);
+	}
+
+	/**
+	 * 查看详情页面
+	 */
+	@RequiresPermissions("tianHong:machineAttrHistory:view")
+	@RequestMapping(value = "form")
+	public String form(MachineAttrHistory machineAttrHistory, Model model) {
+		model.addAttribute("machineAttrHistory", machineAttrHistory);
+		return "modules/tianHong/machineAttrHistoryForm";
+	}
+
+}

+ 29 - 0
src/main/resources/db/create/mysql/tianhong_backup.sql

@@ -0,0 +1,29 @@
+-- mes_cloud_ljr.tianhong_backup
+-- Window: [window_start, window_end) e.g. 14:00:00 ~ 14:10:00
+-- Curves: value#value, 600 slots (1s)
+
+CREATE TABLE IF NOT EXISTS `tianhong_backup` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT,
+  `eqp_id` VARCHAR(40) NOT NULL,
+  `window_start` DATETIME NOT NULL,
+  `window_end` DATETIME NOT NULL,
+  `cru_temp` MEDIUMTEXT,
+  `alu_temp` MEDIUMTEXT,
+  `mould_info` MEDIUMTEXT,
+  `theory_pressure` MEDIUMTEXT,
+  `real_pressure` MEDIUMTEXT,
+  `pressure_level` MEDIUMTEXT,
+  `ele_consume` MEDIUMTEXT,
+  `alu_val` MEDIUMTEXT,
+  `pressurize_time` MEDIUMTEXT,
+  `cooling_time` MEDIUMTEXT,
+  `pre_pres` MEDIUMTEXT,
+  `hold_pressure` MEDIUMTEXT,
+  `mold_filling_pre` MEDIUMTEXT,
+  `point_count` INT DEFAULT NULL,
+  `create_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_eqp_window` (`eqp_id`, `window_start`),
+  KEY `idx_window` (`window_start`, `window_end`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

+ 183 - 0
src/main/resources/mappings/modules/tianHong/MachineAttrHistoryDao.xml

@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesite.modules.tianHong.dao.MachineAttrHistoryDao">
+
+	<!-- machine_attr_history 时间范围 -->
+	<sql id="attrEventTimeRange">
+		<if test="beginTime != null">
+			AND event_time &gt;= #{beginTime}
+		</if>
+		<if test="endTime != null">
+			AND event_time &lt;= #{endTime}
+		</if>
+	</sql>
+
+	<!-- 按秒列表:每个秒做索引范围,避免大表 DATE_FORMAT / 开放式 JOIN -->
+	<sql id="eventTimeInSecList">
+		<foreach collection="secList" item="sec" separator=" OR ">
+			(EventTime &gt;= #{sec} AND EventTime &lt; DATE_ADD(#{sec}, INTERVAL 1 SECOND))
+		</foreach>
+	</sql>
+
+	<!--
+		主列表:只查属性主表(快)。温压电量在 Service 里按「秒」二次补齐。
+	-->
+	<select id="findList" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT
+			m.eqp_id AS "eqpId",
+			CONCAT(m.eqp_id, '_', DATE_FORMAT(page.event_time, '%Y%m%d%H%i%s%f')) AS "id",
+			MAX(CASE WHEN m.attr = 'alu_val' THEN m.val END) AS "aluVal",
+			MAX(CASE WHEN m.attr = 'pressurizeTime' THEN m.val END) AS "pressurizeTime",
+			MAX(CASE WHEN m.attr = 'coolingTime' THEN m.val END) AS "coolingTime",
+			MAX(CASE WHEN m.attr = 'PrePres' THEN m.val END) AS "prePres",
+			MAX(CASE WHEN m.attr = 'HoldPressure' THEN m.val END) AS "holdPressure",
+			MAX(CASE WHEN m.attr = 'moldFillingPre' THEN m.val END) AS "moldFillingPre",
+			page.event_time AS "attrEventTime"
+		FROM (
+			SELECT event_time
+			FROM (
+				SELECT event_time
+				FROM machine_attr_history
+				WHERE eqp_id = #{eqpId}
+				<include refid="attrEventTimeRange"/>
+				GROUP BY event_time
+				ORDER BY event_time DESC
+				LIMIT #{limit} OFFSET #{offset}
+			) x
+		) page
+		LEFT JOIN machine_attr_history m
+			ON m.eqp_id = #{eqpId} AND m.event_time = page.event_time
+		GROUP BY page.event_time, m.eqp_id
+		ORDER BY page.event_time DESC
+	</select>
+
+	<select id="findHistoryCount" resultType="java.lang.Long">
+		SELECT COUNT(*)
+		FROM (
+			SELECT event_time
+			FROM machine_attr_history
+			WHERE eqp_id = #{eqpId}
+			<include refid="attrEventTimeRange"/>
+			GROUP BY event_time
+		) t
+	</select>
+
+	<select id="findCruBySecList" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT Value AS "cruTemp", EventTime AS "cruTempTime", EventTime AS "eventTime"
+		FROM crutemphistory
+		WHERE EQP_ID = #{eqpId}
+		AND (<include refid="eventTimeInSecList"/>)
+	</select>
+
+	<select id="findAluBySecList" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT Value AS "aluTemp", EventTime AS "aluTempTime", EventTime AS "eventTime"
+		FROM alutemphistory
+		WHERE EQP_ID = #{eqpId}
+		AND (<include refid="eventTimeInSecList"/>)
+	</select>
+
+	<select id="findMouldBySecList" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT
+			MAX(EventTime) AS "eventTime",
+			GROUP_CONCAT(CONCAT(MouldID, ':', Value) ORDER BY MouldID SEPARATOR ', ') AS "mouldInfo",
+			MAX(EventTime) AS "mouldTempTime"
+		FROM mouldtemphistory
+		WHERE EQP_ID = #{eqpId}
+		AND (<include refid="eventTimeInSecList"/>)
+		GROUP BY DATE_FORMAT(EventTime, '%Y-%m-%d %H:%i:%s')
+	</select>
+
+	<select id="findPressureBySecList" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT TheoryValue AS "theoryPressure", RealValue AS "realPressure",
+			LevelID AS "pressureLevel", EventTime AS "pressureTime", EventTime AS "eventTime"
+		FROM pressurehistory
+		WHERE EQP_ID = #{eqpId}
+		AND (<include refid="eventTimeInSecList"/>)
+	</select>
+
+	<select id="findEleBySecList" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT Value AS "eleConsume", EventTime AS "eleConsumeTime", EventTime AS "eventTime"
+		FROM EleConsumeHistory
+		WHERE EQP_ID = #{eqpId}
+		AND (<include refid="eventTimeInSecList"/>)
+	</select>
+
+	<select id="findAttrList" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT
+			eqp_id AS "eqpId",
+			attr AS "attr",
+			val AS "val",
+			event_time AS "eventTime"
+		FROM machine_attr_history
+		WHERE eqp_id = #{eqpId}
+		<!-- 优先:列表行 id 中的时间键,精确到微秒,避免同秒多快照重复 -->
+		<if test="eventTimeKey != null and eventTimeKey != ''">
+			AND event_time = STR_TO_DATE(#{eventTimeKey}, '%Y%m%d%H%i%s%f')
+		</if>
+		<if test="(eventTimeKey == null or eventTimeKey == '') and eventTime != null">
+			AND event_time &gt;= FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(#{eventTime})))
+			AND event_time &lt; FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(#{eventTime})) + 1)
+		</if>
+		<if test="(eventTimeKey == null or eventTimeKey == '') and eventTime == null">
+			<include refid="attrEventTimeRange"/>
+		</if>
+		ORDER BY attr
+	</select>
+
+	<select id="findCruInRange" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT Value AS "cruTemp", EventTime AS "eventTime"
+		FROM crutemphistory
+		WHERE EQP_ID = #{eqpId}
+		  AND EventTime &gt;= #{beginTime}
+		  AND EventTime &lt; #{endTime}
+		ORDER BY EventTime
+	</select>
+
+	<select id="findAluInRange" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT Value AS "aluTemp", EventTime AS "eventTime"
+		FROM alutemphistory
+		WHERE EQP_ID = #{eqpId}
+		  AND EventTime &gt;= #{beginTime}
+		  AND EventTime &lt; #{endTime}
+		ORDER BY EventTime
+	</select>
+
+	<select id="findMouldInRange" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT MouldID AS "mouldInfo", Value AS "val", EventTime AS "eventTime"
+		FROM mouldtemphistory
+		WHERE EQP_ID = #{eqpId}
+		  AND EventTime &gt;= #{beginTime}
+		  AND EventTime &lt; #{endTime}
+		ORDER BY EventTime, MouldID
+	</select>
+
+	<select id="findPressureInRange" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT TheoryValue AS "theoryPressure", RealValue AS "realPressure",
+			LevelID AS "pressureLevel", EventTime AS "eventTime"
+		FROM pressurehistory
+		WHERE EQP_ID = #{eqpId}
+		  AND EventTime &gt;= #{beginTime}
+		  AND EventTime &lt; #{endTime}
+		ORDER BY EventTime
+	</select>
+
+	<select id="findEleInRange" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT Value AS "eleConsume", EventTime AS "eventTime"
+		FROM EleConsumeHistory
+		WHERE EQP_ID = #{eqpId}
+		  AND EventTime &gt;= #{beginTime}
+		  AND EventTime &lt; #{endTime}
+		ORDER BY EventTime
+	</select>
+
+	<select id="findAttrInRange" resultType="com.jeesite.modules.tianHong.entity.MachineAttrHistory">
+		SELECT attr AS "attr", val AS "val", event_time AS "eventTime"
+		FROM machine_attr_history
+		WHERE eqp_id = #{eqpId}
+		  AND attr = #{attr}
+		  AND event_time &gt;= #{beginTime}
+		  AND event_time &lt; #{endTime}
+		ORDER BY event_time
+	</select>
+
+</mapper>

+ 9 - 0
src/main/resources/views/modules/mes/mesMaterialLlList.html

@@ -7,6 +7,7 @@
 			</div>
 			<div class="box-tools pull-right">
 				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
+				<a href="#" class="btn btn-default" id="btnExport"><i class="glyphicon glyphicon-export"></i> 导出</a>
 				<% if(hasPermi('mes:mesMaterialLl:edit')){ %>
 					<a href="${ctx}/mes/mesMaterialLl/form" class="btn btn-default btnTool" title="${text('新增生产领料单')}"><i class="fa fa-plus"></i> ${text('新增')}</a>
 				<% } %>
@@ -146,4 +147,12 @@ $('#dataGrid').dataGrid({
 		
 	}
 });
+</script>
+<script>
+$('#btnExport').click(function(){
+	js.ajaxSubmitForm($('#searchForm'), {
+		url:'${ctx}/mes/mesMaterialLl/exportData',
+		downloadFile:true
+	});
+});
 </script>

+ 66 - 0
src/main/resources/views/modules/tianHong/machineAttrHistoryForm.html

@@ -0,0 +1,66 @@
+<% layout('/layouts/default.html', {title: '设备历史属性详情', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header with-border">
+			<div class="box-title">
+				<i class="fa icon-notebook"></i> ${text('设备历史属性详情')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="#" class="btn btn-default" id="btnCancel" onclick="js.closeCurrentTabPage()"><i class="fa fa-reply"></i> ${text('返回')}</a>
+			</div>
+		</div>
+		<div class="box-body">
+			<form id="inputForm" class="form-horizontal">
+
+				<h4 class="form-header">${text('属性明细列表')}</h4>
+				<table id="attrDataGrid"></table>
+			</form>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+// 属性字段 -> 中文属性名 映射(含通配规则)
+var attrNameMap = {
+	'alu_val': '剩余模次',
+	'pressurizeTime': '保压时间',
+	'coolingTime': '冷却时间',
+	'PrePres': '预压压力设定',
+	'HoldPressure': '保压开始信号',
+	'moldFillingPre': '各段压力(冲型压力)'
+};
+var attrNameRules = [
+	{re:/^COOL_FLOW_(\d+)$/, name:function(m){ return '冷却流量['+m[1]+']'; }},
+	{re:/^f_stTime_(\d+)$/,  name:function(m){ return '风冷等待时间['+m[1]+']'; }},
+	{re:/^f_coolingTime_(\d+)$/, name:function(m){ return '风冷开启时间['+m[1]+']'; }},
+	{re:/^s_stTime_(\d+)$/,  name:function(m){ return '水冷等待时间['+m[1]+']'; }},
+	{re:/^s_coolingTime_(\d+)$/, name:function(m){ return '水冷开启时间['+m[1]+']'; }},
+	{re:/^F_SWITCH_(\d+)$/,  name:function(m){ return '风冷控制开关['+m[1]+']'; }},
+	{re:/^W_SWITCH_(\d+)$/,  name:function(m){ return '水冷控制开关['+m[1]+']'; }}
+];
+function getAttrCnName(attr){
+	if(attr == null) return '';
+	if(attrNameMap[attr] != null) return attrNameMap[attr];
+	for(var i=0; i<attrNameRules.length; i++){
+		var m = attr.match(attrNameRules[i].re);
+		if(m){ return attrNameRules[i].name(m); }
+	}
+	return attr;
+}
+
+$('#attrDataGrid').dataGrid({
+	data: ${toJson(machineAttrHistory.attrList)!'[]'},
+	datatype: "local", // 设置本地数据,避免 dataGrid 走 ajax 加载校验 searchForm/url
+	columnModel: [
+		{header:'${text("属性名")}', name:'attrCnName', width:220, align:"center", formatter:function(val, obj, row, act){
+			return getAttrCnName(js.val(row,'attr'));
+		}},
+		{header:'${text("属性字段")}', name:'attr', width:220, align:"center"},
+		{header:'${text("属性值")}', name:'val', width:200, align:"center"},
+		{header:'${text("更新时间")}', name:'eventTime', width:180, align:"center"}
+	],
+	showRownum: true,
+	// 固定显示10条数据的高度,扩大属性明细列表的显示面积
+	autoGridHeight: function(){ return 540; }
+});
+</script>

+ 93 - 0
src/main/resources/views/modules/tianHong/machineAttrHistoryList.html

@@ -0,0 +1,93 @@
+<% layout('/layouts/default.html', {title: '设备历史属性', libs: ['dataGrid','laydate']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa icon-notebook"></i> ${text('设备历史属性')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
+				<a href="#" class="btn btn-default" id="btnRefresh" title="${text('刷新')}"><i class="fa fa-refresh"></i> ${text('刷新')}</a>
+				<a href="#" class="btn btn-default" id="btnSetting" title="${text('设置')}"><i class="fa fa-navicon"></i></a>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${machineAttrHistory}" action="${ctx}/tianHong/machineAttrHistory/listData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="20" data-order-by="${parameter.orderBy}">
+				<div class="form-group">
+				<label class="control-label">${text('设备号')}:</label>
+				<div class="control-inline">
+					<#form:input path="eqpId" maxlength="40" class="form-control width-120" required="true"/>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="control-label">${text('开始时间')}:</label>
+				<div class="control-inline">
+					<#form:input path="beginTime" maxlength="20" class="form-control laydate width-160"
+						dateFmt="yyyy-MM-dd HH:mm:ss" />
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="control-label">${text('结束时间')}:</label>
+				<div class="control-inline">
+					<#form:input path="endTime" maxlength="20" class="form-control laydate width-160"
+						dateFmt="yyyy-MM-dd HH:mm:ss" />
+				</div>
+			</div>
+				<div class="form-group">
+					<button type="submit" class="btn btn-primary btn-sm">${text('查询')}</button>
+					<button type="reset" class="btn btn-default btn-sm">${text('重置')}</button>
+				</div>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+// 初始化DataGrid对象(须先填设备号;每页 20,时间倒序由后端保证)
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	pageSize: 20,
+	columnModel: [
+		{header:'${text("设备号")}', name:'eqpId', index:'t.eqp_id', width:120, align:"center", frozen:true},
+		{header:'${text("炉膛温度(℃)")}', name:'cruTemp', index:'c.Value', width:110, align:"center"},
+		{header:'${text("铝水温度(℃)")}', name:'aluTemp', index:'a.Value', width:110, align:"center"},
+		{header:'${text("模具温度(℃)")}', name:'mouldInfo', index:'mt.mould_info', width:200, align:"center"},
+		{header:'${text("理论压力")}', name:'theoryPressure', index:'p.TheoryValue', width:100, align:"center"},
+		{header:'${text("实际压力")}', name:'realPressure', index:'p.RealValue', width:100, align:"center"},
+		{header:'${text("压力段位")}', name:'pressureLevel', index:'p.LevelID', width:90, align:"center"},
+		{header:'${text("电量总计")}', name:'eleConsume', index:'e.Value', width:110, align:"center"},
+		{header:'${text("剩余模次")}', name:'aluVal', width:90, align:"center"},
+		{header:'${text("保压时间")}', name:'pressurizeTime', width:90, align:"center"},
+		{header:'${text("冷却时间")}', name:'coolingTime', width:90, align:"center"},
+		{header:'${text("预压压力设定")}', name:'prePres', width:110, align:"center"},
+		{header:'${text("保压开始信号")}', name:'holdPressure', width:110, align:"center"},
+		{header:'${text("充型压力")}', name:'moldFillingPre', width:100, align:"center"},
+		{header:'${text("属性更新时间")}', name:'attrEventTime', width:150, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:120, align:"center", formatter: function(val, obj, row, act){
+			var actions = [];
+			var eqpId = js.val(row,'eqpId');
+			var rowId = js.val(row,'id') || '';
+			var href = '${ctx}/tianHong/machineAttrHistory/form?id=' + encodeURIComponent(eqpId || '');
+			// id 形如 D001_20260803172341921000,取末尾 20 位时间键精确查明细
+			if (rowId && /_\d{20}$/.test(rowId)) {
+				href += '&eventTimeKey=' + encodeURIComponent(rowId.slice(-20));
+			} else {
+				var et = js.val(row,'attrEventTime');
+				if (et) {
+					href += '&eventTime=' + encodeURIComponent(et);
+				}
+			}
+			actions.push('<a href="'+href+'" class="addTabPage" data-title="${text("设备属性明细")}"><i class="fa fa-list"></i> ${text("属性明细")}</a>&nbsp;');
+			return actions.join('');
+		}}
+	]
+});
+
+// 刷新按钮
+$('#btnRefresh').click(function(){
+	$('#dataGrid').dataGrid('refresh');
+});
+</script>