Kaynağa Gözat

冷板装配静置3.5个小时

hbx 1 hafta önce
ebeveyn
işleme
675e1a65e8

+ 0 - 2
.vscode/settings.json

@@ -1,2 +0,0 @@
-{
-}

+ 27 - 0
db/assembly-rest-backfill-example.sql

@@ -0,0 +1,27 @@
+-- 由数据库管理员核实后,替换四个值,再整体执行。日期使用现场/数据库一致时区。
+-- 不可用create_date/update_date/piecework_date直接批量推算。
+SET @assembly_record_id = 'REPLACE_WITH_RECORD_ID';
+SET @verified_completed_at = '2000-01-01 00:00:00';
+SET @verified_operator = 'REPLACE_WITH_OPERATOR';
+SET @verified_reason = 'REPLACE_WITH_EVIDENCE';
+
+START TRANSACTION;
+SELECT id, sn, oprno, content, assembly_completed_at
+FROM mes_product_record WHERE id = @assembly_record_id FOR UPDATE;
+
+INSERT INTO mes_assembly_clock_audit
+    (record_id, old_completed_at, new_completed_at, operator_name, reason)
+SELECT id, assembly_completed_at, @verified_completed_at, @verified_operator, @verified_reason
+FROM mes_product_record
+WHERE id = @assembly_record_id AND craft = '100000' AND content = 'OK'
+  AND oprno REGEXP '^OP200[A-Za-z]?$' AND assembly_completed_at IS NULL
+  AND @verified_completed_at <= CURRENT_TIMESTAMP
+  AND @assembly_record_id != 'REPLACE_WITH_RECORD_ID'
+  AND @verified_operator != 'REPLACE_WITH_OPERATOR'
+  AND @verified_reason != 'REPLACE_WITH_EVIDENCE'
+  AND @verified_completed_at != '2000-01-01 00:00:00';
+SET @clock_audit_inserted = ROW_COUNT();
+
+UPDATE mes_product_record SET assembly_completed_at = @verified_completed_at
+WHERE id = @assembly_record_id AND @clock_audit_inserted = 1;
+COMMIT;

+ 17 - 0
db/ddl-assembly-rest.sql

@@ -0,0 +1,17 @@
+-- OP200 合格完成后用于半成品气密(INSP080)静置计时。
+-- 生产库执行一次;已存在字段时跳过。
+ALTER TABLE mes_product_record
+    ADD COLUMN assembly_completed_at DATETIME NULL COMMENT 'OP200有效完工时间';
+
+CREATE INDEX idx_mpr_assembly_rest
+    ON mes_product_record (sn, line_sn, craft, oprno, assembly_completed_at);
+
+CREATE TABLE mes_assembly_clock_audit (
+    id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+    record_id VARCHAR(64) NOT NULL,
+    old_completed_at DATETIME NULL,
+    new_completed_at DATETIME NOT NULL,
+    operator_name VARCHAR(100) NOT NULL,
+    reason VARCHAR(1000) NOT NULL,
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+) COMMENT='装配完工时间核实补录审计';

+ 0 - 23
db/ddl-piecework-date.sql

@@ -1,33 +0,0 @@
-
-USE mes_cloud_t09;
-
-ALTER TABLE mes_product_record
-ADD COLUMN piecework_date DATETIME NULL
-COMMENT '计件时间(首次加工完成时间)'
-AFTER update_date;
-
-UPDATE mes_product_record
-SET piecework_date = update_date
-WHERE craft = '100000'
-  AND content NOT IN ('UDF', '')
-  AND piecework_date IS NULL
-  AND update_date >= '2026-06-01';
-
-SELECT
-    COUNT(*) AS total_100000,
-    SUM(CASE WHEN piecework_date IS NOT NULL THEN 1 ELSE 0 END) AS has_piecework_date,
-    SUM(CASE WHEN content = 'UDF' THEN 1 ELSE 0 END) AS udf_count,
-    SUM(CASE WHEN content = 'OK'  THEN 1 ELSE 0 END) AS ok_count,
-    SUM(CASE WHEN content = 'NG'  THEN 1 ELSE 0 END) AS ng_count
-FROM mes_product_record
-WHERE craft = '100000';

+ 45 - 0
docs/assembly-rest.md

@@ -0,0 +1,45 @@
+# 半成品气密静置规则与上线步骤
+
+1. 先执行 `db/ddl-assembly-rest.sql`,再发布服务端。脚本未自动执行。
+2. 应用、数据库和工控机时间统一为现场时区并校时。
+3. 盘点在制品。历史OP200记录不自动补完工时间;核实后可使用
+   `db/assembly-rest-backfill-example.sql`逐件补录,保留操作人、依据和时间。
+4. 客户端联调通过后启用现场设备。当前仓库不含工控机客户端代码。
+
+## 规则
+
+同一sn、line_sn的OP200主结果(craft=100000)必须唯一且为OK,有有效完工时间,
+连续静置210分钟才能进行INSP080。支持A/B子工位。万能码、不防呆、允许重复
+不能绕过该条件。其他工序保持原逻辑。
+
+普通生产上报由未完成/NG变为OK时记录数据库当前时间。同一次OK重传保持原时间。
+重置为UDF或NG后旧时间失效;实际重新装配前必须先重置,再重新报工。
+仅再次上传OK不能表示重新装配。网页人工改成OK不作为有效完工依据;需核实补录。
+首次直接建档为OK不推测完工时间,同样需要核实。
+
+## 客户端接口
+
+HTTP `mesProductRecord/qmcheck`保持既有`result/message/data`结构。
+拦截时`result="false"`、`data="WT"`,message包含原因;有可靠时间时额外返回
+`allowedAt`与`remainingSeconds`(秒)。客户端显示message、禁止启动检测。
+倒计时结束须重新请求qmcheck,不能本地自行放行。
+
+TCP AQDW质量查询返回`RSWT`,客户端需映射为静置不足/装配时间无效并禁止启动。
+所有非允许加工响应及网络失败均不能启动设备。
+
+HTTP qmresult要求先成功调用qmcheck,使用服务端工位开始时间再次验证;
+还验证上传testTime不能早于静置到期、不能在未来、不能缺失。
+违规原始气密数据保留在气密记录中,但不更新工序结果,返回失败并记日志。
+TCP及其他正常报工路径也检查服务端开始记录,禁止直接上传绕过。
+现有协议没有检测会话ID,开始记录按工位、工件、产线取最近一条;
+客户端需按“扫码校验→测试→上传”顺序执行,不应并发测试同工件或重扫覆盖开始记录。
+
+## 现场验收
+
+- OP200未完成/NG/无完工时间,INSP080不可加工。
+- 到期前1秒拒绝,到期时允许;跨班跨日继续累计。
+- OP200重复OK不延后;重置再报OK重新等待210分钟。
+- INSP080 A/B、TCP和HTTP均受限制;其他气密工位不受影响。
+- 提前检测、到期后上传仍拒绝放行,原始数据留存。
+- 未扫码直接上传、未来/缺失检测时间拒绝;正常流程能成功放行。
+- 检查历史补录审计及客户端设备启动锁是否有效。

+ 1 - 0
src/main/java/com/jeesite/modules/Application.java

@@ -64,6 +64,7 @@ public class Application extends SpringBootServletInitializer implements Command
 //				e.printStackTrace();
 //			}
 //		}).start();
+
 	}
 	public static void initDatabaseConnection() {
 		System.out.println(BaseConfig.url);

+ 2 - 1
src/main/java/com/jeesite/modules/mes/dao/MesProductRecordDao.java

@@ -16,6 +16,7 @@ import java.util.Map;
  */
 @MyBatisDao
 public interface MesProductRecordDao extends CrudDao<MesProductRecord> {
+    int updateAssemblyClock(MesProductRecord record);
     MesProductRecord findBySn(MesProductRecord mesProductRecord);
     MesProductRecord findInfo(MesProductRecord mesProductRecord);
 //    void batchInsertList(@Param("insertBugList") List<MesProductRecord> insertBugList);
@@ -37,4 +38,4 @@ public interface MesProductRecordDao extends CrudDao<MesProductRecord> {
      */
     List<Map<String, Object>> pieceworkHourlyStat(@Param("params") Map<String, Object> params);
 
-}
+}

+ 5 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProductRecord.java

@@ -44,6 +44,7 @@ import java.util.List;
 		@Column(name="update_by", attrName="updateBy", label="update_by", isQuery=false),
 		@Column(name="update_date", attrName="updateDate", label="update_date", isUpdateForce=true),
 		@Column(name="piecework_date", attrName="pieceworkDate", label="计件时间"),
+		@Column(name="assembly_completed_at", attrName="assemblyCompletedAt", label="装配有效完工时间", isInsert=false, isUpdate=false),
 	},
 //		joinTable={
 //		@JoinTable(type= JoinTable.Type.JOIN, entity=MesTemplateItems.class, attrName="this", alias="b",
@@ -79,6 +80,10 @@ public class MesProductRecord extends DataEntity<MesProductRecord> {
 	private Long cdate;
 	private Date updateDate;
 	private Date pieceworkDate;  // 计件时间(首次加工完成时间)
+	private Date assemblyCompletedAt;
+
+	public Date getAssemblyCompletedAt() { return assemblyCompletedAt; }
+	public void setAssemblyCompletedAt(Date value) { assemblyCompletedAt = value; }
 	private String lineSn;
 	private String zcontent;
 	private String ngType;

+ 13 - 0
src/main/java/com/jeesite/modules/mes/resp/AssemblyRestResp.java

@@ -0,0 +1,13 @@
+package com.jeesite.modules.mes.resp;
+
+import java.util.Date;
+
+/** 保留既有data状态码,同时提供客户端倒计时字段。 */
+public class AssemblyRestResp extends CommonResp<String> {
+    private Date allowedAt;
+    private Long remainingSeconds;
+    public Date getAllowedAt() { return allowedAt; }
+    public void setAllowedAt(Date value) { allowedAt = value; }
+    public Long getRemainingSeconds() { return remainingSeconds; }
+    public void setRemainingSeconds(Long value) { remainingSeconds = value; }
+}

+ 83 - 8
src/main/java/com/jeesite/modules/mes/service/MesProductRecordService.java

@@ -17,6 +17,7 @@ import com.jeesite.modules.mes.dao.*;
 import com.jeesite.modules.mes.entity.*;
 import com.jeesite.modules.mes.resp.*;
 import com.jeesite.modules.mes.util.CommonUitl;
+import com.jeesite.modules.mes.util.AssemblyRestRule;
 import com.jeesite.modules.mes.util.CopyUtil;
 import com.jeesite.modules.sys.dao.UserDao;
 import com.jeesite.modules.utils.ProtocolParam;
@@ -42,6 +43,66 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 	@Autowired
 	private MesProductRecordDao mesProductRecordDao;
 
+	/** 通过数据库原结果原子地维护计时,防止重复OK上报延长静置。 */
+	@Override
+	@Transactional
+	public void update(MesProductRecord record) {
+		validateAssemblyResultUpdate(record);
+		if (!StringUtils.isEmpty(record.getId()) && !StringUtils.isEmpty(record.getContent())) {
+			mesProductRecordDao.updateAssemblyClock(record);
+		}
+		super.update(record);
+	}
+
+	private void validateAssemblyResultUpdate(MesProductRecord record) {
+		if (!"OK".equals(record.getContent())) return;
+		MesProductRecord existing = StringUtils.isEmpty(record.getId()) ? null : super.get(record);
+		MesProductRecord target = existing == null ? record : existing;
+		if (!"100000".equals(target.getCraft())) return;
+		String blocked = checkAssemblyUpload(target.getSn(), target.getOprno(), target.getLineSn(), null);
+		if (blocked != null) throw new ServiceException(blocked);
+	}
+
+	public MesProductRecord findAssemblyRecord(String sn, String lineSn) {
+		MesProductRecord query = new MesProductRecord();
+		query.setSn(sn);
+		query.setCraft("100000");
+		query.getSqlMap().getWhere().and("a.line_sn", QueryType.EQ, lineSn);
+		MesProductRecord result = null;
+		for (MesProductRecord row : findList(query)) {
+			if (!"OP200".equals(CommonUitl.formatOprno(row.getOprno()))) continue;
+			// 多条装配主结果不猜测有效记录,交由现场核实。
+			if (result != null) return null;
+			result = row;
+		}
+		return result;
+	}
+
+	public String checkAssemblyRest(String sn, String oprno, String lineSn, Date at) {
+		if (!"INSP080".equals(CommonUitl.formatOprno(oprno))) return null;
+		MesProductRecord assembly = findAssemblyRecord(sn, lineSn);
+		return AssemblyRestRule.check(assembly != null && "OK".equals(assembly.getContent()),
+				assembly == null ? null : assembly.getAssemblyCompletedAt(), at);
+	}
+
+	public String checkAssemblyUpload(String sn, String oprno, String lineSn, Date testTime) {
+		if (!"INSP080".equals(CommonUitl.formatOprno(oprno))) return null;
+		Date now = new Date();
+		String blocked = checkAssemblyRest(sn, oprno, lineSn, now);
+		if (blocked != null) return blocked;
+		MesDeviceTime timing = mesDeviceTimeService.getInfoByOprnoAndSnAndCate(oprno, sn, lineSn);
+		if (timing == null || timing.getStartDate() == null || timing.getStartDate().after(now)) {
+			return "缺少有效的半成品气密加工前校验记录,请重新扫码校验";
+		}
+		blocked = checkAssemblyRest(sn, oprno, lineSn, timing.getStartDate());
+		if (blocked != null) return "气密开始时未满足静置要求:" + blocked;
+		if (testTime != null) {
+			if (testTime.after(now)) return "气密检测时间不能晚于服务端当前时间";
+			blocked = checkAssemblyRest(sn, oprno, lineSn, testTime);
+		}
+		return blocked;
+	}
+
 	@Autowired
 	private MesProductCateService mesProductCateService;
 
@@ -141,13 +202,18 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 	}
 
 	@Override
+	@Transactional
 	public void save(MesProductRecord mesProductRecord) {
+		validateAssemblyResultUpdate(mesProductRecord);
 		if (StringUtils.isEmpty(mesProductRecord.getSource())) {
 			mesProductRecord.setSource("0");
 		}
 
 		// 设置计件时间
 		setPieceworkDateIfNeeded(mesProductRecord);
+		if (!mesProductRecord.getIsNewRecord() && !StringUtils.isEmpty(mesProductRecord.getContent())) {
+			mesProductRecordDao.updateAssemblyClock(mesProductRecord);
+		}
 
 		super.save(mesProductRecord);
 	}
@@ -243,6 +309,9 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 		return super.findPage(mesProductRecord);
 	}
 	public boolean updateAll(MesProductRecord mesProductRecord) {
+		String blocked = checkAssemblyUpload(mesProductRecord.getSn(), mesProductRecord.getOprno(),
+				mesProductRecord.getLineSn(), null);
+		if (blocked != null) throw new ServiceException(blocked);
 		return mesProductRecordDao.updateAll(mesProductRecord);
 	}
 	/**
@@ -661,6 +730,8 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 	 */
 	@Transactional
 	public void updateRecordResult(String sn, String oprno, String craft, String materielSn, String content, String userCode, List<ParamsResp> paramsResps,String lineSn,Boolean optime,List<BindMaterialResp> bmlists) {
+		String restBlocked = checkAssemblyUpload(sn, oprno, lineSn, null);
+		if (restBlocked != null) throw new ServiceException(restBlocked);
 		String oldOprno = oprno;
 		oprno = CommonUitl.formatOprno(oprno);
 		sn = sn.trim();
@@ -791,7 +862,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 						}
 						mesProductRecord.setSource("0");
 						setPieceworkDateIfNeeded(mesProductRecord);
-						super.update(mesProductRecord);
+						this.update(mesProductRecord);
 						break;
 					}
 				}
@@ -805,7 +876,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 				mesProductRecord.setMaterielSn(mesLineProcess1.getDevices());
 				mesProductRecord.setContent("OK");
 				setPieceworkDateIfNeeded(mesProductRecord);
-				super.update(mesProductRecord);
+				this.update(mesProductRecord);
 			}else if(mesProductRecord.getCraft().equals("400003")){
 				if(!oldOprno.equals(oprno)){
 					mesProductRecord.setOprno(oldOprno);
@@ -813,7 +884,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 				mesProductRecord.setMaterielSn(mesLineProcess1.getGzs());
 				mesProductRecord.setContent("OK");
 				setPieceworkDateIfNeeded(mesProductRecord);
-				super.update(mesProductRecord);
+				this.update(mesProductRecord);
 			}
 
 			// 更新主结果
@@ -828,8 +899,9 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 
 				mesProductRecord.setMaterielSn(materielSn);
 				mesProductRecord.setContent(content);
+				mesProductRecord.setSource("0");
 				setPieceworkDateIfNeeded(mesProductRecord);
-				super.update(mesProductRecord);
+				this.update(mesProductRecord);
 			}
 		}
 
@@ -1172,7 +1244,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 		mesProductRecord.setContent(content);
 		mesProductRecord.setSource("0");
 		setPieceworkDateIfNeeded(mesProductRecord);
-		super.update(mesProductRecord);
+		this.update(mesProductRecord);
 
 		/*if(CommonUitl.formatOprno(oprno).equals("OP290") && craft.equals("400002")){
 			MesProductDhb mesProductDhb = new MesProductDhb();
@@ -1262,7 +1334,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 		mesProductRecord.setMaterielSn(materielSn);
 		mesProductRecord.setContent(content);
 		setPieceworkDateIfNeeded(mesProductRecord);
-		super.update(mesProductRecord);
+		this.update(mesProductRecord);
 
 		// 更新用户
 		MesProductRecord mesProductRecord2 = findByUpdateCate(sn,oprno,"400002",lineSn);
@@ -1294,7 +1366,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 		mesProductRecord.setMaterielSn("");
 		mesProductRecord.setContent(content);
 		setPieceworkDateIfNeeded(mesProductRecord);
-		super.update(mesProductRecord);
+		this.update(mesProductRecord);
 
 		if(!craft.equals("400001")){ // 不是绑定操作人员的
 			// 记录解绑物料批次
@@ -1442,6 +1514,9 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 
 	//通过首件检查结果OK/NG    修改工位结果 10000
 	public void updateResultByCheckFirst(MesProductRecord mesProductRecord){
+		String blocked = checkAssemblyUpload(mesProductRecord.getSn(), mesProductRecord.getOprno(),
+				mesProductRecord.getLineSn(), null);
+		if (blocked != null) throw new ServiceException(blocked);
 		mesProductRecordDao.updateResultByCheckFirst(mesProductRecord);
 	}
 
@@ -1458,4 +1533,4 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 	public List<Map<String, Object>> pieceworkHourlyStat(Map<String, Object> params){
 		return mesProductRecordDao.pieceworkHourlyStat(params);
 	}
-}
+}

+ 22 - 0
src/main/java/com/jeesite/modules/mes/util/AssemblyRestRule.java

@@ -0,0 +1,22 @@
+package com.jeesite.modules.mes.util;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/** 冷板装配合格完成后的连续静置要求。 */
+public final class AssemblyRestRule {
+    public static final long REST_MILLIS = 210L * 60 * 1000;
+
+    private AssemblyRestRule() { }
+
+    public static String check(boolean completed, Date completedAt, Date at) {
+        if (!completed) return "冷板装配OP200未完成或不合格,不能进行半成品气密";
+        if (completedAt == null) return "冷板装配OP200缺少有效完工时间,请核实并补录后再进行半成品气密";
+        if (at == null) return "缺少有效检测开始时间,不能放行半成品气密";
+        long remaining = completedAt.getTime() + REST_MILLIS - at.getTime();
+        if (remaining <= 0) return null;
+        return "冷板装配完成后需静置3小时30分钟,剩余" + ((remaining + 59999) / 60000)
+                + "分钟,预计" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+                .format(new Date(completedAt.getTime() + REST_MILLIS)) + "可加工";
+    }
+}

+ 30 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java

@@ -1993,6 +1993,10 @@ public class MesProductRecordController extends BaseController {
 
 	// 检查质量逻辑代码
 	private String checkQualityResult(String oprno,String sn,String lineSn,String userCode){
+		// 静置是独立工艺要求,必须在万能码、不防呆及重复加工的提前返回之前校验。
+		if (mesProductRecordService.checkAssemblyRest(sn, oprno, lineSn, new Date()) != null) {
+			return "RSWT";
+		}
 		if(oprno.length() == 5){
 			oprno = oprno + "A";
 		}
@@ -3751,6 +3755,22 @@ public class MesProductRecordController extends BaseController {
 			return resp;
 		}
 		String userCode = workNum;
+		Date restNow = new Date();
+		String restBlocked = mesProductRecordService.checkAssemblyRest(sn, oldOprno, lineSn, restNow);
+		if (restBlocked != null) {
+			com.jeesite.modules.mes.resp.AssemblyRestResp restResp = new com.jeesite.modules.mes.resp.AssemblyRestResp();
+			restResp.setResult(Global.FALSE);
+			restResp.setData("WT");
+			restResp.setMessage(restBlocked);
+			MesProductRecord assembly = mesProductRecordService.findAssemblyRecord(sn, lineSn);
+			if (assembly != null && "OK".equals(assembly.getContent()) && assembly.getAssemblyCompletedAt() != null) {
+				long allowedAt = assembly.getAssemblyCompletedAt().getTime()
+						+ com.jeesite.modules.mes.util.AssemblyRestRule.REST_MILLIS;
+				restResp.setAllowedAt(new Date(allowedAt));
+				restResp.setRemainingSeconds(Math.max(0L, (allowedAt - restNow.getTime() + 999) / 1000));
+			}
+			return restResp;
+		}
 		String ret = checkQualityResult(oldOprno,sn,lineSn,userCode);
 		if(ret.equals("RSUD")){
 			// 新规则:连续N次NG后重测需间隔(优先判断,命中直接返回,不占用 MesDeviceTime)
@@ -3864,6 +3884,16 @@ public class MesProductRecordController extends BaseController {
 			mesProductQm.setRemark(remark);
 			mesProductQmService.add(mesProductQm);
 
+			// 原始检测结果留存;不满足静置/无有效开工校验时不更新工序结果。
+			String restBlocked = mesProductRecordService.checkAssemblyUpload(sn, oprno, lineSn, mesProductQm.getTestTime());
+			if ("INSP080".equals(CommonUitl.formatOprno(oprno)) && mesProductQm.getTestTime() == null) {
+				restBlocked = "缺少有效气密检测时间,原始数据已保留,工序未放行";
+			}
+			if (restBlocked != null) {
+				logger.warn("半成品气密上传未放行 sn={}, oprno={}, reason={}", sn, oprno, restBlocked);
+				return renderResult(Global.FALSE, text(restBlocked + ";原始检测数据已保留,工序未放行"));
+			}
+
 			List<ParamsResp> params = new ArrayList<>();
 			ParamsResp paramsResp1 = new ParamsResp();
 			paramsResp1.setCraft("400001");

+ 2 - 0
src/main/java/com/jeesite/modules/utils/ErrorMsg.java

@@ -36,6 +36,8 @@ public class ErrorMsg {
                 lmsg = "配件寿命不足";
             }else if(processMsgRet.equalsIgnoreCase("QM")) {
                 lmsg = "两次气密必须间隔15分钟";
+            }else if(processMsgRet.equalsIgnoreCase("WT")) {
+                lmsg = "冷板装配OP200须合格完成且静置3小时30分钟;未完成、计时不足或缺少有效完工时间均不可加工";
             }else if(processMsgRet.equalsIgnoreCase("QR")) {
                 lmsg = "连续两次NG,需间隔指定分钟后再测试";
             }else if(processMsgRet.equalsIgnoreCase("GS")) {

+ 21 - 2
src/main/resources/mappings/modules/mes/MesProductRecordDao.xml

@@ -2,6 +2,18 @@
 <!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.MesProductRecordDao">
 
+    <!-- 在修改结果之前执行;同一次OK重传不刷新,非OK/人工改结果使旧时间失效。 -->
+    <update id="updateAssemblyClock">
+        UPDATE mes_product_record
+        SET assembly_completed_at = CASE
+            WHEN #{content} != 'OK' THEN NULL
+            WHEN content = 'OK' THEN assembly_completed_at
+            WHEN #{source} = '1' THEN NULL
+            ELSE CURRENT_TIMESTAMP END
+        WHERE id = #{id} AND craft = '100000'
+          AND oprno REGEXP '^OP200[A-Za-z]?$'
+    </update>
+
     <!-- 查询数据 -->
     <select id="findList" resultType="MesProductRecord">
         SELECT ${sqlMap.column.toSql()}
@@ -42,11 +54,18 @@
     </select>
     <update id="updateAll">
        update mes_product_record
-       set content=#{content},materiel_sn=#{materielSn}
+       set assembly_completed_at = CASE
+               WHEN craft = '100000' AND oprno REGEXP '^OP200[A-Za-z]?$'
+                    AND (#{content} != 'OK' OR content != 'OK') THEN NULL
+               ELSE assembly_completed_at END,
+           content=#{content},materiel_sn=#{materielSn}
        WHERE sn = #{sn} and  oprno = #{oprno}
     </update>
     <update id="updateResultByCheckFirst">
-        update mes_product_record set content =#{content}
+        update mes_product_record set assembly_completed_at = CASE
+            WHEN craft = '100000' AND oprno REGEXP '^OP200[A-Za-z]?$'
+                AND (#{content} != 'OK' OR content != 'OK') THEN NULL
+            ELSE assembly_completed_at END, content =#{content}
         where sn = #{sn} and oprno = #{oprno} and line_sn = #{lineSn} and craft = #{craft}
     </update>
 

+ 34 - 0
src/test/java/com/jeesite/modules/mes/util/AssemblyRestRuleTest.java

@@ -0,0 +1,34 @@
+package com.jeesite.modules.mes.util;
+
+import org.junit.Test;
+import java.util.Date;
+import static org.junit.Assert.*;
+
+public class AssemblyRestRuleTest {
+    private final Date completed = new Date(0);
+
+    @Test public void exactBoundary() {
+        assertNotNull(AssemblyRestRule.check(true, completed, new Date(AssemblyRestRule.REST_MILLIS - 1)));
+        assertNull(AssemblyRestRule.check(true, completed, new Date(AssemblyRestRule.REST_MILLIS)));
+        assertNull(AssemblyRestRule.check(true, completed, new Date(48L * 60 * 60 * 1000)));
+    }
+
+    @Test public void missingOrUnqualifiedAssemblyCannotPass() {
+        assertNotNull(AssemblyRestRule.check(false, completed, new Date(Long.MAX_VALUE / 2)));
+        assertNotNull(AssemblyRestRule.check(true, null, new Date()));
+        assertNotNull(AssemblyRestRule.check(true, completed, null));
+    }
+
+    @Test public void delayedUploadDoesNotMakeEarlyTestValid() {
+        Date earlyStart = new Date(AssemblyRestRule.REST_MILLIS - 60000);
+        Date upload = new Date(AssemblyRestRule.REST_MILLIS + 60000);
+        assertNull(AssemblyRestRule.check(true, completed, upload));
+        assertNotNull(AssemblyRestRule.check(true, completed, earlyStart));
+    }
+
+    @Test public void reassemblyRestartsWaiting() {
+        Date reassembled = new Date(AssemblyRestRule.REST_MILLIS);
+        assertNotNull(AssemblyRestRule.check(true, reassembled, reassembled));
+        assertNull(AssemblyRestRule.check(true, reassembled, new Date(2 * AssemblyRestRule.REST_MILLIS)));
+    }
+}