Gqingci 15 時間 前
コミット
64a7032dbe

+ 12 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalCheckController.java

@@ -340,5 +340,17 @@ public class PortalCheckController extends BaseController {
         private Boolean submitted;
         /** 候选人姓名(用于回显) */
         private String candidateName;
+        /** 应聘职位(用于回显) */
+        private String applyPosition;
+        /** 毕业院校(用于回显) */
+        private String gradSchool;
+        /** 学历证书编号(用于回显) */
+        private String eduCertNo;
+        /** 学历核实结果: 属实/不属实/无法核实 */
+        private String eduCheckResult;
+        /** 工作单位核实结果: 属实/部分属实/不属实 */
+        private String companyCheckResult;
+        /** 表现核实结果: 良好/一般/需关注 */
+        private String performCheckResult;
     }
 }

+ 17 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalOssController.java

@@ -9,12 +9,17 @@ import org.dromara.system.domain.vo.SysOssUploadVo;
 import org.dromara.system.domain.vo.SysOssVo;
 import org.dromara.system.service.ISysOssService;
 import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestPart;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.Arrays;
+import java.util.List;
+
 /**
  * 门户文件上传Controller(公开接口,无需登录)
  *
@@ -45,4 +50,16 @@ public class PortalOssController extends BaseController {
         uploadVo.setOssId(oss.getOssId().toString());
         return R.ok(uploadVo);
     }
+
+    /**
+     * 根据ossId获取文件信息
+     */
+    @GetMapping("/listByIds/{ossIds}")
+    public R<List<SysOssVo>> listByIds(@PathVariable String ossIds) {
+        List<Long> idList = Arrays.stream(ossIds.split(","))
+            .map(Long::parseLong)
+            .toList();
+        List<SysOssVo> list = ossService.listByIds(idList);
+        return R.ok(list);
+    }
 }

+ 6 - 4
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/WithdrawController.java

@@ -57,16 +57,18 @@ public class WithdrawController extends BaseController {
     @SaCheckPermission("system:withdraw:audit")
     @Log(title = "提现管理", businessType = BusinessType.UPDATE)
     @PostMapping("/audit/pass/{id}")
-    public R<Void> auditPass(@PathVariable Long id, @RequestParam(required = false) String remark) {
-        withdrawService.auditAndTransfer(id, remark);
+    public R<Void> auditPass(@PathVariable Long id, @RequestParam(required = false) String remark,
+                             @RequestParam(required = false) String fileUrl) {
+        withdrawService.auditAndTransfer(id, remark, fileUrl);
         return R.ok("审核通过,打款成功");
     }
 
     @SaCheckPermission("system:withdraw:audit")
     @Log(title = "提现管理", businessType = BusinessType.UPDATE)
     @PostMapping("/audit/reject/{id}")
-    public R<Void> auditReject(@PathVariable Long id, @RequestParam String remark) {
-        withdrawService.auditReject(id, remark);
+    public R<Void> auditReject(@PathVariable Long id, @RequestParam String remark,
+                               @RequestParam(required = false) String fileUrl) {
+        withdrawService.auditReject(id, remark, fileUrl);
         return R.ok("审核拒绝成功");
     }
 }

+ 2 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/Withdraw.java

@@ -45,6 +45,8 @@ public class Withdraw extends BaseEntity {
 
     private String failReason;
 
+    private String fileUrl;
+
     private Date transferTime;
 
     @TableLogic

+ 4 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/BackCheckReportSubmitBo.java

@@ -11,6 +11,10 @@ import java.util.List;
 @Data
 public class BackCheckReportSubmitBo {
 
+    // ===== 0. 审核状态 =====
+    /** 报告状态: 2=已审核(通过), 3=已拒绝(不通过) */
+    private Integer reportStatus;
+
     // ===== 1. 候选人基本信息 =====
     private String candidateName;
     private String applyPosition;

+ 6 - 1
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/BackgroundCheckFormBo.java

@@ -23,8 +23,9 @@ public class BackgroundCheckFormBo {
     // ===== 2. 学历核实 =====
     private String gradSchool;
     private String eduCertNo;
-    /** 属实 / 不属实 / 无法核实 */
     private String eduVerifyStatus;
+    /** 学历核实结果: 属实/不属实/无法核实 */
+    private String eduVerifyResult;
 
     // ===== 3. 最近工作单位 =====
     private String companyName;
@@ -35,6 +36,8 @@ public class BackgroundCheckFormBo {
     private String leaveReasonVerify;
     /** 备注/信息来源(社保记录/证明人) */
     private String attachment1Remark;
+    /** 工作单位核实结果: 属实/部分属实/不属实 */
+    private String companyVerifyResult;
 
     // ===== 4. 工作表现评估 - 上级评价 =====
     private String leaderEvalAdvantage;
@@ -55,6 +58,8 @@ public class BackgroundCheckFormBo {
     private String hrEvalTransfer;
     /** 备注/信息来源(访谈记录/补充材料) */
     private String attachment2Remark;
+    /** 表现核实结果: 良好/一般/需关注 */
+    private String evalResult;
 
     // ===== 5. 法务风险 =====
     private Boolean hasNonCompete;

+ 3 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/BackCheckReportVo.java

@@ -11,6 +11,9 @@ import java.util.List;
 @Data
 public class BackCheckReportVo {
 
+    /** 报告状态: 0待填写,1已出具,2已审核,3已拒绝 */
+    private Integer reportStatus;
+
     // ===== 1. 候选人基本信息 =====
     private String candidateName;
     private String applyPosition;

+ 3 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/WithdrawVo.java

@@ -56,6 +56,9 @@ public class WithdrawVo implements Serializable {
     @ExcelProperty(value = "失败原因")
     private String failReason;
 
+    @ExcelProperty(value = "文件")
+    private String fileUrl;
+
     @ExcelProperty(value = "创建时间")
     private Date createTime;
 

+ 2 - 2
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IWithdrawService.java

@@ -18,9 +18,9 @@ public interface IWithdrawService {
 
     Long applyWithdraw(WithdrawBo bo);
 
-    Boolean auditAndTransfer(Long id, String auditRemark);
+    Boolean auditAndTransfer(Long id, String auditRemark, String fileUrl);
 
-    Boolean auditReject(Long id, String auditRemark);
+    Boolean auditReject(Long id, String auditRemark, String fileUrl);
 
     String handleAlipayTransferNotify(Map<String, String> params);
 

+ 63 - 4
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainBackOrderServiceImpl.java

@@ -191,6 +191,7 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
             }
             MainBackOrderVo vo = MapstructUtils.convert(backOrder, MainBackOrderVo.class);
             fillPortalFields(vo, mainOrder);
+            vo.setCandidates(buildPortalCandidates(backOrder.getId()));
             result.add(vo);
         }
         return result;
@@ -764,7 +765,7 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
                     .eq(MainBackRecord::getOrderId, vo.getId())
             );
             boolean allReportIssued = records.stream()
-                .allMatch(r -> r.getReportStatus() != null && r.getReportStatus() == 1);
+                .allMatch(r -> r.getReportStatus() != null && r.getReportStatus() >= 2);
             if (!allReportIssued) {
                 mainStatus = 7; // 报告未全部出具,显示待背调
             }
@@ -1476,6 +1477,7 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
         checkData.setGradSchool(bo.getGradSchool());
         checkData.setEduCertNo(bo.getEduCertNo());
         checkData.setEduVerifyStatus(bo.getEduVerifyStatus());
+        checkData.setEduCheckResult(bo.getEduVerifyResult());
 
         // 最近工作单位
         checkData.setCompanyName(bo.getCompanyName());
@@ -1487,6 +1489,7 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
         checkData.setLastSalary(bo.getLastSalary());
         checkData.setLeaveReasonVerify(bo.getLeaveReasonVerify());
         checkData.setCompanyCheckRemark(bo.getAttachment1Remark());
+        checkData.setCompanyCheckResult(bo.getCompanyVerifyResult());
 
         // 工作表现评估 - 上级评价
         checkData.setLeaderEvalAdvantage(bo.getLeaderEvalAdvantage());
@@ -1504,6 +1507,7 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
         checkData.setHrEvalDispute(bo.getHrEvalDispute());
         checkData.setHrEvalTransfer(bo.getHrEvalTransfer());
         checkData.setPerformCheckRemark(bo.getAttachment2Remark());
+        checkData.setPerformCheckResult(bo.getEvalResult());
 
         // 法务风险
         checkData.setHasNonCompete(bo.getHasNonCompete() != null && bo.getHasNonCompete() ? 1 : 0);
@@ -1571,10 +1575,17 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
     @Override
     public org.dromara.main.controller.PortalCheckController.BgFormStatusVo getBgFormStatus(Long recordId) {
         MainBackRecord record = recordMapper.selectById(recordId);
-        boolean submitted = record != null && record.getReportStatus() != null && record.getReportStatus() == 1;
+        boolean submitted = record != null && record.getReportStatus() != null && record.getReportStatus() >= 1;
         String candidateName = null;
+        String applyPosition = null;
+        String gradSchool = null;
+        String eduCertNo = null;
+        String eduCheckResult = null;
+        String companyCheckResult = null;
+        String performCheckResult = null;
+        MainBackCandidate candidate = null;
         if (record != null && record.getCandidateId() != null) {
-            MainBackCandidate candidate = mainBackCandidateMapper.selectById(record.getCandidateId());
+            candidate = mainBackCandidateMapper.selectById(record.getCandidateId());
             if (candidate != null && candidate.getStudentId() != null) {
                 MainStudent student = mainStudentMapper.selectById(candidate.getStudentId());
                 if (student != null) {
@@ -1582,7 +1593,55 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
                 }
             }
         }
-        return new org.dromara.main.controller.PortalCheckController.BgFormStatusVo(submitted, candidateName);
+        // 从已保存的表单数据获取应聘职位、毕业院校、证书编号、三个核实结果
+        if (record != null) {
+            MainBackCheckData checkData = mainBackCheckDataMapper.selectOne(
+                new LambdaQueryWrapper<MainBackCheckData>()
+                    .eq(MainBackCheckData::getRecordId, recordId)
+                    .last("limit 1")
+            );
+            if (checkData != null) {
+                if (StringUtils.isNotBlank(checkData.getApplyPosition())) {
+                    applyPosition = checkData.getApplyPosition();
+                }
+                if (StringUtils.isNotBlank(checkData.getGradSchool())) {
+                    gradSchool = checkData.getGradSchool();
+                }
+                if (StringUtils.isNotBlank(checkData.getEduCertNo())) {
+                    eduCertNo = checkData.getEduCertNo();
+                }
+                if (StringUtils.isNotBlank(checkData.getEduCheckResult())) {
+                    eduCheckResult = checkData.getEduCheckResult();
+                }
+                if (StringUtils.isNotBlank(checkData.getCompanyCheckResult())) {
+                    companyCheckResult = checkData.getCompanyCheckResult();
+                }
+                if (StringUtils.isNotBlank(checkData.getPerformCheckResult())) {
+                    performCheckResult = checkData.getPerformCheckResult();
+                }
+            }
+        }
+        // 表单数据中没有应聘职位,则从候选人关联的岗位获取
+        if (applyPosition == null && candidate != null && candidate.getPostId() != null) {
+            MainPosition position = mainPositionMapper.selectById(candidate.getPostId());
+            if (position != null && StringUtils.isNotBlank(position.getPostName())) {
+                applyPosition = position.getPostName();
+            }
+        }
+        // 表单数据中没有毕业院校,则从学员教育经历获取
+        if (gradSchool == null && candidate != null && candidate.getStudentId() != null) {
+            MainStudentEducation education = mainStudentEducationMapper.selectOne(
+                Wrappers.<MainStudentEducation>lambdaQuery()
+                    .eq(MainStudentEducation::getStudentId, candidate.getStudentId())
+                    .orderByDesc(MainStudentEducation::getEndTime)
+                    .orderByDesc(MainStudentEducation::getCreateTime)
+                    .last("limit 1")
+            );
+            if (education != null && StringUtils.isNotBlank(education.getSchool())) {
+                gradSchool = education.getSchool();
+            }
+        }
+        return new org.dromara.main.controller.PortalCheckController.BgFormStatusVo(submitted, candidateName, applyPosition, gradSchool, eduCertNo, eduCheckResult, companyCheckResult, performCheckResult);
     }
 
     private static class ReportContext {

+ 8 - 2
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainBackRecordServiceImpl.java

@@ -90,6 +90,12 @@ public class MainBackRecordServiceImpl implements IMainBackRecordService {
     public BackCheckReportVo getBackCheckReport(Long recordId) {
         BackCheckReportVo vo = new BackCheckReportVo();
 
+        // 0. 查询 record 获取 reportStatus
+        MainBackRecord record = baseMapper.selectById(recordId);
+        if (record != null) {
+            vo.setReportStatus(record.getReportStatus());
+        }
+
         // 1. 查询表单数据
         MainBackCheckData checkData = checkDataMapper.selectOne(
             new LambdaQueryWrapper<MainBackCheckData>()
@@ -242,10 +248,10 @@ public class MainBackRecordServiceImpl implements IMainBackRecordService {
             }
         }
 
-        // 5. 更新 record 状态为 2(已提交
+        // 5. 更新 record 状态(根据前端传入的 reportStatus:2=已审核,3=已拒绝
         MainBackRecord update = new MainBackRecord();
         update.setId(recordId);
-        update.setReportStatus(2);
+        update.setReportStatus(bo.getReportStatus() != null ? bo.getReportStatus() : 2);
         update.setReportUrl(null);
         update.setFinishTime(LocalDateTime.now());
         baseMapper.updateById(update);

+ 5 - 3
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/WithdrawServiceImpl.java

@@ -132,7 +132,7 @@ public class WithdrawServiceImpl implements IWithdrawService {
 
         if (withdrawConfig.isAutoAudit() && shouldAutoAudit(company.getId(), amount)) {
             try {
-                auditAndTransfer(withdraw.getId(), "系统自动审核通过");
+                auditAndTransfer(withdraw.getId(), "系统自动审核通过", null);
             } catch (Exception ignored) {
                 // 自动审核失败时保留待审核状态,转人工处理
             }
@@ -143,7 +143,7 @@ public class WithdrawServiceImpl implements IWithdrawService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public Boolean auditAndTransfer(Long id, String auditRemark) {
+    public Boolean auditAndTransfer(Long id, String auditRemark, String fileUrl) {
         Withdraw withdraw = withdrawMapper.selectById(id);
         if (withdraw == null) {
             throw new ServiceException("提现申请不存在");
@@ -162,6 +162,7 @@ public class WithdrawServiceImpl implements IWithdrawService {
         withdraw.setAuditorId(LoginHelper.getUserId());
         withdraw.setAuditTime(new Date());
         withdraw.setAuditRemark(auditRemark);
+        withdraw.setFileUrl(fileUrl);
         withdrawMapper.updateById(withdraw);
 
         try {
@@ -207,7 +208,7 @@ public class WithdrawServiceImpl implements IWithdrawService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public Boolean auditReject(Long id, String auditRemark) {
+    public Boolean auditReject(Long id, String auditRemark, String fileUrl) {
         Withdraw withdraw = withdrawMapper.selectById(id);
         if (withdraw == null) {
             throw new ServiceException("提现申请不存在");
@@ -222,6 +223,7 @@ public class WithdrawServiceImpl implements IWithdrawService {
         withdraw.setAuditorId(LoginHelper.getUserId());
         withdraw.setAuditTime(new Date());
         withdraw.setAuditRemark(auditRemark);
+        withdraw.setFileUrl(fileUrl);
         withdrawMapper.updateById(withdraw);
         return true;
     }

+ 146 - 43
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/WordReportService.java

@@ -227,7 +227,7 @@ public class WordReportService {
 
     /**
      * 替换段落中的普通 {{placeholder}} 占位符和 {{CB_xxx}} checkbox 占位符。
-     * CB_ 占位符会被转换为 ☑/☐ 纯文本内联替换,支持与普通文字在同一段落中
+     * CB_ 占位符会为每个选项创建独立的 run:checkbox 字符用 Wingdings 字体,标签用普通字体
      */
     private void replacePlaceholdersInParagraph(XWPFParagraph paragraph, Map<String, String> placeholders) {
         String fullText = paragraph.getText();
@@ -249,52 +249,142 @@ public class WordReportService {
         }
 
         String concatenated = sb.toString();
-        // 先检查是否有普通占位符或 CB_ 占位符
         Matcher placeholderMatcher = PLACEHOLDER_PATTERN.matcher(concatenated);
         if (!placeholderMatcher.find()) {
             return;
         }
 
-        // 替换所有占位符(包括 CB_ 前缀的),CB_ 的值转换为 ☑/☐ 纯文本
-        String replaced = PLACEHOLDER_PATTERN.matcher(concatenated).replaceAll(mr -> {
-            String key = mr.group(1).trim();
-            String value = placeholders.getOrDefault(key, " ");
-            if (value.isEmpty()) {
-                return " ";
-            }
-            // 如果是 CB_ 前缀的占位符,将 checkbox 格式转为 ☑/☐ 纯文本
-            if (key.startsWith("CB_")) {
-                return Matcher.quoteReplacement(convertCheckboxToPlainText(value));
+        // 检查是否包含 CB_ 占位符
+        boolean hasCheckboxPlaceholder = false;
+        Matcher cbCheck = PLACEHOLDER_PATTERN.matcher(concatenated);
+        while (cbCheck.find()) {
+            if (cbCheck.group(1).trim().startsWith("CB_")) {
+                hasCheckboxPlaceholder = true;
+                break;
             }
-            return Matcher.quoteReplacement(value);
-        });
-
-        if (replaced.contains("\n")) {
-            String[] lines = replaced.split("\n", -1);
-            setRunTextSafely(runs.get(0), lines[0]);
-            for (int i = 1; i < runs.size(); i++) {
-                setRunTextSafely(runs.get(i), "");
+        }
+
+        if (!hasCheckboxPlaceholder) {
+            // 纯文本替换,无 checkbox
+            String replaced = PLACEHOLDER_PATTERN.matcher(concatenated).replaceAll(mr -> {
+                String key = mr.group(1).trim();
+                String value = placeholders.getOrDefault(key, " ");
+                if (value.isEmpty()) return " ";
+                return Matcher.quoteReplacement(value);
+            });
+
+            if (replaced.contains("\n")) {
+                String[] lines = replaced.split("\n", -1);
+                setRunTextSafely(runs.get(0), lines[0]);
+                for (int i = 1; i < runs.size(); i++) {
+                    setRunTextSafely(runs.get(i), "");
+                }
+                for (int i = 1; i < lines.length; i++) {
+                    runs.get(0).addBreak();
+                    runs.get(0).setText(lines[i]);
+                }
+            } else {
+                setRunTextSafely(runs.get(0), replaced);
+                for (int i = 1; i < runs.size(); i++) {
+                    setRunTextSafely(runs.get(i), "");
+                }
             }
-            for (int i = 1; i < lines.length; i++) {
-                runs.get(0).addBreak();
-                runs.get(0).setText(lines[i]);
+            return;
+        }
+
+        // 包含 CB_ 占位符:需要按片段拆分,每个片段用独立 run
+        // 先清空所有现有 run 的文本
+        for (XWPFRun run : runs) {
+            setRunTextSafely(run, "");
+        }
+
+        // 获取第一个 run 的格式信息作为参考
+        XWPFRun firstRun = runs.get(0);
+        Double fontSize = firstRun.getFontSizeAsDouble();
+        String fontFamily = firstRun.getFontFamily();
+
+        // 逐段解析拼接文本,将 CB_ 和非 CB_ 部分分别处理
+        Matcher matcher = PLACEHOLDER_PATTERN.matcher(concatenated);
+        int lastEnd = 0;
+        List<XWPFRun> newRuns = new ArrayList<>();
+        // 复用第一个 run
+        newRuns.add(firstRun);
+        int currentRunIndex = 0;
+        boolean isFirstText = true;
+
+        while (matcher.find()) {
+            // 占位符之前的普通文本
+            if (matcher.start() > lastEnd) {
+                String beforeText = concatenated.substring(lastEnd, matcher.start());
+                XWPFRun textRun = getOrCreateRun(paragraph, newRuns, currentRunIndex++, isFirstText);
+                isFirstText = false;
+                textRun.setFontFamily(fontFamily);
+                if (fontSize != null) textRun.setFontSize(fontSize);
+                setRunTextSafely(textRun, beforeText);
             }
-        } else {
-            setRunTextSafely(runs.get(0), replaced);
-            for (int i = 1; i < runs.size(); i++) {
-                setRunTextSafely(runs.get(i), "");
+
+            String key = matcher.group(1).trim();
+            String value = placeholders.getOrDefault(key, " ");
+
+            if (key.startsWith("CB_")) {
+                // CB_ 占位符:为每个选项创建 Wingdings checkbox run + 普通标签 run
+                List<RunFragment> fragments = buildCheckboxFragments(value);
+                for (RunFragment frag : fragments) {
+                    XWPFRun run = getOrCreateRun(paragraph, newRuns, currentRunIndex++, isFirstText);
+                    isFirstText = false;
+                    if (frag.isCheckbox) {
+                        setWingdingsFont(run);
+                        if (fontSize != null) run.setFontSize(fontSize);
+                    } else {
+                        run.setFontFamily(fontFamily);
+                        if (fontSize != null) run.setFontSize(fontSize);
+                    }
+                    setRunTextSafely(run, frag.text);
+                }
+            } else {
+                // 普通占位符
+                XWPFRun textRun = getOrCreateRun(paragraph, newRuns, currentRunIndex++, isFirstText);
+                isFirstText = false;
+                textRun.setFontFamily(fontFamily);
+                if (fontSize != null) textRun.setFontSize(fontSize);
+                setRunTextSafely(textRun, value.isEmpty() ? " " : value);
             }
+
+            lastEnd = matcher.end();
+        }
+
+        // 剩余普通文本
+        if (lastEnd < concatenated.length()) {
+            String remaining = concatenated.substring(lastEnd);
+            XWPFRun textRun = getOrCreateRun(paragraph, newRuns, currentRunIndex++, isFirstText);
+            textRun.setFontFamily(fontFamily);
+            if (fontSize != null) textRun.setFontSize(fontSize);
+            setRunTextSafely(textRun, remaining);
+        }
+    }
+
+    /** 文本片段,标记是否为 checkbox(需要 Wingdings 字体) */
+    private static class RunFragment {
+        final String text;
+        final boolean isCheckbox;
+        RunFragment(String text, boolean isCheckbox) {
+            this.text = text;
+            this.isCheckbox = isCheckbox;
         }
     }
 
     /**
-     * 将 CB_ checkbox 格式的值转换为纯文本。
+     * 将 CB_ checkbox 格式的值拆分为 RunFragment 列表。
+     * 每个 checkbox 符号(☑/☐)和标签文字分别作为独立片段,
+     * checkbox 用 Wingdings 字体,标签用普通字体。
      * 输入格式: "选项1,选项2,选项3|SELECTED:选中项"
-     * 输出格式: "☑ 选项1 ☐ 选项2 ☐ 选项3"
      */
-    private String convertCheckboxToPlainText(String value) {
+    private List<RunFragment> buildCheckboxFragments(String value) {
+        List<RunFragment> fragments = new ArrayList<>();
+
         if (value == null || value.isEmpty()) {
-            return " ";
+            fragments.add(new RunFragment(" ", false));
+            return fragments;
         }
 
         String optionsPart = value;
@@ -316,20 +406,33 @@ public class WordReportService {
             }
         }
 
-        StringBuilder result = new StringBuilder();
-        for (String option : options) {
-            option = option.trim();
-            if (result.length() > 0) {
-                result.append(" ");
-            }
-            if (selectedSet.contains(option)) {
-                result.append("☑ ");
-            } else {
-                result.append("☐ ");
+        for (int i = 0; i < options.length; i++) {
+            String option = options[i].trim();
+            if (i > 0) {
+                // 选项间加空格(用普通字体)
+                fragments.add(new RunFragment(" ", false));
             }
-            result.append(option);
+            // checkbox 符号用 Wingdings 字体
+            char boxChar = selectedSet.contains(option) ? WINGDINGS_CHECKED : WINGDINGS_UNCHECKED;
+            fragments.add(new RunFragment(String.valueOf(boxChar), true));
+            // 标签用普通字体
+            fragments.add(new RunFragment(option, false));
+        }
+
+        return fragments;
+    }
+
+    /**
+     * 获取或创建 run。
+     * 如果 newRuns 列表中有足够的 run 则复用,否则新建。
+     */
+    private XWPFRun getOrCreateRun(XWPFParagraph paragraph, List<XWPFRun> newRuns, int index, boolean isFirst) {
+        if (index < newRuns.size()) {
+            return newRuns.get(index);
         }
-        return result.toString();
+        XWPFRun newRun = paragraph.createRun();
+        newRuns.add(newRun);
+        return newRun;
     }
 
     private void setRunTextSafely(XWPFRun run, String text) {