cnas-manage/src/main/java/com/ruoyi/manage/controller/ClientSatisfactionController.java
@@ -52,6 +52,10 @@ @Operation(summary = "客户满意度调查新增") @PostMapping("/addClientSatisfaction") public Result addClientSatisfaction(@RequestBody ClientSatisfaction clientSatisfaction){ Result<?> error = fillAssessment(clientSatisfaction); if (error != null) { return error; } clientSatisfactionService.save(clientSatisfaction); // 返回保存后的记录(含自增id),前端据此上传新增时暂存的照片 return Result.success(clientSatisfaction); @@ -64,7 +68,35 @@ @Operation(summary = "客户满意度调查新增") @PostMapping("/updateClientSatisfaction") public Result updateClientSatisfaction(@RequestBody ClientSatisfaction clientSatisfaction){ Result<?> error = fillAssessment(clientSatisfaction); if (error != null) { return error; } return Result.success(clientSatisfactionService.updateById(clientSatisfaction)); } /** * 根据10道题的评分计算总分和满意度评定(80分及以上为满意) */ private Result<?> fillAssessment(ClientSatisfaction clientSatisfaction) { Integer[] scores = {clientSatisfaction.getScore1(), clientSatisfaction.getScore2(), clientSatisfaction.getScore3(), clientSatisfaction.getScore4(), clientSatisfaction.getScore5(), clientSatisfaction.getScore6(), clientSatisfaction.getScore7(), clientSatisfaction.getScore8(), clientSatisfaction.getScore9(), clientSatisfaction.getScore10()}; int total = 0; for (int i = 0; i < scores.length; i++) { if (scores[i] == null) { return Result.fail("请完成第" + (i + 1) + "题评分"); } if (scores[i] < 5 || scores[i] > 10) { return Result.fail("评分必须在5到10分之间"); } total += scores[i]; } clientSatisfaction.setTotalScore(total); clientSatisfaction.setSatisfactionResult(total >= 80 ? 0 : 1); return null; } /** @@ -93,6 +125,16 @@ } /** * 客户满意度年度汇总导出 * @param year 汇总年度 */ @Operation(summary = "客户满意度年度汇总导出") @GetMapping("/exportAnnualSummary") public void exportAnnualSummary(Integer year, HttpServletResponse response){ clientSatisfactionService.exportAnnualSummary(year, response); } /** * 确认客户满意度 * @param clientSatisfaction 要修改客户满意度的状态对象 * @param userId 修改人id cnas-manage/src/main/java/com/ruoyi/manage/pojo/ClientSatisfaction.java
@@ -37,6 +37,58 @@ @Schema(description = "联系电话") private String contactNumber; @Schema(description = "地址") private String address; @Schema(description = "联系人") private String contactPerson; @Schema(description = "第1题评分") private Integer score1; @Schema(description = "第2题评分") private Integer score2; @Schema(description = "第3题评分") private Integer score3; @Schema(description = "第4题评分") private Integer score4; @Schema(description = "第5题评分") private Integer score5; @Schema(description = "第6题评分") private Integer score6; @Schema(description = "第7题评分") private Integer score7; @Schema(description = "第8题评分") private Integer score8; @Schema(description = "第9题评分") private Integer score9; @Schema(description = "第10题评分") private Integer score10; @Schema(description = "总分") private Integer totalScore; @Schema(description = "满意度评定, 0:满意, 1:不满意") private Integer satisfactionResult; @Schema(description = "实验室意见") private String labOpinion; @Schema(description = "质量负责人") private String qualityManager; @Schema(description = "实验室填写日期") @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate labDate; @Schema(description = "服务态度, 0:满意, 1:一般, 2:不满意") private Integer serviceAttitude; cnas-manage/src/main/java/com/ruoyi/manage/service/ClientSatisfactionService.java
@@ -51,6 +51,14 @@ void exportWordClientSatisfaction(Integer clientSatisfactionId, HttpServletResponse response); /** * 客户满意度年度汇总导出 * * @param year 汇总年度 * @param response */ void exportAnnualSummary(Integer year, HttpServletResponse response); /** * 确认客户满意度 * * @param clientSatisfaction 要修改客户满意度的状态对象 cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ClientSatisfactionServiceImpl.java
@@ -22,20 +22,37 @@ 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; /** @@ -178,6 +195,182 @@ } /** * 客户满意度年度汇总导出 * * @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 要修改客户满意度的状态对象 cnas-manage/src/main/resources/static/customer-satisfaction-annual-summary.docxBinary files differ
inspect-server/src/main/java/com/ruoyi/inspect/controller/InsOrderRatesController.java
@@ -5,12 +5,14 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.common.core.domain.Result; import com.ruoyi.inspect.dto.SampleOrderDto; import com.ruoyi.inspect.pojo.InsOrder; import com.ruoyi.inspect.service.InsOrderRatesService; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import java.math.BigDecimal; /** @@ -36,8 +38,44 @@ */ @Operation(summary = "查询订单列表") @GetMapping("/selectRatesPage") public Result selectInsOrderRates(Page page, SampleOrderDto sampleOrderDto) { return Result.success(insOrderRatesService.selectInsOrderRates(page, sampleOrderDto)); public Result selectInsOrderRates(Page page, SampleOrderDto sampleOrderDto, String startMonth, String endMonth) { return Result.success(insOrderRatesService.selectInsOrderRates(page, sampleOrderDto, startMonth, endMonth)); } /** * 更新费用优惠与备注 * @param insOrder * @return */ @Operation(summary = "更新费用优惠与备注") @PostMapping("/updateFee") public Result<?> updateFee(@RequestBody InsOrder insOrder) { if (insOrder.getId() == null) { return Result.fail("订单id不能为空"); } Integer isDiscount = insOrder.getIsDiscount(); if (isDiscount == null || (isDiscount != 0 && isDiscount != 1)) { return Result.fail("是否优惠参数不正确"); } BigDecimal discountAmount = insOrder.getDiscountAmount(); if (isDiscount == 1) { if (discountAmount == null) { return Result.fail("有优惠时请填写优惠后金额"); } if (discountAmount.compareTo(BigDecimal.ZERO) < 0) { return Result.fail("优惠后金额不能小于0"); } } else { discountAmount = null; } String feeRemark = insOrder.getFeeRemark(); if (feeRemark != null && feeRemark.length() > 500) { return Result.fail("备注不能超过500字"); } boolean success = insOrderRatesService.updateOrderFee(insOrder.getId(), isDiscount, discountAmount, feeRemark); return success ? Result.success() : Result.fail("保存失败,订单不存在"); } inspect-server/src/main/java/com/ruoyi/inspect/mapper/InsOrderRatesMapper.java
@@ -9,6 +9,8 @@ import com.ruoyi.inspect.pojo.InsOrderRates; import org.apache.ibatis.annotations.Param; import java.math.BigDecimal; /** * 订单费用表 * @@ -24,6 +26,15 @@ IPage<SampleOrderDto> selectInsOrderRates(Page page, @Param("ew") QueryWrapper<SampleOrderDto> ew); /** * 更新订单的费用优惠与备注 * @return */ int updateOrderFee(@Param("id") Integer id, @Param("isDiscount") Integer isDiscount, @Param("discountAmount") BigDecimal discountAmount, @Param("feeRemark") String feeRemark); /** * 查询订单费用详情(模板价格) * @return */ inspect-server/src/main/java/com/ruoyi/inspect/pojo/InsOrder.java
@@ -9,6 +9,7 @@ import org.springframework.format.annotation.DateTimeFormat; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -86,6 +87,15 @@ @Schema(description = "备注英文") private String remarkEn; @Schema(description = "是否优惠 0无 1有") private Integer isDiscount; @Schema(description = "优惠后金额") private BigDecimal discountAmount; @Schema(description = "费用备注") private String feeRemark; @Schema(description = "OTC订单号") private String otcCode; inspect-server/src/main/java/com/ruoyi/inspect/service/InsOrderRatesService.java
@@ -7,6 +7,8 @@ import com.ruoyi.inspect.dto.TemplateRateDto; import com.ruoyi.inspect.pojo.InsOrderRates; import java.math.BigDecimal; /** * 订单费用表 * @@ -16,12 +18,20 @@ public interface InsOrderRatesService extends IService<InsOrderRates> { /** * * 查询订单费用列表(可按送样月份区间筛选) * @param page * @param sampleOrderDto * @param startMonth 起始月份 yyyy-MM * @param endMonth 结束月份 yyyy-MM * @return */ IPage<SampleOrderDto> selectInsOrderRates(Page page, SampleOrderDto sampleOrderDto); IPage<SampleOrderDto> selectInsOrderRates(Page page, SampleOrderDto sampleOrderDto, String startMonth, String endMonth); /** * 更新订单的费用优惠与备注 * @return */ boolean updateOrderFee(Integer id, Integer isDiscount, BigDecimal discountAmount, String feeRemark); /** * 查询订单费用详情(模板价格) inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderRatesServiceImpl.java
@@ -1,15 +1,23 @@ package com.ruoyi.inspect.service.impl; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.common.utils.QueryWrappers; import com.ruoyi.framework.exception.ErrorException; import com.ruoyi.inspect.dto.SampleOrderDto; import com.ruoyi.inspect.dto.TemplateRateDto; import com.ruoyi.inspect.mapper.InsOrderRatesMapper; import com.ruoyi.inspect.pojo.InsOrderRates; import com.ruoyi.inspect.service.InsOrderRatesService; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.time.YearMonth; import java.time.format.DateTimeParseException; /** * 订单费用表 @@ -21,14 +29,37 @@ public class InsOrderRatesServiceImpl extends ServiceImpl<InsOrderRatesMapper, InsOrderRates> implements InsOrderRatesService { /** * 查询订单费用列表 * 查询订单费用列表(可按送样月份区间筛选) * @param page * @param sampleOrderDto * @param startMonth 起始月份 yyyy-MM * @param endMonth 结束月份 yyyy-MM * @return */ @Override public IPage<SampleOrderDto> selectInsOrderRates(Page page, SampleOrderDto sampleOrderDto) { return baseMapper.selectInsOrderRates(page, QueryWrappers.queryWrappers(sampleOrderDto)); public IPage<SampleOrderDto> selectInsOrderRates(Page page, SampleOrderDto sampleOrderDto, String startMonth, String endMonth) { QueryWrapper<SampleOrderDto> wrapper = QueryWrappers.queryWrappers(sampleOrderDto); if (wrapper == null) { wrapper = Wrappers.query(); } if (StrUtil.isNotBlank(startMonth) || StrUtil.isNotBlank(endMonth)) { try { if (StrUtil.isNotBlank(startMonth)) { wrapper.ge("sample_date", YearMonth.parse(startMonth).atDay(1)); } if (StrUtil.isNotBlank(endMonth)) { wrapper.le("sample_date", YearMonth.parse(endMonth).atEndOfMonth()); } } catch (DateTimeParseException e) { throw new ErrorException("送样时间筛选格式不正确,应为 yyyy-MM"); } } return baseMapper.selectInsOrderRates(page, wrapper); } @Override public boolean updateOrderFee(Integer id, Integer isDiscount, BigDecimal discountAmount, String feeRemark) { return baseMapper.updateOrderFee(id, isDiscount, discountAmount, feeRemark) > 0; } @Override inspect-server/src/main/resources/mapper/InsOrderRatesMapper.xml
@@ -22,8 +22,18 @@ <if test="ew.customSqlSegment != null and ew.customSqlSegment != ''"> ${ew.customSqlSegment} </if> order by a.sample_date desc, a.id desc </select> <!-- 更新订单的费用优惠与备注 --> <update id="updateOrderFee"> update ins_order set is_discount = #{isDiscount}, discount_amount = #{discountAmount}, fee_remark = #{feeRemark} where id = #{id} </update> <!-- 查询订单费用详情(模板价格) --> <select id="selectTemplateRatesDetail" resultType="com.ruoyi.inspect.dto.TemplateRateDto"> select iost.number, iost.name, COALESCE(st.price, 0) as price