【新增】 wps_word相关方法(考点创建、文件对比等)

This commit is contained in:
RENWEIBING\letre
2025-05-04 22:39:59 +08:00
parent ecdca2f3a3
commit 3a0886b94a
21 changed files with 1236 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
package pc.exam.pp.module.judgement.controller.admin.WpsWord;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import pc.exam.pp.framework.common.enums.CommonStatusEnum;
import pc.exam.pp.framework.common.pojo.CommonResult;
import pc.exam.pp.framework.common.util.object.BeanUtils;
import pc.exam.pp.module.judgement.controller.admin.WpsWord.vo.WordListReqVO;
import pc.exam.pp.module.judgement.controller.admin.WpsWord.vo.WordRespVO;
import pc.exam.pp.module.judgement.controller.admin.WpsWord.vo.WordSaveReqVO;
import pc.exam.pp.module.judgement.controller.admin.WpsWord.vo.WordSimpleRespVO;
import pc.exam.pp.module.judgement.dal.dataobject.wpsword.WpsWordLinkDO;
import pc.exam.pp.module.judgement.service.wps_word.WpsWordLinkService;
import pc.exam.pp.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
import pc.exam.pp.module.system.controller.admin.dept.vo.dept.DeptRespVO;
import pc.exam.pp.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO;
import pc.exam.pp.module.system.controller.admin.dept.vo.dept.DeptSimpleRespVO;
import pc.exam.pp.module.system.dal.dataobject.dept.DeptDO;
import java.util.List;
import static pc.exam.pp.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - wps_word")
@RestController
@RequestMapping("/wps/word")
@Validated
public class WordController {
@Resource
private WpsWordLinkService wpsWordLinkService;
@PostMapping("create")
@Operation(summary = "创建wps_word")
public CommonResult<Long> createWord(@Valid @RequestBody WordSaveReqVO createReqVO) {
Long wordId = wpsWordLinkService.createWord(createReqVO);
return success(wordId);
}
@PutMapping("update")
@Operation(summary = "更新wps_word")
public CommonResult<Boolean> updateWord(@Valid @RequestBody WordSaveReqVO updateReqVO) {
wpsWordLinkService.updateWord(updateReqVO);
return success(true);
}
@DeleteMapping("delete")
@Operation(summary = "删除wps_word")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<Boolean> deleteWord(@RequestParam("id") Long id) {
wpsWordLinkService.deleteWord(id);
return success(true);
}
@GetMapping("/list")
@Operation(summary = "获取wps_word列表")
public CommonResult<List<WordRespVO>> getWordList(WordListReqVO reqVO) {
List<WpsWordLinkDO> list = wpsWordLinkService.getWordList(reqVO);
return success(BeanUtils.toBean(list, WordRespVO.class));
}
@GetMapping(value = {"/list-all-simple", "/simple-list"})
@Operation(summary = "获取wps_word精简信息列表", description = "只包含被开启的wps_word主要用于前端的下拉选项")
public CommonResult<List<WordSimpleRespVO>> getSimpleWordList() {
List<WpsWordLinkDO> list = wpsWordLinkService.getWordList(
new WordListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()));
return success(BeanUtils.toBean(list, WordSimpleRespVO.class));
}
@GetMapping("/get")
@Operation(summary = "获得wps_word信息")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<WordRespVO> getDept(@RequestParam("id") Long id) {
WpsWordLinkDO word = wpsWordLinkService.getWord(id);
return success(BeanUtils.toBean(word, WordRespVO.class));
}
}

View File

@@ -0,0 +1,40 @@
package pc.exam.pp.module.judgement.controller.admin.WpsWord;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import pc.exam.pp.framework.common.pojo.CommonResult;
import pc.exam.pp.module.judgement.service.wps_word.JudgementWpsWordService;
import pc.exam.pp.module.judgement.utils.wps_word.vo.WordVO;
import java.io.IOException;
import java.util.List;
/**
* wps word
* rwb
*/
@RestController
@RequestMapping("/tool/wps_word")
@Tag( name = "WPSWORD")
@Validated
public class WpsWordController {
@Autowired
private JudgementWpsWordService judgementWpsWordService;
/**
* wps word
* @return 判分
*/
@GetMapping("/run_wps_word")
public CommonResult<List<WordVO>> run_wps_word() throws Exception {
return CommonResult.success(judgementWpsWordService.ProgrammingWpsWord("1"));
}
}

View File

@@ -0,0 +1,21 @@
package pc.exam.pp.module.judgement.controller.admin.WpsWord.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - WpsWord对应关系 Request VO")
@Data
public class WordListReqVO {
@Schema(description = "节点名称模糊匹配", example = "芋道")
private String name;
@Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1")
private Integer status;
/**
* 类型
*/
private Integer type;
private Integer belongTo;
}

View File

@@ -0,0 +1,40 @@
package pc.exam.pp.module.judgement.controller.admin.WpsWord.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 节点信息 Response VO")
@Data
public class WordRespVO {
@Schema(description = "节点编号", example = "1024")
private Long id;
@Schema(description = "节点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
private String name;
@Schema(description = "父节点 ID", example = "1024")
private Long parentId;
@Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Integer sort;
@Schema(description = "节点方法")
private String nodeFunction;
@Schema(description = "转中文")
private String toChinese;
@Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式")
private LocalDateTime createTime;
/**
* 类型
*/
private Integer type;
private Integer belongTo;
}

View File

@@ -0,0 +1,47 @@
package pc.exam.pp.module.judgement.controller.admin.WpsWord.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import pc.exam.pp.framework.common.enums.CommonStatusEnum;
import pc.exam.pp.framework.common.validation.InEnum;
@Schema(description = "管理后台 - 节点创建/修改 Request VO")
@Data
public class WordSaveReqVO {
@Schema(description = "节点编号", example = "1024")
private Long id;
@Schema(description = "节点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
@NotBlank(message = "节点名称不能为空")
@Size(max = 30, message = "节点名称长度不能超过 30 个字符")
private String name;
/**
* 类型
*/
private Integer type;
private Integer belongTo;
@Schema(description = "父节点 ID", example = "1024")
private Long parentId;
@Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "显示顺序不能为空")
private Integer sort;
@Schema(description = "节点方法")
private String nodeFunction;
@Schema(description = "转中文")
private String toChinese;
@Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "状态不能为空")
@InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}")
private Integer status;
}

View File

@@ -0,0 +1,27 @@
package pc.exam.pp.module.judgement.controller.admin.WpsWord.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Schema(description = "管理后台 - 节点精简信息 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WordSimpleRespVO {
@Schema(description = "节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "节点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
private String name;
@Schema(description = "父节点 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long parentId;
/**
* 类型
*/
private Integer type;
private Integer belongTo;
}

View File

@@ -0,0 +1,67 @@
package pc.exam.pp.module.judgement.dal.dataobject.wpsword;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import pc.exam.pp.framework.common.enums.CommonStatusEnum;
import pc.exam.pp.framework.tenant.core.db.TenantBaseDO;
import pc.exam.pp.module.judgement.utils.tree.vo.TreeVO;
import java.util.ArrayList;
import java.util.List;
/**
* wps word关系对应表
*
*/
@TableName("wps_word_link")
@Data
@EqualsAndHashCode(callSuper = true)
public class WpsWordLinkDO extends TenantBaseDO {
public static final Long PARENT_ID_ROOT = 0L;
/**
* 部门ID
*/
@TableId
private Long id;
/**
* 部门名称
*/
private String name;
/**
* 父部门ID
*
* 关联 {@link #id}
*/
private Long parentId;
/**
* 显示顺序
*/
private Integer sort;
/**
* 节点功能
*/
private String nodeFunction;
/**
* 中文描述
*/
private String toChinese;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 类型
*/
private Integer type;
private String belongTo;
@TableField(exist = false)
private List<WpsWordLinkDO> children = new ArrayList<>();
}

View File

@@ -0,0 +1,37 @@
package pc.exam.pp.module.judgement.dal.mysql.wpsword;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import pc.exam.pp.framework.mybatis.core.mapper.BaseMapperX;
import pc.exam.pp.framework.mybatis.core.query.LambdaQueryWrapperX;
import pc.exam.pp.module.judgement.controller.admin.WpsWord.vo.WordListReqVO;
import pc.exam.pp.module.judgement.dal.dataobject.wpsword.WpsWordLinkDO;
import pc.exam.pp.module.judgement.utils.tree.vo.TreeVO;
import java.util.Collection;
import java.util.List;
@Mapper
public interface WpsWordLinkMapper extends BaseMapperX<WpsWordLinkDO> {
default List<WpsWordLinkDO> selectList(WordListReqVO reqVO) {
return selectList(new LambdaQueryWrapperX<WpsWordLinkDO>()
.likeIfPresent(WpsWordLinkDO::getName, reqVO.getName())
.eqIfPresent(WpsWordLinkDO::getStatus, reqVO.getStatus()));
}
default WpsWordLinkDO selectByParentIdAndName(Long parentId, String name) {
return selectOne(WpsWordLinkDO::getParentId, parentId, WpsWordLinkDO::getName, name);
}
default Long selectCountByParentId(Long parentId) {
return selectCount(WpsWordLinkDO::getParentId, parentId);
}
default List<WpsWordLinkDO> selectListByParentId(Collection<Long> parentIds) {
return selectList(WpsWordLinkDO::getParentId, parentIds);
}
List<TreeVO> selectTreeListByNodeFunction();
}

View File

@@ -0,0 +1,23 @@
package pc.exam.pp.module.judgement.service.wps_word;
import pc.exam.pp.module.judgement.utils.wps_word.vo.WordVO;
import java.util.List;
/**
* 判分逻辑集合wps word
*
* @author rwb
*/
public interface JudgementWpsWordService {
/**
* 获取word文件内得考点及描述
* @param path minio文件路径
* @return 文件内得考点及描述
* @throws Exception 异常
*/
public List<WordVO> ProgrammingWpsWord(String path) throws Exception;
}

View File

@@ -0,0 +1,50 @@
package pc.exam.pp.module.judgement.service.wps_word;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import pc.exam.pp.module.infra.dal.dataobject.config.ConfigDO;
import pc.exam.pp.module.infra.service.config.ConfigService;
import pc.exam.pp.module.judgement.controller.admin.WpsWord.vo.WordListReqVO;
import pc.exam.pp.module.judgement.dal.dataobject.wpsword.WpsWordLinkDO;
import pc.exam.pp.module.judgement.dal.mysql.wpsword.WpsWordLinkMapper;
import pc.exam.pp.module.judgement.service.auto_tools.AutoToolsService;
import pc.exam.pp.module.judgement.utils.wps_word.WpsWordUtils;
import pc.exam.pp.module.judgement.utils.wps_word.vo.WordVO;
import java.io.File;
import java.util.List;
@Service
public class JudgementWpsWordServiceImpl implements JudgementWpsWordService {
@Resource
WpsWordLinkMapper wpsWordLinkMapper;
@Resource
AutoToolsService autoToolsService;
@Resource
ConfigService configService;
@Override
public List<WordVO> ProgrammingWpsWord(String path) throws Exception {
// 1、获取文件临时下载路径
ConfigDO config = configService.getConfigByKey("file_down_path");
// 2、下载文件并返回文件完整路径
String pathName = autoToolsService.downloadStudentFile(path, config.getValue());
// 3、创建word考点tree
WordListReqVO wordListReqVO = new WordListReqVO();
wordListReqVO.setBelongTo(0);
List<WpsWordLinkDO> list = wpsWordLinkMapper.selectList(wordListReqVO);
// 4、docx文件读取并返回考点及说明信息
List<WordVO> margins1 = WpsWordUtils.wps_word(pathName, list);
// 5、已经读取完得考点删除源文件
File file = new File(pathName);
file.delete();
return margins1;
}
}

View File

@@ -0,0 +1,112 @@
package pc.exam.pp.module.judgement.service.wps_word;
import pc.exam.pp.framework.common.util.collection.CollectionUtils;
import pc.exam.pp.module.judgement.controller.admin.WpsWord.vo.WordListReqVO;
import pc.exam.pp.module.judgement.controller.admin.WpsWord.vo.WordSaveReqVO;
import pc.exam.pp.module.judgement.dal.dataobject.wpsword.WpsWordLinkDO;
import pc.exam.pp.module.judgement.utils.tree.vo.TreeVO;
import java.util.*;
/**
* 节点 Service 接口
*
* @author 芋道源码
*/
public interface WpsWordLinkService {
/**
* 创建节点
*
* @param createReqVO 节点信息
* @return 节点编号
*/
Long createWord(WordSaveReqVO createReqVO);
/**
* 更新节点
*
* @param updateReqVO 节点信息
*/
void updateWord(WordSaveReqVO updateReqVO);
/**
* 删除节点
*
* @param id 节点编号
*/
void deleteWord(Long id);
/**
* 获得节点信息
*
* @param id 节点编号
* @return 节点信息
*/
WpsWordLinkDO getWord(Long id);
/**
* 获得节点信息数组
*
* @param ids 节点编号数组
* @return 节点信息数组
*/
List<WpsWordLinkDO> getWordList(Collection<Long> ids);
/**
* 筛选节点列表
*
* @param reqVO 筛选条件请求 VO
* @return 节点列表
*/
List<WpsWordLinkDO> getWordList(WordListReqVO reqVO);
/**
* 获得指定编号的节点 Map
*
* @param ids 节点编号数组
* @return 节点 Map
*/
default Map<Long, WpsWordLinkDO> getWordMap(Collection<Long> ids) {
List<WpsWordLinkDO> list = getWordList(ids);
return CollectionUtils.convertMap(list, WpsWordLinkDO::getId);
}
/**
* 获得指定节点的所有子节点
*
* @param id 节点编号
* @return 子节点列表
*/
default List<WpsWordLinkDO> getChildWordList(Long id) {
return getChildWordList(Collections.singleton(id));
}
/**
* 获得指定节点的所有子节点
*
* @param ids 节点编号数组
* @return 子节点列表
*/
List<WpsWordLinkDO> getChildWordList(Collection<Long> ids);
/**
* 获得所有子节点,从缓存中
*
* @param id 父节点编号
* @return 子节点列表
*/
Set<Long> getChildWordIdListFromCache(Long id);
/**
* 校验节点们是否有效。如下情况,视为无效:
* 1. 节点编号不存在
* 2. 节点被禁用
*
* @param ids 角色编号数组
*/
void validateWordList(Collection<Long> ids);
List<TreeVO> getWordTreeList();
}

View File

@@ -0,0 +1,225 @@
package pc.exam.pp.module.judgement.service.wps_word;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import pc.exam.pp.framework.common.enums.CommonStatusEnum;
import pc.exam.pp.framework.common.util.object.BeanUtils;
import pc.exam.pp.framework.datapermission.core.annotation.DataPermission;
import pc.exam.pp.framework.tenant.core.aop.TenantIgnore;
import pc.exam.pp.module.judgement.controller.admin.WpsWord.vo.WordListReqVO;
import pc.exam.pp.module.judgement.controller.admin.WpsWord.vo.WordSaveReqVO;
import pc.exam.pp.module.judgement.dal.dataobject.wpsword.WpsWordLinkDO;
import pc.exam.pp.module.judgement.dal.mysql.wpsword.WpsWordLinkMapper;
import pc.exam.pp.module.judgement.utils.tree.vo.TreeVO;
import pc.exam.pp.module.system.dal.redis.RedisKeyConstants;
import java.util.*;
import static pc.exam.pp.framework.common.exception.util.ServiceExceptionUtil.exception;
import static pc.exam.pp.framework.common.util.collection.CollectionUtils.convertSet;
import static pc.exam.pp.module.system.enums.ErrorCodeConstants.*;
/**
* 节点 Service 实现类
*
*/
@Service
@Validated
@Slf4j
public class WpsWordLinkServiceImpl implements WpsWordLinkService {
@Resource
private WpsWordLinkMapper wpsWordLinkMapper;
@Override
@CacheEvict(cacheNames = RedisKeyConstants.WPS_WORD_CHILDREN_ID_LIST,
allEntries = true) // allEntries 清空所有缓存,因为操作一个节点,涉及到多个缓存
public Long createWord(WordSaveReqVO createReqVO) {
if (createReqVO.getParentId() == null) {
createReqVO.setParentId(WpsWordLinkDO.PARENT_ID_ROOT);
}
// 校验父节点的有效性
validateParentWord(null, createReqVO.getParentId());
// 校验节点名的唯一性
validateWordNameUnique(null, createReqVO.getParentId(), createReqVO.getName());
// 插入节点
WpsWordLinkDO wpsWordLinkDO = BeanUtils.toBean(createReqVO, WpsWordLinkDO.class);
wpsWordLinkMapper.insert(wpsWordLinkDO);
return wpsWordLinkDO.getId();
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.WPS_WORD_CHILDREN_ID_LIST,
allEntries = true) // allEntries 清空所有缓存,因为操作一个节点,涉及到多个缓存
public void updateWord(WordSaveReqVO updateReqVO) {
if (updateReqVO.getParentId() == null) {
updateReqVO.setParentId(WpsWordLinkDO.PARENT_ID_ROOT);
}
// 校验自己存在
validateWordExists(updateReqVO.getId());
// 校验父节点的有效性
validateParentWord(updateReqVO.getId(), updateReqVO.getParentId());
// 校验节点名的唯一性
validateWordNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName());
// 更新节点
WpsWordLinkDO updateObj = BeanUtils.toBean(updateReqVO, WpsWordLinkDO.class);
wpsWordLinkMapper.updateById(updateObj);
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.WPS_WORD_CHILDREN_ID_LIST,
allEntries = true) // allEntries 清空所有缓存,因为操作一个节点,涉及到多个缓存
public void deleteWord(Long id) {
// 校验是否存在
validateWordExists(id);
// 校验是否有子节点
if (wpsWordLinkMapper.selectCountByParentId(id) > 0) {
throw exception(WORD_NOT_FOUND);
}
// 删除节点
wpsWordLinkMapper.deleteById(id);
}
@VisibleForTesting
void validateWordExists(Long id) {
if (id == null) {
return;
}
WpsWordLinkDO wpsWordLinkDO = wpsWordLinkMapper.selectById(id);
if (wpsWordLinkDO == null) {
throw exception(WORD_NOT_FOUND);
}
}
@VisibleForTesting
void validateParentWord(Long id, Long parentId) {
if (parentId == null || WpsWordLinkDO.PARENT_ID_ROOT.equals(parentId)) {
return;
}
// 1. 不能设置自己为父节点
if (Objects.equals(id, parentId)) {
throw exception(DEPT_PARENT_ERROR);
}
// 2. 父节点不存在
WpsWordLinkDO parentWord = wpsWordLinkMapper.selectById(parentId);
if (parentWord == null) {
throw exception(WORD_PARENT_NOT_EXITS);
}
// 3. 递归校验父节点,如果父节点是自己的子节点,则报错,避免形成环路
if (id == null) { // id 为空,说明新增,不需要考虑环路
return;
}
for (int i = 0; i < Short.MAX_VALUE; i++) {
// 3.1 校验环路
parentId = parentWord.getParentId();
if (Objects.equals(id, parentId)) {
throw exception(WORD_PARENT_IS_CHILD);
}
// 3.2 继续递归下一级父节点
if (parentId == null || WpsWordLinkDO.PARENT_ID_ROOT.equals(parentId)) {
break;
}
parentWord = wpsWordLinkMapper.selectById(parentId);
if (parentWord == null) {
break;
}
}
}
@VisibleForTesting
void validateWordNameUnique(Long id, Long parentId, String name) {
WpsWordLinkDO wpsWordLinkDO = wpsWordLinkMapper.selectByParentIdAndName(parentId, name);
if (wpsWordLinkDO == null) {
return;
}
// 如果 id 为空,说明不用比较是否为相同 id 的节点
if (id == null) {
throw exception(WORD_NAME_DUPLICATE);
}
if (ObjectUtil.notEqual(wpsWordLinkDO.getId(), id)) {
throw exception(WORD_NAME_DUPLICATE);
}
}
@Override
public WpsWordLinkDO getWord(Long id) {
return wpsWordLinkMapper.selectById(id);
}
@Override
public List<WpsWordLinkDO> getWordList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return Collections.emptyList();
}
return wpsWordLinkMapper.selectBatchIds(ids);
}
@Override
public List<WpsWordLinkDO> getWordList(WordListReqVO reqVO) {
List<WpsWordLinkDO> list = wpsWordLinkMapper.selectList(reqVO);
list.sort(Comparator.comparing(WpsWordLinkDO::getSort));
return list;
}
@Override
public List<WpsWordLinkDO> getChildWordList(Collection<Long> ids) {
List<WpsWordLinkDO> children = new LinkedList<>();
// 遍历每一层
Collection<Long> parentIds = ids;
for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下,存在死循环
// 查询当前层,所有的子节点
List<WpsWordLinkDO> words = wpsWordLinkMapper.selectListByParentId(parentIds);
// 1. 如果没有子节点,则结束遍历
if (CollUtil.isEmpty(words)) {
break;
}
// 2. 如果有子节点,继续遍历
children.addAll(words);
parentIds = convertSet(words, WpsWordLinkDO::getId);
}
return children;
}
@Override
@DataPermission(enable = false) // 禁用数据权限,避免建立不正确的缓存
@Cacheable(cacheNames = RedisKeyConstants.WPS_WORD_CHILDREN_ID_LIST, key = "#id")
public Set<Long> getChildWordIdListFromCache(Long id) {
List<WpsWordLinkDO> children = getChildWordList(id);
return convertSet(children, WpsWordLinkDO::getId);
}
@Override
public void validateWordList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
// 获得科室信息
Map<Long, WpsWordLinkDO> wordMap = getWordMap(ids);
// 校验
ids.forEach(id -> {
WpsWordLinkDO word = wordMap.get(id);
if (word == null) {
throw exception(WORD_NOT_FOUND);
}
if (!CommonStatusEnum.ENABLE.getStatus().equals(word.getStatus())) {
throw exception(WORD_NOT_ENABLE, word.getName());
}
});
}
@Override
@TenantIgnore
public List<TreeVO> getWordTreeList() {
return wpsWordLinkMapper.selectTreeListByNodeFunction();
}
}

View File

@@ -0,0 +1,21 @@
package pc.exam.pp.module.judgement.utils.count;
// 工具方法twips 转厘米
public class TwipsToCmUtils {
// 工具方法twips 转厘米(保留两位小数)
public static double convertTwipsToCm(String twipsStr) {
if (twipsStr == null || twipsStr.isEmpty()) {
return 0.0;
}
try {
int twips = Integer.parseInt(twipsStr);
double cm = twips * 2.54 / 1440;
// 四舍五入保留两位小数
return Math.round(cm * 100.0) / 100.0;
} catch (NumberFormatException e) {
return 0.0; // 解析失败返回0
}
}
}

View File

@@ -0,0 +1,35 @@
package pc.exam.pp.module.judgement.utils.tree;
import pc.exam.pp.module.judgement.dal.dataobject.wpsword.WpsWordLinkDO;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TreeUtils {
public static List<WpsWordLinkDO> buildTree(List<WpsWordLinkDO> flatList) {
Map<Integer, WpsWordLinkDO> nodeMap = new HashMap<>();
List<WpsWordLinkDO> roots = new ArrayList<>();
// 先放入 map
for (WpsWordLinkDO node : flatList) {
nodeMap.put(Math.toIntExact(node.getId()), node);
}
// 构建树关系
for (WpsWordLinkDO node : flatList) {
if (node.getParentId() == 0) {
roots.add(node);
} else {
WpsWordLinkDO parent = nodeMap.get(node.getParentId().intValue());
if (parent != null) {
parent.getChildren().add(node);
}
}
}
return roots;
}
}

View File

@@ -0,0 +1,31 @@
package pc.exam.pp.module.judgement.utils.tree.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import org.apache.ibatis.type.JdbcType;
import pc.exam.pp.framework.tenant.core.db.TenantBaseDO;
import java.util.ArrayList;
import java.util.List;
@Data
public class TreeVO extends TenantBaseDO {
private int id;
private int parentId;
private String name;
private String nodeFunction;
private String toChinese;
private int type;
private List<TreeVO> children = new ArrayList<>();
public TreeVO(int id, int parentId, String name, String nodeFunction, String toChinese, int type) {
this.id = id;
this.parentId = parentId;
this.name = name;
this.nodeFunction = nodeFunction;
this.toChinese = toChinese;
this.type = type;
}
}

View File

@@ -0,0 +1,282 @@
package pc.exam.pp.module.judgement.utils.wps_word;
import jakarta.annotation.Resource;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlObject;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import pc.exam.pp.module.judgement.dal.dataobject.wpsword.WpsWordLinkDO;
import pc.exam.pp.module.judgement.utils.tree.TreeUtils;
import pc.exam.pp.module.judgement.utils.wps_word.vo.WordVO;
import java.io.FileInputStream;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
public class WpsWordUtils {
/**
* 获取文档段落W:P标签得数量判断出一个有多少段
*/
public static List<WordVO> wps_word(String filePath, List<WpsWordLinkDO> wpsWordLinkDOS) throws Exception {
List<WordVO> wordVO = new ArrayList<>();
int count = 0;
// 路径初始化
String xmlpath = "";
XWPFDocument document = new XWPFDocument(new FileInputStream(filePath));
String xmlString = XmlUtil.getDocumentXml(document);
// 获取CTDocument对象
XmlObject docXml = document.getDocument();
List<WpsWordLinkDO> tree = TreeUtils.buildTree(wpsWordLinkDOS); // flatList 是你贴出的 JSON 解析出来的
XmlObject xmlObject = document.getDocument(); // Word XML document
// 创建一个list存放word读取的数据
for (WpsWordLinkDO wpsWordLinkDO : tree) {
// 段落划分 根据w:p标签
// 到达参数节点,构造 XPath 路径查询它的值
String xpath = "declare namespace w='http://schemas.openxmlformats.org/wordprocessingml/2006/main' ";
xpath += "//"+wpsWordLinkDO.getName();
XmlCursor cursor = docXml.newCursor();
int index = 0;
cursor.selectPath(xpath);
String xpathName = "declare namespace w='http://schemas.openxmlformats.org/wordprocessingml/2006/main' ";
while (cursor.toNextSelection()) {
// 每一段进行赋值,进行判断
XmlObject paraObj = cursor.getObject();
String paraText = paraObj.xmlText();
// System.out.println(paraText);
// System.out.println(cursor.getTextValue());
index += 1;
for (WpsWordLinkDO root : wpsWordLinkDO.getChildren()) {
traverseTreeAndQueryXml(wordVO, wpsWordLinkDO.getName(), cursor.getTextValue(), xpathName, root, paraObj, new ArrayList<>(), new ArrayList<>(), index,1);
}
}
}
// 读取页眉的方法(需要通过页眉查询,水印,页眉文字等信息)
List<XWPFHeader> headers = document.getHeaderList();
for (XWPFHeader header : headers) {
if (!Objects.equals(header.getText(), "")) {
headerAndFooter(header, null, wordVO);
}
// 获取底层 XML 对象
CTHdrFtr ctHdrFtr = header._getHdrFtr(); // ✅ 正确方法
String xpath = "declare namespace w='http://schemas.openxmlformats.org/wordprocessingml/2006/main' ";
xpath += "//w:hdr/w:p/w:r/w:pict";
XmlCursor cursor = ctHdrFtr.newCursor();
cursor.selectPath(xpath);
while (cursor.toNextSelection()) {
// 查询 v:textpath 节点
cursor.selectPath(
"declare namespace v='urn:schemas-microsoft-com:vml' " +
".//v:textpath"
);
while (cursor.toNextSelection()) {
XmlObject textpathObj = cursor.getObject();
String xml = textpathObj.xmlText();
WordVO attr_word = new WordVO();
// 提取属性值
String stringAttr = cursor.getAttributeText(new javax.xml.namespace.QName("string"));
String styleAttr = cursor.getAttributeText(new javax.xml.namespace.QName("style"));
attr_word.setWordText("水印");
List<String> attr = new ArrayList<>();
attr.add("水印文字 string 属性: " + stringAttr);
attr.add("水印 style 属性: " + styleAttr);
attr_word.setKeynoteChinese(attr);
List<String> path = new ArrayList<>();
path.add("name:"+stringAttr);
path.add("style:"+styleAttr);
attr_word.setExamKeynote(path);
wordVO.add(attr_word);
}
}
cursor.close();
}
// 获取页脚
List<XWPFFooter> footers = document.getFooterList();
for (XWPFFooter footer : footers) {
if (!Objects.equals(footer.getText(), "")) {
headerAndFooter(null, footer, wordVO);
}
}
// 邮件合并
List<String> email = getMailMergeFields(document);
if (!email.isEmpty()) {
// 使用了邮件合并功能
WordVO email_word = new WordVO();
email_word.setWordText("邮件合并");
email_word.setExamKeynote(email);
email_word.setKeynoteChinese(email);
wordVO.add(email_word);
}
return wordVO;
}
// 页眉页脚
public static void headerAndFooter(XWPFHeader header, XWPFFooter footer, List<WordVO> wordVO) {
if (header != null) {
for (XWPFParagraph para : header.getParagraphs()) {
for (XWPFRun run : para.getRuns()) {
String text = run.getText(0);
if (text != null) {
WordVO wordVO1 = new WordVO();
wordVO1.setWordText("页眉:" + text);
List<String> style_list = new ArrayList<>();
style_list.add("字体: " + run.getFontFamily());
style_list.add("字号: " + run.getFontSize());
style_list.add("颜色: " + run.getColor());
style_list.add("加粗: " + run.isBold());
style_list.add("斜体: " + run.isItalic());
wordVO1.setKeynoteChinese(style_list);
wordVO.add(wordVO1);
}
}
}
}
if (footer != null) {
for (XWPFParagraph para : footer.getParagraphs()) {
for (XWPFRun run : para.getRuns()) {
String text = run.getText(0);
if (text != null) {
WordVO wordVO1 = new WordVO();
wordVO1.setWordText("页脚:" + text);
List<String> style_list = new ArrayList<>();
style_list.add("字体: " + run.getFontFamily());
style_list.add("字号: " + run.getFontSize());
style_list.add("颜色: " + run.getColor());
style_list.add("加粗: " + run.isBold());
style_list.add("斜体: " + run.isItalic());
wordVO1.setKeynoteChinese(style_list);
wordVO.add(wordVO1);
}
}
}
}
}
public static void traverseTreeAndQueryXml(List<WordVO> wordVOs, String firstTitle, String text, String xpath, WpsWordLinkDO node, XmlObject currentXml, List<String> pathSoFar, List<String> pathChinese, int index, int beginIndex) {
// 到达参数节点,构造 XPath 路径查询它的值
if (beginIndex == 1) {
xpath += "//" + firstTitle + "[" + index + "]/";
}
pathSoFar.add(node.getName());
pathChinese.add(node.getToChinese() + index);
if (node.getType() == 1) {
// pathSoFar.remove(0);
xpath += String.join("/", pathSoFar);
// System.out.println(xpath);
try (XmlCursor cursors = currentXml.newCursor()) {
// System.out.println(xpath);
cursors.selectPath(xpath);
if (cursors.toNextSelection()) {
// System.out.println(cursors.xmlText());
// cursors.selectPath("declare namespace w='http://schemas.openxmlformats.org/wordprocessingml/2006/main' //@w:firstLine");
// if (cursors.toNextSelection()) {
// String texts = cursors.getTextValue();
// System.out.println("值:" + texts);
// }
String text_value = cursors.getTextValue();
if (!Objects.equals(text_value, "")) {
// 查找List里面是否已经存在相应文本的数据
String finalText = text;
int list_index = IntStream.range(0, wordVOs.size())
.filter(i -> finalText.equals(wordVOs.get(i).getWordText()))
.findFirst()
.orElse(-1);
// 判断在段落内的样式,如果出现样式,向上兼容,直接再出现样式标签
// System.out.println(xpath);
if (xpath.indexOf("w:sectPr") > 0 && xpath.indexOf("w:p[") > 0) {
// 说明出现了文本页面设置,需要进行向上绑定
// 在获取当前的数组长度,获取上面的属性 遍历已经存在的数据
int tindex = 0;
for (WordVO woVo : wordVOs) {
// 判断标识是否为false,,代表没有进行赋值
if (!woVo.getIsTrue()) {
WordVO wordVO = wordVOs.get(tindex);
wordVO.setWordText(wordVO.getWordText());
woVo.setIsTrue(true);
List<String> kchinese = woVo.getKeynoteChinese();
List<String> examKeynote = woVo.getExamKeynote();
examKeynote.add(String.join("", pathSoFar) + "value" + text_value);
kchinese.add(String.join("", pathChinese) + ",值:" + text_value);
wordVO.setKeynoteChinese(kchinese);
wordVO.setExamKeynote(examKeynote);
wordVOs.set(tindex, woVo);
}
tindex += 1;
}
} else if (list_index < 0) { // 如果没有查询到了
if (xpath.indexOf("w:sectPr[") > 0) {
// 页面属性
// 文本
text = "页面属性";
}
if (!Objects.equals(text, "")) {
if (!text.contains("MERGEFIELD")) {
// 给标志符
WordVO wordVO = new WordVO();
wordVO.setWordText(text);
// 创建list进行存放数据
List<String> kchinese = new ArrayList<>();
List<String> examKeynote = new ArrayList<>();
examKeynote.add(String.join("", pathSoFar) + "value" + text_value);
kchinese.add(String.join("", pathChinese) + ",值:" + text_value);
// 组合完数据后进行存放数据
wordVO.setKeynoteChinese(kchinese);
wordVO.setExamKeynote(examKeynote);
wordVO.setIsTrue(false);
wordVOs.add(wordVO);
}
}
} else { // 如果找到了
WordVO wordVO = wordVOs.get(list_index);
List<String> kchinese = wordVO.getKeynoteChinese();
List<String> examKeynote = wordVO.getExamKeynote();
examKeynote.add(String.join("", pathSoFar) + "value" + text_value);
kchinese.add(String.join("", pathChinese) + ",值:" + text_value);
wordVO.setKeynoteChinese(kchinese);
wordVO.setExamKeynote(examKeynote);
wordVO.setIsTrue(false);
wordVOs.remove(list_index);
wordVOs.add(wordVO);
}
// System.out.println("文本:" + text + " 参数路径:" + String.join(" → ", pathChinese) + ",值:" + text_value);
}
}
}
} else {
for (WpsWordLinkDO child : node.getChildren()) {
traverseTreeAndQueryXml(wordVOs, firstTitle, text, xpath, child, currentXml, new ArrayList<>(pathSoFar), new ArrayList<>(pathChinese), index,2);
}
}
pathSoFar.remove(pathSoFar.size() - 1);
pathChinese.remove(pathChinese.size() - 1);
}
/**
* 使用邮件合并
*/
public static List<String> getMailMergeFields(XWPFDocument document) {
List<String> fieldNames = new ArrayList<>();
for (XWPFParagraph paragraph : document.getParagraphs()) {
for (XWPFRun run : paragraph.getRuns()) {
// 获取底层 CTR 对象
CTR ctr = run.getCTR();
for (CTText instrText : ctr.getInstrTextList()) {
String text = instrText.getStringValue();
if (text != null && text.contains("MERGEFIELD")) {
// 正则提取字段名
Pattern pattern = Pattern.compile("MERGEFIELD\\s+\"?([^\"\\s]+)\"?");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
String fieldName = matcher.group(1).trim();
fieldNames.add(fieldName);
}
}
}
}
}
return fieldNames;
}
}

View File

@@ -0,0 +1,25 @@
package pc.exam.pp.module.judgement.utils.wps_word;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.xmlbeans.XmlObject;
import java.io.StringWriter;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
public class XmlUtil {
public static String getDocumentXml(XWPFDocument doc) throws Exception {
XmlObject xml = doc.getDocument(); // 获取底层 CTDocument1org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDocument1
// 转换为 DOM
org.w3c.dom.Node domNode = xml.getDomNode();
Transformer tf = TransformerFactory.newInstance().newTransformer();
tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
tf.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
tf.transform(new DOMSource(domNode), new StreamResult(writer));
return writer.toString();
}
}

View File

@@ -0,0 +1,24 @@
package pc.exam.pp.module.judgement.utils.wps_word.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class WordVO {
@Schema(description = "相关文本")
private String wordText;
@Schema(description = "考试考点")
private List<String> examKeynote;
@Schema(description = "考点汉化")
private List<String> keynoteChinese;
@Schema(description = "是否有样式修饰")
private Boolean isTrue;
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="pc.exam.pp.module.judgement.dal.mysql.wpsword.WpsWordLinkMapper">
<!--
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
文档可见https://www.iocoder.cn/MyBatis/x-plugins/
-->
<resultMap id="NodeResultMap" type="pc.exam.pp.module.judgement.utils.tree.vo.TreeVO">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="parentId" column="parent_id" jdbcType="BIGINT"/>
<result property="nodeFunction" column="node_function" jdbcType="VARCHAR"/>
<result property="toChinese" column="to_chinese" jdbcType="VARCHAR"/>
<result property="type" column="type" jdbcType="TINYINT"/>
</resultMap>
<select id="selectTreeListByNodeFunction" resultMap="NodeResultMap">
SELECT
*
FROM
wps_word_link
</select>
</mapper>

View File

@@ -174,4 +174,13 @@ public interface ErrorCodeConstants {
ErrorCode STUDENT_USERNAME_LOGIN = new ErrorCode(1_002_029_002, "学生账号无法登陆!");
ErrorCode STUDENT_USERNAME_NOTLOGIN = new ErrorCode(1_002_029_003, "非学生账号无法登陆!");
// ========== WpsWOrd 1-002-030-000 ==========
ErrorCode WORD_NAME_DUPLICATE = new ErrorCode(1_002_030_000, "已经存在该名字的WORD节点");
ErrorCode WORD_PARENT_NOT_EXITS = new ErrorCode(1_002_030_001,"父级WORD节点不存在");
ErrorCode WORD_NOT_FOUND = new ErrorCode(1_002_030_002, "当前WORD节点不存在");
ErrorCode WORD_EXITS_CHILDREN = new ErrorCode(1_002_030_003, "存在子WORD节点无法删除");
ErrorCode WORD_PARENT_ERROR = new ErrorCode(1_002_030_004, "不能设置自己为父WORD节点");
ErrorCode WORD_NOT_ENABLE = new ErrorCode(1_002_030_006, "WORD节点({})不处于开启状态,不允许选择");
ErrorCode WORD_PARENT_IS_CHILD = new ErrorCode(1_002_030_007, "不能设置自己的子WORD节点为父WORD节点");
}

View File

@@ -17,6 +17,14 @@ public interface RedisKeyConstants {
*/
String DEPT_CHILDREN_ID_LIST = "dept_children_ids";
/**
* 指定WPS_WORD的所有子WPS_WORD编号数组的缓存
* <p>
* KEY 格式wps_word_children_ids:{id}
* VALUE 数据类型String 子WPS_WORD编号集合
*/
String WPS_WORD_CHILDREN_ID_LIST = "wps_word_children_ids";
/**
* 角色的缓存
* <p>