AI审核改为 列表页+详情页 结构,列表带审核摘要

- 表 oa_ai_review 增加 summary 列(审核结论摘要,纯文本,列表展示用),
  已应用到生产库;分析时由结果 Markdown 提炼前160字纯文本写入
- 列表查询清空大字段 result_md 减小响应体,详情接口仍返回完整结果
- 前端拆分:
  · index.vue 重写为列表页:搜索(类型/关键字)+表格(类型/文件名/岗位/结论标签/
    审核摘要/时间)+分页,「新增审核」改为弹窗上传(类型/岗位/文件),
    审核完成后跳转详情;行可删除
  · 新增 detail.vue 详情页:元信息(文件名+下载原件/岗位/模型/时间/审核人)
    + 结论标签 + 完整 Markdown 结果,返回列表按钮
  · router 增加 /hint/aiReview/detail/:id 隐藏路由
- 原件已通过 OSS 留存,详情页可下载,下次可直接查看

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 10:04:16 +08:00
parent faca2f85eb
commit d46754ede8
7 changed files with 293 additions and 208 deletions

View File

@@ -41,6 +41,9 @@ public class OaAiReview extends BaseEntity {
/** 合同总体风险评级:高/中/低(简历为空) */
private String riskLevel;
/** AI 审核结论摘要(列表展示,纯文本) */
private String summary;
/** AI 审核结果Markdown */
private String resultMd;

View File

@@ -22,6 +22,7 @@ public class OaAiReviewVo implements Serializable {
private String position;
private Integer matchScore;
private String riskLevel;
private String summary;
private String resultMd;
private String model;
private Integer tokens;

View File

@@ -121,6 +121,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
entity.setFileUrl(fileUrl);
entity.setPosition(position);
entity.setResultMd(result);
entity.setSummary(buildSummary(result));
entity.setModel(miMoProps.getModel());
if ("resume".equals(reviewType)) {
entity.setMatchScore(parseInt(SCORE_PATTERN, result, 100));
@@ -137,6 +138,16 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
return text.length() > MAX_TEXT_LEN ? text.substring(0, MAX_TEXT_LEN) : text;
}
/** 从 Markdown 结果里提炼一段纯文本摘要,供列表展示 */
private String buildSummary(String md) {
if (StringUtils.isBlank(md)) return null;
String text = md.replaceAll("(?m)^#+\\s*", "") // 标题符号
.replaceAll("[*`>#\\-]", " ") // markdown 符号
.replaceAll("\\s+", " ") // 折叠空白
.trim();
return text.length() > 160 ? text.substring(0, 160) : text;
}
private Integer parseInt(Pattern p, String text, int max) {
Matcher m = p.matcher(text == null ? "" : text);
if (m.find()) {
@@ -194,6 +205,10 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService {
}
lqw.orderByDesc(OaAiReview::getCreateTime);
Page<OaAiReviewVo> page = baseMapper.selectVoPage(pageQuery.build(), lqw);
// 列表只需 summary清空大字段 resultMd 减小响应体
if (page.getRecords() != null) {
page.getRecords().forEach(v -> v.setResultMd(null));
}
return TableDataInfo.build(page);
}

View File

@@ -177,6 +177,12 @@ export const constantRoutes = [
name: "editMeetingMinutes",
meta: { title: "编辑会议纪要", activeMenu: "/hint/meeting" },
},
{
path: "aiReview/detail/:id(\\d+)",
component: () => import("@/views/oa/aiReview/detail"),
name: "aiReviewDetail",
meta: { title: "审核详情", activeMenu: "/hint/aiReview" },
},
],
},
{

View File

@@ -0,0 +1,110 @@
<template>
<div class="app-container ai-review-detail" v-loading="loading">
<el-card shadow="never" class="panel">
<div slot="header" class="hd">
<div class="hd-left">
<el-button size="small" icon="el-icon-back" @click="goBack">返回列表</el-button>
<span class="title"><i class="el-icon-document-checked" /> 审核详情</span>
</div>
<div v-if="info" class="hd-right">
<el-tag size="small" :type="info.reviewType === 'contract' ? 'warning' : 'success'">
{{ info.reviewType === 'contract' ? '合同审核' : '简历审核' }}
</el-tag>
<el-tag v-if="info.reviewType === 'resume' && info.matchScore != null"
size="small" type="success" effect="dark">匹配度 {{ info.matchScore }}</el-tag>
<el-tag v-if="info.reviewType === 'contract' && info.riskLevel"
size="small" :type="riskTagType(info.riskLevel)" effect="dark">{{ info.riskLevel }}风险</el-tag>
</div>
</div>
<el-descriptions v-if="info" :column="3" size="small" border class="meta">
<el-descriptions-item label="文件名">
<span>{{ info.fileName }}</span>
<el-button v-if="info.fileUrl" type="text" size="mini" icon="el-icon-download"
style="margin-left:8px" @click="downloadFile">下载原件</el-button>
</el-descriptions-item>
<el-descriptions-item v-if="info.reviewType === 'resume'" label="目标岗位">
{{ info.position || '' }}
</el-descriptions-item>
<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>
<div v-if="info" class="md-body" v-html="renderedMd" />
</el-card>
</div>
</template>
<script>
import { getAiReview } from '@/api/oa/aiReview'
const marked = require('marked')
export default {
name: 'OaAiReviewDetail',
data () {
return {
loading: true,
info: null
}
},
computed: {
renderedMd () {
if (!this.info || !this.info.resultMd) return ''
try { return marked(this.info.resultMd) } catch (e) { return this.info.resultMd }
}
},
created () {
marked.setOptions({ breaks: true })
const id = this.$route.params.id
if (id) this.load(id)
else this.loading = false
},
methods: {
load (id) {
this.loading = true
getAiReview(id).then(res => { this.info = res.data })
.finally(() => { this.loading = false })
},
goBack () {
this.$router.push('/hint/aiReview')
},
downloadFile () {
if (this.info && this.info.fileUrl) window.open(this.info.fileUrl, '_blank')
},
riskTagType (r) {
return r === '高' ? 'danger' : (r === '中' ? 'warning' : 'success')
}
}
}
</script>
<style scoped lang="scss">
.ai-review-detail { padding: 10px; }
.panel { ::v-deep .el-card__header { padding: 10px 14px; } }
.hd { display: flex; justify-content: space-between; align-items: center;
.hd-left { display: flex; align-items: center; gap: 12px;
.title { font-weight: 600; font-size: 14px; color: #303133; i { color: #409eff; margin-right: 4px; } }
}
.hd-right { display: flex; align-items: center; gap: 8px; }
}
.meta { margin-bottom: 16px; }
.md-body {
font-size: 14px; color: #2c3e50; line-height: 1.8; padding: 4px 6px;
::v-deep {
h1, h2, h3 { color: #303133; margin: 18px 0 10px; font-weight: 600; }
h1 { font-size: 19px; } h2 { font-size: 16px; border-left: 3px solid #409eff; padding-left: 8px; }
h3 { font-size: 14px; }
p { margin: 8px 0; }
ul, ol { padding-left: 22px; margin: 8px 0; }
li { margin: 4px 0; }
strong { color: #c0392b; }
code { background: #f4f7fa; padding: 1px 5px; border-radius: 3px; font-size: 13px; color: #e6a23c; }
table { border-collapse: collapse; width: 100%; margin: 10px 0; }
th, td { border: 1px solid #ebeef5; padding: 6px 10px; font-size: 13px; }
th { background: #f5f7fa; }
blockquote { border-left: 3px solid #dcdfe6; padding-left: 10px; color: #909399; margin: 8px 0; }
}
}
</style>

View File

@@ -1,155 +1,189 @@
<template>
<div class="app-container ai-review">
<el-row :gutter="12">
<!-- 上传 + 历史 -->
<el-col :span="8" :xs="24">
<el-card shadow="never" class="panel">
<div slot="header" class="hd">
<i class="el-icon-cpu" /> AI 智能审核
</div>
<div class="app-container">
<!-- 搜索 -->
<el-form :model="query" ref="queryForm" size="small" :inline="true" label-width="68px">
<el-form-item label="类型" prop="reviewType">
<el-select v-model="query.reviewType" placeholder="全部" clearable style="width: 120px" @change="handleQuery">
<el-option label="合同" value="contract" />
<el-option label="简历" value="resume" />
</el-select>
</el-form-item>
<el-form-item label="关键字" prop="keyword">
<el-input v-model="query.keyword" placeholder="文件名 / 岗位" clearable style="width: 220px"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-radio-group v-model="reviewType" size="small" class="type-switch" @change="onTypeChange">
<!-- 工具栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-magic-stick" size="mini" @click="openUpload">新增审核</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 列表 -->
<el-table v-loading="loading" :data="list" @row-dblclick="goDetail">
<el-table-column label="类型" align="center" width="80">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.reviewType === 'contract' ? 'warning' : 'success'">
{{ scope.row.reviewType === 'contract' ? '合同' : '简历' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="文件名" align="left" prop="fileName" min-width="180" show-overflow-tooltip />
<el-table-column label="岗位" align="center" prop="position" width="130" show-overflow-tooltip>
<template slot-scope="scope">
<span v-if="scope.row.position">{{ scope.row.position }}</span>
<span v-else style="color:#c0c4cc"></span>
</template>
</el-table-column>
<el-table-column label="结论" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.reviewType === 'resume' && scope.row.matchScore != null"
size="mini" type="success" effect="dark">匹配 {{ scope.row.matchScore }}</el-tag>
<el-tag v-else-if="scope.row.reviewType === 'contract' && scope.row.riskLevel"
size="mini" :type="riskTagType(scope.row.riskLevel)" effect="dark">
{{ scope.row.riskLevel }}风险</el-tag>
<span v-else style="color:#c0c4cc"></span>
</template>
</el-table-column>
<el-table-column label="审核摘要" align="left" prop="summary" min-width="280" show-overflow-tooltip>
<template slot-scope="scope">
<span class="summary-cell">{{ scope.row.summary || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="审核时间" align="center" prop="createTime" width="150" />
<el-table-column label="操作" align="center" width="130" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="goDetail(scope.row)">详情</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total"
:page.sync="query.pageNum" :limit.sync="query.pageSize" @pagination="getList" />
<!-- 新增审核弹窗 -->
<el-dialog title="新增 AI 审核" :visible.sync="uploadVisible" width="520px" :close-on-click-modal="false"
@closed="resetUpload">
<el-form label-width="80px" size="small">
<el-form-item label="审核类型">
<el-radio-group v-model="reviewType">
<el-radio-button label="contract">合同审核</el-radio-button>
<el-radio-button label="resume">简历审核</el-radio-button>
</el-radio-group>
<div class="hint">
<div class="dialog-hint">
{{ reviewType === 'contract'
? '从“我方”利益角度审查合同,找出不利条款并给出利好我方的修改/补充建议。'
? '从“我方”利益角度审查合同,找出不利条款并给出利好我方的修改建议。'
: '评估候选人,分析与目标岗位的匹配度、优势短板与面试建议。' }}
</div>
<el-input v-if="reviewType === 'resume'" v-model="position" size="small" class="pos-input"
placeholder="目标岗位(选填,如:机械设计工程师" clearable />
</el-form-item>
<el-form-item v-if="reviewType === 'resume'" label="目标岗位">
<el-input v-model="position" placeholder="选填,如:机械设计工程师" clearable />
</el-form-item>
<el-form-item label="上传文件">
<el-upload ref="uploader" drag action="#" :auto-upload="false" :limit="1"
:show-file-list="true" :on-change="onFileChange" :on-remove="onFileRemove"
:file-list="fileList" accept=".pdf,.doc,.docx" class="uploader">
:on-change="onFileChange" :on-remove="onFileRemove" :file-list="fileList"
accept=".pdf,.doc,.docx" class="uploader">
<i class="el-icon-upload" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div slot="tip" class="el-upload__tip">支持 PDF / Word(.doc/.docx)单个文件 20MB</div>
<div slot="tip" class="el-upload__tip">支持 PDF / Word(.doc/.docx) 20MB</div>
</el-upload>
<el-button type="primary" icon="el-icon-magic-stick" :loading="analyzing"
style="width:100%" @click="doAnalyze">
{{ analyzing ? 'AI 审核中' : '开始审核' }}
</el-button>
<div class="hist-hd">
<span><i class="el-icon-time" /> 历史记录</span>
<el-input v-model="query.keyword" size="mini" placeholder="搜索文件名/岗位" clearable
prefix-icon="el-icon-search" style="width:150px"
@keyup.enter.native="searchHist" @clear="searchHist" />
</div>
<div v-loading="histLoading" class="hist-list">
<div v-if="histList.length === 0 && !histLoading" class="hist-empty">暂无记录</div>
<div v-for="r in histList" :key="r.id" class="hist-item"
:class="{ sel: current && current.id === r.id }" @click="viewHistory(r.id)">
<div class="hi-top">
<el-tag size="mini" :type="r.reviewType === 'contract' ? 'warning' : 'success'" effect="plain">
{{ r.reviewType === 'contract' ? '合同' : '简历' }}
</el-tag>
<span v-if="r.reviewType === 'resume' && r.matchScore != null" class="hi-badge score">
{{ r.matchScore }}
</span>
<span v-if="r.reviewType === 'contract' && r.riskLevel" class="hi-badge"
:class="riskClass(r.riskLevel)">{{ r.riskLevel }}风险</span>
<el-button type="text" icon="el-icon-delete" class="hi-del" @click.stop="delHistory(r)" />
</div>
<div class="hi-name">{{ r.fileName }}</div>
<div class="hi-meta">{{ r.position || '—' }} · {{ r.createTime }}</div>
</div>
</div>
<pagination v-show="total > 0" :total="total" :page.sync="query.pageNum"
:limit.sync="query.pageSize" :page-sizes="[10,20,50]" small @pagination="loadHist" />
</el-card>
</el-col>
<!-- 结果 -->
<el-col :span="16" :xs="24">
<el-card shadow="never" class="panel result-panel">
<div slot="header" class="hd result-hd">
<span><i class="el-icon-document-checked" /> 审核结果</span>
<div v-if="current" class="result-tags">
<el-tag size="mini" :type="current.reviewType === 'contract' ? 'warning' : 'success'">
{{ current.reviewType === 'contract' ? '合同' : '简历' }}
</el-tag>
<span class="r-file">{{ current.fileName }}</span>
<el-tag v-if="current.reviewType === 'resume' && current.matchScore != null"
size="mini" type="success" effect="dark">匹配度 {{ current.matchScore }}</el-tag>
<el-tag v-if="current.reviewType === 'contract' && current.riskLevel"
size="mini" :type="riskTagType(current.riskLevel)" effect="dark">
{{ current.riskLevel }}风险</el-tag>
</div>
</div>
<div v-loading="analyzing" :element-loading-text="loadingText" class="result-body">
<div v-if="!current && !analyzing" class="result-empty">
<i class="el-icon-cpu" />
<div>上传合同或简历点击开始审核获取 AI 分析结果</div>
</div>
<div v-else-if="current" class="md-body" v-html="renderedMd" />
<div v-else class="result-empty"><div>{{ loadingText }}</div></div>
</div>
</el-card>
</el-col>
</el-row>
</el-form-item>
</el-form>
<div slot="footer">
<el-button size="small" @click="uploadVisible = false"> </el-button>
<el-button size="small" type="primary" :loading="analyzing" @click="doAnalyze">
{{ analyzing ? 'AI 审核中' : '开始审核' }}
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { analyzeAiReview, listAiReview, getAiReview, delAiReview } from '@/api/oa/aiReview'
const marked = require('marked')
import { analyzeAiReview, listAiReview, delAiReview } from '@/api/oa/aiReview'
export default {
name: 'OaAiReview',
data () {
return {
loading: true,
showSearch: true,
total: 0,
list: [],
query: { pageNum: 1, pageSize: 10, keyword: '', reviewType: '' },
uploadVisible: false,
reviewType: 'contract',
position: '',
fileList: [],
rawFile: null,
analyzing: false,
loadingText: 'AI 审核中,长文档可能需要 30~90 秒,请稍候…',
current: null,
histList: [],
total: 0,
histLoading: false,
query: { pageNum: 1, pageSize: 10, keyword: '', reviewType: '' }
}
},
computed: {
renderedMd () {
if (!this.current || !this.current.resultMd) return ''
try { return marked(this.current.resultMd) } catch (e) { return this.current.resultMd }
analyzing: false
}
},
created () {
marked.setOptions({ breaks: true })
this.loadHist()
this.getList()
},
activated () {
this.getList()
},
methods: {
onTypeChange () {
getList () {
this.loading = true
listAiReview(this.query).then(res => {
this.list = res.rows || []
this.total = res.total || 0
}).finally(() => { this.loading = false })
},
handleQuery () {
this.query.pageNum = 1
this.getList()
},
resetQuery () {
this.resetForm('queryForm')
this.query.reviewType = ''
this.loadHist()
this.handleQuery()
},
goDetail (row) {
this.$router.push('/hint/aiReview/detail/' + row.id)
},
handleDelete (row) {
this.$modal.confirm(`确认删除「${row.fileName}」的审核记录?`).then(() => {
return delAiReview(row.id)
}).then(() => {
this.$modal.msgSuccess('已删除')
this.getList()
}).catch(() => {})
},
// ===== 新增审核 =====
openUpload () {
this.uploadVisible = true
},
resetUpload () {
this.reviewType = 'contract'
this.position = ''
this.fileList = []
this.rawFile = null
this.analyzing = false
if (this.$refs.uploader) this.$refs.uploader.clearFiles()
},
onFileChange (file, fileList) {
// 仅保留最后一个文件
if (fileList.length > 1) fileList.splice(0, fileList.length - 1)
const isOk = /\.(pdf|doc|docx)$/i.test(file.name)
if (!isOk) {
if (!/\.(pdf|doc|docx)$/i.test(file.name)) {
this.$modal.msgError('仅支持 PDF / Word(.doc/.docx)')
this.fileList = []
this.rawFile = null
return
this.fileList = []; this.rawFile = null; return
}
if (file.size > 20 * 1024 * 1024) {
this.$modal.msgError('文件不能超过 20MB')
this.fileList = []
this.rawFile = null
return
this.fileList = []; this.rawFile = null; return
}
this.rawFile = file.raw
this.fileList = [file]
@@ -165,12 +199,12 @@ export default {
fd.append('reviewType', this.reviewType)
if (this.reviewType === 'resume' && this.position) fd.append('position', this.position)
this.analyzing = true
this.current = null
try {
const res = await analyzeAiReview(fd)
this.current = res.data
this.$modal.msgSuccess('审核完成')
this.loadHist()
this.uploadVisible = false
this.getList()
if (res.data && res.data.id) this.goDetail(res.data)
} catch (e) {
// request.js 已弹错误提示
} finally {
@@ -178,34 +212,6 @@ export default {
}
},
searchHist () {
this.query.pageNum = 1
this.loadHist()
},
loadHist () {
this.histLoading = true
this.query.reviewType = this.reviewType
listAiReview(this.query).then(res => {
this.histList = res.rows || []
this.total = res.total || 0
}).finally(() => { this.histLoading = false })
},
viewHistory (id) {
getAiReview(id).then(res => { this.current = res.data })
},
delHistory (r) {
this.$modal.confirm(`确认删除「${r.fileName}」的审核记录?`).then(() => {
return delAiReview(r.id)
}).then(() => {
this.$modal.msgSuccess('已删除')
if (this.current && this.current.id === r.id) this.current = null
this.loadHist()
}).catch(() => {})
},
riskClass (r) {
return r === '高' ? 'risk-high' : (r === '中' ? 'risk-mid' : 'risk-low')
},
riskTagType (r) {
return r === '高' ? 'danger' : (r === '中' ? 'warning' : 'success')
}
@@ -214,71 +220,11 @@ export default {
</script>
<style scoped lang="scss">
.ai-review { padding: 10px; }
.panel { ::v-deep .el-card__header { padding: 10px 14px; } }
.hd { font-weight: 600; font-size: 14px; color: #303133; i { color: #409eff; margin-right: 4px; } }
.type-switch { margin-bottom: 10px; width: 100%;
::v-deep .el-radio-button { width: 50%; }
::v-deep .el-radio-button__inner { width: 100%; }
}
.hint { font-size: 12px; color: #909399; line-height: 1.6; margin-bottom: 10px; }
.pos-input { margin-bottom: 10px; }
.uploader { margin-bottom: 10px;
.summary-cell { color: #606266; font-size: 12px; }
.dialog-hint { font-size: 12px; color: #909399; line-height: 1.6; margin-top: 4px; }
.uploader {
::v-deep .el-upload, ::v-deep .el-upload-dragger { width: 100%; }
::v-deep .el-upload-dragger { height: 130px; }
.el-icon-upload { font-size: 40px; color: #c0c4cc; margin: 18px 0 8px; line-height: 1; }
}
.hist-hd { display: flex; justify-content: space-between; align-items: center;
margin: 14px 0 8px; font-size: 13px; font-weight: 600; color: #303133;
i { color: #409eff; margin-right: 4px; }
}
.hist-list { max-height: calc(100vh - 470px); overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
.hist-empty { text-align: center; color: #c0c4cc; padding: 20px 0; font-size: 12px; }
.hist-item { border: 1px solid #ebeef5; border-radius: 4px; padding: 6px 10px; cursor: pointer; transition: .15s;
&:hover { border-color: #409eff; background: #fafdff; }
&.sel { border-color: #409eff; background: #ecf5ff; }
.hi-top { display: flex; align-items: center; gap: 6px; }
.hi-badge { font-size: 11px; padding: 0 5px; border-radius: 8px; line-height: 16px;
&.score { background: #f0f9eb; color: #67c23a; }
&.risk-high { background: #fef0f0; color: #f56c6c; }
&.risk-mid { background: #fdf6ec; color: #e6a23c; }
&.risk-low { background: #f0f9eb; color: #67c23a; }
}
.hi-del { margin-left: auto; padding: 0; color: #c0c4cc; &:hover { color: #f56c6c; } }
.hi-name { font-size: 13px; color: #303133; margin: 3px 0 1px; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap; }
.hi-meta { font-size: 11px; color: #909399; }
}
.result-panel { min-height: calc(100vh - 140px); }
.result-hd { display: flex; justify-content: space-between; align-items: center;
.result-tags { display: flex; align-items: center; gap: 8px;
.r-file { font-size: 12px; color: #606266; max-width: 320px; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap; }
}
}
.result-body { min-height: 400px; }
.result-empty { text-align: center; color: #c0c4cc; padding: 100px 0;
i { font-size: 56px; opacity: .4; display: block; margin-bottom: 14px; }
}
.md-body {
font-size: 14px; color: #2c3e50; line-height: 1.8; padding: 4px 6px;
::v-deep {
h1, h2, h3 { color: #303133; margin: 18px 0 10px; font-weight: 600; }
h1 { font-size: 19px; } h2 { font-size: 16px; border-left: 3px solid #409eff; padding-left: 8px; }
h3 { font-size: 14px; }
p { margin: 8px 0; }
ul, ol { padding-left: 22px; margin: 8px 0; }
li { margin: 4px 0; }
strong { color: #c0392b; }
code { background: #f4f7fa; padding: 1px 5px; border-radius: 3px; font-size: 13px; color: #e6a23c; }
table { border-collapse: collapse; width: 100%; margin: 10px 0; }
th, td { border: 1px solid #ebeef5; padding: 6px 10px; font-size: 13px; }
th { background: #f5f7fa; }
blockquote { border-left: 3px solid #dcdfe6; padding-left: 10px; color: #909399; margin: 8px 0; }
}
}
</style>

View File

@@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS `oa_ai_review` (
`position` varchar(255) 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审核结论摘要列表展示',
`result_md` longtext COMMENT 'AI 审核结果Markdown',
`model` varchar(50) DEFAULT NULL COMMENT '使用的模型',
`tokens` int(11) DEFAULT NULL COMMENT '消耗 token',
@@ -30,6 +31,9 @@ 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重复执行报错可忽略
-- ALTER TABLE `oa_ai_review` ADD COLUMN `summary` varchar(500) DEFAULT NULL COMMENT 'AI审核结论摘要列表展示' AFTER `risk_level`;
-- ---------------- 菜单:信息 > AI审核 ----------------
-- 父菜单 1774989374680858626 = 「信息」
INSERT IGNORE INTO `sys_menu`