Files
tuanwei-java/src/main/java/com/gxwebsoft/gxmu/controller/WxxzxFormController.java
T
weicw 0e4986330a feat(declare): 同一申报项目进行中只允许一条申报
- 新增 DeclareGuardService:按已通过/未处理驳回/待审核数判重,申报项目行加锁防连点
- 9 张申报表补 declare_id / declare_status / declare_remark,提交时写入并随审核结果回写
- 新增撤回、作废(强制放行)接口;挑战杯团队成员按手机号判重
- 申报项目支持配置允许重复提交与最多同时条数
- 迁移脚本 sql/20261102_add_declare_uniqueness.sql(含存量回填与重复清单)
2026-11-02 19:28:56 +08:00

907 lines
40 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.gxwebsoft.gxmu.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import cn.hutool.core.util.StrUtil;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.FileRecord;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.FileRecordService;
import com.gxwebsoft.gxmu.entity.QmgcForm;
import com.gxwebsoft.gxmu.entity.SjqnForm;
import com.gxwebsoft.gxmu.entity.SjtbzbsjForm;
import com.gxwebsoft.gxmu.entity.TzbcyForm;
import com.gxwebsoft.gxmu.entity.WxxzxForm;
import com.gxwebsoft.gxmu.entity.WshqtwForm;
import com.gxwebsoft.gxmu.entity.WshqtzbForm;
import com.gxwebsoft.gxmu.entity.YxgqtdgbForm;
import com.gxwebsoft.gxmu.entity.YxgqtyForm;
import com.gxwebsoft.gxmu.model.WxxzxDocxValidationResult;
import com.gxwebsoft.gxmu.model.WxxzxTopicSemanticSearchResult;
import com.gxwebsoft.gxmu.param.WxxzxMaterialUpdateParam;
import com.gxwebsoft.gxmu.param.WxxzxFormParam;
import com.gxwebsoft.gxmu.service.QmgcFormService;
import com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
import com.gxwebsoft.gxmu.model.DeclareCheckResult;
import com.gxwebsoft.gxmu.service.DeclareGuardService;
import org.springframework.transaction.annotation.Transactional;
import com.gxwebsoft.gxmu.service.SjqnFormService;
import com.gxwebsoft.gxmu.service.SjtbzbsjFormService;
import com.gxwebsoft.gxmu.service.TzbcyFormService;
import com.gxwebsoft.gxmu.service.WxxzxFormService;
import com.gxwebsoft.gxmu.service.WshqtwFormService;
import com.gxwebsoft.gxmu.service.WshqtzbFormService;
import com.gxwebsoft.gxmu.service.YxgqtdgbFormService;
import com.gxwebsoft.gxmu.service.YxgqtyFormService;
import com.gxwebsoft.gxmu.util.GxmuQueryHelper;
import com.gxwebsoft.gxmu.util.WxxzxDocxExportUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.zip.ZipOutputStream;
/**
* 未来学术之星课题申报表控制器
*/
@Api(tags = "未来学术之星课题申报表管理")
@RestController
@RequestMapping("/api/gxmu/wxxzx-form")
public class WxxzxFormController extends BaseController {
private static final String PROJECT_BOOK_KEYWORD = "项目申报书";
private static final String TITLE_FONT_CN = "黑体";
private static final String TITLE_FONT_EN = "SimHei";
private static final double TITLE_FONT_SIZE = 26D;
private static final String BODY_FONT_CN = "宋体";
private static final String BODY_FONT_EN = "SimSun";
private static final double BODY_FONT_SIZE = 10.5D;
private static final int MAX_DETAIL_ITEMS = 20;
private static final int MAX_TOPIC_SEARCH_RESULTS = 30;
private static final String DEFAULT_TOPIC_SEARCH_MODULE = "gxmu_wxxzx_project";
private static final Map<String, String> TOPIC_SEARCH_MODULE_NAME_MAP;
static {
Map<String, String> moduleNameMap = new LinkedHashMap<>();
moduleNameMap.put("gxmu_sjqn", "十佳青年岗位能手");
moduleNameMap.put("gxmu_sjtbzbsj", "十佳团支部书记");
moduleNameMap.put("gxmu_wshqtw", "五四红旗团委");
moduleNameMap.put("gxmu_wshqtzb", "五四红旗团支部");
moduleNameMap.put("gxmu_yxgqtdgb", "优秀共青团干部");
moduleNameMap.put("gxmu_yxgqty", "优秀共青团员");
moduleNameMap.put("gxmu_tzbcy_form", "挑战杯");
moduleNameMap.put("gxmu_qmgc_form", "青马工程");
moduleNameMap.put(DEFAULT_TOPIC_SEARCH_MODULE, "未来学术之星");
TOPIC_SEARCH_MODULE_NAME_MAP = Collections.unmodifiableMap(moduleNameMap);
}
@Resource
private WxxzxFormService wxxzxFormService;
@Resource
private SjqnFormService sjqnFormService;
@Resource
private SjtbzbsjFormService sjtbzbsjFormService;
@Resource
private WshqtwFormService wshqtwFormService;
@Resource
private WshqtzbFormService wshqtzbFormService;
@Resource
private YxgqtdgbFormService yxgqtdgbFormService;
@Resource
private YxgqtyFormService yxgqtyFormService;
@Resource
private TzbcyFormService tzbcyFormService;
@Resource
private QmgcFormService qmgcFormService;
@Resource
private ReviewFlowStarterService reviewFlowStarterService;
@Resource
private DeclareGuardService declareGuardService;
@Resource
private FileRecordService fileRecordService;
@Value("${config.upload-path}")
private String uploadPath;
@Value("${config.server-url}")
private String requestURL;
@ApiOperation("分页查询未来学术之星课题申报表")
@GetMapping("/page")
public ApiResult<PageResult<WxxzxForm>> page(WxxzxFormParam param) {
applyBackendUserScope(param, param::setUserId, param::setUserIds, true);
PageParam<WxxzxForm, WxxzxFormParam> page = new PageParam<>(param);
page.setDefaultOrder("id desc");
return success(new PageResult<>(wxxzxFormService.page(page,
GxmuQueryHelper.applyKeywords(page.getWrapper("keywords"), param.getKeywords(),
"topic_name", "leader", "college_grade_class")).getRecords(), page.getTotal()));
}
@ApiOperation("分页查询当前用户未来学术之星课题申报表")
@GetMapping("/userPage")
public ApiResult<PageResult<WxxzxForm>> userPage(WxxzxFormParam param) {
applyBackendUserScope(param, param::setUserId, param::setUserIds, true);
if (param.getUserId() == null && (param.getUserIds() == null || param.getUserIds().isEmpty())) {
param.setUserId(getLoginUserId());
}
PageParam<WxxzxForm, WxxzxFormParam> page = new PageParam<>(param);
page.setDefaultOrder("id desc");
return success(new PageResult<>(wxxzxFormService.page(page,
GxmuQueryHelper.applyKeywords(page.getWrapper("keywords"), param.getKeywords(),
"topic_name", "leader", "college_grade_class")).getRecords(), page.getTotal()));
}
@ApiOperation("查询全部未来学术之星课题申报表")
@GetMapping()
public ApiResult<List<WxxzxForm>> list(WxxzxFormParam param) {
PageParam<WxxzxForm, WxxzxFormParam> page = new PageParam<>(param);
page.setDefaultOrder("id desc");
return success(wxxzxFormService.list(page.getOrderWrapper(
GxmuQueryHelper.applyKeywords(page.getWrapper("keywords"), param.getKeywords(),
"topic_name", "leader", "college_grade_class"))));
}
@OperationLog
@ApiOperation("导出未来学术之星课题申报材料")
@PostMapping("/export")
public ApiResult<?> export(@RequestBody WxxzxFormParam param, HttpServletRequest request) throws IOException {
applyBackendUserScope(param, param::setUserId, param::setUserIds, true);
if (param.getUserId() == null && (param.getUserIds() == null || param.getUserIds().isEmpty())) {
param.setUserId(getLoginUserId());
}
PageParam<WxxzxForm, WxxzxFormParam> page = new PageParam<>(param);
page.setDefaultOrder("id desc");
List<WxxzxForm> baseRecords = wxxzxFormService.list(page.getOrderWrapper(
GxmuQueryHelper.applyKeywords(page.getWrapper("keywords"), param.getKeywords(),
"topic_name", "leader", "college_grade_class")));
if (baseRecords == null || baseRecords.isEmpty()) {
return fail("暂无可导出的数据");
}
List<WxxzxForm> records = new ArrayList<>();
for (WxxzxForm item : baseRecords) {
if (item == null || item.getId() == null) {
continue;
}
WxxzxForm full = wxxzxFormService.getById(item.getId());
if (full != null) {
records.add(full);
}
}
if (records.isEmpty()) {
return fail("暂无可导出的数据");
}
WxxzxDocxExportUtil.SummaryMeta summaryMeta = new WxxzxDocxExportUtil.SummaryMeta();
summaryMeta.setYearText(resolveYearText(param, records));
summaryMeta.setReviewCollege(resolveReviewCollege(records));
summaryMeta.setReviewReporter(resolveReviewReporter(records));
summaryMeta.setReviewDate(resolveReviewDate(records));
String relativePath = "file/docx/未来学术之星申报材料_" + System.currentTimeMillis() + ".zip";
File targetFile = new File(uploadPath + relativePath);
File parentFile = targetFile.getParentFile();
if (parentFile != null && !parentFile.exists()) {
parentFile.mkdirs();
}
try (FileOutputStream outputStream = new FileOutputStream(targetFile);
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
WxxzxDocxExportUtil.writeExportZip(zipOutputStream, records, summaryMeta);
zipOutputStream.finish();
}
FileRecord result = new FileRecord();
result.setCreateUserId(getLoginUserId());
result.setName(targetFile.getName());
result.setPath(targetFile.getAbsolutePath());
result.setContentType("application/zip");
result.setUrl(requestURL + "/" + relativePath);
fileRecordService.save(result);
return success(result);
}
@ApiOperation("根据id查询未来学术之星课题申报表")
@GetMapping("/{id:\\d+}")
public ApiResult<WxxzxForm> get(@PathVariable("id") Integer id) {
return success(wxxzxFormService.getById(id));
}
@ApiOperation("项目主题语义检索")
@GetMapping("/semantic-search")
public ApiResult<List<WxxzxTopicSemanticSearchResult>> semanticSearch(
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "module", required = false) String module) {
String normalizedKeyword = normalizeDisplayText(keyword);
if (StrUtil.isBlank(normalizedKeyword)) {
return success(new ArrayList<>());
}
String normalizedModule = resolveTopicSearchModule(module);
if (normalizedModule == null) {
return success(new ArrayList<>());
}
return success(searchTopicByModule(normalizedModule, normalizedKeyword));
}
@OperationLog
@ApiOperation("添加未来学术之星课题申报表")
@PostMapping()
@Transactional(rollbackFor = Exception.class)
public ApiResult<?> save(@RequestBody WxxzxForm wxxzxForm) {
User loginUser = getLoginUser();
if (loginUser != null) {
wxxzxForm.setUserId(loginUser.getUserId());
}
DeclareCheckResult declareCheck = declareGuardService.checkBeforeCreate("gxmu_wxxzx_project",
wxxzxForm.getDeclareId(), wxxzxForm.getYear(), wxxzxForm.getUserId());
if (declareCheck.isRejected()) {
return fail(declareCheck.getError());
}
if (declareCheck.getDeclareId() != null) {
wxxzxForm.setDeclareId(declareCheck.getDeclareId());
}
wxxzxForm.setDeclareStatus(1);
if (wxxzxFormService.save(wxxzxForm)) {
reviewFlowStarterService.startReview("gxmu_wxxzx_project", wxxzxForm.getId());
return success("添加成功");
}
return fail("添加失败");
}
@OperationLog
@ApiOperation("修改未来学术之星课题申报表")
@PutMapping()
public ApiResult<?> update(@RequestBody WxxzxForm wxxzxForm) {
if (wxxzxFormService.updateById(wxxzxForm)) {
reviewFlowStarterService.restartReviewWhenLastRejected("gxmu_wxxzx_project", wxxzxForm.getId());
return success("修改成功");
}
return fail("修改失败");
}
@OperationLog
@ApiOperation("更新未来学术之星过程材料")
@PutMapping("/materials/{id}")
public ApiResult<?> updateMaterials(@PathVariable("id") Integer id,
@RequestBody WxxzxMaterialUpdateParam param) {
return wxxzxFormService.updateMaterials(id, param) ? success("保存成功") : fail("保存失败");
}
@OperationLog
@ApiOperation("校验未来学术之星项目申报书docx格式")
@PostMapping("/validate-project-book")
public ApiResult<WxxzxDocxValidationResult> validateProjectBook(@RequestParam("file") MultipartFile file) {
if (file == null || file.isEmpty()) {
return fail("请上传docx文件", null);
}
String originalFilename = file.getOriginalFilename();
if (StrUtil.isBlank(originalFilename) || !StrUtil.endWithIgnoreCase(originalFilename, ".docx")) {
return fail("只能上传docx文件", null);
}
WxxzxDocxValidationResult result = new WxxzxDocxValidationResult();
result.setFileName(originalFilename);
try (InputStream inputStream = file.getInputStream();
XWPFDocument document = new XWPFDocument(inputStream)) {
buildValidationResult(document, result);
return success(result);
} catch (Exception e) {
e.printStackTrace();
return fail("文件解析失败,请确认上传的是有效的docx文件", null);
}
}
@OperationLog
@ApiOperation("删除未来学术之星课题申报表")
@DeleteMapping("/{id:\\d+}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
return wxxzxFormService.removeById(id) ? success("删除成功") : fail("删除失败");
}
@OperationLog
@ApiOperation("批量修改未来学术之星课题申报表")
@PutMapping("/batch")
public ApiResult<?> updateBatch(@RequestBody BatchParam<WxxzxForm> batchParam) {
return batchParam.update(wxxzxFormService, "id") ? success("修改成功") : fail("修改失败");
}
@OperationLog
@ApiOperation("批量删除未来学术之星课题申报表")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
return wxxzxFormService.removeByIds(ids) ? success("删除成功") : fail("删除失败");
}
private String resolveYearText(WxxzxFormParam param, List<WxxzxForm> records) {
if (param.getYear() != null) {
return String.valueOf(param.getYear());
}
if (records != null && !records.isEmpty() && records.get(0).getYear() != null) {
return String.valueOf(records.get(0).getYear());
}
return String.valueOf(java.time.LocalDate.now().getYear());
}
private String resolveReviewCollege(List<WxxzxForm> records) {
if (records == null) {
return "";
}
for (WxxzxForm item : records) {
if (StrUtil.isNotBlank(item.getReviewCollege())) {
return item.getReviewCollege().trim();
}
}
return "";
}
private String resolveReviewReporter(List<WxxzxForm> records) {
if (records == null) {
return "";
}
for (WxxzxForm item : records) {
if (StrUtil.isNotBlank(item.getReviewReporter())) {
return item.getReviewReporter().trim();
}
}
User loginUser = getLoginUser();
if (loginUser == null) {
return "";
}
if (StrUtil.isNotBlank(loginUser.getRealName())) {
return loginUser.getRealName().trim();
}
if (StrUtil.isNotBlank(loginUser.getNickname())) {
return loginUser.getNickname().trim();
}
return loginUser.getUsername();
}
private String resolveReviewDate(List<WxxzxForm> records) {
if (records == null) {
return "";
}
for (WxxzxForm item : records) {
if (StrUtil.isNotBlank(item.getReviewDate())) {
return item.getReviewDate().trim();
}
}
return "";
}
private WxxzxTopicSemanticSearchResult toTopicSearchResult(WxxzxForm form, String keyword) {
WxxzxTopicSemanticSearchResult result = new WxxzxTopicSemanticSearchResult();
result.setId(form.getId());
result.setModule(DEFAULT_TOPIC_SEARCH_MODULE);
result.setModuleName(TOPIC_SEARCH_MODULE_NAME_MAP.get(DEFAULT_TOPIC_SEARCH_MODULE));
result.setTopicName(form.getTopicName());
result.setTopicType(form.getTopicType());
result.setRationale(form.getRationale());
result.setSnippet(buildSnippet(form.getRationale(), keyword));
result.setScore(calculateSemanticScore(form, keyword));
return result;
}
private String resolveTopicSearchModule(String module) {
String normalizedModule = StrUtil.isBlank(module) ? DEFAULT_TOPIC_SEARCH_MODULE : module.trim();
return TOPIC_SEARCH_MODULE_NAME_MAP.containsKey(normalizedModule) ? normalizedModule : null;
}
private List<WxxzxTopicSemanticSearchResult> searchTopicByModule(String module, String keyword) {
if ("gxmu_sjqn".equals(module)) {
return finishTopicSearchResults(sjqnFormService.list(new LambdaQueryWrapper<SjqnForm>()
.select(SjqnForm::getId, SjqnForm::getName, SjqnForm::getApplyType,
SjqnForm::getMainStory, SjqnForm::getCreateTime, SjqnForm::getUpdateTime)
.eq(SjqnForm::getTenantId, getTenantId())
.like(SjqnForm::getName, keyword)
.orderByDesc(SjqnForm::getUpdateTime)
.orderByDesc(SjqnForm::getCreateTime))
.stream()
.map(item -> toTopicSearchResult(module, item.getId(), item.getName(), item.getApplyType(),
item.getMainStory(), keyword))
.collect(Collectors.toList()));
}
if ("gxmu_sjtbzbsj".equals(module)) {
return finishTopicSearchResults(sjtbzbsjFormService.list(new LambdaQueryWrapper<SjtbzbsjForm>()
.select(SjtbzbsjForm::getId, SjtbzbsjForm::getName, SjtbzbsjForm::getBranch,
SjtbzbsjForm::getMainStory, SjtbzbsjForm::getCreateTime, SjtbzbsjForm::getUpdateTime)
.eq(SjtbzbsjForm::getTenantId, getTenantId())
.like(SjtbzbsjForm::getName, keyword)
.orderByDesc(SjtbzbsjForm::getUpdateTime)
.orderByDesc(SjtbzbsjForm::getCreateTime))
.stream()
.map(item -> toTopicSearchResult(module, item.getId(), item.getName(), item.getBranch(),
item.getMainStory(), keyword))
.collect(Collectors.toList()));
}
if ("gxmu_wshqtw".equals(module)) {
return finishTopicSearchResults(wshqtwFormService.list(new LambdaQueryWrapper<WshqtwForm>()
.select(WshqtwForm::getId, WshqtwForm::getOrgName, WshqtwForm::getLeader,
WshqtwForm::getWorkSummaryThreeYears, WshqtwForm::getCreateTime, WshqtwForm::getUpdateTime)
.eq(WshqtwForm::getTenantId, getTenantId())
.like(WshqtwForm::getOrgName, keyword)
.orderByDesc(WshqtwForm::getUpdateTime)
.orderByDesc(WshqtwForm::getCreateTime))
.stream()
.map(item -> toTopicSearchResult(module, item.getId(), item.getOrgName(), item.getLeader(),
item.getWorkSummaryThreeYears(), keyword))
.collect(Collectors.toList()));
}
if ("gxmu_wshqtzb".equals(module)) {
return finishTopicSearchResults(wshqtzbFormService.list(new LambdaQueryWrapper<WshqtzbForm>()
.select(WshqtzbForm::getId, WshqtzbForm::getBranchName, WshqtzbForm::getSecondOrg,
WshqtzbForm::getWorkSummaryThreeYears, WshqtzbForm::getCreateTime, WshqtzbForm::getUpdateTime)
.eq(WshqtzbForm::getTenantId, getTenantId())
.like(WshqtzbForm::getBranchName, keyword)
.orderByDesc(WshqtzbForm::getUpdateTime)
.orderByDesc(WshqtzbForm::getCreateTime))
.stream()
.map(item -> toTopicSearchResult(module, item.getId(), item.getBranchName(), item.getSecondOrg(),
item.getWorkSummaryThreeYears(), keyword))
.collect(Collectors.toList()));
}
if ("gxmu_yxgqtdgb".equals(module)) {
return finishTopicSearchResults(yxgqtdgbFormService.list(new LambdaQueryWrapper<YxgqtdgbForm>()
.select(YxgqtdgbForm::getId, YxgqtdgbForm::getName, YxgqtdgbForm::getOrganization,
YxgqtdgbForm::getMainStory, YxgqtdgbForm::getCreateTime, YxgqtdgbForm::getUpdateTime)
.eq(YxgqtdgbForm::getTenantId, getTenantId())
.like(YxgqtdgbForm::getName, keyword)
.orderByDesc(YxgqtdgbForm::getUpdateTime)
.orderByDesc(YxgqtdgbForm::getCreateTime))
.stream()
.map(item -> toTopicSearchResult(module, item.getId(), item.getName(), item.getOrganization(),
item.getMainStory(), keyword))
.collect(Collectors.toList()));
}
if ("gxmu_yxgqty".equals(module)) {
return finishTopicSearchResults(yxgqtyFormService.list(new LambdaQueryWrapper<YxgqtyForm>()
.select(YxgqtyForm::getId, YxgqtyForm::getName, YxgqtyForm::getCollegeMajorClass,
YxgqtyForm::getMainStory, YxgqtyForm::getCreateTime, YxgqtyForm::getUpdateTime)
.eq(YxgqtyForm::getTenantId, getTenantId())
.like(YxgqtyForm::getName, keyword)
.orderByDesc(YxgqtyForm::getUpdateTime)
.orderByDesc(YxgqtyForm::getCreateTime))
.stream()
.map(item -> toTopicSearchResult(module, item.getId(), item.getName(), item.getCollegeMajorClass(),
item.getMainStory(), keyword))
.collect(Collectors.toList()));
}
if ("gxmu_tzbcy_form".equals(module)) {
return finishTopicSearchResults(tzbcyFormService.list(new LambdaQueryWrapper<TzbcyForm>()
.select(TzbcyForm::getId, TzbcyForm::getProjectName, TzbcyForm::getProjectType,
TzbcyForm::getProjectBrief, TzbcyForm::getCreateTime, TzbcyForm::getUpdateTime)
.eq(TzbcyForm::getTenantId, getTenantId())
.like(TzbcyForm::getProjectName, keyword)
.orderByDesc(TzbcyForm::getUpdateTime)
.orderByDesc(TzbcyForm::getCreateTime))
.stream()
.map(item -> toTopicSearchResult(module, item.getId(), item.getProjectName(), item.getProjectType(),
item.getProjectBrief(), keyword))
.collect(Collectors.toList()));
}
if ("gxmu_qmgc_form".equals(module)) {
return finishTopicSearchResults(qmgcFormService.list(new LambdaQueryWrapper<QmgcForm>()
.select(QmgcForm::getId, QmgcForm::getName, QmgcForm::getSchoolInfo,
QmgcForm::getResume, QmgcForm::getCreateTime, QmgcForm::getUpdateTime)
.eq(QmgcForm::getTenantId, getTenantId())
.like(QmgcForm::getName, keyword)
.orderByDesc(QmgcForm::getUpdateTime)
.orderByDesc(QmgcForm::getCreateTime))
.stream()
.map(item -> toTopicSearchResult(module, item.getId(), item.getName(), item.getSchoolInfo(),
item.getResume(), keyword))
.collect(Collectors.toList()));
}
return finishTopicSearchResults(wxxzxFormService.list(new LambdaQueryWrapper<WxxzxForm>()
.select(WxxzxForm::getId, WxxzxForm::getTopicName, WxxzxForm::getTopicType,
WxxzxForm::getRationale, WxxzxForm::getCreateTime, WxxzxForm::getUpdateTime)
.eq(WxxzxForm::getTenantId, getTenantId())
.like(WxxzxForm::getTopicName, keyword)
.orderByDesc(WxxzxForm::getUpdateTime)
.orderByDesc(WxxzxForm::getCreateTime))
.stream()
.map(item -> toTopicSearchResult(item, keyword))
.collect(Collectors.toList()));
}
private WxxzxTopicSemanticSearchResult toTopicSearchResult(
String module, Integer id, String title, String type, String content, String keyword) {
WxxzxTopicSemanticSearchResult result = new WxxzxTopicSemanticSearchResult();
result.setId(id);
result.setModule(module);
result.setModuleName(TOPIC_SEARCH_MODULE_NAME_MAP.get(module));
result.setTopicName(title);
result.setTopicType(type);
result.setRationale(content);
result.setSnippet(buildSnippet(content, keyword));
result.setScore(calculateTitleScore(title, keyword));
return result;
}
private List<WxxzxTopicSemanticSearchResult> finishTopicSearchResults(List<WxxzxTopicSemanticSearchResult> results) {
return results.stream()
.filter(item -> item.getScore() != null && item.getScore() > 0)
.sorted(Comparator.comparing(WxxzxTopicSemanticSearchResult::getScore,
Comparator.nullsLast(Comparator.reverseOrder())))
.limit(MAX_TOPIC_SEARCH_RESULTS)
.collect(Collectors.toList());
}
private int calculateSemanticScore(WxxzxForm form, String keyword) {
List<String> terms = extractSearchTerms(keyword);
int score = 0;
score += calculateFieldScore(form.getTopicName(), keyword, terms, 140, 28, true);
score += calculateFieldScore(form.getTopicType(), keyword, terms, 90, 20, false);
score += calculateFieldScore(form.getRationale(), keyword, terms, 60, 12, false);
return score;
}
private int calculateTitleScore(String title, String keyword) {
return calculateFieldScore(title, keyword, extractSearchTerms(keyword), 140, 28, true);
}
private int calculateFieldScore(String fieldValue, String keyword, List<String> terms,
int exactScore, int termScore, boolean titleField) {
String normalizedField = normalizeCheckText(fieldValue);
if (StrUtil.isBlank(normalizedField)) {
return 0;
}
String normalizedKeyword = normalizeCheckText(keyword);
int score = 0;
if (normalizedField.contains(normalizedKeyword)) {
score += exactScore;
if (normalizedField.startsWith(normalizedKeyword)) {
score += exactScore / 2;
}
score += Math.min(countOccurrences(normalizedField, normalizedKeyword) * 10, 30);
}
int matchedTerms = 0;
for (String term : terms) {
String normalizedTerm = normalizeCheckText(term);
if (StrUtil.isBlank(normalizedTerm) || normalizedTerm.length() < 2) {
continue;
}
if (normalizedField.contains(normalizedTerm)) {
matchedTerms++;
score += termScore;
}
}
if (titleField && matchedTerms == terms.size() && !terms.isEmpty()) {
score += 30;
}
return score;
}
private List<String> extractSearchTerms(String keyword) {
String[] splits = normalizeDisplayText(keyword).split("[\\s,,;;。、】【()()]+");
List<String> terms = new ArrayList<>();
for (String split : splits) {
if (StrUtil.isNotBlank(split)) {
terms.add(split.trim());
}
}
if (terms.isEmpty() && StrUtil.isNotBlank(keyword)) {
terms.add(keyword.trim());
}
return terms;
}
private int countOccurrences(String text, String pattern) {
if (StrUtil.isBlank(text) || StrUtil.isBlank(pattern)) {
return 0;
}
int count = 0;
int index = 0;
while ((index = text.indexOf(pattern, index)) >= 0) {
count++;
index += pattern.length();
}
return count;
}
private String buildSnippet(String rationale, String keyword) {
String content = normalizeDisplayText(rationale);
if (StrUtil.isBlank(content)) {
return "暂无立论依据";
}
String normalizedKeyword = normalizeDisplayText(keyword);
int index = content.indexOf(normalizedKeyword);
if (index < 0) {
for (String term : extractSearchTerms(keyword)) {
index = content.indexOf(term);
if (index >= 0) {
break;
}
}
}
if (index < 0) {
return abbreviate(content, 120);
}
int start = Math.max(index - 28, 0);
int end = Math.min(index + normalizedKeyword.length() + 68, content.length());
String snippet = content.substring(start, end);
if (start > 0) {
snippet = "..." + snippet;
}
if (end < content.length()) {
snippet = snippet + "...";
}
return snippet;
}
private void buildValidationResult(XWPFDocument document, WxxzxDocxValidationResult result) {
List<XWPFParagraph> paragraphs = document.getParagraphs();
List<WxxzxDocxValidationResult.Item> detailItems = new ArrayList<>();
boolean titleFound = false;
boolean titlePassed = true;
boolean bodyPassed = true;
int bodyCount = 0;
int issueCount = 0;
for (int i = 0; i < paragraphs.size(); i++) {
XWPFParagraph paragraph = paragraphs.get(i);
String displayText = normalizeDisplayText(paragraph.getText());
if (StrUtil.isBlank(displayText)) {
continue;
}
boolean isTitleParagraph = normalizeCheckText(displayText).contains(PROJECT_BOOK_KEYWORD);
ValidationIssue issue = isTitleParagraph
? validateParagraph(paragraph, i + 1, TITLE_FONT_CN, TITLE_FONT_EN, TITLE_FONT_SIZE)
: validateParagraph(paragraph, i + 1, BODY_FONT_CN, BODY_FONT_EN, BODY_FONT_SIZE);
if (isTitleParagraph) {
titleFound = true;
if (issue != null) {
titlePassed = false;
issueCount++;
addDetailItem(detailItems, "标题格式", false, issue.getDetail(), issue.getSuggestion(),
issue.getParagraphIndex(), abbreviate(issue.getParagraphText()));
}
continue;
}
bodyCount++;
if (issue != null) {
bodyPassed = false;
issueCount++;
addDetailItem(detailItems, "正文格式", false, issue.getDetail(), issue.getSuggestion(),
issue.getParagraphIndex(), abbreviate(issue.getParagraphText()));
}
}
if (!titleFound) {
titlePassed = false;
issueCount++;
addDetailItem(detailItems, "标题格式", false, "未检测到含“项目申报书”字样的标题段落",
"请新增或修改标题段落,并设置为黑体一号,且标题中包含“项目申报书”字样", null, null);
}
if (bodyCount == 0) {
bodyPassed = false;
issueCount++;
addDetailItem(detailItems, "正文格式", false, "未检测到可校验的正文段落",
"请补充正文内容,并统一设置为宋体五号", null, null);
}
List<WxxzxDocxValidationResult.Item> finalItems = new ArrayList<>();
if (titlePassed) {
addDetailItem(finalItems, "标题格式", true,
"已检测到含“项目申报书”字样的标题,格式为黑体一号",
"无需修改", null, null);
}
if (bodyPassed && bodyCount > 0) {
addDetailItem(finalItems, "正文格式", true,
String.format(Locale.ROOT, "已检测 %d 个正文段落,格式均为宋体五号", bodyCount),
"无需修改", null, null);
}
finalItems.addAll(detailItems);
if (issueCount > MAX_DETAIL_ITEMS) {
addDetailItem(finalItems, "结果说明", false,
String.format(Locale.ROOT, "问题较多,当前仅展示前 %d 处明细", MAX_DETAIL_ITEMS),
"请优先按已展示的修改意见逐项调整后重新上传校验",
null, null);
}
result.setPassed(titlePassed && bodyPassed);
result.setTotalIssues(issueCount);
result.setSummary(result.getPassed()
? "校验通过:标题和正文格式均符合要求"
: String.format(Locale.ROOT, "校验不通过:共发现 %d 处问题", issueCount));
result.setItems(finalItems);
}
private ValidationIssue validateParagraph(XWPFParagraph paragraph, int paragraphIndex,
String expectedFontCn, String expectedFontEn, double expectedSize) {
List<XWPFRun> runs = paragraph.getRuns();
if (runs == null || runs.isEmpty()) {
return new ValidationIssue(paragraphIndex, paragraph.getText(),
String.format(Locale.ROOT, "未检测到文本样式信息,应为%s %s",
expectedFontCn, formatExpectedSize(expectedSize)),
buildSuggestion(expectedFontCn, expectedSize));
}
for (XWPFRun run : runs) {
String runText = normalizeDisplayText(run.text());
if (StrUtil.isBlank(runText)) {
continue;
}
String actualFont = resolveFontFamily(run);
Double actualSize = resolveFontSize(run);
boolean fontMatched = matchesFont(actualFont, expectedFontCn, expectedFontEn);
boolean sizeMatched = matchesSize(actualSize, expectedSize);
if (fontMatched && sizeMatched) {
continue;
}
String detail = String.format(Locale.ROOT,
"应为%s %s,当前检测到字体“%s”、字号“%s”",
expectedFontCn,
formatExpectedSize(expectedSize),
StrUtil.blankToDefault(actualFont, "未设置"),
formatActualSize(actualSize));
return new ValidationIssue(paragraphIndex, paragraph.getText(), detail,
buildSuggestion(expectedFontCn, expectedSize));
}
return null;
}
private void addDetailItem(List<WxxzxDocxValidationResult.Item> items, String rule, boolean passed,
String detail, String suggestion, Integer paragraphIndex, String paragraphText) {
if (!passed && "结果说明".equals(rule)) {
WxxzxDocxValidationResult.Item item = new WxxzxDocxValidationResult.Item();
item.setRule(rule);
item.setPassed(false);
item.setDetail(detail);
item.setSuggestion(suggestion);
items.add(item);
return;
}
if (!passed) {
long failedCount = items.stream().filter(item -> Boolean.FALSE.equals(item.getPassed())).count();
if (failedCount >= MAX_DETAIL_ITEMS) {
return;
}
}
WxxzxDocxValidationResult.Item item = new WxxzxDocxValidationResult.Item();
item.setRule(rule);
item.setPassed(passed);
item.setDetail(detail);
item.setSuggestion(suggestion);
item.setParagraphIndex(paragraphIndex);
item.setParagraphText(paragraphText);
items.add(item);
}
private String resolveFontFamily(XWPFRun run) {
String[] candidates = new String[]{
run.getFontFamily(XWPFRun.FontCharRange.eastAsia),
run.getFontFamily(XWPFRun.FontCharRange.ascii),
run.getFontFamily(XWPFRun.FontCharRange.hAnsi),
run.getFontFamily(),
run.getFontName()
};
for (String candidate : candidates) {
if (StrUtil.isNotBlank(candidate)) {
return candidate.trim();
}
}
return null;
}
private Double resolveFontSize(XWPFRun run) {
Double fontSize = run.getFontSizeAsDouble();
if (fontSize != null && fontSize > 0) {
return fontSize;
}
int fontSizeInt = run.getFontSize();
return fontSizeInt > 0 ? (double) fontSizeInt : null;
}
private boolean matchesFont(String actualFont, String expectedCn, String expectedEn) {
if (StrUtil.isBlank(actualFont)) {
return false;
}
String normalized = actualFont.replace(" ", "").trim().toLowerCase(Locale.ROOT);
return normalized.equals(expectedCn.toLowerCase(Locale.ROOT))
|| normalized.equals(expectedEn.toLowerCase(Locale.ROOT));
}
private boolean matchesSize(Double actualSize, double expectedSize) {
return actualSize != null && Math.abs(actualSize - expectedSize) < 0.11D;
}
private String normalizeCheckText(String text) {
return text == null ? "" : text.replace("\u3000", "").replaceAll("\\s+", "");
}
private String normalizeDisplayText(String text) {
return text == null ? "" : text.replace('\u00A0', ' ').replaceAll("\\s+", " ").trim();
}
private String abbreviate(String text) {
String displayText = normalizeDisplayText(text);
return abbreviate(displayText, 80);
}
private String abbreviate(String text, int maxLength) {
String displayText = normalizeDisplayText(text);
if (displayText.length() <= maxLength) {
return displayText;
}
return displayText.substring(0, maxLength) + "...";
}
private String formatExpectedSize(double size) {
if (Math.abs(size - TITLE_FONT_SIZE) < 0.01D) {
return "一号";
}
if (Math.abs(size - BODY_FONT_SIZE) < 0.01D) {
return "五号";
}
return formatActualSize(size);
}
private String formatActualSize(Double size) {
if (size == null) {
return "未设置";
}
if (Math.abs(size - Math.rint(size)) < 0.01D) {
return String.format(Locale.ROOT, "%.0fpt", size);
}
return String.format(Locale.ROOT, "%.1fpt", size);
}
private String buildSuggestion(String expectedFontCn, double expectedSize) {
return String.format(Locale.ROOT, "请将该段调整为%s%s", expectedFontCn, formatExpectedSize(expectedSize));
}
private static class ValidationIssue {
private final Integer paragraphIndex;
private final String paragraphText;
private final String detail;
private final String suggestion;
private ValidationIssue(Integer paragraphIndex, String paragraphText, String detail, String suggestion) {
this.paragraphIndex = paragraphIndex;
this.paragraphText = paragraphText;
this.detail = detail;
this.suggestion = suggestion;
}
public Integer getParagraphIndex() {
return paragraphIndex;
}
public String getParagraphText() {
return paragraphText;
}
public String getDetail() {
return detail;
}
public String getSuggestion() {
return suggestion;
}
}
}