feat(报销/拨款): 进入页面检测OCR服务状态

- 后端新增 GET /ocr-health 端点,探测 Python OCR 服务 /health
- 前端页面 created 时调用健康检查,服务不可用时顶部显示红色警告
  "发票识别服务已停止,请联系信息化部门"
- 服务不可用时禁用附件上传区域(FileUpload 新增 disabled prop)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 18:47:51 +08:00
parent 6055f06f83
commit 40fdd14d13
9 changed files with 86 additions and 14 deletions

View File

@@ -68,9 +68,11 @@ public class HrmAppropriationReqController extends BaseController {
return R.ok(service.queryList(bo)); return R.ok(service.queryList(bo));
} }
/** @GetMapping("/ocr-health")
* 通过ossId触发发票OCR识别返回识别条目不保存供前端实时回显 public R<Boolean> ocrHealth() {
*/ return R.ok(invoiceOcrService.isAlive());
}
@PostMapping("/ocr-by-oss") @PostMapping("/ocr-by-oss")
public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) { public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) {
return R.ok(invoiceOcrService.recognizeByOssId(ossId)); return R.ok(invoiceOcrService.recognizeByOssId(ossId));

View File

@@ -68,9 +68,11 @@ public class HrmReimburseReqController extends BaseController {
return R.ok(service.queryList(bo)); return R.ok(service.queryList(bo));
} }
/** @GetMapping("/ocr-health")
* 通过ossId触发发票OCR识别返回识别条目不保存供前端实时回显 public R<Boolean> ocrHealth() {
*/ return R.ok(invoiceOcrService.isAlive());
}
@PostMapping("/ocr-by-oss") @PostMapping("/ocr-by-oss")
public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) { public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) {
return R.ok(invoiceOcrService.recognizeByOssId(ossId)); return R.ok(invoiceOcrService.recognizeByOssId(ossId));

View File

@@ -9,9 +9,11 @@ public interface IHrmInvoiceOcrService {
/** /**
* 通过ossId识别发票 * 通过ossId识别发票
*
* @param ossId 附件ID
* @return 识别结果
*/ */
HrmInvoiceOcrResultVo recognizeByOssId(Long ossId); HrmInvoiceOcrResultVo recognizeByOssId(Long ossId);
/**
* 检查OCR服务是否存活
*/
boolean isAlive();
} }

View File

@@ -193,6 +193,20 @@ public class HrmInvoiceOcrServiceImpl implements IHrmInvoiceOcrService {
return val.toString(); return val.toString();
} }
@Override
public boolean isAlive() {
String ocrUrl = ocrProperties.getUrl();
if (StringUtils.isBlank(ocrUrl)) return false;
try {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> resp = restTemplate.getForEntity(ocrUrl + "/health", String.class);
return resp.getStatusCode().is2xxSuccessful();
} catch (Exception e) {
log.warn("[OCR] 健康检查失败: {}", e.getMessage());
return false;
}
}
private String getStringOrFieldValue(JSONObject obj, String key) { private String getStringOrFieldValue(JSONObject obj, String key) {
Object val = obj.get(key); Object val = obj.get(key);
if (val == null) return null; if (val == null) return null;

View File

@@ -58,6 +58,10 @@ export function getAppropriationStats (query) {
}) })
} }
export function checkAppropriationOcrHealth () {
return request({ url: '/hrm/appropriation/ocr-health', method: 'get' })
}
/** /**
* 通过ossId触发发票OCR识别返回识别条目不保存 * 通过ossId触发发票OCR识别返回识别条目不保存
*/ */

View File

@@ -47,6 +47,10 @@ export function allReimburseReq(query) {
}) })
} }
export function checkReimburseOcrHealth() {
return request({ url: '/hrm/reimburse/ocr-health', method: 'get' })
}
/** /**
* 通过ossId触发发票OCR识别返回识别条目不保存 * 通过ossId触发发票OCR识别返回识别条目不保存
*/ */

View File

@@ -3,7 +3,7 @@
<el-upload multiple :drag="dragShow" :action="uploadFileUrl" :before-upload="handleBeforeUpload" <el-upload multiple :drag="dragShow" :action="uploadFileUrl" :before-upload="handleBeforeUpload"
:file-list="fileList" :limit="limit" :on-error="handleUploadError" :on-exceed="handleExceed" :file-list="fileList" :limit="limit" :on-error="handleUploadError" :on-exceed="handleExceed"
:on-success="handleUploadSuccess" :show-file-list="false" :data="{ isPublic: extraData }" :headers="headers" :on-success="handleUploadSuccess" :show-file-list="false" :data="{ isPublic: extraData }" :headers="headers"
class="upload-file-uploader" ref="fileUpload"> :disabled="disabled" class="upload-file-uploader" ref="fileUpload">
<i class="el-icon-upload" v-if="dragShow"></i> <i class="el-icon-upload" v-if="dragShow"></i>
<div class="el-upload__text" v-if="dragShow"> <div class="el-upload__text" v-if="dragShow">
将文件拖到此处<em>点击上传</em> 将文件拖到此处<em>点击上传</em>
@@ -71,6 +71,10 @@ export default {
isShowTip: { isShowTip: {
type: Boolean, type: Boolean,
default: true default: true
},
disabled: {
type: Boolean,
default: false
} }
}, },
inject: ['$folder'], inject: ['$folder'],
@@ -126,6 +130,7 @@ export default {
this.$download.oss(file.ossId); this.$download.oss(file.ossId);
}, },
handleBeforeUpload (file) { handleBeforeUpload (file) {
if (this.disabled) return false;
const ext = file.name.split('.').pop().toLowerCase(); const ext = file.name.split('.').pop().toLowerCase();
if (this.fileType && !this.fileType.includes(ext)) { if (this.fileType && !this.fileType.includes(ext)) {
this.$modal.msgError(`文件格式不正确,请上传${this.fileType.join('/')}格式文件!`); this.$modal.msgError(`文件格式不正确,请上传${this.fileType.join('/')}格式文件!`);

View File

@@ -9,6 +9,10 @@
</div> </div>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" size="small" class="metal-form"> <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" size="small" class="metal-form">
<el-alert v-if="ocrAvailable === false" type="error" :closable="false" show-icon style="margin-bottom:14px">
<span slot="title">发票识别服务已停止请联系信息化部门在服务恢复前您暂时无法上传发票附件</span>
</el-alert>
<div class="form-summary"> <div class="form-summary">
<div class="summary-left"> <div class="summary-left">
<div class="summary-title">发起拨款申请</div> <div class="summary-title">发起拨款申请</div>
@@ -57,8 +61,11 @@
<el-form-item label="拨款单据附件" prop="accessoryApplyIds"> <el-form-item label="拨款单据附件" prop="accessoryApplyIds">
<file-upload v-model="form.accessoryApplyIds" :limit="50" :file-size="50" <file-upload v-model="form.accessoryApplyIds" :limit="50" :file-size="50"
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple :file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple
:disabled="ocrAvailable === false"
@delete="onFileDelete" /> @delete="onFileDelete" />
<div class="hint-text">上传发票收据付款截图等支持 PDF/图片上传后自动识别明细</div> <div class="hint-text">
{{ ocrAvailable === false ? '识别服务不可用,暂时无法上传' : '上传发票、收据、付款截图等(支持 PDF/图片,上传后自动识别明细)' }}
</div>
</el-form-item> </el-form-item>
<!-- OCR 识别中提示 --> <!-- OCR 识别中提示 -->
@@ -247,7 +254,7 @@
</template> </template>
<script> <script>
import { addAppropriationReq, listFlowNode, listFlowTemplate, ocrAppropriationInvoice } from '@/api/hrm'; import { addAppropriationReq, checkAppropriationOcrHealth, listFlowNode, listFlowTemplate, ocrAppropriationInvoice } from '@/api/hrm';
import { getEmployeeByUserId } from '@/api/hrm/employee'; import { getEmployeeByUserId } from '@/api/hrm/employee';
import { ccFlowTask } from '@/api/hrm/flow'; import { ccFlowTask } from '@/api/hrm/flow';
import FileUpload from '@/components/FileUpload'; import FileUpload from '@/components/FileUpload';
@@ -271,6 +278,9 @@ export default {
assigneeUserName: '', assigneeUserName: '',
appropriationTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'], appropriationTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'],
// 发票明细条目 // 发票明细条目
// OCR服务状态null=检测中true=正常false=不可用
ocrAvailable: null,
// 发票明细条目
invoiceItems: [], invoiceItems: [],
// OCR加载状态 { ossId: true/false } // OCR加载状态 { ossId: true/false }
ocrLoadingMap: {}, ocrLoadingMap: {},
@@ -309,6 +319,7 @@ export default {
created () { created () {
this.loadCurrentEmployee() this.loadCurrentEmployee()
this.loadTemplates() this.loadTemplates()
this.checkOcrHealth()
}, },
computed: { computed: {
currentApplicantText () { currentApplicantText () {
@@ -346,6 +357,15 @@ export default {
onCcUsersSelected (users) { this.ccForm.selectedUsers = users || [] }, onCcUsersSelected (users) { this.ccForm.selectedUsers = users || [] },
removeCcUser (user) { this.ccForm.selectedUsers = this.ccForm.selectedUsers.filter(u => u.userId !== user.userId) }, removeCcUser (user) { this.ccForm.selectedUsers = this.ccForm.selectedUsers.filter(u => u.userId !== user.userId) },
async checkOcrHealth () {
try {
const res = await checkAppropriationOcrHealth()
this.ocrAvailable = res.code === 200 && res.data === true
} catch (e) {
this.ocrAvailable = false
}
},
async triggerOcr (ossId) { async triggerOcr (ossId) {
if (this.ocrDoneSet.has(ossId)) return if (this.ocrDoneSet.has(ossId)) return
this.$set(this.ocrLoadingMap, ossId, true) this.$set(this.ocrLoadingMap, ossId, true)

View File

@@ -9,6 +9,10 @@
</div> </div>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" size="small" class="metal-form"> <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" size="small" class="metal-form">
<el-alert v-if="ocrAvailable === false" type="error" :closable="false" show-icon style="margin-bottom:14px">
<span slot="title">发票识别服务已停止请联系信息化部门在服务恢复前您暂时无法上传发票附件</span>
</el-alert>
<div class="form-summary"> <div class="form-summary">
<div class="summary-left"> <div class="summary-left">
<div class="summary-title">发起日常报销</div> <div class="summary-title">发起日常报销</div>
@@ -57,8 +61,11 @@
<el-form-item label="报销单据附件" prop="accessoryApplyIds"> <el-form-item label="报销单据附件" prop="accessoryApplyIds">
<file-upload v-model="form.accessoryApplyIds" :limit="200" :file-size="50" <file-upload v-model="form.accessoryApplyIds" :limit="200" :file-size="50"
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple :file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple
:disabled="ocrAvailable === false"
@delete="onFileDelete" /> @delete="onFileDelete" />
<div class="hint-text">上传发票收据付款截图等支持 PDF/图片上传后自动识别明细</div> <div class="hint-text">
{{ ocrAvailable === false ? '识别服务不可用,暂时无法上传' : '上传发票、收据、付款截图等(支持 PDF/图片,上传后自动识别明细)' }}
</div>
</el-form-item> </el-form-item>
<!-- OCR 识别中提示 --> <!-- OCR 识别中提示 -->
@@ -224,7 +231,7 @@
</template> </template>
<script> <script>
import { addReimburseReq, listFlowNode, listFlowTemplate, ocrReimburseInvoice } from '@/api/hrm'; import { addReimburseReq, checkReimburseOcrHealth, listFlowNode, listFlowTemplate, ocrReimburseInvoice } from '@/api/hrm';
import { getEmployeeByUserId } from '@/api/hrm/employee'; import { getEmployeeByUserId } from '@/api/hrm/employee';
import { ccFlowTask } from '@/api/hrm/flow'; import { ccFlowTask } from '@/api/hrm/flow';
import FileUpload from '@/components/FileUpload'; import FileUpload from '@/components/FileUpload';
@@ -247,6 +254,8 @@ export default {
assigneeUserId: null, assigneeUserId: null,
assigneeUserName: '', assigneeUserName: '',
reimburseTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'], reimburseTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'],
// OCR服务状态null=检测中true=正常false=不可用
ocrAvailable: null,
// 发票明细条目 // 发票明细条目
invoiceItems: [], invoiceItems: [],
// OCR加载状态 { ossId: true/false } // OCR加载状态 { ossId: true/false }
@@ -284,6 +293,7 @@ export default {
created () { created () {
this.loadCurrentEmployee() this.loadCurrentEmployee()
this.loadTemplates() this.loadTemplates()
this.checkOcrHealth()
}, },
computed: { computed: {
currentApplicantText () { currentApplicantText () {
@@ -321,6 +331,15 @@ export default {
onCcUsersSelected (users) { this.ccForm.selectedUsers = users || [] }, onCcUsersSelected (users) { this.ccForm.selectedUsers = users || [] },
removeCcUser (user) { this.ccForm.selectedUsers = this.ccForm.selectedUsers.filter(u => u.userId !== user.userId) }, removeCcUser (user) { this.ccForm.selectedUsers = this.ccForm.selectedUsers.filter(u => u.userId !== user.userId) },
async checkOcrHealth () {
try {
const res = await checkReimburseOcrHealth()
this.ocrAvailable = res.code === 200 && res.data === true
} catch (e) {
this.ocrAvailable = false
}
},
async triggerOcr (ossId) { async triggerOcr (ossId) {
if (this.ocrDoneSet.has(ossId)) return if (this.ocrDoneSet.has(ossId)) return
this.$set(this.ocrLoadingMap, ossId, true) this.$set(this.ocrLoadingMap, ossId, true)