恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。 == 此快照内容 == - 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091) - 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static) - 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB) - 交接文档 go.md + go-code-reference/endpoints/entities/database.md - 多代理建设脚本 .claude/wf-*.js == 状态 == - 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板) - 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用 - W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成) - 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456 == 排除(gitignore, 可再生) == node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui) 完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules) 时间戳: 20260615-191517 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
5.0 KiB
Java
126 lines
5.0 KiB
Java
package com.kaidi.oa.web;
|
||
|
||
import com.kaidi.oa.common.ApiException;
|
||
import com.kaidi.oa.common.ApiResp;
|
||
import com.kaidi.oa.common.NotFoundException;
|
||
import com.kaidi.oa.domain.Expert;
|
||
import com.kaidi.oa.repository.ExpertRepository;
|
||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||
import org.springframework.web.bind.annotation.GetMapping;
|
||
import org.springframework.web.bind.annotation.PatchMapping;
|
||
import org.springframework.web.bind.annotation.PathVariable;
|
||
import org.springframework.web.bind.annotation.PostMapping;
|
||
import org.springframework.web.bind.annotation.RequestBody;
|
||
import org.springframework.web.bind.annotation.RequestMapping;
|
||
import org.springframework.web.bind.annotation.RequestParam;
|
||
import org.springframework.web.bind.annotation.RestController;
|
||
|
||
import java.time.Instant;
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
|
||
/**
|
||
* Declaration expert pool (申报专家库). field (专业领域) is one of 给排水 /
|
||
* 环境工程 / 市政 / 财务 / 法律 / 水利; expertType is one of 评审专家 / 咨询专家 /
|
||
* 行业专家; status is one of 在库 / 暂停.
|
||
*/
|
||
@RestController
|
||
@RequestMapping("/api/oa/experts")
|
||
public class ExpertController {
|
||
|
||
private final ExpertRepository expertRepo;
|
||
|
||
public ExpertController(ExpertRepository expertRepo) {
|
||
this.expertRepo = expertRepo;
|
||
}
|
||
|
||
@GetMapping
|
||
public ApiResp<List<Expert>> list(@RequestParam(required = false) String expertType) {
|
||
List<Expert> list;
|
||
if (expertType != null && !expertType.isBlank()) {
|
||
list = expertRepo.findByExpertType(expertType);
|
||
} else {
|
||
list = expertRepo.findAll();
|
||
}
|
||
return ApiResp.ok(list);
|
||
}
|
||
|
||
@GetMapping("/{id}")
|
||
public ApiResp<Expert> get(@PathVariable Long id) {
|
||
return ApiResp.ok(expertRepo.findById(id)
|
||
.orElseThrow(() -> new NotFoundException("expert not found: " + id)));
|
||
}
|
||
|
||
public record CreateExpertRequest(
|
||
String name, String field, String org, String title, String expertType,
|
||
String phone, String status) {
|
||
}
|
||
|
||
@PostMapping
|
||
public ApiResp<Expert> create(@RequestBody CreateExpertRequest req) {
|
||
if (req.name() == null || req.name().isBlank()) {
|
||
throw new ApiException(400, "name is required");
|
||
}
|
||
Expert e = new Expert();
|
||
e.setName(req.name());
|
||
e.setField(req.field());
|
||
e.setOrg(req.org());
|
||
e.setTitle(req.title());
|
||
e.setExpertType(req.expertType());
|
||
e.setPhone(req.phone());
|
||
e.setStatus(req.status() == null || req.status().isBlank() ? "在库" : req.status());
|
||
e.setCreatedAt(Instant.now());
|
||
return ApiResp.ok(expertRepo.save(e));
|
||
}
|
||
|
||
@PatchMapping("/{id}")
|
||
public ApiResp<Expert> update(@PathVariable Long id, @RequestBody CreateExpertRequest req) {
|
||
Expert e = expertRepo.findById(id)
|
||
.orElseThrow(() -> new NotFoundException("expert not found: " + id));
|
||
if (req.name() != null && !req.name().isBlank()) e.setName(req.name());
|
||
if (req.field() != null) e.setField(req.field());
|
||
if (req.org() != null) e.setOrg(req.org());
|
||
if (req.title() != null) e.setTitle(req.title());
|
||
if (req.expertType() != null && !req.expertType().isBlank()) e.setExpertType(req.expertType());
|
||
if (req.phone() != null) e.setPhone(req.phone());
|
||
if (req.status() != null && !req.status().isBlank()) e.setStatus(req.status());
|
||
return ApiResp.ok(expertRepo.save(e));
|
||
}
|
||
|
||
@DeleteMapping("/{id}")
|
||
public ApiResp<Void> delete(@PathVariable Long id) {
|
||
if (!expertRepo.existsById(id)) {
|
||
throw new NotFoundException("expert not found: " + id);
|
||
}
|
||
expertRepo.deleteById(id);
|
||
return ApiResp.ok(null);
|
||
}
|
||
|
||
/**
|
||
* GET /experts/match?declType=高企 — 按申报类型关键词匹配「在库」专家。
|
||
* 规则:declType 用逗号分隔可传多个关键词;专家 field 或 expertType 含任一关键词则命中;
|
||
* 仅返回 status=在库 的专家,按 field 字母序排列。
|
||
* 补完 Gap 6 缺口:「按擅长项目类型匹配」端点。
|
||
*/
|
||
@GetMapping("/match")
|
||
public ApiResp<List<Expert>> match(@RequestParam(required = false) String declType) {
|
||
List<Expert> active = expertRepo.findByStatus("在库");
|
||
if (declType == null || declType.isBlank()) {
|
||
return ApiResp.ok(active);
|
||
}
|
||
String[] keywords = declType.split("[,,]");
|
||
List<Expert> matched = new ArrayList<>();
|
||
for (Expert ex : active) {
|
||
String haystack = ((ex.getField() == null ? "" : ex.getField()) + " "
|
||
+ (ex.getExpertType() == null ? "" : ex.getExpertType())).toLowerCase();
|
||
for (String kw : keywords) {
|
||
if (kw != null && !kw.isBlank() && haystack.contains(kw.trim().toLowerCase())) {
|
||
matched.add(ex);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return ApiResp.ok(matched);
|
||
}
|
||
}
|