zhanghe před 4 měsíci
rodič
revize
7e31e216d1

+ 12 - 0
pom.xml

@@ -167,6 +167,18 @@
 			<artifactId>bcprov-jdk15on</artifactId>
 			<version>1.70</version>
 		</dependency>
+		<!-- bcpkix/bcutil 与 bcprov 1.70 对齐,否则 Milo 生成证书时报
+		     NoSuchMethodError: SubjectPublicKeyInfo.getPublicKeyData() -->
+		<dependency>
+			<groupId>org.bouncycastle</groupId>
+			<artifactId>bcpkix-jdk15on</artifactId>
+			<version>1.70</version>
+		</dependency>
+		<dependency>
+			<groupId>org.bouncycastle</groupId>
+			<artifactId>bcutil-jdk15on</artifactId>
+			<version>1.70</version>
+		</dependency>
 
 		<dependency>
 			<groupId>commons-net</groupId>

+ 15 - 0
src/main/java/com/jeesite/modules/mes/dao/MesFswDao.java

@@ -0,0 +1,15 @@
+package com.jeesite.modules.mes.dao;
+
+import com.jeesite.common.dao.CrudDao;
+import com.jeesite.common.mybatis.annotation.MyBatisDao;
+import com.jeesite.modules.mes.entity.MesFsw;
+
+/**
+ * FSW设备数据采集DAO接口
+ * @author system
+ * @version 2026-06-08
+ */
+@MyBatisDao
+public interface MesFswDao extends CrudDao<MesFsw> {
+
+}

+ 169 - 0
src/main/java/com/jeesite/modules/mes/entity/MesFsw.java

@@ -0,0 +1,169 @@
+package com.jeesite.modules.mes.entity;
+
+import javax.validation.constraints.Size;
+
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.Table;
+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;
+
+/**
+ * FSW设备数据采集Entity(通过 OPC UA 采集)
+ * @author system
+ * @version 2026-06-08
+ */
+@Table(name="mes_fsw", alias="a", label="FSW设备数据采集信息", columns={
+		@Column(name="id", attrName="id", label="id", isPK=true),
+		@Column(name="sp_speed", attrName="spSpeed", label="主轴转速"),
+		@Column(name="x_mcs", attrName="xMcs", label="X轴机械坐标"),
+		@Column(name="y_mcs", attrName="yMcs", label="Y轴机械坐标"),
+		@Column(name="z_mcs", attrName="zMcs", label="Z轴机械坐标"),
+		@Column(name="actual_pressure", attrName="actualPressure", label="实际压力值"),
+		@Column(name="aa_off_val_z", attrName="aaOffValZ", label="Z轴叠加总量"),
+		@Column(name="x_feed", attrName="xFeed", label="X轴进给率实际值"),
+		@Column(name="y_feed", attrName="yFeed", label="Y轴进给率实际值"),
+		@Column(name="z_feed", attrName="zFeed", label="Z轴进给率实际值"),
+		@Column(name="r210", attrName="r210", label="R210-恒压力开启延时"),
+		@Column(name="r211", attrName="r211", label="R211-设定压力值"),
+		@Column(name="r212", attrName="r212", label="R212-调整下限值"),
+		@Column(name="r213", attrName="r213", label="R213-调整速度"),
+		@Column(name="r214", attrName="r214", label="R214-叠加极限值"),
+		@Column(name="r215", attrName="r215", label="R215-压力波动值"),
+		@Column(name="r216", attrName="r216", label="R216-采集取样次数"),
+		@Column(name="r217", attrName="r217", label="R217-手动调整值"),
+		@Column(name="r218", attrName="r218", label="R218-暂停再启动延时"),
+		@Column(name="r999", attrName="r999", label="R999-投影半径"),
+		@Column(name="create_date", attrName="createDate", label="采集时间", isUpdate=false, isQuery=false, isUpdateForce=true),
+	}, orderBy="a.id DESC"
+)
+public class MesFsw extends DataEntity<MesFsw> {
+
+	private static final long serialVersionUID = 1L;
+	private String spSpeed;			// 主轴转速
+	private String xMcs;			// X轴机械坐标
+	private String yMcs;			// Y轴机械坐标
+	private String zMcs;			// Z轴机械坐标
+	private String actualPressure;	// 实际压力值
+	private String aaOffValZ;		// Z轴叠加总量
+	private String xFeed;			// X轴进给率实际值
+	private String yFeed;			// Y轴进给率实际值
+	private String zFeed;			// Z轴进给率实际值
+	private String r210;			// R210-恒压力开启延时
+	private String r211;			// R211-设定压力值
+	private String r212;			// R212-调整下限值
+	private String r213;			// R213-调整速度
+	private String r214;			// R214-叠加极限值
+	private String r215;			// R215-压力波动值
+	private String r216;			// R216-采集取样次数
+	private String r217;			// R217-手动调整值
+	private String r218;			// R218-暂停再启动延时
+	private String r999;			// R999-投影半径
+
+	@ExcelFields({
+		@ExcelField(title="主轴转速", attrName="spSpeed", align=Align.CENTER, sort=20),
+		@ExcelField(title="X轴机械坐标", attrName="xMcs", align=Align.CENTER, sort=30),
+		@ExcelField(title="Y轴机械坐标", attrName="yMcs", align=Align.CENTER, sort=40),
+		@ExcelField(title="Z轴机械坐标", attrName="zMcs", align=Align.CENTER, sort=50),
+		@ExcelField(title="实际压力值", attrName="actualPressure", align=Align.CENTER, sort=60),
+		@ExcelField(title="Z轴叠加总量", attrName="aaOffValZ", align=Align.CENTER, sort=70),
+		@ExcelField(title="X轴进给率实际值", attrName="xFeed", align=Align.CENTER, sort=80),
+		@ExcelField(title="Y轴进给率实际值", attrName="yFeed", align=Align.CENTER, sort=90),
+		@ExcelField(title="Z轴进给率实际值", attrName="zFeed", align=Align.CENTER, sort=100),
+		@ExcelField(title="R210-恒压力开启延时", attrName="r210", align=Align.CENTER, sort=110),
+		@ExcelField(title="R211-设定压力值", attrName="r211", align=Align.CENTER, sort=120),
+		@ExcelField(title="R212-调整下限值", attrName="r212", align=Align.CENTER, sort=130),
+		@ExcelField(title="R213-调整速度", attrName="r213", align=Align.CENTER, sort=140),
+		@ExcelField(title="R214-叠加极限值", attrName="r214", align=Align.CENTER, sort=150),
+		@ExcelField(title="R215-压力波动值", attrName="r215", align=Align.CENTER, sort=160),
+		@ExcelField(title="R216-采集取样次数", attrName="r216", align=Align.CENTER, sort=170),
+		@ExcelField(title="R217-手动调整值", attrName="r217", align=Align.CENTER, sort=180),
+		@ExcelField(title="R218-暂停再启动延时", attrName="r218", align=Align.CENTER, sort=190),
+		@ExcelField(title="R999-投影半径", attrName="r999", align=Align.CENTER, sort=200),
+		@ExcelField(title="采集时间", attrName="createDate", align=Align.CENTER, sort=210),
+	})
+	public MesFsw() {
+		this(null);
+	}
+
+	public MesFsw(String id){
+		super(id);
+	}
+
+	@Size(max=64, message="主轴转速长度不能超过 64 个字符")
+	public String getSpSpeed() { return spSpeed; }
+	public void setSpSpeed(String spSpeed) { this.spSpeed = spSpeed; }
+
+	@Size(max=64, message="X轴机械坐标长度不能超过 64 个字符")
+	public String getXMcs() { return xMcs; }
+	public void setXMcs(String xMcs) { this.xMcs = xMcs; }
+
+	@Size(max=64, message="Y轴机械坐标长度不能超过 64 个字符")
+	public String getYMcs() { return yMcs; }
+	public void setYMcs(String yMcs) { this.yMcs = yMcs; }
+
+	@Size(max=64, message="Z轴机械坐标长度不能超过 64 个字符")
+	public String getZMcs() { return zMcs; }
+	public void setZMcs(String zMcs) { this.zMcs = zMcs; }
+
+	@Size(max=64, message="实际压力值长度不能超过 64 个字符")
+	public String getActualPressure() { return actualPressure; }
+	public void setActualPressure(String actualPressure) { this.actualPressure = actualPressure; }
+
+	@Size(max=64, message="Z轴叠加总量长度不能超过 64 个字符")
+	public String getAaOffValZ() { return aaOffValZ; }
+	public void setAaOffValZ(String aaOffValZ) { this.aaOffValZ = aaOffValZ; }
+
+	@Size(max=64, message="X轴进给率实际值长度不能超过 64 个字符")
+	public String getXFeed() { return xFeed; }
+	public void setXFeed(String xFeed) { this.xFeed = xFeed; }
+
+	@Size(max=64, message="Y轴进给率实际值长度不能超过 64 个字符")
+	public String getYFeed() { return yFeed; }
+	public void setYFeed(String yFeed) { this.yFeed = yFeed; }
+
+	@Size(max=64, message="Z轴进给率实际值长度不能超过 64 个字符")
+	public String getZFeed() { return zFeed; }
+	public void setZFeed(String zFeed) { this.zFeed = zFeed; }
+
+	@Size(max=64, message="R210长度不能超过 64 个字符")
+	public String getR210() { return r210; }
+	public void setR210(String r210) { this.r210 = r210; }
+
+	@Size(max=64, message="R211长度不能超过 64 个字符")
+	public String getR211() { return r211; }
+	public void setR211(String r211) { this.r211 = r211; }
+
+	@Size(max=64, message="R212长度不能超过 64 个字符")
+	public String getR212() { return r212; }
+	public void setR212(String r212) { this.r212 = r212; }
+
+	@Size(max=64, message="R213长度不能超过 64 个字符")
+	public String getR213() { return r213; }
+	public void setR213(String r213) { this.r213 = r213; }
+
+	@Size(max=64, message="R214长度不能超过 64 个字符")
+	public String getR214() { return r214; }
+	public void setR214(String r214) { this.r214 = r214; }
+
+	@Size(max=64, message="R215长度不能超过 64 个字符")
+	public String getR215() { return r215; }
+	public void setR215(String r215) { this.r215 = r215; }
+
+	@Size(max=64, message="R216长度不能超过 64 个字符")
+	public String getR216() { return r216; }
+	public void setR216(String r216) { this.r216 = r216; }
+
+	@Size(max=64, message="R217长度不能超过 64 个字符")
+	public String getR217() { return r217; }
+	public void setR217(String r217) { this.r217 = r217; }
+
+	@Size(max=64, message="R218长度不能超过 64 个字符")
+	public String getR218() { return r218; }
+	public void setR218(String r218) { this.r218 = r218; }
+
+	@Size(max=64, message="R999长度不能超过 64 个字符")
+	public String getR999() { return r999; }
+	public void setR999(String r999) { this.r999 = r999; }
+}

+ 109 - 0
src/main/java/com/jeesite/modules/mes/service/MesFswService.java

@@ -0,0 +1,109 @@
+package com.jeesite.modules.mes.service;
+
+import java.util.List;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.common.service.ServiceException;
+import com.jeesite.common.utils.excel.ExcelImport;
+import com.jeesite.common.validator.ValidatorUtils;
+import com.jeesite.modules.mes.dao.MesFswDao;
+import com.jeesite.modules.mes.entity.MesFsw;
+
+/**
+ * FSW设备数据采集Service
+ * @author system
+ * @version 2026-06-08
+ */
+@Service
+public class MesFswService extends CrudService<MesFswDao, MesFsw> {
+
+	@Override
+	public MesFsw get(MesFsw mesFsw) {
+		return super.get(mesFsw);
+	}
+
+	@Override
+	public Page<MesFsw> findPage(MesFsw mesFsw) {
+		return super.findPage(mesFsw);
+	}
+
+	@Override
+	public List<MesFsw> findList(MesFsw mesFsw) {
+		return super.findList(mesFsw);
+	}
+
+	@Override
+	@Transactional
+	public void save(MesFsw mesFsw) {
+		super.save(mesFsw);
+	}
+
+	@Override
+	@Transactional
+	public void updateStatus(MesFsw mesFsw) {
+		super.updateStatus(mesFsw);
+	}
+
+	@Override
+	@Transactional
+	public void delete(MesFsw mesFsw) {
+		super.delete(mesFsw);
+	}
+
+	/**
+	 * 导入数据
+	 */
+	@Transactional
+	public String importData(MultipartFile file) {
+		if (file == null){
+			throw new ServiceException(text("请选择导入的数据文件!"));
+		}
+		int successNum = 0; int failureNum = 0;
+		StringBuilder successMsg = new StringBuilder();
+		StringBuilder failureMsg = new StringBuilder();
+		try(ExcelImport ei = new ExcelImport(file, 2, 0)){
+			List<MesFsw> list = ei.getDataList(MesFsw.class);
+			for (MesFsw mesFsw : list) {
+				try{
+					ValidatorUtils.validateWithException(mesFsw);
+					this.save(mesFsw);
+					successNum++;
+					successMsg.append("<br/>" + successNum + "、编号 " + mesFsw.getId() + " 导入成功");
+				} catch (Exception e) {
+					failureNum++;
+					String msg = "<br/>" + failureNum + "、编号 " + mesFsw.getId() + " 导入失败:";
+					if (e instanceof ConstraintViolationException){
+						ConstraintViolationException cve = (ConstraintViolationException)e;
+						for (ConstraintViolation<?> violation : cve.getConstraintViolations()) {
+							msg += Global.getText(violation.getMessage()) + " ("+violation.getPropertyPath()+")";
+						}
+					}else{
+						msg += e.getMessage();
+					}
+					failureMsg.append(msg);
+					logger.error(msg, e);
+				}
+			}
+		} catch (Exception e) {
+			logger.error(e.getMessage(), e);
+			failureMsg.append(e.getMessage());
+			return failureMsg.toString();
+		}
+		if (failureNum > 0) {
+			failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
+			throw new ServiceException(failureMsg.toString());
+		}else{
+			successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
+		}
+		return successMsg.toString();
+	}
+}

+ 138 - 0
src/main/java/com/jeesite/modules/mes/web/MesFswController.java

@@ -0,0 +1,138 @@
+package com.jeesite.modules.mes.web;
+
+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.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.jeesite.common.config.Global;
+import com.jeesite.common.collect.ListUtils;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.DateUtils;
+import com.jeesite.common.utils.excel.ExcelExport;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.entity.MesFsw;
+import com.jeesite.modules.mes.service.MesFswService;
+
+/**
+ * FSW设备数据采集Controller
+ * @author system
+ * @version 2026-06-08
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesFsw")
+public class MesFswController extends BaseController {
+
+	@Autowired
+	private MesFswService mesFswService;
+
+	@ModelAttribute
+	public MesFsw get(String id, boolean isNewRecord) {
+		return mesFswService.get(id, isNewRecord);
+	}
+
+	@RequiresPermissions("mes:mesFsw:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(MesFsw mesFsw, Model model) {
+		model.addAttribute("mesFsw", mesFsw);
+		return "modules/mes/mesFswList";
+	}
+
+	@RequiresPermissions("mes:mesFsw:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesFsw> listData(MesFsw mesFsw, HttpServletRequest request, HttpServletResponse response) {
+		mesFsw.setPage(new Page<>(request, response));
+		return mesFswService.findPage(mesFsw);
+	}
+
+	@RequiresPermissions("mes:mesFsw:view")
+	@RequestMapping(value = "form")
+	public String form(MesFsw mesFsw, Model model) {
+		model.addAttribute("mesFsw", mesFsw);
+		return "modules/mes/mesFswForm";
+	}
+
+	/**
+	 * 实时数据看板页面
+	 */
+	@RequiresPermissions("mes:mesFsw:view")
+	@RequestMapping(value = "board")
+	public String board(Model model) {
+		return "modules/mes/mesFswBoard";
+	}
+
+	/**
+	 * 实时数据看板 - 获取最新一条采集数据
+	 */
+	@RequiresPermissions("mes:mesFsw:view")
+	@RequestMapping(value = "latestData")
+	@ResponseBody
+	public MesFsw latestData() {
+		MesFsw query = new MesFsw();
+		query.setPage(new Page<>(1, 1)); // 取最新一条(orderBy a.id DESC)
+		Page<MesFsw> page = mesFswService.findPage(query);
+		List<MesFsw> list = page.getList();
+		return ListUtils.isEmpty(list) ? new MesFsw() : list.get(0);
+	}
+
+	@RequiresPermissions("mes:mesFsw:edit")
+	@PostMapping(value = "save")
+	@ResponseBody
+	public String save(@Validated MesFsw mesFsw) {
+		mesFswService.save(mesFsw);
+		return renderResult(Global.TRUE, text("保存FSW设备数据采集成功!"));
+	}
+
+	@RequiresPermissions("mes:mesFsw:view")
+	@RequestMapping(value = "exportData")
+	public void exportData(MesFsw mesFsw, HttpServletResponse response) {
+		List<MesFsw> list = mesFswService.findList(mesFsw);
+		String fileName = "FSW设备数据采集" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+		try(ExcelExport ee = new ExcelExport("FSW设备数据采集", MesFsw.class)){
+			ee.setDataList(list).write(response, fileName);
+		}
+	}
+
+	@RequiresPermissions("mes:mesFsw:view")
+	@RequestMapping(value = "importTemplate")
+	public void importTemplate(HttpServletResponse response) {
+		MesFsw mesFsw = new MesFsw();
+		List<MesFsw> list = ListUtils.newArrayList(mesFsw);
+		String fileName = "FSW设备数据采集模板.xlsx";
+		try(ExcelExport ee = new ExcelExport("FSW设备数据采集", MesFsw.class, Type.IMPORT)){
+			ee.setDataList(list).write(response, fileName);
+		}
+	}
+
+	@ResponseBody
+	@RequiresPermissions("mes:mesFsw:edit")
+	@PostMapping(value = "importData")
+	public String importData(MultipartFile file) {
+		try {
+			String message = mesFswService.importData(file);
+			return renderResult(Global.TRUE, "posfull:"+message);
+		} catch (Exception ex) {
+			return renderResult(Global.FALSE, "posfull:"+ex.getMessage());
+		}
+	}
+
+	@RequiresPermissions("mes:mesFsw:edit")
+	@RequestMapping(value = "delete")
+	@ResponseBody
+	public String delete(MesFsw mesFsw) {
+		mesFswService.delete(mesFsw);
+		return renderResult(Global.TRUE, text("删除FSW设备数据采集成功!"));
+	}
+}

+ 298 - 0
src/main/java/com/jeesite/modules/scheduling/FswScheduling.java

@@ -0,0 +1,298 @@
+package com.jeesite.modules.scheduling;
+
+import com.jeesite.common.config.Global;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.modules.mes.entity.MesFsw;
+import com.jeesite.modules.mes.service.MesFswService;
+import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
+import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig;
+import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider;
+import org.eclipse.milo.opcua.sdk.client.api.identity.IdentityProvider;
+import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider;
+import org.eclipse.milo.opcua.stack.client.DiscoveryClient;
+import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
+import org.eclipse.milo.opcua.stack.core.util.EndpointUtil;
+import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateBuilder;
+import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator;
+import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
+import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
+import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
+import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
+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 java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
+
+/**
+ * FSW 设备数据采集定时任务(OPC UA点位)
+ *   在 application.yml 或 sys_config 中配置:
+ *   mes.fsw.enabled     是否启用采集(默认 true)
+ * @author system
+ * @version 2026-06-08  OpcUaClient oPCuA_123
+ */
+@Configuration
+@EnableScheduling
+public class FswScheduling {
+
+    private static final Logger logger = LoggerFactory.getLogger(FswScheduling.class);
+
+    /** 默认 OPC UA 端点地址 */
+    private static final String DEFAULT_ENDPOINT = "opc.tcp://10.0.20.141:4840";
+
+    // ===== 采集点位地址(万宇科技采集地址)=====
+    private static final String NODE_SP_SPEED        = "ns=2;s=/Channel/Spindle/actSpeed";
+    private static final String NODE_X_MCS           = "ns=2;s=/Channel/MachineAxis/actToolBasePos[u1,1]";
+    private static final String NODE_Y_MCS           = "ns=2;s=/Channel/MachineAxis/actToolBasePos[u1,2]";
+    private static final String NODE_Z_MCS           = "ns=2;s=/Channel/MachineAxis/actToolBasePos[u1,3]";
+    private static final String NODE_ACTUAL_PRESSURE = "ns=2;s=/Nck/State/aDbr[12]";
+    private static final String NODE_AA_OFF_VAL_Z    = "ns=2;s=/Channel/MachineAxis/aaOffVal[u1, 3]";
+    private static final String NODE_X_FEED          = "ns=2;s=/Channel/GeometricAxis/actFeedRate[u1,1]";
+    private static final String NODE_Y_FEED          = "ns=2;s=/Channel/GeometricAxis/actFeedRate[u1,2]";
+    private static final String NODE_Z_FEED          = "ns=2;s=/Channel/GeometricAxis/actFeedRate[u1,3]";
+    private static final String NODE_R210            = "ns=2;s=/Channel/Parameter/rpa[u1, 210]";
+    private static final String NODE_R211            = "ns=2;s=/Channel/Parameter/rpa[u1, 211]";
+    private static final String NODE_R212            = "ns=2;s=/Channel/Parameter/rpa[u1, 212]";
+    private static final String NODE_R213            = "ns=2;s=/Channel/Parameter/rpa[u1, 213]";
+    private static final String NODE_R214            = "ns=2;s=/Channel/Parameter/rpa[u1, 214]";
+    private static final String NODE_R215            = "ns=2;s=/Channel/Parameter/rpa[u1, 215]";
+    private static final String NODE_R216            = "ns=2;s=/Channel/Parameter/rpa[u1, 216]";
+    private static final String NODE_R217            = "ns=2;s=/Channel/Parameter/rpa[u1, 217]";
+    private static final String NODE_R218            = "ns=2;s=/Channel/Parameter/rpa[u1, 218]";
+    private static final String NODE_R999            = "ns=2;s=/Channel/Parameter/rpa[u1, 999]";
+
+    @Autowired
+    private MesFswService mesFswService;
+
+    /**
+     * 每 30 秒采集一次
+     */
+    @Scheduled(fixedRate = 30000)
+    public void collect() {
+        String enabled = Global.getConfig("mes.fsw.enabled");
+        if (StringUtils.isNotBlank(enabled) && !"true".equalsIgnoreCase(enabled.trim())) {
+            return;
+        }
+
+        String url = Global.getConfig("mes.fsw.opcua.url");
+        if (StringUtils.isBlank(url)) {
+            url = DEFAULT_ENDPOINT;
+        }
+
+        OpcUaClient client = null;
+        try {
+            client = createClient(url);
+            client.connect().get();
+
+            MesFsw mesFsw = new MesFsw();
+            mesFsw.setSpSpeed(readValue(client, NODE_SP_SPEED));
+            mesFsw.setXMcs(readValue(client, NODE_X_MCS));
+            mesFsw.setYMcs(readValue(client, NODE_Y_MCS));
+            mesFsw.setZMcs(readValue(client, NODE_Z_MCS));
+            mesFsw.setActualPressure(readValue(client, NODE_ACTUAL_PRESSURE));
+            mesFsw.setAaOffValZ(readValue(client, NODE_AA_OFF_VAL_Z));
+            mesFsw.setXFeed(readValue(client, NODE_X_FEED));
+            mesFsw.setYFeed(readValue(client, NODE_Y_FEED));
+            mesFsw.setZFeed(readValue(client, NODE_Z_FEED));
+            mesFsw.setR210(readValue(client, NODE_R210));
+            mesFsw.setR211(readValue(client, NODE_R211));
+            mesFsw.setR212(readValue(client, NODE_R212));
+            mesFsw.setR213(readValue(client, NODE_R213));
+            mesFsw.setR214(readValue(client, NODE_R214));
+            mesFsw.setR215(readValue(client, NODE_R215));
+            mesFsw.setR216(readValue(client, NODE_R216));
+            mesFsw.setR217(readValue(client, NODE_R217));
+            mesFsw.setR218(readValue(client, NODE_R218));
+            mesFsw.setR999(readValue(client, NODE_R999));
+
+            mesFswService.save(mesFsw);
+
+            logger.info("FSW数据采集成功[{}] 主轴转速:{} 实际压力:{}",
+                    url, mesFsw.getSpSpeed(), mesFsw.getActualPressure());
+        } catch (Exception e) {
+            logger.error("FSW数据采集失败[{}]:{}", url, e.getMessage());
+        } finally {
+            if (client != null) {
+                try {
+                    client.disconnect().get();
+                } catch (Exception e) {
+                    logger.warn("关闭 OPC UA 连接异常:{}", e.getMessage());
+                }
+            }
+        }
+    }
+
+    /**
+     * 读取单个节点值,失败返回 null,不中断整体采集
+     */
+    private String readValue(OpcUaClient client, String nodeIdStr) {
+        try {
+            NodeId nodeId = NodeId.parse(nodeIdStr);
+            DataValue dv = client.readValue(0, TimestampsToReturn.Both, nodeId).get();
+            if (dv == null) {
+                logger.warn("读取节点[{}] 返回 null DataValue", nodeIdStr);
+                return null;
+            }
+            // 节点不存在 / 无权限时,statusCode 通常 Bad,且 value 为空
+            if (dv.getValue() == null || dv.getValue().getValue() == null) {
+                logger.warn("读取节点[{}] 值为空,statusCode={} ({})",
+                        nodeIdStr, dv.getStatusCode(),
+                        dv.getStatusCode() == null ? "" : dv.getStatusCode().toString());
+                return null;
+            }
+            return formatVal(dv.getValue().getValue());
+        } catch (Exception e) {
+            logger.warn("读取节点[{}]失败:{}", nodeIdStr, e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 数值格式化:浮点保留 3 位小数;整数原样输出;其它类型 toString
+     */
+    private String formatVal(Object val) {
+        if (val == null) return null;
+        if (val instanceof Double || val instanceof Float) {
+            double d = ((Number) val).doubleValue();
+            // 用 BigDecimal 半进位四舍五入,避免 String.format 在科学计数法或负零上的小坑
+            return new java.math.BigDecimal(d)
+                    .setScale(3, java.math.RoundingMode.HALF_UP)
+                    .toPlainString();
+        }
+        return String.valueOf(val);
+    }
+
+    /**
+     * 构建 OpcUaClient。
+     * 该服务器只提供加密端点(Basic128Rsa15/Basic256/Basic256Sha256),
+     * 因此需要为客户端生成自签名证书 + 密钥对,并选用加密端点连接。
+     */
+    private OpcUaClient createClient(String endpointUrl) throws Exception {
+        List<EndpointDescription> endpoints = DiscoveryClient.getEndpoints(endpointUrl).get();
+
+        if (endpoints == null || endpoints.isEmpty()) {
+            throw new IllegalStateException("服务器未返回任何 Endpoint:" + endpointUrl);
+        }
+
+        for (EndpointDescription ep : endpoints) {
+            logger.info("发现OPC UA端点: url={} securityPolicy={} securityMode={}",
+                    ep.getEndpointUrl(), ep.getSecurityPolicyUri(), ep.getSecurityMode());
+        }
+
+        // 准备客户端证书 / 密钥对(只生成一次)
+        ensureKeyPair();
+
+        // 端点选择优先级:Basic256Sha256(SignAndEncrypt) > 任意加密 > None > 第一个
+        EndpointDescription endpoint = endpoints.stream()
+                .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.Basic256Sha256.getUri())
+                        && e.getSecurityMode() == MessageSecurityMode.SignAndEncrypt)
+                .findFirst()
+                .orElseGet(() -> endpoints.stream()
+                        .filter(e -> !e.getSecurityPolicyUri().endsWith("#None"))
+                        .findFirst()
+                        .orElse(endpoints.get(0)));
+
+        // 服务器返回的端点 URL 可能是它自己的主机名,改写为实际拨号地址
+        endpoint = EndpointUtil.updateUrl(endpoint, parseHost(endpointUrl), parsePort(endpointUrl));
+
+        logger.info("选用OPC UA端点: url={} securityPolicy={} securityMode={}",
+                endpoint.getEndpointUrl(), endpoint.getSecurityPolicyUri(), endpoint.getSecurityMode());
+
+        OpcUaClientConfig config;
+        boolean encrypted = !endpoint.getSecurityPolicyUri().endsWith("#None")
+                && clientCertificate != null && clientKeyPair != null;
+        // 身份认证:默认用户名密码(西门子 SINUMERIK 不支持匿名);配置 mes.fsw.opcua.username 为空时回退匿名
+        IdentityProvider identity = buildIdentityProvider();
+        if (encrypted) {
+            config = OpcUaClientConfig.builder()
+                    .setApplicationName(LocalizedText.english("MES FSW OPC UA Client"))
+                    .setApplicationUri(CLIENT_APP_URI)
+                    .setEndpoint(endpoint)
+                    .setIdentityProvider(identity)
+                    .setRequestTimeout(uint(10000))
+                    .setCertificate(clientCertificate)
+                    .setKeyPair(clientKeyPair)
+                    .build();
+        } else {
+            config = OpcUaClientConfig.builder()
+                    .setApplicationName(LocalizedText.english("MES FSW OPC UA Client"))
+                    .setApplicationUri(CLIENT_APP_URI)
+                    .setEndpoint(endpoint)
+                    .setIdentityProvider(identity)
+                    .setRequestTimeout(uint(10000))
+                    .build();
+        }
+
+        return OpcUaClient.create(config);
+    }
+
+    /**
+     * 构建身份认证提供者:默认匿名(服务器已开启匿名访问),
+     * 配置了 mes.fsw.opcua.username 才走用户名密码。
+     */
+    private IdentityProvider buildIdentityProvider() {
+        String user = Global.getConfig("mes.fsw.opcua.username");
+        if (StringUtils.isBlank(user)) {
+            return new AnonymousProvider();
+        }
+        String pwd = Global.getConfig("mes.fsw.opcua.password");
+        return new UsernameProvider(user, pwd == null ? "" : pwd);
+    }
+
+    /** 客户端证书 / 密钥对(静态缓存,避免每次采集都重新生成) */
+    private static volatile X509Certificate clientCertificate;
+    private static volatile KeyPair clientKeyPair;
+    private static final String CLIENT_APP_URI = "urn:mes:opcua:fsw:client";
+
+    /** 生成自签名证书 + 密钥对(仅首次调用时执行) */
+    private synchronized void ensureKeyPair() throws Exception {
+        if (clientCertificate != null && clientKeyPair != null) {
+            return;
+        }
+        KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048);
+        SelfSignedCertificateBuilder certBuilder = new SelfSignedCertificateBuilder(keyPair)
+                .setCommonName("MES FSW OPC UA Client")
+                .setOrganization("MES")
+                .setOrganizationalUnit("dev")
+                .setLocalityName("CN")
+                .setStateName("CN")
+                .setCountryCode("CN")
+                .setApplicationUri(CLIENT_APP_URI)
+                .addDnsName("localhost")
+                .addIpAddress("127.0.0.1");
+        clientKeyPair = keyPair;
+        clientCertificate = certBuilder.build();
+        logger.info("已生成 OPC UA 客户端自签名证书");
+    }
+
+    /** 从 opc.tcp://host:port 中解析主机 */
+    private String parseHost(String url) {
+        try {
+            String s = url.substring(url.indexOf("://") + 3);
+            String hostPort = s.contains("/") ? s.substring(0, s.indexOf("/")) : s;
+            return hostPort.contains(":") ? hostPort.substring(0, hostPort.indexOf(":")) : hostPort;
+        } catch (Exception e) {
+            return "127.0.0.1";
+        }
+    }
+
+    /** 从 opc.tcp://host:port 中解析端口,默认 4840 */
+    private int parsePort(String url) {
+        try {
+            String s = url.substring(url.indexOf("://") + 3);
+            String hostPort = s.contains("/") ? s.substring(0, s.indexOf("/")) : s;
+            return hostPort.contains(":") ? Integer.parseInt(hostPort.substring(hostPort.indexOf(":") + 1)) : 4840;
+        } catch (Exception e) {
+            return 4840;
+        }
+    }
+}

+ 7 - 0
src/main/resources/mappings/modules/mes/MesFswDao.xml

@@ -0,0 +1,7 @@
+<?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.mes.dao.MesFswDao">
+
+	<!-- 使用 JeeSite 内置 SQL 构建,无需手写 CRUD -->
+
+</mapper>

+ 161 - 0
src/main/resources/views/modules/mes/mesFswBoard.html

@@ -0,0 +1,161 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>FSW设备实时数据看板</title>
+	<script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
+	<style>
+		* { margin: 0; padding: 0; box-sizing: border-box; }
+		html, body { width: 100%; height: 100%; }
+		body {
+			font-family: "Microsoft YaHei", sans-serif;
+			background: #04122b;
+			background-image: radial-gradient(circle at 50% 0%, #0a2350 0%, #04122b 60%);
+			color: #d6e6ff;
+			overflow: auto;
+			padding-bottom: 24px;
+		}
+		.bd-header {
+			height: 64px; display: flex; align-items: center; justify-content: center;
+			position: relative; border-bottom: 1px solid rgba(0,160,255,0.25);
+		}
+		.bd-title { font-size: 24px; letter-spacing: 4px; color: #5ad8ff; font-weight: bold; text-shadow: 0 0 12px rgba(0,180,255,0.6); }
+		.bd-time { position: absolute; right: 24px; font-size: 14px; color: #8fb6e6; }
+		.bd-status { position: absolute; left: 24px; font-size: 14px; }
+		.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
+		.dot-on { background: #25e29a; box-shadow: 0 0 8px #25e29a; }
+		.dot-off { background: #ff5a6a; box-shadow: 0 0 8px #ff5a6a; }
+		.section-title { font-size: 16px; color: #7fd6ff; letter-spacing: 2px; margin: 24px 0 12px 32px; border-left: 4px solid #2fa8ff; padding-left: 10px; }
+		.cards { display: flex; flex-wrap: wrap; gap: 18px; padding: 0 32px; }
+		.card {
+			flex: 1 1 200px; min-width: 180px; height: 130px; border-radius: 12px;
+			background: linear-gradient(160deg, rgba(13,45,95,0.9), rgba(8,26,56,0.9));
+			border: 1px solid rgba(0,160,255,0.35);
+			display: flex; flex-direction: column; align-items: center; justify-content: center;
+			position: relative; overflow: hidden;
+		}
+		.card::before { content:""; position:absolute; top:0; left:0; right:0; height:3px; background: linear-gradient(90deg, transparent, #2fa8ff, transparent); }
+		.card-label { font-size: 15px; color: #9cc4f0; margin-bottom: 12px; }
+		.card-value { font-size: 38px; font-weight: bold; color: #41e6c3; line-height: 1; text-shadow: 0 0 14px rgba(65,230,195,0.5); }
+		.params { padding: 0 32px; }
+		.params table { width: 100%; border-collapse: collapse; }
+		.params th, .params td { border: 1px solid rgba(0,160,255,0.2); padding: 10px 12px; font-size: 14px; text-align: center; }
+		.params th { background: rgba(13,45,95,0.8); color: #9cc4f0; }
+		.params td { color: #d6e6ff; }
+		.params td.pv { color: #41e6c3; font-weight: bold; }
+		.bd-foot { text-align: center; color: #6f93c4; font-size: 13px; margin-top: 20px; }
+	</style>
+</head>
+<body>
+	<div class="bd-header">
+		<span class="bd-status"><i id="connDot" class="dot dot-off"></i><span id="connText">未连接</span></span>
+		<span class="bd-title">FSW设备实时数据看板</span>
+		<span class="bd-time" id="nowTime"></span>
+	</div>
+
+	<div class="section-title">运动 / 压力状态</div>
+	<div class="cards">
+		<div class="card"><div class="card-label">主轴转速</div><div class="card-value" id="spSpeed">--</div></div>
+		<div class="card"><div class="card-label">实际压力值</div><div class="card-value" id="actualPressure">--</div></div>
+		<div class="card"><div class="card-label">X轴机械坐标</div><div class="card-value" id="xMcs">--</div></div>
+		<div class="card"><div class="card-label">Y轴机械坐标</div><div class="card-value" id="yMcs">--</div></div>
+		<div class="card"><div class="card-label">Z轴机械坐标</div><div class="card-value" id="zMcs">--</div></div>
+	</div>
+	<div class="cards" style="margin-top:18px;">
+		<div class="card"><div class="card-label">Z轴叠加总量</div><div class="card-value" id="aaOffValZ">--</div></div>
+		<div class="card"><div class="card-label">X轴进给率</div><div class="card-value" id="xFeed">--</div></div>
+		<div class="card"><div class="card-label">Y轴进给率</div><div class="card-value" id="yFeed">--</div></div>
+		<div class="card"><div class="card-label">Z轴进给率</div><div class="card-value" id="zFeed">--</div></div>
+	</div>
+
+	<div class="section-title">工艺参数 (R 参数)</div>
+	<div class="params">
+		<table>
+			<tr>
+				<th>R210<br>恒压力开启延时</th>
+				<th>R211<br>设定压力值</th>
+				<th>R212<br>调整下限值</th>
+				<th>R213<br>调整速度</th>
+				<th>R214<br>叠加极限值</th>
+				<th>R215<br>压力波动值</th>
+			</tr>
+			<tr>
+				<td class="pv" id="r210">--</td>
+				<td class="pv" id="r211">--</td>
+				<td class="pv" id="r212">--</td>
+				<td class="pv" id="r213">--</td>
+				<td class="pv" id="r214">--</td>
+				<td class="pv" id="r215">--</td>
+			</tr>
+			<tr>
+				<th>R216<br>采集取样次数</th>
+				<th>R217<br>手动调整值</th>
+				<th>R218<br>暂停再启动延时</th>
+				<th>R999<br>投影半径</th>
+				<th colspan="2">采集时间</th>
+			</tr>
+			<tr>
+				<td class="pv" id="r216">--</td>
+				<td class="pv" id="r217">--</td>
+				<td class="pv" id="r218">--</td>
+				<td class="pv" id="r999">--</td>
+				<td colspan="2" id="dataTime">--</td>
+			</tr>
+		</table>
+	</div>
+
+	<div class="bd-foot">每 5 秒自动刷新</div>
+
+<script>
+	var CTX = "${ctx}";
+	function pad(n){ return n < 10 ? '0'+n : ''+n; }
+	function updateClock(){
+		var d = new Date();
+		document.getElementById('nowTime').textContent =
+			d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+' '+pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds());
+	}
+	function setConn(ok){
+		var dot = document.getElementById('connDot'), txt = document.getElementById('connText');
+		if(ok){ dot.className='dot dot-on'; txt.textContent='采集正常'; }
+		else { dot.className='dot dot-off'; txt.textContent='采集异常 / 无数据'; }
+	}
+	function setVal(id, val){
+		var el = document.getElementById(id);
+		if(el) el.textContent = (val===null||val===undefined||val==='') ? '--' : val;
+	}
+	// JSON 实际字段名是全小写(xmcs/ymcs/zmcs/xfeed/yfeed/zfeed),
+	// 这是 Jackson 处理 getXMcs 这种连续大写 getter 的标准行为
+	var FIELDS = ['spSpeed','actualPressure','xmcs','ymcs','zmcs','aaOffValZ','xfeed','yfeed','zfeed',
+		'r210','r211','r212','r213','r214','r215','r216','r217','r218','r999'];
+	// HTML 元素 id 是驼峰 xMcs 等,做一层映射
+	var ID_MAP = {
+		xmcs: 'xMcs', ymcs: 'yMcs', zmcs: 'zMcs',
+		xfeed: 'xFeed', yfeed: 'yFeed', zfeed: 'zFeed'
+	};
+	function loadData(){
+		$.ajax({
+			url: CTX + '/mes/mesFsw/latestData',
+			type: 'POST',
+			dataType: 'json',
+			success: function(data){
+				if(!data || !data.id){ setConn(false); return; }
+				for(var i=0;i<FIELDS.length;i++){
+					var f = FIELDS[i];
+					setVal(ID_MAP[f] || f, data[f]);
+				}
+				setVal('dataTime', data.createDate || '--');
+				setConn(true);
+			},
+			error: function(){ setConn(false); }
+		});
+	}
+	$(function(){
+		updateClock();
+		setInterval(updateClock, 1000);
+		loadData();
+		setInterval(loadData, 5000);
+	});
+</script>
+</body>
+</html>

+ 122 - 0
src/main/resources/views/modules/mes/mesFswForm.html

@@ -0,0 +1,122 @@
+<% layout('/layouts/default.html', {title: 'FSW设备数据采集管理', libs: ['validate']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header with-border">
+			<div class="box-title">
+				<i class="fa icon-note"></i> ${text(mesFsw.isNewRecord ? '新增FSW设备数据采集' : '查看FSW设备数据采集')}
+			</div>
+			<div class="box-tools pull-right">
+				<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-minus"></i></button>
+			</div>
+		</div>
+		<#form:form id="inputForm" model="${mesFsw}" action="${ctx}/mes/mesFsw/save" method="post" class="form-horizontal">
+			<div class="box-body">
+				<div class="form-unit">${text('采集数据')}</div>
+				<#form:hidden path="id"/>
+				<div class="row">
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('主轴转速')}:</label>
+						<div class="col-sm-8"><#form:input path="spSpeed" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('实际压力值')}:</label>
+						<div class="col-sm-8"><#form:input path="actualPressure" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('X轴机械坐标')}:</label>
+						<div class="col-sm-8"><#form:input path="xMcs" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('Y轴机械坐标')}:</label>
+						<div class="col-sm-8"><#form:input path="yMcs" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('Z轴机械坐标')}:</label>
+						<div class="col-sm-8"><#form:input path="zMcs" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('Z轴叠加总量')}:</label>
+						<div class="col-sm-8"><#form:input path="aaOffValZ" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('X轴进给率')}:</label>
+						<div class="col-sm-8"><#form:input path="xFeed" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('Y轴进给率')}:</label>
+						<div class="col-sm-8"><#form:input path="yFeed" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('Z轴进给率')}:</label>
+						<div class="col-sm-8"><#form:input path="zFeed" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('R210-恒压力开启延时')}:</label>
+						<div class="col-sm-8"><#form:input path="r210" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('R211-设定压力值')}:</label>
+						<div class="col-sm-8"><#form:input path="r211" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('R212-调整下限值')}:</label>
+						<div class="col-sm-8"><#form:input path="r212" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('R213-调整速度')}:</label>
+						<div class="col-sm-8"><#form:input path="r213" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('R214-叠加极限值')}:</label>
+						<div class="col-sm-8"><#form:input path="r214" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('R215-压力波动值')}:</label>
+						<div class="col-sm-8"><#form:input path="r215" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('R216-采集取样次数')}:</label>
+						<div class="col-sm-8"><#form:input path="r216" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('R217-手动调整值')}:</label>
+						<div class="col-sm-8"><#form:input path="r217" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('R218-暂停再启动延时')}:</label>
+						<div class="col-sm-8"><#form:input path="r218" maxlength="64" class="form-control"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('R999-投影半径')}:</label>
+						<div class="col-sm-8"><#form:input path="r999" maxlength="64" class="form-control"/></div>
+					</div></div>
+				</div>
+			</div>
+			<div class="box-footer">
+				<div class="row">
+					<div class="col-sm-offset-2 col-sm-10">
+						<% if (hasPermi('mes:mesFsw:edit')){ %>
+							<button type="submit" class="btn btn-sm btn-primary" id="btnSubmit"><i class="fa fa-check"></i> ${text('保 存')}</button>&nbsp;
+						<% } %>
+						<button type="button" class="btn btn-sm btn-default" id="btnCancel" onclick="js.closeCurrentTabPage()"><i class="fa fa-reply-all"></i> ${text('关 闭')}</button>
+					</div>
+				</div>
+			</div>
+		</#form:form>
+	</div>
+</div>
+<% } %>
+<script>
+$("#inputForm").validate({
+	submitHandler: function(form){
+		js.ajaxSubmitForm($(form), function(data){
+			js.showMessage(data.message);
+			if(data.result == Global.TRUE){
+				js.closeCurrentTabPage(function(contentWindow){
+					contentWindow.page();
+				});
+			}
+		}, "json");
+	}
+});
+</script>

+ 133 - 0
src/main/resources/views/modules/mes/mesFswList.html

@@ -0,0 +1,133 @@
+<% layout('/layouts/default.html', {title: 'FSW设备数据采集管理', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa icon-notebook"></i> ${text('FSW设备数据采集管理')}
+			</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="${ctx}/mes/mesFsw/board" class="btn btn-info btnTool" data-title="${text('实时数据看板')}" title="${text('实时数据看板')}"><i class="fa fa-dashboard"></i> ${text('实时看板')}</a>
+				<a href="#" class="btn btn-default" id="btnExport"><i class="glyphicon glyphicon-export"></i> 导出</a>
+				<% if(hasPermi('mes:mesFsw:edit')){ %>
+					<a href="#" class="btn btn-default" id="btnImport"><i class="glyphicon glyphicon-import"></i> 导入</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="${mesFsw}" action="${ctx}/mes/mesFsw/listData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<div class="form-group">
+					<label class="control-label">${text('主轴转速')}:</label>
+					<div class="control-inline">
+						<#form:input path="spSpeed" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('实际压力值')}:</label>
+					<div class="control-inline">
+						<#form:input path="actualPressure" maxlength="64" class="form-control width-120"/>
+					</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').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("采集时间")}', name:'createDate', index:'a.create_date', width:150, align:"center", frozen:true, formatter: function(val, obj, row, act){
+			return '<a href="${ctx}/mes/mesFsw/form?id='+row.id+'" class="btnList" data-title="${text("查看FSW采集数据")}">'+(val||row.id)+'</a>';
+		}},
+		{header:'${text("主轴转速")}', name:'spSpeed', index:'a.sp_speed', width:90, align:"center"},
+		// {header:'${text("X轴机械坐标")}', name:'xMcs', index:'a.x_mcs', width:100, align:"center"},
+		// {header:'${text("Y轴机械坐标")}', name:'yMcs', index:'a.y_mcs', width:100, align:"center"},
+		// {header:'${text("Z轴机械坐标")}', name:'zMcs', index:'a.z_mcs', width:100, align:"center"},
+		{header:'${text("实际压力值")}', name:'actualPressure', index:'a.actual_pressure', width:90, align:"center"},
+		// {header:'${text("Z轴叠加总量")}', name:'aaOffValZ', index:'a.aa_off_val_z', width:100, align:"center"},
+		// {header:'${text("X进给率")}', name:'xFeed', index:'a.x_feed', width:80, align:"center"},
+		// {header:'${text("Y进给率")}', name:'yFeed', index:'a.y_feed', width:80, align:"center"},
+		// {header:'${text("Z进给率")}', name:'zFeed', index:'a.z_feed', width:80, align:"center"},
+		// {header:'${text("R210")}', name:'r210', index:'a.r210', width:70, align:"center"},
+		{header:'${text("设定压力值")}', name:'r211', index:'a.r211', width:70, align:"center"},
+		// {header:'${text("R212")}', name:'r212', index:'a.r212', width:70, align:"center"},
+		{header:'${text("调整速度")}', name:'r213', index:'a.r213', width:70, align:"center"},
+		// {header:'${text("R214")}', name:'r214', index:'a.r214', width:70, align:"center"},
+		// {header:'${text("R215")}', name:'r215', index:'a.r215', width:70, align:"center"},
+		{header:'${text("采集取样次数")}', name:'r216', index:'a.r216', width:70, align:"center"},
+		// {header:'${text("R217")}', name:'r217', index:'a.r217', width:70, align:"center"},
+		// {header:'${text("R218")}', name:'r218', index:'a.r218', width:70, align:"center"},
+		// {header:'${text("R999")}', name:'r999', index:'a.r999', width:70, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:80, formatter: function(val, obj, row, act){
+			var actions = [];
+			//<% if(hasPermi('mes:mesFsw:edit')){ %>
+			actions.push('<a href="${ctx}/mes/mesFsw/delete?id='+row.id+'" class="btnList" title="${text("删除")}" data-confirm="${text("确认要删除该采集记录吗?")}"><i class="fa fa-trash-o"></i></a>&nbsp;');
+			//<% } %>
+			return actions.join('');
+		}}
+	]
+});
+</script>
+<script>
+$('#btnExport').click(function(){
+	js.ajaxSubmitForm($('#searchForm'), {
+		url:'${ctx}/mes/mesFsw/exportData',
+		downloadFile:true
+	});
+});
+$('#btnImport').click(function(){
+	js.layer.open({
+		type: 1,
+		area: ['400px'],
+		title: '${text("导入FSW设备数据采集")}',
+		resize: false,
+		scrollbar: true,
+		content: js.template('importTpl'),
+		btn: ['<i class="fa fa-check"></i> ${text("导入")}',
+			'<i class="fa fa-remove"></i> ${text("关闭")}'],
+		btn1: function(index, layero){
+			var form = {
+				inputForm: layero.find('#inputForm'),
+				file: layero.find('#file').val()
+			};
+			if (form.file == '' || (!js.endWith(form.file, '.xls') && !js.endWith(form.file, '.xlsx'))){
+				js.showMessage("${text('文件不正确,请选择后缀为“xls”或“xlsx”的文件。')}", null, 'warning');
+				return false;
+			}
+			js.ajaxSubmitForm(form.inputForm, function(data){
+				js.showMessage(data.message);
+				if(data.result == Global.TRUE){
+					js.layer.closeAll();
+				}
+				page();
+			}, "json");
+			return true;
+		}
+	});
+});
+</script>
+<script id="importTpl" type="text/template">//<!--
+<form id="inputForm" action="${ctx}/mes/mesFsw/importData" method="post" enctype="multipart/form-data"
+	class="form-horizontal mt20 mb10" style="overflow:auto;max-height:200px;">
+	<div class="row">
+		<div class="col-xs-12 col-xs-offset-1">
+			<input type="file" id="file" name="file" class="form-file"/>
+			<div class="mt10 pt5" style="color:red">
+				${text('提示:仅允许导入“xls”或“xlsx”格式文件!')}
+			</div>
+			<div class="mt10 pt5">
+				<a href="${ctx}/mes/mesFsw/importTemplate" class="btn btn-default btn-xs"><i class="fa fa-file-excel-o"></i> ${text('下载模板')}</a>
+			</div>
+		</div>
+	</div>
+</form>
+//--></script>