吴羡
10 天以前 647960c44d36b97263fee0325487f9dea8b24307
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
package com.ruoyi.manage.service.impl;
 
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.config.ConfigureBuilder;
import com.deepoove.poi.data.Pictures;
import com.ruoyi.common.core.domain.entity.User;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.QueryWrappers;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.manage.dto.ClientSatisfactionDto;
import com.ruoyi.manage.mapper.ClientSatisfactionAnalyseFileMapper;
import com.ruoyi.manage.mapper.ClientSatisfactionAttachmentMapper;
import com.ruoyi.manage.mapper.ClientSatisfactionMapper;
import com.ruoyi.manage.pojo.ClientSatisfaction;
import com.ruoyi.manage.pojo.ClientSatisfactionAnalyseFile;
import com.ruoyi.manage.pojo.ClientSatisfactionAttachment;
import com.ruoyi.manage.service.ClientSatisfactionService;
import com.ruoyi.system.mapper.UserMapper;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
 
 
/**
 * 客户满意度
 *
 * @author zhuo
 * @since 2024-11-09
 */
@Service
public class ClientSatisfactionServiceImpl extends ServiceImpl<ClientSatisfactionMapper, ClientSatisfaction> implements ClientSatisfactionService {
 
    @Resource
    private ClientSatisfactionAnalyseFileMapper clientSatisfactionAnalyseFileMapper;
 
    @Resource
    private ClientSatisfactionAttachmentMapper clientSatisfactionAttachmentMapper;
 
    @Resource
    private UserMapper userMapper;
 
    @Value("${file.path}")
    private String imgUrl;
 
    @Value("${wordUrl}")
    private String wordUrl;
 
    /**
     * 客户满意度调查列表
     *
     * @param page
     * @param clientSatisfaction
     * @return
     */
    @Override
    public IPage<ClientSatisfaction> pageClientSatisfaction(Page page, ClientSatisfaction clientSatisfaction) {
        return baseMapper.pageClientSatisfaction(page, QueryWrappers.queryWrappers(clientSatisfaction));
    }
 
    /**
     * 新增客户分析附件
     *
     * @param file
     * @return
     */
    @Override
    public boolean uploadAnalyseFile(MultipartFile file) {
 
        String urlString;
        String pathName;
        String path;
        String filename = file.getOriginalFilename();
        String contentType = file.getContentType();
        ClientSatisfactionAnalyseFile analyseFile = new ClientSatisfactionAnalyseFile();
        analyseFile.setFileName(filename);
        if (contentType != null && contentType.startsWith("image/")) {
            // 是图片
            path = imgUrl;
            analyseFile.setType(1);
        } else {
            // 是文件
            path = wordUrl;
            analyseFile.setType(2);
        }
        try {
            File realpath = new File(path);
            if (!realpath.exists()) {
                realpath.mkdirs();
            }
            pathName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmss")) + "_" + file.getOriginalFilename();
            urlString = realpath + "/" + pathName;
            file.transferTo(new File(urlString));
            analyseFile.setFileUrl(pathName);
            clientSatisfactionAnalyseFileMapper.insert(analyseFile);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            throw new ErrorException(e.getMessage());
        }
    }
 
    /**
     * 查询客户分析附件
     *
     * @param page
     * @param analyseFile
     * @return
     */
    @Override
    public IPage<ClientSatisfactionAnalyseFile> pageAnalyseFile(Page page, ClientSatisfactionAnalyseFile analyseFile) {
        return clientSatisfactionAnalyseFileMapper.pageAnalyseFile(page, QueryWrappers.queryWrappers(analyseFile));
    }
 
    /**
     * 客户满意度导出
     *
     * @param clientSatisfactionId
     * @param response
     */
    @Override
    public void exportWordClientSatisfaction(Integer clientSatisfactionId, HttpServletResponse response) {
        // 查询客户满意度调查
        ClientSatisfactionDto clientSatisfaction = baseMapper.exportWordClientSatisfaction(clientSatisfactionId);
        if (clientSatisfaction == null) {
            throw new RuntimeException("客户满意度调查不存在");
        }
        // 查询确认人的签名
        String confirmPersonUrl = null;
        // 状态为确认且确认人不为空
        if (clientSatisfaction.getConfirmStatus() != null && clientSatisfaction.getConfirmStatus() != 0 && clientSatisfaction.getConfirmPersonId() != null) {
            confirmPersonUrl = userMapper.selectById(clientSatisfaction.getConfirmPersonId()).getSignatureUrl();
            if (StringUtils.isBlank(confirmPersonUrl)) {
                throw new ErrorException("找不到验证人的签名");
            }
        }
        // 获取路径
        InputStream inputStream = this.getClass().getResourceAsStream("/static/customer-satisfaction-questionnaire.docx");
        ConfigureBuilder builder = Configure.builder();
        String finalConfirmPersonUrl = confirmPersonUrl;
        XWPFTemplate template = XWPFTemplate.compile(inputStream, builder.build()).render(
                new HashMap<String, Object>() {{
                    put("clientSatisfaction", clientSatisfaction);
                    put("finalConfirmPersonUrl", StringUtils.isNotBlank(finalConfirmPersonUrl) ? Pictures.ofLocal(imgUrl + "/" + finalConfirmPersonUrl).create() : null);
                }});
 
        try {
            response.setContentType("application/msword");
            String fileName = URLEncoder.encode(
                    "客户满意度", "UTF-8");
            response.setHeader("Content-disposition",
                    "attachment;filename=" + fileName + ".docx");
            OutputStream os = response.getOutputStream();
            template.write(os);
            os.flush();
            os.close();
            inputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("导出失败");
        }
    }
 
    /**
     * 客户满意度年度汇总导出
     *
     * @param year     汇总年度
     * @param response
     */
    @Override
    public void exportAnnualSummary(Integer year, HttpServletResponse response) {
        int summaryYear = year == null ? LocalDate.now().getYear() : year;
        List<ClientSatisfaction> list = baseMapper.selectList(new QueryWrapper<ClientSatisfaction>().lambda()
                .ge(ClientSatisfaction::getFillDate, LocalDate.of(summaryYear, 1, 1))
                .le(ClientSatisfaction::getFillDate, LocalDate.of(summaryYear, 12, 31))
                .isNotNull(ClientSatisfaction::getTotalScore)
                .orderByAsc(ClientSatisfaction::getFillDate)
                .orderByAsc(ClientSatisfaction::getClientSatisfactionId));
        if (list.isEmpty()) {
            throw new ErrorException(summaryYear + "年度暂无满意度记录");
        }
        InputStream inputStream = this.getClass().getResourceAsStream("/static/customer-satisfaction-annual-summary.docx");
        if (inputStream == null) {
            throw new ErrorException("年度汇总模板不存在");
        }
        try (InputStream is = inputStream) {
            XWPFDocument loaded = new XWPFDocument(is);
            XWPFDocument document = fillAnnualSummaryTable(loaded, summaryYear, list);
            try (XWPFDocument doc = document) {
                response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
                String fileName = URLEncoder.encode("客户满意度年度汇总表_" + summaryYear + "年.docx", "UTF-8");
                response.setHeader("Content-disposition", "attachment;filename=" + fileName);
                try (OutputStream os = response.getOutputStream()) {
                    doc.write(os);
                    os.flush();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("导出失败");
        }
    }
 
    /**
     * 按年度汇总模板填充明细与统计数据
     */
    private XWPFDocument fillAnnualSummaryTable(XWPFDocument document, int year, List<ClientSatisfaction> list) throws IOException {
        XWPFTable table = document.getTables().get(0);
 
        // 模板固定每页 17 行明细:不足不删空行,超过则另起一页,重复标题与表头后继续填
        int dataStart = 3;
        int templateRows = 17;
        int total = list.size();
        int pages = Math.max(1, (total + templateRows - 1) / templateRows);
        if (pages > 1) {
            // 在 XML 层插入续页行;插入后需重新加载文档,POI 的行/单元格包装类才会刷新
            CTTbl ctTbl = table.getCTTbl();
            CTRow titleRow = table.getRow(0).getCtRow();
            CTRow headerRow1 = table.getRow(1).getCtRow();
            CTRow headerRow2 = table.getRow(2).getCtRow();
            CTRow dataRowSample = table.getRow(dataStart + templateRows - 1).getCtRow();
            int insertPos = dataStart + templateRows;
            for (int page = 1; page < pages; page++) {
                CTRow newTitleRow = ctTbl.insertNewTr(insertPos++);
                newTitleRow.set(titleRow.copy());
                removeBookmarks(newTitleRow);
                pageBreakBefore(newTitleRow);
                CTRow newHeader1 = ctTbl.insertNewTr(insertPos++);
                newHeader1.set(headerRow1.copy());
                CTRow newHeader2 = ctTbl.insertNewTr(insertPos++);
                newHeader2.set(headerRow2.copy());
                for (int r = 0; r < templateRows; r++) {
                    CTRow newRow = ctTbl.insertNewTr(insertPos++);
                    newRow.set(dataRowSample.copy());
                }
            }
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            document.write(buffer);
            document.close();
            document = new XWPFDocument(new ByteArrayInputStream(buffer.toByteArray()));
            table = document.getTables().get(0);
        }
 
        setCellText(table.getRow(0).getCell(0), "汇总年度:" + year + "年");
        for (int page = 1; page < pages; page++) {
            setCellText(table.getRow(page * (templateRows + 3)).getCell(0), "汇总年度:" + year + "年");
        }
 
        int satisfied = 0;
        for (int i = 0; i < total; i++) {
            ClientSatisfaction item = list.get(i);
            boolean isSatisfied = item.getSatisfactionResult() != null
                    ? item.getSatisfactionResult() == 0
                    : item.getTotalScore() != null && item.getTotalScore() >= 80;
            if (isSatisfied) {
                satisfied++;
            }
            int pageIndex = i / templateRows;
            int indexInPage = i % templateRows;
            XWPFTableRow row = table.getRow(dataStart + pageIndex * (templateRows + 3) + indexInPage);
            setCellText(row.getCell(0), String.valueOf(i + 1));
            setCellText(row.getCell(1), item.getUnitName());
            setCellText(row.getCell(2), isSatisfied ? "√" : "");
            setCellText(row.getCell(3), isSatisfied ? "" : "√");
            String reason = StringUtils.isNotBlank(item.getLabOpinion()) ? item.getLabOpinion() : item.getRemark();
            setCellText(row.getCell(4), isSatisfied ? "" : reason);
        }
 
        XWPFTableRow statRow = table.getRow(dataStart + (pages - 1) * (templateRows + 3) + templateRows + 2);
        setCellText(statRow.getCell(0), "共 " + total + " 份");
        setCellText(statRow.getCell(1), "共 " + satisfied + " 份");
        setCellText(statRow.getCell(2), "共 " + (total - satisfied) + " 份");
        String percent = String.format(Locale.ROOT, "%.1f", satisfied * 100.0 / total);
        if (percent.endsWith(".0")) {
            percent = percent.substring(0, percent.length() - 2);
        }
        setCellText(statRow.getCell(3), percent + "%");
        return document;
    }
 
    /**
     * 续页首行设置段前分页,保证每页格子数固定
     */
    private void pageBreakBefore(CTRow row) {
        if (row.sizeOfTcArray() == 0) {
            return;
        }
        CTTc cell = row.getTcArray(0);
        if (cell.sizeOfPArray() == 0) {
            return;
        }
        CTP paragraph = cell.getPArray(0);
        CTPPr pPr = paragraph.isSetPPr() ? paragraph.getPPr() : paragraph.addNewPPr();
        if (!pPr.isSetPageBreakBefore()) {
            pPr.addNewPageBreakBefore();
        }
    }
 
    /**
     * 删除克隆行内的书签,避免文档出现重复书签
     */
    private void removeBookmarks(CTRow row) {
        removeBookmarkNodes(row.getDomNode());
    }
 
    private void removeBookmarkNodes(Node node) {
        if (node.getNodeType() == Node.ELEMENT_NODE
                && ("bookmarkStart".equals(node.getLocalName()) || "bookmarkEnd".equals(node.getLocalName()))) {
            Node parent = node.getParentNode();
            if (parent != null) {
                parent.removeChild(node);
            }
            return;
        }
        NodeList children = node.getChildNodes();
        for (int i = children.getLength() - 1; i >= 0; i--) {
            removeBookmarkNodes(children.item(i));
        }
    }
 
    /**
     * 填写单元格文本,保留原有段落格式
     */
    private void setCellText(XWPFTableCell cell, String text) {
        while (cell.getParagraphs().size() > 1) {
            cell.removeParagraph(cell.getParagraphs().size() - 1);
        }
        XWPFParagraph paragraph = cell.getParagraphs().get(0);
        for (int i = paragraph.getRuns().size() - 1; i >= 0; i--) {
            paragraph.removeRun(i);
        }
        XWPFRun run = paragraph.createRun();
        run.setText(text == null ? "" : text);
        run.setFontFamily("宋体");
        run.setFontFamily("宋体", XWPFRun.FontCharRange.eastAsia);
        run.setFontSize(10.5);
        cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
    }
 
    /**
     * 确认客户满意度
     *
     * @param clientSatisfaction 要修改客户满意度的状态对象
     * @param userId             修改人id
     */
    @Override
    public void confirmClientSatisfaction(ClientSatisfaction clientSatisfaction, Integer userId) {
        User user = userMapper.selectById(userId);
        if (user != null) {
            clientSatisfaction.setConfirmPerson(user.getName());
            clientSatisfaction.setConfirmPersonId(user.getId());
        }
        clientSatisfaction.setConfirmStatus(1);
        baseMapper.updateById(clientSatisfaction);
    }
 
    /**
     * 查询满意度附件
     *
     * @param clientSatisfactionId 满意度记录id
     * @return
     */
    @Override
    public List<ClientSatisfactionAttachment> selectAttachments(Integer clientSatisfactionId) {
        return clientSatisfactionAttachmentMapper.selectList(new QueryWrapper<ClientSatisfactionAttachment>().lambda()
                .eq(ClientSatisfactionAttachment::getClientSatisfactionId, clientSatisfactionId)
                .orderByAsc(ClientSatisfactionAttachment::getSort, ClientSatisfactionAttachment::getId));
    }
 
    /**
     * 上传满意度附件
     *
     * @param clientSatisfactionId 满意度记录id
     * @param file
     * @return
     */
    @Override
    public ClientSatisfactionAttachment uploadAttachment(Integer clientSatisfactionId, MultipartFile file) {
        if (baseMapper.selectCount(new QueryWrapper<ClientSatisfaction>()
                .eq("client_satisfaction_id", clientSatisfactionId)) == 0) {
            throw new BaseException("客户满意度调查不存在");
        }
        if (file == null || file.isEmpty()) {
            throw new BaseException("附件不能为空");
        }
        // 系统全局上传上限为200MB,这里提前给出友好提示
        if (file.getSize() > 200L * 1024 * 1024) {
            throw new BaseException("附件不能超过200MB");
        }
        String originalName = file.getOriginalFilename();
        if (originalName == null || originalName.trim().isEmpty()) {
            originalName = "未命名文件";
        }
        // 个别浏览器可能带上路径前缀,只保留文件名本身
        originalName = originalName.replaceAll(".*[/\\\\]", "");
        try {
            // 图片放图片目录(/img 静态路径),其余任意格式放 word 目录(/word 静态路径)
            boolean image = originalName.matches("(?i).+\\.(jpg|jpeg|png|gif)$");
            File directory = new File(image ? imgUrl : wordUrl);
            if (!directory.exists()) {
                directory.mkdirs();
            }
            String fileUrl = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmssSSS")) + "-" + originalName;
            file.transferTo(new File(directory, fileUrl));
            ClientSatisfactionAttachment attachment = new ClientSatisfactionAttachment();
            attachment.setClientSatisfactionId(clientSatisfactionId);
            attachment.setFileName(originalName);
            attachment.setFileUrl(fileUrl);
            attachment.setSort(selectAttachments(clientSatisfactionId).size());
            clientSatisfactionAttachmentMapper.insert(attachment);
            return attachment;
        } catch (IOException e) {
            throw new BaseException("附件上传失败");
        }
    }
 
    /**
     * 删除满意度附件
     *
     * @param id 附件id
     * @return
     */
    @Override
    public boolean deleteAttachment(Long id) {
        return clientSatisfactionAttachmentMapper.deleteById(id) > 0;
    }
}