gaoluyang
5 天以前 3fd8d1f2e73a843acd330e8e8b84f5800f858407
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package cn.iocoder.yudao.module.mes.service.pd.seal;
 
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
import jakarta.annotation.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.io.*;
import java.sql.Statement;
import java.security.MessageDigest;
import java.util.*;
import java.util.zip.*;
 
@Service
public class PdSealService {
    public static final String PROCESS_KEY = "hrm-seal-use-approval";
    @Resource private JdbcTemplate jdbc;
    @Resource private BpmProcessInstanceApi bpm;
    @Resource private cn.iocoder.yudao.module.bpm.service.definition.BpmProcessAutoApproveService autoApprove;
    @Resource private cn.iocoder.yudao.module.bpm.service.task.BpmTaskService taskService;
    @Resource private SecurityFrameworkService security;
    private long tenant() { Long id = cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder.getTenantId(); return id == null ? 0L : id; }
    private ServiceException fail(String text) { return new ServiceException(1_040_990_001, text); }
 
    public List<Map<String,Object>> seals(boolean activeOnly) {
        return jdbc.queryForList("SELECT id,name,enabled,create_time FROM pd_seal WHERE tenant_id=? AND deleted=0" + (activeOnly ? " AND enabled=1" : "") + " ORDER BY id DESC", tenant());
    }
    public byte[] getImage(long id) {
        var rows = jdbc.queryForList("SELECT image_data FROM pd_seal WHERE id=? AND tenant_id=? AND deleted=0", id, tenant());
        if (rows.isEmpty()) throw fail("印章不存在");
        return (byte[]) rows.get(0).get("image_data");
    }
    public long upload(String name, MultipartFile file, Long userId) throws Exception {
        checkName(name);
        byte[] data = readImage(file);
        String imageHash = hash(data);
        var keys = new GeneratedKeyHolder();
        jdbc.update(c -> { var ps=c.prepareStatement("INSERT INTO pd_seal(name,image_data,image_hash,enabled,created_by,tenant_id) VALUES(?,?,?,1,?,?)", Statement.RETURN_GENERATED_KEYS); ps.setString(1,name.trim()); ps.setBytes(2,data); ps.setString(3,imageHash); ps.setLong(4,userId); ps.setLong(5,tenant()); return ps; }, keys);
        return Objects.requireNonNull(keys.getKey()).longValue();
    }
    public void update(long id, String name, MultipartFile file) throws Exception {
        checkName(name);
        int updated;
        if (file == null || file.isEmpty()) {
            updated = jdbc.update("UPDATE pd_seal SET name=? WHERE id=? AND tenant_id=? AND deleted=0", name.trim(), id, tenant());
        } else {
            byte[] data = readImage(file);
            updated = jdbc.update("UPDATE pd_seal SET name=?,image_data=?,image_hash=? WHERE id=? AND tenant_id=? AND deleted=0", name.trim(), data, hash(data), id, tenant());
        }
        if (updated != 1) throw fail("印章不存在");
    }
    public void delete(long id) {
        Integer pending = jdbc.queryForObject("SELECT COUNT(*) FROM pd_seal_application WHERE seal_id=? AND tenant_id=? AND status=1", Integer.class, id, tenant());
        if (pending != null && pending > 0) throw fail("该印章有进行中的用印审批申请,请待审批完成后再删除");
        if (jdbc.update("UPDATE pd_seal SET deleted=1 WHERE id=? AND tenant_id=? AND deleted=0", id, tenant()) != 1) throw fail("印章不存在");
    }
    private void checkName(String name) {
        if (name == null || name.isBlank() || name.length() > 100) throw fail("印章名称不能为空且不能超过100字");
    }
    private byte[] readImage(MultipartFile file) throws Exception {
        if (file == null || file.isEmpty() || file.getSize() > 2 * 1024 * 1024) throw fail("请上传不超过2MB的PNG印章图片");
        byte[] data = file.getBytes();
        byte[] sig = {(byte)137,80,78,71,13,10,26,10};
        if (data.length < 8 || !Arrays.equals(sig, Arrays.copyOf(data, 8))) throw fail("仅支持PNG图片");
        try (var input = ImageIO.createImageInputStream(new ByteArrayInputStream(data))) {
            var readers = ImageIO.getImageReaders(input);
            if (!readers.hasNext()) throw fail("图片无法读取");
            var reader = readers.next();
            try { reader.setInput(input); if (reader.getWidth(0) > 2048 || reader.getHeight(0) > 2048) throw fail("图片尺寸不能超过2048×2048"); reader.read(0); }
            finally { reader.dispose(); }
        }
        return data;
    }
    private String hash(byte[] data) throws Exception {
        return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(data));
    }
    public void setEnabled(long id, boolean enabled) {
        if (jdbc.update("UPDATE pd_seal SET enabled=? WHERE id=? AND tenant_id=? AND deleted=0", enabled, id, tenant()) != 1) throw fail("印章不存在");
    }
    @Transactional(rollbackFor=Exception.class)
    public long apply(long projectId, String projectCode, long sealId, String reason, Long userId, byte[] snapshot) {
        if (reason == null || reason.isBlank() || reason.length() > 500) throw fail("请填写不超过500字的用印事由");
        if (autoApprove.isAutoApprove(PROCESS_KEY)) throw fail("用印流程开启了免审批,请先关闭后再提交");
        List<Map<String,Object>> seals=jdbc.queryForList("SELECT name,image_data FROM pd_seal WHERE id=? AND tenant_id=? AND enabled=1 AND deleted=0 FOR UPDATE",sealId,tenant());
        if (seals.isEmpty()) throw fail("印章不存在或已停用");
        var seal=seals.get(0); var keys=new GeneratedKeyHolder();
        jdbc.update(c -> { var ps=c.prepareStatement("INSERT INTO pd_seal_application(project_id,project_code,seal_id,seal_name,seal_image,document_snapshot,reason,applicant_id,status,tenant_id) VALUES(?,?,?,?,?,?,?,?,1,?)",Statement.RETURN_GENERATED_KEYS); ps.setLong(1,projectId); ps.setString(2,projectCode); ps.setLong(3,sealId); ps.setString(4,(String)seal.get("name")); ps.setBytes(5,(byte[])seal.get("image_data")); ps.setBytes(6,snapshot); ps.setString(7,reason.trim()); ps.setLong(8,userId); ps.setLong(9,tenant()); return ps; },keys);
        long id=Objects.requireNonNull(keys.getKey()).longValue();
        var variables=new HashMap<String,Object>(); variables.put("projectCode", projectCode); variables.put("sealName",seal.get("name")); variables.put("reason",reason.trim());
        String instance=bpm.createProcessInstance(userId,new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(PROCESS_KEY).setBusinessKey(String.valueOf(id)).setVariables(variables));
        jdbc.update("UPDATE pd_seal_application SET process_instance_id=? WHERE id=? AND tenant_id=? AND (process_instance_id IS NULL OR process_instance_id=?)",instance,id,tenant(),instance);
        return id;
    }
    public void status(BpmProcessInstanceStatusEvent event) {
        if (!PROCESS_KEY.equals(event.getProcessDefinitionKey()) || event.getStatus() == null || event.getStatus() < 2 || event.getStatus() > 4) return;
        try { long id=Long.parseLong(event.getBusinessKey()); jdbc.update("UPDATE pd_seal_application SET status=?,process_instance_id=?,finish_time=NOW() WHERE id=? AND tenant_id=? AND status=1 AND (process_instance_id IS NULL OR process_instance_id=?)",event.getStatus(),event.getId(),id,tenant(),event.getId()); } catch (NumberFormatException ignored) { }
    }
    public List<Map<String,Object>> applications(long userId, Long projectId) {
        return jdbc.queryForList("SELECT id,project_id,project_code,seal_name,reason,status,process_instance_id,create_time,finish_time FROM pd_seal_application WHERE tenant_id=? AND applicant_id=?"+(projectId == null ? "" : " AND project_id="+projectId)+" ORDER BY id DESC LIMIT 200",tenant(),userId);
    }
 
    /**
     * 审批详情:查询用印申请的业务信息(含使用的印章名称与申请时间等)
     *
     * 可见范围:申请人本人、该流程实例的审批人(含已办)、以及具备用印/项目/流程查询权限的管理人员。
     */
    public Map<String,Object> applicationDetail(long id, long userId) {
        var rows = jdbc.queryForList("SELECT a.id,a.project_id,COALESCE(a.project_code,p.task_code) project_code,p.task_name project_name,"
                + "a.seal_id,a.seal_name,a.reason,a.status,a.process_instance_id,a.applicant_id,"
                + "u.nickname applicant_name,a.create_time,a.finish_time "
                + "FROM pd_seal_application a "
                + "LEFT JOIN mes_pd_project p ON p.id=a.project_id AND p.tenant_id=a.tenant_id "
                + "LEFT JOIN system_users u ON u.id=a.applicant_id "
                + "WHERE a.id=? AND a.tenant_id=?", id, tenant());
        if (rows.isEmpty()) throw fail("用印申请不存在");
        var row = rows.get(0);
        if (!canViewApplication(row, userId)) throw fail("无权查看该用印申请,仅申请人、本流程审批人和用印管理人员可查看");
        return row;
    }
 
    /**
     * 审批详情:获取申请时使用的印章图片(申请提交时已快照,后续更换印章图片不影响历史记录)
     */
    public byte[] applicationImage(long id, long userId) {
        var rows = jdbc.queryForList("SELECT seal_image,applicant_id,process_instance_id FROM pd_seal_application WHERE id=? AND tenant_id=?", id, tenant());
        if (rows.isEmpty()) throw fail("用印申请不存在");
        var row = rows.get(0);
        if (!canViewApplication(row, userId)) throw fail("无权查看该用印申请,仅申请人、本流程审批人和用印管理人员可查看");
        return (byte[]) row.get("seal_image");
    }
 
    private boolean canViewApplication(Map<String,Object> row, long userId) {
        Object applicant = row.get("applicant_id");
        if (applicant instanceof Number number && number.longValue() == userId) return true;
        // 用印申请人/审批人之外的可见范围:项目、印章、流程/待办的管理与审批权限
        if (security.hasAnyPermissions("mes:pd-project:export", "mes:pd-seal:manage",
                "bpm:process-instance:query", "bpm:process-instance:manager-query",
                "bpm:task:update", "bpm:task:manager-query")) return true;
        Object instance = row.get("process_instance_id");
        if (instance == null || instance.toString().isBlank()) return false;
        return taskService.getTaskListByProcessInstanceId(instance.toString(), true).stream()
                .anyMatch(task -> task.getAssignee() != null && String.valueOf(userId).equals(task.getAssignee()));
    }
    @Transactional(rollbackFor=Exception.class)
    public byte[] download(long id,long userId) throws IOException {
        var rows=jdbc.queryForList("SELECT document_snapshot,seal_image,process_instance_id FROM pd_seal_application WHERE id=? AND tenant_id=? AND applicant_id=? AND status=2",id,tenant(),userId);
        if(rows.isEmpty()) throw fail("申请不存在、无权下载或审批尚未通过");
        String instance=(String) rows.get(0).get("process_instance_id");
        if (instance == null || instance.isBlank()) throw fail("审批记录不完整,无法下载");
        boolean humanApproval = taskService.getTaskListByProcessInstanceId(instance, true).stream().anyMatch(task -> {
            var vars = task.getTaskLocalVariables();
            if (vars == null || !Integer.valueOf(2).equals(vars.get("TASK_STATUS")) || task.getAssignee() == null) return false;
            Object reason = vars.get("TASK_REASON");
            return Arrays.stream(cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum.values()).noneMatch(item -> Objects.equals(item.getReason(), reason));
        });
        if (!humanApproval) throw fail("未找到人工审批通过记录,无法下载");
        byte[] snapshot=(byte[])rows.get(0).get("document_snapshot"), image=(byte[])rows.get(0).get("seal_image");
        var out=new ByteArrayOutputStream(); boolean found=false;
        try(var zin=new ZipInputStream(new ByteArrayInputStream(snapshot)); var zout=new ZipOutputStream(out)) { ZipEntry entry; while((entry=zin.getNextEntry())!=null){byte[] bytes=zin.readAllBytes(); if(entry.getName().equals("word/media/seal.png")){bytes=image;found=true;} zout.putNextEntry(new ZipEntry(entry.getName())); zout.write(bytes); zout.closeEntry();} }
        if(!found) throw fail("跟踪表模板缺少印章位置");
        jdbc.update("INSERT INTO pd_seal_download_log(application_id,user_id,tenant_id) VALUES(?,?,?)",id,userId,tenant());
        return out.toByteArray();
    }
}