Ver código fonte

禀议书子表赋值时间戳修改

CRK 2 semanas atrás
pai
commit
67bc154774

+ 23 - 7
SEAL_DEPLOYMENT.md

@@ -30,21 +30,37 @@ GET  /api/changguchuan/seal/health
 | 模板图片字段 | `imageField_mubc5ugk` |
 | 回写子表 | `tableField_mv7h9ypq` |
 | 子表节点名称 | `textField_mv7h9yps` |
+| 子表填表人 | `textField_mvafdu7s`,发起人或当前节点审批人名称,与返回值 `approverName` 一致 |
 | 子表时间戳印章 | `imageField_mv7h9ypr`,图片对象数组 |
 
 接口先通过 v2.0 流程实例详情取得真实 `processInstanceId` 并验证所属表单,再通过
 `GetOperationRecords`(`/v1.0/yida/processes/operationRecords`)获取审批历史。
+首行固定生成一条发起人印章:按流程实例 `originator.userId` 匹配个人模板,姓名优先取
+`originator.name.nameInChinese`,为空时取 `nameInEnglish`,日期使用数据创建时间 `createTimeGMT`。
+该行节点名称为“发起人”,返回的 `approvalAction` 为 `submit`,`approvalTime` 为创建时间。
+即使发起人同时参与后续审批,其发起人行与审批行也分别保留;操作记录中的提交动作不会再次生成发起人行。
+没有审批记录时仍生成并回写发起人一行;缺少发起人、姓名、创建时间或个人模板时返回错误,不回写部分子表。
 会签 `domainList` 会展开为每个实际审批人;同意及拒绝/驳回记录均保留,并在返回值中标明
-`approvalAction`。提交、抄送、转交和待审批记录不会盖章。同一人在不同节点或重新审批时
-分别保留,只有完全相同的任务、人员、时间、节点和动作才去重,结果按实际审批时间升序排列。
-
-图片使用该审批人的模板,日期和时分秒分两行绘制(`yyyy-MM-dd` / `HH:mm:ss`),默认时区
-`Asia/Shanghai`。带时区时间和 Unix 时间戳先转换至配置时区,无偏移的时间文本按配置时区
-解读。审批时间缺失或无法解析会报错,不使用服务器当前日期代替。
+`approvalAction`。抄送、转交和待审批记录不会盖章。同一人在不同节点或重新审批时
+分别保留,只有完全相同的任务、人员、时间、节点和动作才去重;发起人之后的审批行按实际审批时间升序排列。
+
+图片使用该审批人的模板,仅绘制一行数字日期(`yyyy-MM-dd`),例如 `2026-10-16`,不显示时分秒。
+接口返回的 `approvalTime` 仍为
+`yyyy-MM-dd HH:mm:ss`。默认时区
+`Asia/Shanghai`。Unix 时间戳和明确的非零偏移时间(如 `+08:00`)转换至配置时区;无偏移
+文本按配置时区解读。针对当前组织反馈的审批时间多加 8 小时,默认启用
+`seal.approval-records.utc-text-is-local=true`:将审批记录中带 `Z` / `+00:00` / `+0000` 的
+文本按已是本地时间处理,例如 `2026-10-16T09:33:00Z` 盖为 `2026-10-16`。
+发起人的 `createTimeGMT` 复用同一时间解析和兼容规则。
+该兼容基于线上时差现象,需用原始 `operateTimeGMT` 与宜搭页面时间核实。如果数据源返回的是
+真正的 UTC 时间,应配置 `seal.approval-records.utc-text-is-local=false`,恢复标准时区转换
+(例如 `2026-10-16T01:33:00Z` 转为北京时间 `09:33:00`)。兼容规则仅作用于本接口的审批
+记录,不修改服务器、邮件同步或旧单张印章接口的时区。日志 `[seal.records.time]` 包含原始字段、
+配置时区及最终盖戳时间,便于核查。审批时间缺失或无法解析会报错,不使用服务器当前日期代替。
 
 所有图片生成成功后,通过流程 `UpdateInstance` 一次性回写完整子表,仅提交该子表字段。
 这会**替换指定子表的原有行**,而不是追加;请将该子表作为此接口生成的审批印章明细使用。
-模板缺失、模板不唯一、图片为空、下载失败或没有已执行审批记录时返回错误,不写入部分列表,
+模板缺失、模板不唯一、图片为空或下载失败时返回错误,不写入部分列表,
 也不清空已有子表。个人模板下载失败不会使用旧接口的固定底图或本地通用底图替代。
 错误发生前已生成的本地图片可能留在 `sealFile` 中;重复请求会重新生成图片并替换子表行。
 

+ 1 - 0
src/main/java/com/muzhi/changguchuan/common/dingtalkwebhook/DingtalkWebhook.java

@@ -31,6 +31,7 @@ public class DingtalkWebhook {
             System.out.println("返回体body{}:" + jsonObject);
         }catch (Exception e){
             e.printStackTrace();
+
         }
 
         return jsonObject;

+ 12 - 0
src/main/java/com/muzhi/changguchuan/seal/config/SealProperties.java

@@ -82,6 +82,10 @@ public class SealProperties {
     @Value("${seal.time-zone:Asia/Shanghai}")
     private String timeZone;
 
+    /** 当前组织时间文本可能已是本地时间却携带 UTC 标记;用于审批记录及发起人创建时间。 */
+    @Value("${seal.approval-records.utc-text-is-local:true}")
+    private boolean approvalRecordUtcTextLocal;
+
     /** 下载底图时的连接和读取超时时间,单位为毫秒。 */
     @Value("${seal.http-timeout-ms:30000}")
     private int httpTimeoutMs;
@@ -191,6 +195,14 @@ public class SealProperties {
         return timeZone;
     }
 
+    /**
+     * 是否将审批记录及发起人创建时间中带零偏移标记的时间文本按业务本地时间解读。
+     * @return true 表示保留文本中的本地时分秒;false 表示遵循标准 UTC 语义
+     */
+    public boolean isApprovalRecordUtcTextLocal() {
+        return approvalRecordUtcTextLocal;
+    }
+
     /** 返回 HTTP 超时时间。 */
     public int getHttpTimeoutMs() {
         return httpTimeoutMs;

+ 2 - 2
src/main/java/com/muzhi/changguchuan/seal/service/SealService.java

@@ -16,9 +16,9 @@ public interface SealService {
     SealResult generateAndWrite(SealApprovalRequest request);
 
     /**
-     * 从实际审批记录生成每位审批人的印章,并替换业务实例的审批印章子表。
+     * 生成发起人创建日期印章及每位审批人的审批日期印章,并替换业务实例的审批印章子表。
      * @param formInstanceId 目标表单的宜搭数据 ID
-     * @return 按实际审批时间排序的印章及节点列表
+     * @return 发起人在首行,其后按实际审批时间排序的印章及节点列表
      */
     List<SealApprovalRecordResult> generateApprovalRecordsAndWrite(String formInstanceId);
 }

+ 10 - 6
src/main/java/com/muzhi/changguchuan/seal/service/impl/SealServiceImpl.java

@@ -82,6 +82,7 @@ public class SealServiceImpl implements SealService {
     static final String APPROVER_IMAGE_FIELD = "imageField_mubc5ugk";
     static final String APPROVAL_TABLE = "tableField_mv7h9ypq";
     static final String NODE_FIELD = "textField_mv7h9yps";
+    static final String APPROVER_NAME_FIELD = "textField_mvafdu7s";
     static final String STAMP_FIELD = "imageField_mv7h9ypr";
 
     @Autowired
@@ -160,9 +161,9 @@ public class SealServiceImpl implements SealService {
     }
 
     /**
-     * 按实际审批记录生成个人时间戳印章,全部成功后统一替换指定子表。
+     * 按创建时间生成发起人印章,并按实际审批记录生成个人印章,全部成功后统一替换指定子表。
      * @param formInstanceId 业务表单的宜搭数据 ID
-     * @return 按审批时间排序的印章列表
+     * @return 发起人印章在首行,其后为按审批时间排序的印章列表
      */
     @Override
     public List<SealApprovalRecordResult> generateApprovalRecordsAndWrite(String formInstanceId) {
@@ -189,12 +190,14 @@ public class SealServiceImpl implements SealService {
             validateApprovalResponse(operations == null ? null : operations.getStatusCode(), "查询审批记录");
             JSONObject operationBody = unwrapContent(operations.getBody());
             List<ApprovalRecordParser.ApprovalRecord> records = ApprovalRecordParser.parse(
-                    operationBody == null ? null : operationBody.getJSONArray("result"), zone);
-            if (records.isEmpty()) throw new IllegalStateException("尚无已执行的人工审批记录,不回写审批印章子表");
+                    operationBody == null ? null : operationBody.getJSONArray("result"), zone,
+                    properties.isApprovalRecordUtcTextLocal());
+            records.add(0, ApprovalRecordParser.originator(instance, zone, properties.isApprovalRecordUtcTextLocal()));
 
             Map<String, byte[]> templateCache = new HashMap<>();
             List<SealApprovalRecordResult> results = new ArrayList<>();
             DateTimeFormatter timestamp = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(zone);
+            DateTimeFormatter stampDate = FILE_DATE.withZone(zone);
             for (ApprovalRecordParser.ApprovalRecord record : records) {
                 byte[] template = templateCache.get(record.getUserId());
                 if (template == null) {
@@ -203,8 +206,8 @@ public class SealServiceImpl implements SealService {
                     templateCache.put(record.getUserId(), template);
                 }
                 String approvalTime = timestamp.format(record.getTime());
-                // 日期和时分秒分两行绘制,避免完整时间在个人印章上被截断。
-                byte[] stamped = renderSeal(template, approvalTime.replace(' ', '\n'));
+                // 印章仅显示数字日期,返回值仍保留完整审批时间。
+                byte[] stamped = renderSeal(template, stampDate.format(record.getTime()));
                 StoredFile file = store(stamped, instanceId, approvalTime);
                 results.add(new SealApprovalRecordResult(record.getUserId(), record.getName(), approvalTime,
                         record.getAction(), record.getNodeName(), file.url, Collections.singletonList(imageValue(file))));
@@ -290,6 +293,7 @@ public class SealServiceImpl implements SealService {
         for (SealApprovalRecordResult result : results) {
             Map<String, Object> row = new LinkedHashMap<>();
             row.put(NODE_FIELD, result.getNodeName());
+            row.put(APPROVER_NAME_FIELD, result.getApproverName());
             row.put(STAMP_FIELD, result.getSealImages());
             rows.add(row);
         }