Merge branch 'claude/sweet-swanson-772e08'
This commit is contained in:
@@ -37,8 +37,9 @@ public class OaAiReviewController extends BaseController {
|
||||
@PostMapping(value = "/analyze", consumes = "multipart/form-data")
|
||||
public R<OaAiReviewVo> analyze(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("reviewType") String reviewType,
|
||||
@RequestParam(value = "position", required = false) String position) {
|
||||
return R.ok("审核完成", service.analyze(file, reviewType, position));
|
||||
@RequestParam(value = "position", required = false) String position,
|
||||
@RequestParam(value = "requirements", required = false) String requirements) {
|
||||
return R.ok("审核完成", service.analyze(file, reviewType, position, requirements));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,8 +49,9 @@ public class OaAiReviewController extends BaseController {
|
||||
@PostMapping(value = "/analyzeStream", consumes = "multipart/form-data", produces = "text/event-stream;charset=UTF-8")
|
||||
public SseEmitter analyzeStream(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("reviewType") String reviewType,
|
||||
@RequestParam(value = "position", required = false) String position) {
|
||||
return service.analyzeStream(file, reviewType, position);
|
||||
@RequestParam(value = "position", required = false) String position,
|
||||
@RequestParam(value = "requirements", required = false) String requirements) {
|
||||
return service.analyzeStream(file, reviewType, position, requirements);
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@@ -57,7 +59,8 @@ public class OaAiReviewController extends BaseController {
|
||||
return service.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
// 限定为数字,避免 /analyzeStream 等子路径被 {id} 误匹配(否则 POST 会得到 405)
|
||||
@GetMapping("/{id:\\d+}")
|
||||
public R<OaAiReviewVo> getInfo(@NotNull @PathVariable Long id) {
|
||||
return R.ok(service.queryById(id));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ public class OaAiReview extends BaseEntity {
|
||||
/** 简历审核的目标岗位 */
|
||||
private String position;
|
||||
|
||||
/** 本次审核的附加要求/审核重点 */
|
||||
private String requirements;
|
||||
|
||||
/** 简历匹配度评分 0-100(合同为空) */
|
||||
private Integer matchScore;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ public class OaAiReviewVo implements Serializable {
|
||||
private Long ossId;
|
||||
private String fileUrl;
|
||||
private String position;
|
||||
private String requirements;
|
||||
private Integer matchScore;
|
||||
private String riskLevel;
|
||||
private String summary;
|
||||
|
||||
@@ -14,16 +14,17 @@ public interface IOaAiReviewService {
|
||||
/**
|
||||
* 上传合同/简历并进行 AI 审核,落库并返回结果
|
||||
*
|
||||
* @param file PDF / Word 文件
|
||||
* @param reviewType contract / resume
|
||||
* @param position 简历审核的目标岗位(合同可空)
|
||||
* @param file PDF / Word 文件
|
||||
* @param reviewType contract / resume
|
||||
* @param position 简历审核的目标岗位(合同可空)
|
||||
* @param requirements 附加审核要求/重点(可空)
|
||||
*/
|
||||
OaAiReviewVo analyze(MultipartFile file, String reviewType, String position);
|
||||
OaAiReviewVo analyze(MultipartFile file, String reviewType, String position, String requirements);
|
||||
|
||||
/**
|
||||
* 流式审核:边生成边推送(SSE),结束后落库
|
||||
*/
|
||||
SseEmitter analyzeStream(MultipartFile file, String reviewType, String position);
|
||||
SseEmitter analyzeStream(MultipartFile file, String reviewType, String position, String requirements);
|
||||
|
||||
TableDataInfo<OaAiReviewVo> queryPageList(OaAiReviewBo bo, PageQuery pageQuery);
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
private static final Pattern RISK_PATTERN = Pattern.compile("风险评级[::\\s]*([高中低])");
|
||||
|
||||
@Override
|
||||
public OaAiReviewVo analyze(MultipartFile file, String reviewType, String position) {
|
||||
Prepared p = prepareSync(file, reviewType, position);
|
||||
public OaAiReviewVo analyze(MultipartFile file, String reviewType, String position, String requirements) {
|
||||
Prepared p = prepareSync(file, reviewType, position, requirements);
|
||||
buildPrompt(p);
|
||||
String result = p.images != null
|
||||
? miMoClient.chatMultimodal(p.system, p.userText, p.images)
|
||||
@@ -70,11 +70,11 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SseEmitter analyzeStream(MultipartFile file, String reviewType, String position) {
|
||||
public SseEmitter analyzeStream(MultipartFile file, String reviewType, String position, String requirements) {
|
||||
// 只在同步阶段做轻量校验 + 读取字节(multipart 必须在请求线程内消费);
|
||||
// 文档解析、渲染、大模型调用全部放到异步线程,任何异常都以 SSE error 事件返回,
|
||||
// 避免在返回 SseEmitter 之前抛异常被全局处理器包成 JSON(前端按流读取会“静默失败”)。
|
||||
Prepared p = prepareSync(file, reviewType, position);
|
||||
Prepared p = prepareSync(file, reviewType, position, requirements);
|
||||
String username = currentUsername();
|
||||
long timeoutMs = ((miMoProps.getTimeout() == null ? 180 : miMoProps.getTimeout()) + 60) * 1000L;
|
||||
SseEmitter emitter = new SseEmitter(timeoutMs);
|
||||
@@ -129,7 +129,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
}
|
||||
|
||||
/** 同步阶段:校验 + 读取字节(不解析,快) */
|
||||
private Prepared prepareSync(MultipartFile file, String reviewType, String position) {
|
||||
private Prepared prepareSync(MultipartFile file, String reviewType, String position, String requirements) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("请上传文件");
|
||||
}
|
||||
@@ -153,6 +153,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
Prepared p = new Prepared();
|
||||
p.reviewType = reviewType;
|
||||
p.position = position;
|
||||
p.requirements = StringUtils.trimToNull(requirements);
|
||||
p.fileName = fileName;
|
||||
p.bytes = bytes;
|
||||
return p;
|
||||
@@ -160,7 +161,12 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
|
||||
/** 解析文档 + 构建提示词(耗时/可能抛异常的部分) */
|
||||
private void buildPrompt(Prepared p) {
|
||||
p.system = "contract".equals(p.reviewType) ? contractSystemPrompt() : resumeSystemPrompt(p.position);
|
||||
String base = "contract".equals(p.reviewType) ? contractSystemPrompt() : resumeSystemPrompt(p.position);
|
||||
if (StringUtils.isNotBlank(p.requirements)) {
|
||||
base += "\n\n【本次额外审核要求 / 重点】请在上述固定结构基础上,务必额外重点关注并逐条回应以下要求:\n"
|
||||
+ p.requirements;
|
||||
}
|
||||
p.system = base;
|
||||
String text = truncate(DocumentParseUtil.extractText(p.fileName, p.bytes));
|
||||
if (StringUtils.length(text) >= MIN_TEXT_LEN) {
|
||||
p.userText = "contract".equals(p.reviewType)
|
||||
@@ -189,6 +195,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
entity.setFileUrl(oss.getUrl());
|
||||
}
|
||||
entity.setPosition(p.position);
|
||||
entity.setRequirements(p.requirements);
|
||||
entity.setResultMd(result);
|
||||
entity.setSummary(buildSummary(result));
|
||||
entity.setModel(miMoProps.getModel());
|
||||
@@ -242,6 +249,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
|
||||
private static class Prepared {
|
||||
String reviewType;
|
||||
String position;
|
||||
String requirements;
|
||||
String fileName;
|
||||
byte[] bytes;
|
||||
String system;
|
||||
|
||||
@@ -30,6 +30,23 @@
|
||||
: '评估候选人,分析与目标岗位的匹配度、优势、短板与面试建议。' }}
|
||||
支持 PDF / Word(.doc/.docx),≤ 20MB。
|
||||
</div>
|
||||
|
||||
<!-- 审核重点 / 附加要求 -->
|
||||
<div class="req-area">
|
||||
<div class="req-line">
|
||||
<span class="req-label">审核重点</span>
|
||||
<el-checkbox-group v-model="checkedItems" size="mini" :disabled="streaming" class="req-items">
|
||||
<el-checkbox v-for="it in itemOptions" :key="it.value" :label="it.label" border>{{ it.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<span class="req-tip">可在「系统管理→字典管理」增删审核项</span>
|
||||
</div>
|
||||
<div class="req-line">
|
||||
<span class="req-label">附加要求</span>
|
||||
<el-input v-model="extraText" type="textarea" :rows="2" :disabled="streaming"
|
||||
class="req-extra" maxlength="500" show-word-limit
|
||||
placeholder="可补充本次审核的特殊关注点,例如:重点核查付款比例是否对我方有利、是否有自动续约陷阱…" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="12" class="body">
|
||||
@@ -96,6 +113,7 @@
|
||||
|
||||
<script>
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { getDicts } from '@/api/system/dict/data'
|
||||
const marked = require('marked')
|
||||
|
||||
export default {
|
||||
@@ -107,6 +125,11 @@ export default {
|
||||
rawFile: null,
|
||||
fileName: '',
|
||||
|
||||
// 审核重点(字典) + 附加要求(自由文本)
|
||||
itemDicts: { contract: [], resume: [] },
|
||||
checkedItems: [],
|
||||
extraText: '',
|
||||
|
||||
streaming: false,
|
||||
done: false,
|
||||
reasoning: '',
|
||||
@@ -124,15 +147,37 @@ export default {
|
||||
computed: {
|
||||
renderedMd () {
|
||||
try { return marked(this.content) } catch (e) { return this.content }
|
||||
},
|
||||
itemOptions () {
|
||||
return this.itemDicts[this.reviewType] || []
|
||||
},
|
||||
requirements () {
|
||||
const parts = []
|
||||
if (this.checkedItems.length) parts.push('重点审核项:' + this.checkedItems.join('、'))
|
||||
if (this.extraText && this.extraText.trim()) parts.push('其他要求:' + this.extraText.trim())
|
||||
return parts.join('\n')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 切换类型时清空已选审核项(两套字典不同)
|
||||
reviewType () { this.checkedItems = [] }
|
||||
},
|
||||
created () {
|
||||
marked.setOptions({ breaks: true })
|
||||
this.loadItemDicts()
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (this.previewUrl) URL.revokeObjectURL(this.previewUrl)
|
||||
},
|
||||
methods: {
|
||||
loadItemDicts () {
|
||||
getDicts('oa_ai_review_item_contract').then(res => {
|
||||
this.itemDicts = { ...this.itemDicts, contract: (res.data || []).map(d => ({ label: d.dictLabel, value: d.dictValue })) }
|
||||
})
|
||||
getDicts('oa_ai_review_item_resume').then(res => {
|
||||
this.itemDicts = { ...this.itemDicts, resume: (res.data || []).map(d => ({ label: d.dictLabel, value: d.dictValue })) }
|
||||
})
|
||||
},
|
||||
goBack () { this.$router.push('/hint/aiReview') },
|
||||
goDetail () { if (this.savedId) this.$router.push('/hint/aiReview/detail/' + this.savedId) },
|
||||
riskTagType (r) { return r === '高' ? 'danger' : (r === '中' ? 'warning' : 'success') },
|
||||
@@ -167,6 +212,7 @@ export default {
|
||||
fd.append('file', this.rawFile)
|
||||
fd.append('reviewType', this.reviewType)
|
||||
if (this.reviewType === 'resume' && this.position) fd.append('position', this.position)
|
||||
if (this.requirements) fd.append('requirements', this.requirements)
|
||||
|
||||
try {
|
||||
const resp = await fetch(process.env.VUE_APP_BASE_API + '/oa/aiReview/analyzeStream', {
|
||||
@@ -264,6 +310,18 @@ export default {
|
||||
}
|
||||
.bar-hint { font-size: 12px; color: #909399; margin-top: 8px; }
|
||||
|
||||
.req-area { margin-top: 10px; border-top: 1px dashed #ebeef5; padding-top: 8px; }
|
||||
.req-line { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px;
|
||||
&:last-child { margin-bottom: 0; }
|
||||
}
|
||||
.req-label { flex: 0 0 56px; font-size: 12px; color: #606266; padding-top: 5px; font-weight: 600; }
|
||||
.req-items { flex: 1; display: flex; flex-wrap: wrap; gap: 6px 0;
|
||||
::v-deep .el-checkbox { margin-right: 8px; margin-left: 0; }
|
||||
::v-deep .el-checkbox.is-bordered { padding: 4px 10px 4px 8px; height: auto; }
|
||||
}
|
||||
.req-tip { flex: 0 0 auto; font-size: 11px; color: #c0c4cc; padding-top: 5px; }
|
||||
.req-extra { flex: 1; }
|
||||
|
||||
.body { margin-top: 0; }
|
||||
.panel { ::v-deep .el-card__header { padding: 9px 14px; } }
|
||||
.hd { display: flex; justify-content: space-between; align-items: center;
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
<el-descriptions-item label="模型">{{ info.model || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核时间">{{ info.createTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核人">{{ info.createBy || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="info.requirements" label="审核重点 / 附加要求" :span="3">
|
||||
<span style="white-space: pre-wrap">{{ info.requirements }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="info" class="md-body" v-html="renderedMd" />
|
||||
|
||||
@@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS `oa_ai_review` (
|
||||
`oss_id` bigint(20) DEFAULT NULL COMMENT 'OSS文件ID(原件留存)',
|
||||
`file_url` varchar(500) DEFAULT NULL COMMENT 'OSS文件地址',
|
||||
`position` varchar(255) DEFAULT NULL COMMENT '简历目标岗位',
|
||||
`requirements` varchar(1000) DEFAULT NULL COMMENT '本次审核的附加要求/审核重点',
|
||||
`match_score` int(11) DEFAULT NULL COMMENT '简历匹配度评分 0-100',
|
||||
`risk_level` varchar(10) DEFAULT NULL COMMENT '合同风险评级 高/中/低',
|
||||
`summary` varchar(500) DEFAULT NULL COMMENT 'AI审核结论摘要(列表展示)',
|
||||
@@ -31,8 +32,38 @@ CREATE TABLE IF NOT EXISTS `oa_ai_review` (
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 审核记录(合同/简历)';
|
||||
|
||||
-- 若表已存在(旧版本),补加 summary 列(MySQL 不支持 ADD COLUMN IF NOT EXISTS,重复执行报错可忽略):
|
||||
-- 若表已存在(旧版本),补加列(MySQL 不支持 ADD COLUMN IF NOT EXISTS,已存在则报错可忽略):
|
||||
-- ALTER TABLE `oa_ai_review` ADD COLUMN `summary` varchar(500) DEFAULT NULL COMMENT 'AI审核结论摘要(列表展示)' AFTER `risk_level`;
|
||||
-- ALTER TABLE `oa_ai_review` ADD COLUMN `requirements` varchar(1000) DEFAULT NULL COMMENT '本次审核的附加要求/审核重点' AFTER `position`;
|
||||
|
||||
-- ---------------- 字典:可选审核重点(合同 / 简历),用户可在字典管理增删 ----------------
|
||||
INSERT IGNORE INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`)
|
||||
VALUES (2063920000000000001, 'AI合同审核项', 'oa_ai_review_item_contract', '0', 'admin', NOW(), 'AI合同审核可选审核重点'),
|
||||
(2063920000000000002, 'AI简历审核项', 'oa_ai_review_item_resume', '0', 'admin', NOW(), 'AI简历审核可选审核重点');
|
||||
|
||||
INSERT IGNORE INTO `sys_dict_data`
|
||||
(`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
|
||||
VALUES
|
||||
(2063920000000000010, 1, '付款条款与节点', '付款条款与节点', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000011, 2, '违约责任与赔偿', '违约责任与赔偿', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000012, 3, '知识产权归属', '知识产权归属', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000013, 4, '保密义务', '保密义务', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000014, 5, '交付与验收标准', '交付与验收标准', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000015, 6, '质保与售后', '质保与售后', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000016, 7, '价格与税费', '价格与税费', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000017, 8, '争议解决与管辖', '争议解决与管辖', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000018, 9, '不可抗力', '不可抗力', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000019, 10,'合同解除条件', '合同解除条件', 'oa_ai_review_item_contract', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000040, 1, '技术能力匹配', '技术能力匹配', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000041, 2, '项目经验深度', '项目经验深度', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000042, 3, '工作稳定性', '工作稳定性', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000043, 4, '团队协作与管理', '团队协作与管理', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000044, 5, '学历与专业背景', '学历与专业背景', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000045, 6, '行业相关经验', '行业相关经验', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000046, 7, '薪资期望匹配', '薪资期望匹配', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000047, 8, '成长潜力', '成长潜力', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000048, 9, '离职风险', '离职风险', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), ''),
|
||||
(2063920000000000049, 10,'外语能力', '外语能力', 'oa_ai_review_item_resume', '', 'default', 'N', '0', 'admin', NOW(), '');
|
||||
|
||||
-- ---------------- 菜单:信息 > AI审核 ----------------
|
||||
-- 父菜单 1774989374680858626 = 「信息」
|
||||
|
||||
Reference in New Issue
Block a user