0e4986330a
- 新增 DeclareGuardService:按已通过/未处理驳回/待审核数判重,申报项目行加锁防连点 - 9 张申报表补 declare_id / declare_status / declare_remark,提交时写入并随审核结果回写 - 新增撤回、作废(强制放行)接口;挑战杯团队成员按手机号判重 - 申报项目支持配置允许重复提交与最多同时条数 - 迁移脚本 sql/20261102_add_declare_uniqueness.sql(含存量回填与重复清单)
907 lines
40 KiB
Java
907 lines
40 KiB
Java
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;
|
||
}
|
||
}
|
||
} |