diff --git a/rdms-gateway/src/main/resources/application-local.yaml b/rdms-gateway/src/main/resources/application-local.yaml index c1e4b08..811726f 100644 --- a/rdms-gateway/src/main/resources/application-local.yaml +++ b/rdms-gateway/src/main/resources/application-local.yaml @@ -6,10 +6,10 @@ spring: username: # Nacos 账号 password: # Nacos 密码 discovery: # 【配置中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 + namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP config: # 【注册中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 + namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP #################### 监控相关配置 #################### diff --git a/rdms-project/rdms-project-boot/pom.xml b/rdms-project/rdms-project-boot/pom.xml index e24d952..e6354e2 100644 --- a/rdms-project/rdms-project-boot/pom.xml +++ b/rdms-project/rdms-project-boot/pom.xml @@ -66,9 +66,15 @@ - org.apache.poi - poi-ooxml - 5.4.1 + commons-io + commons-io + 2.22.0 + + + + org.docx4j + docx4j-JAXB-ReferenceImpl + 11.5.14 diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java index b2539fd..5c47b1c 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java @@ -20,6 +20,11 @@ public final class ProjectObjectConstants { */ public static final String MANAGER_ROLE_CODE = "project_manager"; + /** + * 项目技术负责人对象角色编码。 + */ + public static final String TECHNICAL_OWNER_ROLE_CODE = "technical_owner"; + /** * 项目游客对象角色编码。创建人未成为项目成员时,用于返回只读上下文菜单。 */ diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/OvertimeApplicationController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/OvertimeApplicationController.java index 19dc43a..bdeac0d 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/OvertimeApplicationController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/OvertimeApplicationController.java @@ -6,6 +6,8 @@ import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.framework.excel.core.util.ExcelUtils; import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationBatchActionReqVO; +import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationBatchResultRespVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO; @@ -108,6 +110,22 @@ public class OvertimeApplicationController { return success(true); } + @PostMapping("/batch-approve") + @Operation(summary = "批量审核通过加班申请") + @PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')") + public CommonResult batchApprove( + @Valid @RequestBody OvertimeApplicationBatchActionReqVO reqVO) { + return success(overtimeApplicationService.batchApprove(reqVO)); + } + + @PostMapping("/batch-reject") + @Operation(summary = "批量审核退回加班申请") + @PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')") + public CommonResult batchReject( + @Valid @RequestBody OvertimeApplicationBatchActionReqVO reqVO) { + return success(overtimeApplicationService.batchReject(reqVO)); + } + @DeleteMapping("/{id}") @Operation(summary = "删除已退回的加班申请") @PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_DELETE + "')") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationBatchActionReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationBatchActionReqVO.java new file mode 100644 index 0000000..e049a1b --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationBatchActionReqVO.java @@ -0,0 +1,23 @@ +package com.njcn.rdms.module.project.controller.admin.overtime.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 加班申请批量审批 Request VO") +@Data +public class OvertimeApplicationBatchActionReqVO { + + @Schema(description = "加班申请编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3]") + @NotEmpty(message = "加班申请编号列表不能为空") + private List ids; + + @Schema(description = "审批意见。是否必填以状态机配置为准;当前退回必填", + example = "已确认,同意加班") + @Size(max = 1000, message = "审批意见长度不能超过 1000 个字符") + private String reason; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationBatchResultRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationBatchResultRespVO.java new file mode 100644 index 0000000..ff32849 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/overtime/vo/OvertimeApplicationBatchResultRespVO.java @@ -0,0 +1,37 @@ +package com.njcn.rdms.module.project.controller.admin.overtime.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "管理后台 - 加班申请批量审批结果 Response VO") +@Data +public class OvertimeApplicationBatchResultRespVO { + + @Schema(description = "成功处理的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Integer successCount; + + @Schema(description = "失败的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer failCount; + + @Schema(description = "失败的加班申请编号列表") + private List failItems = new ArrayList<>(); + + @Data + @Schema(description = "批量审批失败项") + public static class FailItem { + + @Schema(description = "加班申请编号", example = "3") + @JsonSerialize(using = ToStringSerializer.class) + private Long id; + + @Schema(description = "失败原因", example = "当前状态不允许该操作") + private String reason; + + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationService.java index c67cc10..4c56288 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationService.java @@ -2,6 +2,8 @@ package com.njcn.rdms.module.project.service.overtime; import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationBatchActionReqVO; +import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationBatchResultRespVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO; @@ -22,6 +24,10 @@ public interface OvertimeApplicationService { void reject(Long id, OvertimeApplicationStatusActionReqVO reqVO); + OvertimeApplicationBatchResultRespVO batchApprove(OvertimeApplicationBatchActionReqVO reqVO); + + OvertimeApplicationBatchResultRespVO batchReject(OvertimeApplicationBatchActionReqVO reqVO); + void deleteApplication(Long id); OvertimeApplicationRespVO getApplication(Long id); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationServiceImpl.java index 6b165b1..34cc379 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/overtime/OvertimeApplicationServiceImpl.java @@ -5,9 +5,12 @@ import com.njcn.rdms.framework.common.pojo.PageParam; import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.framework.common.util.json.JsonUtils; import com.njcn.rdms.framework.common.util.object.BeanUtils; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationApprovalRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationBatchActionReqVO; +import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationBatchResultRespVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO; import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO; @@ -37,6 +40,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -137,6 +141,16 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic processApprovalAction(id, OvertimeApplicationConstants.ACTION_REJECT, reqVO); } + @Override + public OvertimeApplicationBatchResultRespVO batchApprove(OvertimeApplicationBatchActionReqVO reqVO) { + return processBatchApprovalAction(reqVO, OvertimeApplicationConstants.ACTION_APPROVE); + } + + @Override + public OvertimeApplicationBatchResultRespVO batchReject(OvertimeApplicationBatchActionReqVO reqVO) { + return processBatchApprovalAction(reqVO, OvertimeApplicationConstants.ACTION_REJECT); + } + @Override @Transactional(rollbackFor = Exception.class) public void deleteApplication(Long id) { @@ -229,6 +243,119 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic writeAuditLog(after, actionCode, fromStatus, transition.getToStatusCode(), null, reason, null); } + private OvertimeApplicationBatchResultRespVO processBatchApprovalAction( + OvertimeApplicationBatchActionReqVO reqVO, String actionCode) { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + String loginUserName = defaultText(SecurityFrameworkUtils.getLoginUserNickname()); + String reason = normalizeNullableText(reqVO.getReason()); + + // 1. 校验状态机流转(统一校验一次,批量场景下 fromStatus 相同) + ObjectStatusTransitionDO transition = validateTransition(OvertimeApplicationConstants.STATUS_PENDING, actionCode, reason); + LocalDateTime approvalTime = LocalDateTime.now(); + + // 2. 批量查询所有申请 + List applications = overtimeApplicationMapper.selectBatchIds(reqVO.getIds()); + Map appMap = applications.stream() + .collect(Collectors.toMap(OvertimeApplicationDO::getId, a -> a)); + + // 3. 分类:可处理的 vs 需跳过的 + List validList = new ArrayList<>(); + OvertimeApplicationBatchResultRespVO result = new OvertimeApplicationBatchResultRespVO(); + for (Long id : reqVO.getIds()) { + OvertimeApplicationDO app = appMap.get(id); + if (app == null) { + addFailItem(result, id, "加班申请不存在"); + } else if (!Objects.equals(app.getApproverId(), loginUserId)) { + addFailItem(result, id, "您不是该申请的审批人"); + } else if (!Objects.equals(app.getStatusCode(), OvertimeApplicationConstants.STATUS_PENDING)) { + addFailItem(result, id, "当前状态不允许该操作"); + } else { + validList.add(app); + } + } + + if (validList.isEmpty()) { + result.setSuccessCount(0); + result.setFailCount(result.getFailItems().size()); + return result; + } + + // 4. 批量更新申请状态 + List validIds = validList.stream().map(OvertimeApplicationDO::getId).collect(Collectors.toList()); + OvertimeApplicationDO update = new OvertimeApplicationDO(); + update.setStatusCode(transition.getToStatusCode()); + update.setApprovalComment(reason); + update.setApprovalTime(approvalTime); + overtimeApplicationMapper.update(update, new LambdaQueryWrapperX() + .in(OvertimeApplicationDO::getId, validIds) + .eq(OvertimeApplicationDO::getApproverId, loginUserId) + .eq(OvertimeApplicationDO::getStatusCode, OvertimeApplicationConstants.STATUS_PENDING)); + + // 5. 批量写状态日志 + List statusLogs = new ArrayList<>(); + for (OvertimeApplicationDO original : validList) { + OvertimeApplicationStatusLogDO log = new OvertimeApplicationStatusLogDO(); + log.setApplicationId(original.getId()); + log.setActionType(actionCode); + log.setFromStatus(OvertimeApplicationConstants.STATUS_PENDING); + log.setToStatus(transition.getToStatusCode()); + log.setReason(reason); + log.setOperatorUserId(loginUserId); + log.setOperatorName(loginUserName); + log.setApplicantNameSnapshot(original.getApplicantName()); + log.setOvertimeDateSnapshot(original.getOvertimeDate()); + log.setOvertimeDurationSnapshot(original.getOvertimeDuration()); + log.setRemark(buildSnapshotRemark(original)); + statusLogs.add(log); + } + overtimeApplicationStatusLogMapper.insertBatch(statusLogs); + + // 6. 批量写审批记录(需要 statusLogId,逐条设置后批量插入) + List approvalRecords = new ArrayList<>(); + for (int i = 0; i < validList.size(); i++) { + OvertimeApplicationDO original = validList.get(i); + OvertimeApplicationStatusLogDO statusLog = statusLogs.get(i); + OvertimeApplicationApprovalRecordDO record = new OvertimeApplicationApprovalRecordDO(); + record.setOvertimeApplicationId(original.getId()); + record.setStatusLogId(statusLog.getId()); + record.setApprovalRound(overtimeApplicationApprovalRecordMapper.countByApplicationId(original.getId()) + 1); + record.setConclusion(transition.getToStatusCode()); + record.setOpinion(reason); + record.setAuditorUserId(loginUserId); + record.setAuditorName(loginUserName); + approvalRecords.add(record); + } + overtimeApplicationApprovalRecordMapper.insertBatch(approvalRecords); + + // 7. 批量写审计日志 + List auditLogs = new ArrayList<>(); + for (OvertimeApplicationDO original : validList) { + BizAuditLogDO auditLog = new BizAuditLogDO(); + auditLog.setBizType(OvertimeApplicationConstants.BIZ_TYPE); + auditLog.setBizId(original.getId()); + auditLog.setActionType(actionCode); + auditLog.setFromStatus(OvertimeApplicationConstants.STATUS_PENDING); + auditLog.setToStatus(transition.getToStatusCode()); + auditLog.setReason(reason); + auditLog.setOperatorUserId(loginUserId); + auditLog.setOperatorName(loginUserName); + auditLog.setRemark(buildSnapshotRemark(original)); + auditLogs.add(auditLog); + } + bizAuditLogMapper.insertBatch(auditLogs); + + result.setSuccessCount(validList.size()); + result.setFailCount(result.getFailItems().size()); + return result; + } + + private void addFailItem(OvertimeApplicationBatchResultRespVO result, Long id, String reason) { + OvertimeApplicationBatchResultRespVO.FailItem failItem = new OvertimeApplicationBatchResultRespVO.FailItem(); + failItem.setId(id); + failItem.setReason(reason); + result.getFailItems().add(failItem); + } + private OvertimeApplicationDO validateApplicationExists(Long id) { OvertimeApplicationDO application = overtimeApplicationMapper.selectById(id); if (application == null) { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java index 1d8fa6b..9189a36 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java @@ -52,6 +52,8 @@ import com.njcn.rdms.module.system.api.dept.DeptApi; import com.njcn.rdms.module.system.api.dept.PostApi; import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO; import com.njcn.rdms.module.system.api.dept.dto.PostRespDTO; +import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi; +import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO; import com.njcn.rdms.module.system.api.user.AdminUserApi; import com.njcn.rdms.module.system.api.user.UserManagementRelationApi; import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO; @@ -117,6 +119,8 @@ public class WorkReportCommonService { @Resource private UserManagementRelationApi userManagementRelationApi; @Resource + private ObjectPermissionApi objectPermissionApi; + @Resource private ProjectMapper projectMapper; @Resource private UserObjectRoleMapper userObjectRoleMapper; @@ -443,6 +447,7 @@ public class WorkReportCommonService { respVO.setProjectName(project.getProjectName()); respVO.setProjectOwnerId(profile.userId()); respVO.setProjectOwnerName(profile.userName()); + respVO.setTechnicalOwnerName(resolveProjectTechnicalOwnerName(projectId)); respVO.setProjectMemberSnapshot(BeanUtils.toBean(buildProjectMemberSnapshot(projectId), WorkReportMemberSnapshotRespVO.class)); respVO.setSupervisorUserId(profile.directManagerId()); @@ -870,6 +875,29 @@ public class WorkReportCommonService { return user; } + public String resolveProjectTechnicalOwnerName(Long projectId) { + if (projectId == null) { + return null; + } + ObjectRoleRespDTO technicalOwnerRole = objectPermissionApi.getObjectRoleByCode( + ProjectObjectConstants.TECHNICAL_OWNER_ROLE_CODE, + com.njcn.rdms.module.project.constant.ObjectRoleConstants.ROLE_SCOPE_OBJECT, + ProjectObjectConstants.OBJECT_TYPE).getCheckedData(); + if (technicalOwnerRole == null || technicalOwnerRole.getId() == null) { + throw exception(ErrorCodeConstants.PROJECT_INTERNAL_ROLE_NOT_CONFIGURED, + ProjectObjectConstants.TECHNICAL_OWNER_ROLE_CODE); + } + for (UserObjectRoleDO member : userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId)) { + if (member == null || !Objects.equals(member.getStatus(), 0) + || !Objects.equals(member.getRoleId(), technicalOwnerRole.getId()) + || member.getUserId() == null) { + continue; + } + return defaultText(loadUser(member.getUserId()).getNickname()); + } + return null; + } + private ProjectDO validateProjectExists(Long projectId) { ProjectDO project = projectMapper.selectById(projectId); if (project == null) { @@ -941,6 +969,7 @@ public class WorkReportCommonService { target.setProjectName(project.getProjectName()); target.setProjectOwnerId(profile.userId()); target.setProjectOwnerName(profile.userName()); + target.setTechnicalOwnerName(resolveProjectTechnicalOwnerName(project.getId())); target.setProjectMemberSnapshot(buildProjectMemberSnapshot(project.getId())); target.setSupervisorUserId(profile.directManagerId()); target.setSupervisorName(profile.directManagerName()); @@ -1358,4 +1387,4 @@ public class WorkReportCommonService { private record CurrentUserProfile(Long userId, String userName, String deptName, String postName, Long directManagerId, String directManagerName) { } -} +} \ No newline at end of file diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/defaultdraft/WorkReportDefaultDraftService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/defaultdraft/WorkReportDefaultDraftService.java index be9848e..3bab8ba 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/defaultdraft/WorkReportDefaultDraftService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/defaultdraft/WorkReportDefaultDraftService.java @@ -33,6 +33,7 @@ import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper; import com.njcn.rdms.module.project.dal.mysql.project.task.TaskWorklogMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; import com.njcn.rdms.module.project.enums.ErrorCodeConstants; +import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService; import com.njcn.rdms.module.system.api.dept.DeptApi; import com.njcn.rdms.module.system.api.dept.PostApi; import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO; @@ -91,6 +92,8 @@ public class WorkReportDefaultDraftService { private PersonalItemMapper personalItemMapper; @Resource private ProjectExecutionMapper projectExecutionMapper; + @Resource + private WorkReportCommonService workReportCommonService; public WeeklyReportRespVO previewWeeklyDefaultDraft(WeeklyReportDefaultDraftReqVO reqVO) { validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); @@ -155,7 +158,7 @@ public class WorkReportDefaultDraftService { respVO.setProjectName(project.getProjectName()); respVO.setProjectOwnerId(profile.userId()); respVO.setProjectOwnerName(profile.userName()); - respVO.setTechnicalOwnerName(resolveProjectManagerName(project.getManagerUserId())); + respVO.setTechnicalOwnerName(workReportCommonService.resolveProjectTechnicalOwnerName(project.getId())); respVO.setProjectMemberSnapshot(BeanUtils.toBean(buildProjectMemberSnapshot(projectId), WorkReportMemberSnapshotRespVO.class)); respVO.setSupervisorUserId(profile.directManagerId()); @@ -525,14 +528,6 @@ public class WorkReportDefaultDraftService { return user; } - private String resolveProjectManagerName(Long managerUserId) { - if (managerUserId == null) { - return null; - } - AdminUserRespDTO manager = loadUser(managerUserId); - return safeText(manager.getNickname()); - } - private List buildProjectMemberSnapshot(Long projectId) { List members = userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId); Set userIds = new LinkedHashSet<>(); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportContentExportService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportContentExportService.java index cd44aac..0a526d9 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportContentExportService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/export/WorkReportContentExportService.java @@ -21,9 +21,12 @@ import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants; import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService; import com.njcn.rdms.module.system.api.dict.DictDataApi; import jakarta.annotation.Resource; -import org.apache.poi.xwpf.usermodel.*; -import org.apache.xmlbeans.XmlException; -import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow; +import org.docx4j.XmlUtils; +import org.docx4j.jaxb.Context; +import org.docx4j.model.table.TblFactory; +import org.docx4j.openpackaging.exceptions.Docx4JException; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.wml.*; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -32,6 +35,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; +import java.math.BigInteger; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -52,6 +56,10 @@ public class WorkReportContentExportService { private static final String PROJECT_TEMPLATE_PATH = "templates/work-report/project-report-template.docx"; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd"); private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm"); + private static final ObjectFactory WML_FACTORY = Context.getWmlObjectFactory(); + private static final BigInteger DEFAULT_FONT_SIZE = BigInteger.valueOf(18); + private static final BigInteger TITLE_FONT_SIZE = BigInteger.valueOf(36); + private static final BigInteger SECTION_TITLE_FONT_SIZE = BigInteger.valueOf(24); @Resource private WorkReportCommonService workReportCommonService; @@ -174,20 +182,21 @@ public class WorkReportContentExportService { } private DocumentContent buildWeeklyTemplateDocument(WeeklyReportRespVO report, ClassPathResource templateResource) { - try (InputStream inputStream = templateResource.getInputStream(); - XWPFDocument document = new XWPFDocument(inputStream)) { + try (InputStream inputStream = templateResource.getInputStream()) { + WordprocessingMLPackage document = WordprocessingMLPackage.load(inputStream); replacePlaceholders(document, buildWeeklyTemplateData(report)); renderPersonalReviewTable(document, report.getReviewItems()); renderPersonalPlanTable(document, report.getPlanItems()); return new DocumentContent(buildFilename("个人周报", report.getReporterName(), report.getPeriodLabel()), toBytes(document)); - } catch (IOException e) { + } catch (IOException | Docx4JException e) { throw new IllegalStateException("生成周报模板导出文件失败", e); } } private DocumentContent buildWeeklyFallbackDocument(WeeklyReportRespVO report) { - try (XWPFDocument document = new XWPFDocument()) { + try { + WordprocessingMLPackage document = WordprocessingMLPackage.createPackage(); addTitle(document, "个人周报"); addParagraph(document, "填报周期:" + formatPeriod(report), false); addPersonalBaseTable(document, report.getReporterName(), report.getReporterDeptName(), @@ -207,7 +216,7 @@ public class WorkReportContentExportService { addPersonalPlanTable(document, report.getPlanItems(), false); return new DocumentContent(buildFilename("个人周报", report.getReporterName(), report.getPeriodLabel()), toBytes(document)); - } catch (IOException e) { + } catch (Docx4JException | IOException e) { throw new IllegalStateException("生成周报导出文件失败", e); } } @@ -228,42 +237,37 @@ public class WorkReportContentExportService { return data; } - private void replacePlaceholders(XWPFDocument document, Map data) { - for (XWPFParagraph paragraph : document.getParagraphs()) { + private void replacePlaceholders(WordprocessingMLPackage document, Map data) { + replacePlaceholdersInObject(document.getMainDocumentPart(), data); + } + + private void replacePlaceholdersInObject(Object root, Map data) { + for (P paragraph : findAllParagraphs(root)) { replaceParagraphPlaceholders(paragraph, data); } - for (XWPFTable table : document.getTables()) { - for (XWPFTableRow row : table.getRows()) { - for (XWPFTableCell cell : row.getTableCells()) { - for (XWPFParagraph paragraph : cell.getParagraphs()) { - replaceParagraphPlaceholders(paragraph, data); - } - } - } - } } - private void replaceParagraphPlaceholders(XWPFParagraph paragraph, Map data) { - List runs = paragraph.getRuns(); - if (runs == null || runs.isEmpty()) { + private void replaceParagraphPlaceholders(P paragraph, Map data) { + List textNodes = findAllTextNodes(paragraph); + if (textNodes.isEmpty()) { return; } - String sourceText = paragraph.getText(); - if (!StringUtils.hasText(sourceText) || !sourceText.contains("{{")) { + String source = textNodes.stream() + .map(Text::getValue) + .filter(Objects::nonNull) + .collect(java.util.stream.Collectors.joining()); + if (!StringUtils.hasText(source) || !source.contains("{{")) { return; } - String replacedText = sourceText; + String replaced = source; for (Map.Entry entry : data.entrySet()) { - replacedText = replacedText.replace("{{" + entry.getKey() + "}}", entry.getValue()); - } - XWPFRun firstRun = runs.get(0); - applyRunText(firstRun, replacedText); - for (int i = runs.size() - 1; i >= 1; i--) { - paragraph.removeRun(i); + replaced = replaced.replace("{{" + entry.getKey() + "}}", entry.getValue()); } + rewriteParagraphText(paragraph, replaced, true); + applyParagraphFormatting(paragraph, replaced, false); } - private void renderPersonalReviewTable(XWPFDocument document, List items) { + private void renderPersonalReviewTable(WordprocessingMLPackage document, List items) { TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of( "{{itemNumber}}", "{{itemTitleWithHours}}", "{{contentText}}", "{{reflectionText}}")); if (location == null) { @@ -276,12 +280,12 @@ public class WorkReportContentExportService { "contentText", buildStructuredReviewText(item, false), "reflectionText", text(item.getReflectionText())))) .toList(); - List renderedRows = renderRowsByTemplateRow(location, rows, List.of( + List renderedRows = renderRowsByTemplateRow(location, rows, List.of( "itemNumber", "itemTitleWithHours", "contentText", "reflectionText")); rewriteReviewRows(renderedRows, items, false); } - private void renderPersonalPlanTable(XWPFDocument document, List items) { + private void renderPersonalPlanTable(WordprocessingMLPackage document, List items) { TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of( "{{itemNumber}}", "{{itemTitle}}", "{{targetText}}", "{{supportNeed}}")); if (location == null) { @@ -294,16 +298,20 @@ public class WorkReportContentExportService { "targetText", buildStructuredPlanText(item, false), "supportNeed", text(item.getSupportNeed())))) .toList(); - List renderedRows = renderRowsByTemplateRow(location, rows, List.of( + List renderedRows = renderRowsByTemplateRow(location, rows, List.of( "itemNumber", "itemTitle", "targetText", "supportNeed")); rewritePlanRows(renderedRows, items, false); } - private TemplateRowLocation findTemplateRowByPlaceholders(XWPFDocument document, List placeholders) { - for (XWPFTable table : document.getTables()) { - for (int i = 0; i < table.getRows().size(); i++) { - XWPFTableRow row = table.getRow(i); - String rowXml = row.getCtRow().xmlText(); + private TemplateRowLocation findTemplateRowByPlaceholders(WordprocessingMLPackage document, List placeholders) { + for (Tbl table : findAllTables(document.getMainDocumentPart())) { + List rows = table.getContent(); + for (int i = 0; i < rows.size(); i++) { + Object rowObject = XmlUtils.unwrap(rows.get(i)); + if (!(rowObject instanceof Tr row)) { + continue; + } + String rowXml = XmlUtils.marshaltoString(row, true, true, Context.jc, "", "", Tr.class); if (placeholders.stream().allMatch(rowXml::contains)) { return new TemplateRowLocation(table, i); } @@ -312,26 +320,26 @@ public class WorkReportContentExportService { return null; } - private List renderRowsByTemplateRow(TemplateRowLocation location, - List> dataRows, List placeholderNames) { - XWPFTable table = location.table(); + private List renderRowsByTemplateRow(TemplateRowLocation location, + List> dataRows, List placeholderNames) { + Tbl table = location.table(); int templateRowIndex = location.rowIndex(); - int dataStartIndex = templateRowIndex; int dataEndIndex = findPlaceholderBlockEndRowIndex(table, templateRowIndex, placeholderNames); - String templateRowXml = table.getRow(templateRowIndex).getCtRow().xmlText(); - for (int i = dataEndIndex - 1; i >= dataStartIndex; i--) { - table.removeRow(i); + Tr templateRow = tableRow(table, templateRowIndex); + for (int i = dataEndIndex - 1; i >= templateRowIndex; i--) { + table.getContent().remove(i); } List> rows = dataRows == null || dataRows.isEmpty() ? List.of(emptyPlaceholderValues(placeholderNames)) : dataRows; - int insertIndex = dataStartIndex; - List insertedRows = new ArrayList<>(rows.size()); + int insertIndex = templateRowIndex; + List insertedRows = new ArrayList<>(rows.size()); for (Map rowData : rows) { - XWPFTableRow row = new XWPFTableRow(parseRenderedRow(templateRowXml, rowData), table); - table.addRow(row, insertIndex++); - insertedRows.add(table.getRow(insertIndex - 1)); + Tr row = (Tr) XmlUtils.deepCopy(templateRow); + replacePlaceholdersInObject(row, rowData); + table.getContent().add(insertIndex++, row); + insertedRows.add(row); } return insertedRows; } @@ -352,53 +360,78 @@ public class WorkReportContentExportService { return result; } - private CTRow parseRenderedRow(String templateRowXml, Map values) { - String renderedXml = templateRowXml; - for (Map.Entry entry : values.entrySet()) { - renderedXml = renderedXml.replace(entry.getKey(), escapeXmlText(entry.getValue())); - } - try { - return CTRow.Factory.parse(renderedXml); - } catch (XmlException e) { - throw new IllegalStateException("渲染周报模板明细行失败", e); - } - } - - private String escapeXmlText(String value) { - return text(value) - .replace("&", "&") - .replace("<", "<") - .replace(">", ">"); - } - - private int findPlaceholderBlockEndRowIndex(XWPFTable table, int templateRowIndex, List placeholderNames) { + private int findPlaceholderBlockEndRowIndex(Tbl table, int templateRowIndex, List placeholderNames) { List placeholderTokens = placeholderNames.stream() .map(name -> "{{" + name + "}}") .toList(); - for (int i = templateRowIndex; i < table.getRows().size(); i++) { - String rowXml = table.getRow(i).getCtRow().xmlText(); + for (int i = templateRowIndex; i < table.getContent().size(); i++) { + Object rowObject = XmlUtils.unwrap(table.getContent().get(i)); + if (!(rowObject instanceof Tr row)) { + return i; + } + String rowXml = marshalTr(row); boolean containsPlaceholder = placeholderTokens.stream().anyMatch(rowXml::contains); if (!containsPlaceholder) { return i; } } - return table.getNumberOfRows(); + return table.getContent().size(); } - private void applyRunText(XWPFRun run, String value) { - String[] lines = text(value).split("\\R", -1); - run.setText("", 0); - for (int i = 0; i < lines.length; i++) { - if (i == 0) { - run.setText(lines[i], 0); - } else { - run.addBreak(); - run.setText(lines[i]); + private String marshalTr(Tr row) { + return XmlUtils.marshaltoString(row, true, true, Context.jc, "", "", Tr.class); + } + + private Tr tableRow(Tbl table, int rowIndex) { + return (Tr) XmlUtils.unwrap(table.getContent().get(rowIndex)); + } + + private List findAllTextNodes(Object root) { + List result = new ArrayList<>(); + new org.docx4j.TraversalUtil(root, new org.docx4j.TraversalUtil.CallbackImpl() { + @Override + public List apply(Object o) { + Object value = XmlUtils.unwrap(o); + if (value instanceof Text textNode) { + result.add(textNode); + } + return null; } - } + }); + return result; } - private record TemplateRowLocation(XWPFTable table, int rowIndex) { + private List

findAllParagraphs(Object root) { + List

result = new ArrayList<>(); + new org.docx4j.TraversalUtil(root, new org.docx4j.TraversalUtil.CallbackImpl() { + @Override + public List apply(Object o) { + Object value = XmlUtils.unwrap(o); + if (value instanceof P paragraph) { + result.add(paragraph); + } + return null; + } + }); + return result; + } + + private List findAllTables(Object root) { + List result = new ArrayList<>(); + new org.docx4j.TraversalUtil(root, new org.docx4j.TraversalUtil.CallbackImpl() { + @Override + public List apply(Object o) { + Object value = XmlUtils.unwrap(o); + if (value instanceof Tbl table) { + result.add(table); + } + return null; + } + }); + return result; + } + + private record TemplateRowLocation(Tbl table, int rowIndex) { } private DocumentContent buildMonthlyDocument(MonthlyReportRespVO report, @@ -413,22 +446,23 @@ public class WorkReportContentExportService { private DocumentContent buildMonthlyTemplateDocument(MonthlyReportRespVO report, MonthlyReportApprovalRecordRespVO approvalRecord, ClassPathResource templateResource) { - try (InputStream inputStream = templateResource.getInputStream(); - XWPFDocument document = new XWPFDocument(inputStream)) { + try (InputStream inputStream = templateResource.getInputStream()) { + WordprocessingMLPackage document = WordprocessingMLPackage.load(inputStream); replacePlaceholders(document, buildMonthlyTemplateData(report, approvalRecord)); renderMonthlyReviewTable(document, report.getReviewItems()); renderMonthlyFeedbackTables(document, approvalRecord); renderMonthlyPlanTable(document, report.getPlanItems()); return new DocumentContent(buildFilename("个人月报", report.getReporterName(), report.getPeriodLabel()), toBytes(document)); - } catch (IOException e) { + } catch (IOException | Docx4JException e) { throw new IllegalStateException("生成月报模板导出文件失败", e); } } private DocumentContent buildMonthlyFallbackDocument(MonthlyReportRespVO report, MonthlyReportApprovalRecordRespVO approvalRecord) { - try (XWPFDocument document = new XWPFDocument()) { + try { + WordprocessingMLPackage document = WordprocessingMLPackage.createPackage(); addTitle(document, "灿能电力绩效反馈面谈表"); addPersonalBaseTable(document, report.getReporterName(), report.getReporterDeptName(), report.getReporterPostName(), report.getSupervisorName()); @@ -455,7 +489,7 @@ public class WorkReportContentExportService { + " 日期:" + formatDate(approvalRecord == null ? null : approvalRecord.getSupervisorSignedDate()), false); return new DocumentContent(buildFilename("个人月报", report.getReporterName(), report.getPeriodLabel()), toBytes(document)); - } catch (IOException e) { + } catch (Docx4JException | IOException e) { throw new IllegalStateException("生成月报导出文件失败", e); } } @@ -470,18 +504,24 @@ public class WorkReportContentExportService { private DocumentContent buildProjectTemplateDocument(ProjectReportRespVO report, ClassPathResource templateResource) { - try (InputStream inputStream = templateResource.getInputStream(); - XWPFDocument document = new XWPFDocument(inputStream)) { - replacePlaceholders(document, buildProjectTemplateData(report)); + try (InputStream inputStream = templateResource.getInputStream()) { + WordprocessingMLPackage document = WordprocessingMLPackage.load(inputStream); + Map templateData = buildProjectTemplateData(report); + List currentItemLines = splitTextLinesPreserveBlank(templateData.remove("currentItems")); + List nextItemLines = splitTextLinesPreserveBlank(templateData.remove("nextItems")); + replacePlaceholders(document, templateData); + renderTemplateCellParagraphsByPlaceholder(document, "{{currentItems}}", currentItemLines); + renderTemplateCellParagraphsByPlaceholder(document, "{{nextItems}}", nextItemLines); return new DocumentContent(buildFilename("项目半月报", report.getProjectName(), report.getPeriodLabel()), toBytes(document)); - } catch (IOException e) { + } catch (IOException | Docx4JException e) { throw new IllegalStateException("生成项目半月报模板导出文件失败", e); } } private DocumentContent buildProjectFallbackDocument(ProjectReportRespVO report) { - try (XWPFDocument document = new XWPFDocument()) { + try { + WordprocessingMLPackage document = WordprocessingMLPackage.createPackage(); addTitle(document, "研发工作情况简报"); addParagraph(document, "研发工作情况简报(" + formatPeriod(report) + "):", true); addSectionTitle(document, "一、" + text(report.getProjectName())); @@ -504,11 +544,10 @@ public class WorkReportContentExportService { row("项目问题", report.getProjectProblems()))); return new DocumentContent(buildFilename("项目半月报", report.getProjectName(), report.getPeriodLabel()), toBytes(document)); - } catch (IOException e) { + } catch (Docx4JException | IOException e) { throw new IllegalStateException("生成项目半月报导出文件失败", e); } } - private Map buildMonthlyTemplateData(MonthlyReportRespVO report, MonthlyReportApprovalRecordRespVO approvalRecord) { Map data = new HashMap<>(); @@ -549,7 +588,30 @@ public class WorkReportContentExportService { return data; } - private void renderMonthlyReviewTable(XWPFDocument document, List items) { + private void renderTemplateCellParagraphsByPlaceholder(WordprocessingMLPackage document, + String placeholder, List lines) { + if (!StringUtils.hasText(placeholder)) { + return; + } + for (Tbl table : findAllTables(document.getMainDocumentPart())) { + for (Object rowObject : table.getContent()) { + Object rowValue = XmlUtils.unwrap(rowObject); + if (!(rowValue instanceof Tr row)) { + continue; + } + for (Tc cell : rowCells(row)) { + P templateParagraph = findParagraphByPlaceholder(cell, placeholder); + if (templateParagraph == null) { + continue; + } + setTemplateCellParagraphs(cell, templateParagraph, lines, false); + return; + } + } + } + } + + private void renderMonthlyReviewTable(WordprocessingMLPackage document, List items) { TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of( "{{itemNumber}}", "{{itemTitleWithHours}}", "{{contentText}}", "{{reflectionText}}")); if (location == null) { @@ -562,12 +624,12 @@ public class WorkReportContentExportService { "contentText", buildStructuredReviewText(item, false), "reflectionText", text(item.getReflectionText())))) .toList(); - List renderedRows = renderRowsByTemplateRow(location, rows, List.of( + List renderedRows = renderRowsByTemplateRow(location, rows, List.of( "itemNumber", "itemTitleWithHours", "contentText", "reflectionText")); rewriteReviewRows(renderedRows, items, false); } - private void renderMonthlyPlanTable(XWPFDocument document, List items) { + private void renderMonthlyPlanTable(WordprocessingMLPackage document, List items) { TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of( "{{itemNumber}}", "{{itemTitle}}", "{{targetText}}", "{{supportNeed}}")); if (location == null) { @@ -580,12 +642,12 @@ public class WorkReportContentExportService { "targetText", buildStructuredPlanText(item, false), "supportNeed", text(item.getSupportNeed())))) .toList(); - List renderedRows = renderRowsByTemplateRow(location, rows, List.of( + List renderedRows = renderRowsByTemplateRow(location, rows, List.of( "itemNumber", "itemTitle", "targetText", "supportNeed")); rewritePlanRows(renderedRows, items, false); } - private void renderMonthlyFeedbackTables(XWPFDocument document, MonthlyReportApprovalRecordRespVO approvalRecord) { + private void renderMonthlyFeedbackTables(WordprocessingMLPackage document, MonthlyReportApprovalRecordRespVO approvalRecord) { TemplateRowLocation strengthLocation = findTemplateRowByPlaceholders(document, List.of( "优势", "{{strengthDesc}}", "{{strengthExample}}", "{{improvementSuggestion}}")); if (strengthLocation != null) { @@ -606,30 +668,34 @@ public class WorkReportContentExportService { } private void renderSingleTemplateRow(TemplateRowLocation location, Map values) { - XWPFTableRow row = location.table().getRow(location.rowIndex()); - CTRow renderedRow = parseRenderedRow(row.getCtRow().xmlText(), values); - row.getCtRow().set(renderedRow); + Tr renderedRow = (Tr) XmlUtils.deepCopy(tableRow(location.table(), location.rowIndex())); + replacePlaceholdersInObject(renderedRow, values); + location.table().getContent().set(location.rowIndex(), renderedRow); } - private void rewriteReviewRows(List rows, List items, + private void rewriteReviewRows(List rows, List items, boolean includeDetail) { List safeItems = safeList(items); for (int i = 0; i < rows.size() && i < safeItems.size(); i++) { PersonalReportReviewItemRespVO item = safeItems.get(i); - XWPFTableRow row = rows.get(i); - setStructuredReviewCell(row.getCell(2), item, includeDetail); - setCellText(row.getCell(3), text(item.getReflectionText()), false); + List cells = rowCells(rows.get(i)); + setCenteredCellText(cells.get(0), formatNumber(item.getItemNumber()), false); + setCenteredCellText(cells.get(1), appendHours(item.getItemTitle(), item.getWorkHours()), false); + setStructuredReviewCell(cells.get(2), item, includeDetail); + setCellText(cells.get(3), text(item.getReflectionText()), false); } } - private void rewritePlanRows(List rows, List items, + private void rewritePlanRows(List rows, List items, boolean includeDetail) { List safeItems = safeList(items); for (int i = 0; i < rows.size() && i < safeItems.size(); i++) { PersonalReportPlanItemRespVO item = safeItems.get(i); - XWPFTableRow row = rows.get(i); - setStructuredPlanCell(row.getCell(2), item, includeDetail); - setCellText(row.getCell(3), text(item.getSupportNeed()), false); + List cells = rowCells(rows.get(i)); + setCenteredCellText(cells.get(0), formatNumber(item.getItemNumber()), false); + setCenteredCellText(cells.get(1), text(item.getItemTitle()), false); + setStructuredPlanCell(cells.get(2), item, includeDetail); + setCellText(cells.get(3), text(item.getSupportNeed()), false); } } @@ -643,7 +709,7 @@ public class WorkReportContentExportService { item == null ? null : item.getTargetText(), false, includeDetail); } - private void setStructuredReviewCell(XWPFTableCell cell, PersonalReportReviewItemRespVO item, + private void setStructuredReviewCell(Tc cell, PersonalReportReviewItemRespVO item, boolean includeDetail) { setStructuredCellTextForTemplate(cell, item == null ? null : item.getContentJson(), @@ -652,7 +718,7 @@ public class WorkReportContentExportService { includeDetail); } - private void setStructuredPlanCell(XWPFTableCell cell, PersonalReportPlanItemRespVO item, + private void setStructuredPlanCell(Tc cell, PersonalReportPlanItemRespVO item, boolean includeDetail) { setStructuredCellTextForTemplate(cell, item == null ? null : item.getTargetJson(), @@ -661,18 +727,17 @@ public class WorkReportContentExportService { includeDetail); } - private void setStructuredCellTextForTemplate(XWPFTableCell cell, Object jsonValue, + private void setStructuredCellTextForTemplate(Tc cell, Object jsonValue, String fallbackText, boolean includeHours, boolean includeDetail) { List lines = buildStructuredParagraphsForTemplate(jsonValue, fallbackText, includeHours, includeDetail); unsetNoWrap(cell); if (lines.isEmpty()) { - setCellTextByTemplateParagraph(cell, normalizeLineBreaks(fallbackText), false); + setStructuredTemplateCellParagraphs(cell, splitTextLinesPreserveBlank(normalizeLineBreaks(fallbackText)), false); return; } - setCellTextByTemplateParagraph(cell, String.join("\n", lines), false); + setStructuredTemplateCellParagraphs(cell, lines, false); } - private List buildStructuredParagraphsForTemplate(Object jsonValue, String fallbackText, boolean includeHours, boolean includeDetail) { List sections = parseStructuredSections(jsonValue); @@ -687,19 +752,19 @@ public class WorkReportContentExportService { for (StructuredSectionData section : sections) { String category = normalizeSectionCategory(section.category()); if (StringUtils.hasText(category)) { - lines.add("# " + category + "\uFF1A"); + lines.add("#" + category + ":"); } List tasks = safeList(section.tasks()); for (int i = 0; i < tasks.size(); i++) { StructuredTaskData task = tasks.get(i); - String taskLine = (i + 1) + "\u3001 " + formatStructuredTaskForTemplate(task, includeHours, priorityLabelMap); + String taskLine = (i + 1) + "、" + formatStructuredTaskForTemplate(task, includeHours, priorityLabelMap); String detail = includeDetail ? text(task.detail()) : ""; if (StringUtils.hasText(detail)) { List detailLines = splitTextLines(detail); if (detailLines.isEmpty()) { - lines.add(taskLine + "\uFF1A"); + lines.add(taskLine + ":"); } else { - lines.add(taskLine + "\uFF1A" + detailLines.get(0)); + lines.add(taskLine + ":" + detailLines.get(0)); for (int j = 1; j < detailLines.size(); j++) { lines.add(detailLines.get(j)); } @@ -722,12 +787,12 @@ public class WorkReportContentExportService { metrics.add(resolvePriorityLabel(task.priority(), priorityLabelMap)); } if (task.progress() != null) { - metrics.add("\u8FDB\u5EA6 " + formatDecimal(task.progress()) + "%"); + metrics.add("进度 " + formatDecimal(task.progress()) + "%"); } if (includeHours && task.hours() != null) { metrics.add(formatDecimal(task.hours()) + "h"); } - return text(task.title()) + (metrics.isEmpty() ? "" : "\uFF08" + String.join(" / ", metrics) + "\uFF09"); + return text(task.title()) + (metrics.isEmpty() ? "" : "(" + String.join(" / ", metrics) + ")"); } private List parseStructuredSectionsForTemplate(String textValue) { @@ -846,7 +911,7 @@ public class WorkReportContentExportService { Map priorityLabelMap = loadPriorityLabelMap(); List lines = new ArrayList<>(); for (StructuredSectionData section : sections) { - lines.add("# " + normalizeSectionCategory(section.category()) + ":"); + lines.add("#" + normalizeSectionCategory(section.category()) + ":"); List tasks = safeList(section.tasks()); for (int i = 0; i < tasks.size(); i++) { StructuredTaskData task = tasks.get(i); @@ -870,7 +935,7 @@ public class WorkReportContentExportService { return String.join("\n", lines); } - private void setStructuredCellText(XWPFTableCell cell, Object jsonValue, String fallbackText, boolean includeHours) { + private void setStructuredCellText(Tc cell, Object jsonValue, String fallbackText, boolean includeHours) { List lines = buildStructuredLines(jsonValue, fallbackText, includeHours); if (lines.isEmpty()) { setCellText(cell, fallbackText, false); @@ -890,7 +955,7 @@ public class WorkReportContentExportService { Map priorityLabelMap = loadPriorityLabelMap(); List lines = new ArrayList<>(); for (StructuredSectionData section : sections) { - lines.add("# " + normalizeSectionCategory(section.category()) + ":"); + lines.add("#" + normalizeSectionCategory(section.category()) + ":"); List tasks = safeList(section.tasks()); for (int i = 0; i < tasks.size(); i++) { lines.add((i + 1) + "、" + formatStructuredTask(tasks.get(i), includeHours, priorityLabelMap)); @@ -1082,7 +1147,7 @@ public class WorkReportContentExportService { metrics.add(resolvePriorityLabel(task.priority(), priorityLabelMap)); } if (task.progress() != null) { - metrics.add("\u8FDB\u5EA6 " + formatDecimal(task.progress()) + "%"); + metrics.add("进度 " + formatDecimal(task.progress()) + "%"); } if (includeHours && task.hours() != null) { metrics.add(formatDecimal(task.hours()) + "h"); @@ -1291,31 +1356,22 @@ public class WorkReportContentExportService { return String.join("\n", lines); } - private void addTitle(XWPFDocument document, String title) { - XWPFParagraph paragraph = document.createParagraph(); - paragraph.setAlignment(ParagraphAlignment.CENTER); - XWPFRun run = paragraph.createRun(); - run.setBold(true); - run.setFontSize(18); - run.setText(title); + private void addTitle(WordprocessingMLPackage document, String title) { + P paragraph = createParagraph(title, true, TITLE_FONT_SIZE, JcEnumeration.CENTER); + document.getMainDocumentPart().addObject(paragraph); } - private void addSectionTitle(XWPFDocument document, String title) { - XWPFParagraph paragraph = document.createParagraph(); - XWPFRun run = paragraph.createRun(); - run.setBold(true); - run.setFontSize(12); - run.setText(title); + private void addSectionTitle(WordprocessingMLPackage document, String title) { + P paragraph = createParagraph(title, true, SECTION_TITLE_FONT_SIZE, null); + document.getMainDocumentPart().addObject(paragraph); } - private void addParagraph(XWPFDocument document, String text, boolean bold) { - XWPFParagraph paragraph = document.createParagraph(); - XWPFRun run = paragraph.createRun(); - run.setBold(bold); - run.setText(text(text)); + private void addParagraph(WordprocessingMLPackage document, String text, boolean bold) { + P paragraph = createParagraph(text, bold, DEFAULT_FONT_SIZE, null); + document.getMainDocumentPart().addObject(paragraph); } - private void addPersonalBaseTable(XWPFDocument document, String name, String deptName, + private void addPersonalBaseTable(WordprocessingMLPackage document, String name, String deptName, String postName, String managerName) { addKeyValueTable(document, List.of( row("姓名", name), @@ -1324,156 +1380,386 @@ public class WorkReportContentExportService { row("直接上级", managerName))); } - private void addPersonalReviewTable(XWPFDocument document, List items, + private void addPersonalReviewTable(WordprocessingMLPackage document, List items, boolean includeDetail) { - XWPFTable table = document.createTable(1, 4); + Tbl table = TblFactory.createTable(1, 4, 2000); setTableWidth(table); - setRowText(table.getRow(0), List.of("序号", "工作事项", "具体工作内容及成果描述", "工作感悟"), true); + setRowText(tableRow(table, 0), List.of("序号", "工作事项", "具体工作内容及成果描述", "工作感悟"), true); for (PersonalReportReviewItemRespVO item : safeList(items)) { - XWPFTableRow row = table.createRow(); - setCellText(row.getCell(0), formatNumber(item.getItemNumber()), false); - setCellText(row.getCell(1), appendHours(item.getItemTitle(), item.getWorkHours()), false); - setCellText(row.getCell(2), buildStructuredReviewText(item, includeDetail), false); - setCellText(row.getCell(3), item.getReflectionText(), false); + Tr row = addTableRow(table, 4); + List cells = rowCells(row); + setCellText(cells.get(0), formatNumber(item.getItemNumber()), false); + setCellText(cells.get(1), appendHours(item.getItemTitle(), item.getWorkHours()), false); + setCellText(cells.get(2), buildStructuredReviewText(item, includeDetail), false); + setCellText(cells.get(3), item.getReflectionText(), false); } + document.getMainDocumentPart().addObject(table); } - private void addPersonalPlanTable(XWPFDocument document, List items, + private void addPersonalPlanTable(WordprocessingMLPackage document, List items, boolean includeDetail) { - XWPFTable table = document.createTable(1, 4); + Tbl table = TblFactory.createTable(1, 4, 2000); setTableWidth(table); - setRowText(table.getRow(0), List.of("序号", "工作事项", "具体目标", "对他人协助的需求"), true); + setRowText(tableRow(table, 0), List.of("序号", "工作事项", "具体目标", "对他人协助的需求"), true); for (PersonalReportPlanItemRespVO item : safeList(items)) { - XWPFTableRow row = table.createRow(); - setCellText(row.getCell(0), formatNumber(item.getItemNumber()), false); - setCellText(row.getCell(1), item.getItemTitle(), false); - setCellText(row.getCell(2), buildStructuredPlanText(item, includeDetail), false); - setCellText(row.getCell(3), item.getSupportNeed(), false); + Tr row = addTableRow(table, 4); + List cells = rowCells(row); + setCellText(cells.get(0), formatNumber(item.getItemNumber()), false); + setCellText(cells.get(1), item.getItemTitle(), false); + setCellText(cells.get(2), buildStructuredPlanText(item, includeDetail), false); + setCellText(cells.get(3), item.getSupportNeed(), false); } + document.getMainDocumentPart().addObject(table); } - private void addFeedbackTable(XWPFDocument document, MonthlyReportApprovalRecordRespVO record) { - XWPFTable table = document.createTable(1, 4); + private void addFeedbackTable(WordprocessingMLPackage document, MonthlyReportApprovalRecordRespVO record) { + Tbl table = TblFactory.createTable(1, 4, 2000); setTableWidth(table); - setRowText(table.getRow(0), List.of("优势/劣势项", "优势/劣势描述", "行为事例", "改进提升建议"), true); - XWPFTableRow strengthRow = table.createRow(); + setRowText(tableRow(table, 0), List.of("优势/劣势项", "优势/劣势描述", "行为事例", "改进提升建议"), true); + Tr strengthRow = addTableRow(table, 4); setRowText(strengthRow, values("优势", record == null ? null : record.getStrengthDesc(), record == null ? null : record.getStrengthExample(), ""), false); - XWPFTableRow weaknessRow = table.createRow(); + Tr weaknessRow = addTableRow(table, 4); setRowText(weaknessRow, values("劣势", record == null ? null : record.getWeaknessDesc(), record == null ? null : record.getWeaknessExample(), record == null ? null : record.getImprovementSuggestion()), false); + document.getMainDocumentPart().addObject(table); } - private void addProjectItemTable(XWPFDocument document, List items, boolean showHours) { - XWPFTable table = document.createTable(1, showHours ? 4 : 3); + private void addProjectItemTable(WordprocessingMLPackage document, List items, boolean showHours) { + Tbl table = TblFactory.createTable(1, showHours ? 4 : 3, 2000); setTableWidth(table); - setRowText(table.getRow(0), showHours + setRowText(tableRow(table, 0), showHours ? List.of("工作内容", "工时", "优先级", "进度") : List.of("工作内容", "优先级", "进度"), true); for (ProjectReportItemRespVO item : safeList(items)) { - XWPFTableRow row = table.createRow(); + Tr row = addTableRow(table, showHours ? 4 : 3); setRowText(row, showHours ? values(item.getItemTitle(), formatDecimal(item.getWorkHours()), item.getPriorityCode(), formatProgress(item.getProgressRate())) : values(item.getItemTitle(), item.getPriorityCode(), formatProgress(item.getProgressRate())), false); } + document.getMainDocumentPart().addObject(table); } - private void addKeyValueTable(XWPFDocument document, List> rows) { - XWPFTable table = document.createTable(rows.size(), 2); + private void addKeyValueTable(WordprocessingMLPackage document, List> rows) { + Tbl table = TblFactory.createTable(rows.size(), 2, 4000); setTableWidth(table); for (int i = 0; i < rows.size(); i++) { - setRowText(table.getRow(i), rows.get(i), i == 0 && rows.size() == 1); + setRowText(tableRow(table, i), rows.get(i), i == 0 && rows.size() == 1); + } + document.getMainDocumentPart().addObject(table); + } + + private void setTableWidth(Tbl table) { + TblPr tblPr = table.getTblPr(); + if (tblPr == null) { + tblPr = WML_FACTORY.createTblPr(); + table.setTblPr(tblPr); + } + TblWidth width = tblPr.getTblW(); + if (width == null) { + width = WML_FACTORY.createTblWidth(); + tblPr.setTblW(width); + } + width.setType("pct"); + width.setW(BigInteger.valueOf(5000)); + } + + private void setRowText(Tr row, List values, boolean bold) { + List cells = rowCells(row); + for (int i = 0; i < values.size() && i < cells.size(); i++) { + setCellText(cells.get(i), values.get(i), bold); } } - private void setTableWidth(XWPFTable table) { - table.setWidth("100%"); + private void setCellText(Tc cell, String value, boolean bold) { + clearCellContent(cell); + cell.getContent().add(createParagraphWithBreaks(value, bold, true)); } - private void setRowText(XWPFTableRow row, List values, boolean bold) { - for (int i = 0; i < values.size(); i++) { - setCellText(row.getCell(i), values.get(i), bold); - } + private void setCenteredCellText(Tc cell, String value, boolean bold) { + setCellText(cell, value, bold); + applyCellAlignment(cell, JcEnumeration.CENTER, STVerticalJc.CENTER, false); } - private void setCellText(XWPFTableCell cell, String value, boolean bold) { - while (cell.getParagraphs().size() > 0) { - cell.removeParagraph(0); - } - XWPFParagraph paragraph = cell.addParagraph(); - XWPFRun run = paragraph.createRun(); - run.setBold(bold); - String[] lines = text(value).split("\\R", -1); - for (int i = 0; i < lines.length; i++) { - if (i > 0) { - run.addBreak(); - } - run.setText(lines[i]); - } - } - - private void setCellTextByTemplateParagraph(XWPFTableCell cell, String value, boolean bold) { + private void setStructuredTemplateCellParagraphs(Tc cell, List lines, boolean bold) { unsetNoWrap(cell); - XWPFParagraph paragraph; - if (cell.getParagraphs() == null || cell.getParagraphs().isEmpty()) { - paragraph = cell.addParagraph(); - } else { - paragraph = cell.getParagraphs().get(0); - for (int i = cell.getParagraphs().size() - 1; i >= 1; i--) { - cell.removeParagraph(i); - } + clearCellContent(cell); + List safeLines = (lines == null || lines.isEmpty()) ? List.of("") : lines; + for (String line : safeLines) { + P paragraph = createParagraph(line, bold, DEFAULT_FONT_SIZE, null); + applyParagraphFormatting(paragraph, line, false); + cell.getContent().add(paragraph); } - List runs = paragraph.getRuns(); - XWPFRun firstRun; - if (runs == null || runs.isEmpty()) { - firstRun = paragraph.createRun(); - } else { - firstRun = runs.get(0); - for (int i = runs.size() - 1; i >= 1; i--) { - paragraph.removeRun(i); - } - } - firstRun.setBold(bold); - applyRunText(firstRun, value); } - private void unsetNoWrap(XWPFTableCell cell) { - if (cell == null || cell.getCTTc() == null) { + private void applyCellAlignment(Tc cell, JcEnumeration horizontal, STVerticalJc vertical, boolean firstLineIndent) { + if (cell == null) { return; } - var tcPr = cell.getCTTc().getTcPr(); - if (tcPr != null && tcPr.isSetNoWrap()) { - tcPr.unsetNoWrap(); + TcPr tcPr = cell.getTcPr(); + if (tcPr == null) { + tcPr = WML_FACTORY.createTcPr(); + cell.setTcPr(tcPr); + } + if (vertical != null) { + CTVerticalJc verticalJc = WML_FACTORY.createCTVerticalJc(); + verticalJc.setVal(vertical); + tcPr.setVAlign(verticalJc); + } + for (P paragraph : findAllParagraphs(cell)) { + ensureParagraphFormatting(paragraph, horizontal, firstLineIndent); } } - private void setCellParagraphs(XWPFTableCell cell, List lines, boolean bold) { - while (cell.getParagraphs().size() > 0) { - cell.removeParagraph(0); + private void applyParagraphFormatting(P paragraph, String value, boolean firstLineIndent) { + ensureParagraphFormatting(paragraph, null, firstLineIndent); + PPr pPr = paragraph.getPPr(); + PPrBase.Spacing spacing = pPr.getSpacing(); + if (spacing == null) { + spacing = WML_FACTORY.createPPrBaseSpacing(); + pPr.setSpacing(spacing); } + spacing.setBefore(BigInteger.ZERO); + spacing.setAfter(BigInteger.ZERO); + if (!StringUtils.hasText(value)) { + spacing.setLineRule(STLineSpacingRule.AUTO); + } + } + + private void ensureParagraphFormatting(P paragraph, JcEnumeration alignment, boolean firstLineIndent) { + PPr pPr = paragraph.getPPr(); + if (pPr == null) { + pPr = WML_FACTORY.createPPr(); + paragraph.setPPr(pPr); + } + pPr.setNumPr(null); + if (alignment != null) { + Jc jc = pPr.getJc(); + if (jc == null) { + jc = WML_FACTORY.createJc(); + pPr.setJc(jc); + } + jc.setVal(alignment); + } + PPrBase.Ind ind = pPr.getInd(); + if (ind == null) { + ind = WML_FACTORY.createPPrBaseInd(); + pPr.setInd(ind); + } + ind.setLeft(BigInteger.ZERO); + ind.setLeftChars(BigInteger.ZERO); + ind.setRight(BigInteger.ZERO); + ind.setRightChars(BigInteger.ZERO); + ind.setStart(BigInteger.ZERO); + ind.setStartChars(BigInteger.ZERO); + ind.setEnd(BigInteger.ZERO); + ind.setEndChars(BigInteger.ZERO); + ind.setHanging(BigInteger.ZERO); + ind.setHangingChars(BigInteger.ZERO); + ind.setFirstLine(firstLineIndent ? BigInteger.valueOf(420) : BigInteger.ZERO); + ind.setFirstLineChars(BigInteger.ZERO); + } + + private void unsetNoWrap(Tc cell) { + if (cell == null) { + return; + } + TcPr tcPr = cell.getTcPr(); + if (tcPr != null) { + tcPr.setNoWrap(null); + } + } + + private void setCellParagraphs(Tc cell, List lines, boolean bold) { + clearCellContent(cell); List safeLines = lines == null || lines.isEmpty() ? List.of("") : lines; for (String line : safeLines) { - XWPFParagraph paragraph = cell.addParagraph(); - paragraph.setSpacingBefore(0); - paragraph.setSpacingAfter(0); - XWPFRun run = paragraph.createRun(); - run.setBold(bold); - run.setText(text(line)); + P paragraph = createParagraph(line, bold, DEFAULT_FONT_SIZE, null); + applyParagraphFormatting(paragraph, line, false); + cell.getContent().add(paragraph); } } - private byte[] toBytes(XWPFDocument document) throws IOException { + private byte[] toBytes(WordprocessingMLPackage document) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - document.write(outputStream); + try { + document.save(outputStream); + } catch (Docx4JException e) { + throw new IOException("save docx failed", e); + } return outputStream.toByteArray(); } + private P createParagraph(String value, boolean bold, BigInteger fontSize, JcEnumeration alignment) { + P paragraph = WML_FACTORY.createP(); + ensureParagraphFormatting(paragraph, alignment, false); + R run = createRun(value, bold, fontSize); + paragraph.getContent().add(run); + return paragraph; + } + + private P createParagraphWithBreaks(String value, boolean bold, boolean preserveEmptyLines) { + P paragraph = WML_FACTORY.createP(); + R run = WML_FACTORY.createR(); + run.setRPr(createRunProperties(bold, DEFAULT_FONT_SIZE)); + String[] lines = splitRawLines(value); + for (int i = 0; i < lines.length; i++) { + if (i > 0) { + run.getContent().add(WML_FACTORY.createBr()); + } + if (preserveEmptyLines || StringUtils.hasText(lines[i])) { + Text textNode = WML_FACTORY.createText(); + textNode.setValue(lines[i]); + if (lines[i].startsWith(" ") || lines[i].endsWith(" ")) { + textNode.setSpace("preserve"); + } + run.getContent().add(textNode); + } + } + paragraph.getContent().add(run); + applyParagraphFormatting(paragraph, value, false); + return paragraph; + } + + private void rewriteParagraphText(P paragraph, String value, boolean preserveEmptyLines) { + R firstRun = findFirstRun(paragraph); + RPr originalRunProperties = firstRun == null ? null : firstRun.getRPr(); + paragraph.getContent().clear(); + R run = WML_FACTORY.createR(); + if (originalRunProperties != null) { + run.setRPr((RPr) XmlUtils.deepCopy(originalRunProperties)); + } + String[] lines = splitRawLines(value); + for (int i = 0; i < lines.length; i++) { + if (i > 0) { + run.getContent().add(WML_FACTORY.createBr()); + } + if (preserveEmptyLines || StringUtils.hasText(lines[i])) { + Text textNode = WML_FACTORY.createText(); + textNode.setValue(lines[i]); + if (lines[i].startsWith(" ") || lines[i].endsWith(" ")) { + textNode.setSpace("preserve"); + } + run.getContent().add(textNode); + } + } + paragraph.getContent().add(run); + } + + private P findParagraphByPlaceholder(Tc cell, String placeholder) { + if (cell == null || !StringUtils.hasText(placeholder)) { + return null; + } + for (P paragraph : findAllParagraphs(cell)) { + String paragraphText = findAllTextNodes(paragraph).stream() + .map(Text::getValue) + .filter(Objects::nonNull) + .collect(java.util.stream.Collectors.joining()); + if (paragraphText.contains(placeholder)) { + return paragraph; + } + } + return null; + } + + private void setTemplateCellParagraphs(Tc cell, P templateParagraph, List lines, boolean bold) { + unsetNoWrap(cell); + clearCellContent(cell); + List safeLines = (lines == null || lines.isEmpty()) ? List.of("") : lines; + for (String line : safeLines) { + P paragraph = templateParagraph == null + ? createParagraph(line, bold, DEFAULT_FONT_SIZE, null) + : (P) XmlUtils.deepCopy(templateParagraph); + rewriteParagraphText(paragraph, line, true); + applyParagraphFormatting(paragraph, line, false); + cell.getContent().add(paragraph); + } + } + + private R findFirstRun(P paragraph) { + if (paragraph == null) { + return null; + } + for (Object content : paragraph.getContent()) { + Object value = XmlUtils.unwrap(content); + if (value instanceof R run) { + return run; + } + } + return null; + } + + private String[] splitRawLines(String value) { + String source = value == null ? "" : value; + source = source.replace("\r\n", "\n").replace('\r', '\n'); + return source.split("\\R", -1); + } + + private List splitTextLinesPreserveBlank(String value) { + String normalized = normalizeLineBreaks(text(value)); + if (!StringUtils.hasText(normalized)) { + return List.of(); + } + return Arrays.asList(normalized.split("\\R", -1)); + } + + private R createRun(String value, boolean bold, BigInteger fontSize) { + R run = WML_FACTORY.createR(); + run.setRPr(createRunProperties(bold, fontSize)); + Text textNode = WML_FACTORY.createText(); + textNode.setValue(text(value)); + run.getContent().add(textNode); + return run; + } + + private RPr createRunProperties(boolean bold, BigInteger fontSize) { + RPr rPr = WML_FACTORY.createRPr(); + if (bold) { + BooleanDefaultTrue b = WML_FACTORY.createBooleanDefaultTrue(); + b.setVal(Boolean.TRUE); + rPr.setB(b); + rPr.setBCs(b); + } + HpsMeasure size = WML_FACTORY.createHpsMeasure(); + size.setVal(fontSize); + rPr.setSz(size); + HpsMeasure sizeCs = WML_FACTORY.createHpsMeasure(); + sizeCs.setVal(fontSize); + rPr.setSzCs(sizeCs); + return rPr; + } + + private Tr addTableRow(Tbl table, int cellCount) { + Tr row = WML_FACTORY.createTr(); + for (int i = 0; i < cellCount; i++) { + Tc cell = WML_FACTORY.createTc(); + cell.getContent().add(WML_FACTORY.createP()); + row.getContent().add(cell); + } + table.getContent().add(row); + return row; + } + + private List rowCells(Tr row) { + List cells = new ArrayList<>(); + for (Object content : row.getContent()) { + Object value = XmlUtils.unwrap(content); + if (value instanceof Tc cell) { + cells.add(cell); + } + } + return cells; + } + + private void clearCellContent(Tc cell) { + cell.getContent().clear(); + } private String buildTravelText(List travelSegments) { if (travelSegments == null || travelSegments.isEmpty()) { return ""; diff --git a/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml b/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml index 4135ce4..f47cbf8 100644 --- a/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml +++ b/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml @@ -6,12 +6,12 @@ spring: username: # Nacos 账号 password: # Nacos 密码 discovery: # 【配置中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 + namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP metadata: version: 1.0.0 # 服务实例的版本号,可用于灰度发布 config: # 【注册中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 + namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP #################### 数据库相关配置 #################### @@ -55,7 +55,7 @@ spring: primary: master datasource: master: - url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root password: njcnpqs @@ -63,7 +63,7 @@ spring: data: redis: host: 127.0.0.1 # 地址 - port: 16379 # 端口 + port: 6379 # 端口 database: 1 # 数据库索引 # password: njcnpqs # 密码,建议生产环境开启 diff --git a/rdms-project/rdms-project-boot/src/main/resources/templates/work-report/weekly-report-template.docx b/rdms-project/rdms-project-boot/src/main/resources/templates/work-report/weekly-report-template.docx index 97bb06c..0968abd 100644 Binary files a/rdms-project/rdms-project-boot/src/main/resources/templates/work-report/weekly-report-template.docx and b/rdms-project/rdms-project-boot/src/main/resources/templates/work-report/weekly-report-template.docx differ diff --git a/rdms-system/rdms-system-boot/src/main/resources/application-local.yaml b/rdms-system/rdms-system-boot/src/main/resources/application-local.yaml index 81d8577..698521b 100644 --- a/rdms-system/rdms-system-boot/src/main/resources/application-local.yaml +++ b/rdms-system/rdms-system-boot/src/main/resources/application-local.yaml @@ -6,12 +6,12 @@ spring: username: # Nacos 账号 password: # Nacos 密码 discovery: # 【配置中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 + namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP metadata: version: 1.0.0 # 服务实例的版本号,可用于灰度发布 config: # 【注册中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 + namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP #################### 数据库相关配置 #################### @@ -55,7 +55,7 @@ spring: primary: master datasource: master: - url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root password: njcnpqs @@ -63,7 +63,7 @@ spring: data: redis: host: 127.0.0.1 # 地址 - port: 16379 # 端口 + port: 6379 # 端口 database: 1 # 数据库索引 # password: njcnpqs # 密码,建议生产环境开启