feat(报销/拨款): 新增发票明细子表与OCR自动识别
- 新增 hrm_invoice_item 共享子表(biz_type区分报销/拨款),每条记录对应一张发票条目 - 新增 HrmInvoiceOcrService,上传附件后自动调用 ai-ocr Python服务识别发票,结果逐条回填表单 - 报销/拨款申请提交及更新时同步保存发票明细;queryById 返回关联发票条目列表 - 前端:附件上传后自动触发OCR,展示"模型思考中"状态,识别完成后自动填充金额 - 详情页新增发票明细只读表格展示,兼容无明细的历史记录 - application.yml 增加 fad.ocr 配置项 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.hrm.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 发票OCR服务配置
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "fad.ocr")
|
||||
public class HrmOcrProperties {
|
||||
|
||||
/** OCR服务地址,如 http://127.0.0.1:8000 */
|
||||
private String url = "http://127.0.0.1:8000";
|
||||
|
||||
/** OCR服务 API Key */
|
||||
private String apiKey = "";
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.hrm.domain.bo.HrmAppropriationReqBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmAppropriationReqVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmInvoiceOcrResultVo;
|
||||
import com.ruoyi.hrm.service.IHrmAppropriationReqService;
|
||||
import com.ruoyi.hrm.service.IHrmInvoiceOcrService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -29,6 +31,7 @@ import java.util.List;
|
||||
public class HrmAppropriationReqController extends BaseController {
|
||||
|
||||
private final IHrmAppropriationReqService service;
|
||||
private final IHrmInvoiceOcrService invoiceOcrService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<HrmAppropriationReqVo> list(HrmAppropriationReqBo bo, PageQuery pageQuery) {
|
||||
@@ -64,5 +67,13 @@ public class HrmAppropriationReqController extends BaseController {
|
||||
bo.setCreateBy(String.valueOf(LoginHelper.getUserId()));
|
||||
return R.ok(service.queryList(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过ossId触发发票OCR识别,返回识别条目(不保存,供前端实时回显)
|
||||
*/
|
||||
@PostMapping("/ocr-by-oss")
|
||||
public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) {
|
||||
return R.ok(invoiceOcrService.recognizeByOssId(ossId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.hrm.domain.bo.HrmReimburseReqBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmInvoiceOcrResultVo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmReimburseReqVo;
|
||||
import com.ruoyi.hrm.service.IHrmInvoiceOcrService;
|
||||
import com.ruoyi.hrm.service.IHrmReimburseReqService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -29,6 +31,7 @@ import java.util.List;
|
||||
public class HrmReimburseReqController extends BaseController {
|
||||
|
||||
private final IHrmReimburseReqService service;
|
||||
private final IHrmInvoiceOcrService invoiceOcrService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<HrmReimburseReqVo> list(HrmReimburseReqBo bo, PageQuery pageQuery) {
|
||||
@@ -64,5 +67,13 @@ public class HrmReimburseReqController extends BaseController {
|
||||
bo.setCreateBy(LoginHelper.getUsername());
|
||||
return R.ok(service.queryList(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过ossId触发发票OCR识别,返回识别条目(不保存,供前端实时回显)
|
||||
*/
|
||||
@PostMapping("/ocr-by-oss")
|
||||
public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) {
|
||||
return R.ok(invoiceOcrService.recognizeByOssId(ossId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.ruoyi.hrm.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 发票条目子表(报销单/拨款单共用)
|
||||
*/
|
||||
@Data
|
||||
@TableName("hrm_invoice_item")
|
||||
public class HrmInvoiceItem {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
/** 业务类型 reimburse / appropriation */
|
||||
private String bizType;
|
||||
|
||||
/** 关联业务单ID */
|
||||
private Long bizId;
|
||||
|
||||
/** 来源附件ossId */
|
||||
private Long ossId;
|
||||
|
||||
/** 排序序号 */
|
||||
private Integer sortNo;
|
||||
|
||||
/** OCR识别项目名称 */
|
||||
private String itemName;
|
||||
|
||||
/** 事由说明(用户可编辑) */
|
||||
private String reason;
|
||||
|
||||
/** 金额 */
|
||||
private BigDecimal amount;
|
||||
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拨款申请 Bo
|
||||
@@ -59,5 +60,8 @@ public class HrmAppropriationReqBo extends BaseEntity {
|
||||
private String remark;
|
||||
|
||||
private Long tplId;
|
||||
|
||||
/** 发票条目列表(前端提交时携带) */
|
||||
private List<HrmInvoiceItemBo> invoiceItems;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ruoyi.hrm.domain.bo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 发票条目 BO(用于表单提交)
|
||||
*/
|
||||
@Data
|
||||
public class HrmInvoiceItemBo {
|
||||
|
||||
/** 来源附件ossId */
|
||||
private Long ossId;
|
||||
|
||||
/** 排序序号 */
|
||||
private Integer sortNo;
|
||||
|
||||
/** OCR识别项目名称 */
|
||||
private String itemName;
|
||||
|
||||
/** 事由说明 */
|
||||
private String reason;
|
||||
|
||||
/** 金额 */
|
||||
private BigDecimal amount;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@@ -39,5 +40,8 @@ public class HrmReimburseReqBo extends BaseEntity {
|
||||
private String remark;
|
||||
|
||||
private Long tplId;
|
||||
|
||||
/** 发票条目列表(前端提交时携带) */
|
||||
private List<HrmInvoiceItemBo> invoiceItems;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.ruoyi.hrm.domain.HrmInvoiceItem;
|
||||
|
||||
/**
|
||||
* 拨款申请 VO
|
||||
@@ -108,5 +110,7 @@ public class HrmAppropriationReqVo implements Serializable {
|
||||
|
||||
/** 流程实例ID */
|
||||
private Long instId;
|
||||
|
||||
private List<HrmInvoiceItem> invoiceItems;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.ruoyi.hrm.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 单张发票OCR识别结果(返回给前端用于填充发票条目)
|
||||
*/
|
||||
@Data
|
||||
public class HrmInvoiceOcrResultVo {
|
||||
|
||||
/** 发票类型 */
|
||||
private String invoiceType;
|
||||
|
||||
/** 销售方名称 */
|
||||
private String sellerName;
|
||||
|
||||
/** 开票日期 */
|
||||
private String invoiceDate;
|
||||
|
||||
/** 价税合计 */
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
/** 识别出的条目列表 */
|
||||
private List<Item> items;
|
||||
|
||||
@Data
|
||||
public static class Item {
|
||||
/** OCR识别名称 */
|
||||
private String itemName;
|
||||
/** 金额(不含税) */
|
||||
private BigDecimal amount;
|
||||
/** 税率 */
|
||||
private String taxRate;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.ruoyi.hrm.domain.HrmInvoiceItem;
|
||||
|
||||
@Data
|
||||
public class HrmReimburseReqVo implements Serializable {
|
||||
@@ -95,5 +97,7 @@ public class HrmReimburseReqVo implements Serializable {
|
||||
private Date updateTime;
|
||||
|
||||
private Long instId;
|
||||
|
||||
private List<HrmInvoiceItem> invoiceItems;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.ruoyi.hrm.mapper;
|
||||
|
||||
import com.ruoyi.common.core.mapper.BaseMapperPlus;
|
||||
import com.ruoyi.hrm.domain.HrmInvoiceItem;
|
||||
|
||||
public interface HrmInvoiceItemMapper extends BaseMapperPlus<HrmInvoiceItemMapper, HrmInvoiceItem, HrmInvoiceItem> {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ruoyi.hrm.service;
|
||||
|
||||
import com.ruoyi.hrm.domain.vo.HrmInvoiceOcrResultVo;
|
||||
|
||||
/**
|
||||
* 发票OCR识别服务(调用Python OCR微服务)
|
||||
*/
|
||||
public interface IHrmInvoiceOcrService {
|
||||
|
||||
/**
|
||||
* 通过ossId识别发票
|
||||
*
|
||||
* @param ossId 附件ID
|
||||
* @return 识别结果
|
||||
*/
|
||||
HrmInvoiceOcrResultVo recognizeByOssId(Long ossId);
|
||||
}
|
||||
@@ -9,11 +9,14 @@ import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.helper.LoginHelper;
|
||||
import com.ruoyi.hrm.domain.HrmAppropriationReq;
|
||||
import com.ruoyi.hrm.domain.HrmFlowTemplate;
|
||||
import com.ruoyi.hrm.domain.HrmInvoiceItem;
|
||||
import com.ruoyi.hrm.domain.bo.HrmAppropriationReqBo;
|
||||
import com.ruoyi.hrm.domain.bo.HrmFlowStartBo;
|
||||
import com.ruoyi.hrm.domain.bo.HrmInvoiceItemBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmAppropriationReqVo;
|
||||
import com.ruoyi.hrm.mapper.HrmAppropriationReqMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmFlowTemplateMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmInvoiceItemMapper;
|
||||
import com.ruoyi.hrm.service.IHrmAppropriationReqService;
|
||||
import com.ruoyi.hrm.service.IHrmFlowInstanceService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -33,10 +36,18 @@ public class HrmAppropriationReqServiceImpl implements IHrmAppropriationReqServi
|
||||
private final HrmAppropriationReqMapper baseMapper;
|
||||
private final HrmFlowTemplateMapper flowTemplateMapper;
|
||||
private final IHrmFlowInstanceService flowInstanceService;
|
||||
private final HrmInvoiceItemMapper invoiceItemMapper;
|
||||
|
||||
@Override
|
||||
public HrmAppropriationReqVo queryById(Long bizId) {
|
||||
return baseMapper.selectVoWithProjectById(bizId);
|
||||
HrmAppropriationReqVo vo = baseMapper.selectVoWithProjectById(bizId);
|
||||
if (vo != null) {
|
||||
vo.setInvoiceItems(invoiceItemMapper.selectList(Wrappers.<HrmInvoiceItem>lambdaQuery()
|
||||
.eq(HrmInvoiceItem::getBizType, "appropriation")
|
||||
.eq(HrmInvoiceItem::getBizId, bizId)
|
||||
.orderByAsc(HrmInvoiceItem::getSortNo)));
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,6 +75,12 @@ public class HrmAppropriationReqServiceImpl implements IHrmAppropriationReqServi
|
||||
boolean ok = baseMapper.insert(add) > 0;
|
||||
|
||||
HrmAppropriationReqVo bean = BeanUtil.toBean(add, HrmAppropriationReqVo.class);
|
||||
|
||||
// 保存发票条目
|
||||
if (ok && bo.getInvoiceItems() != null && !bo.getInvoiceItems().isEmpty()) {
|
||||
saveInvoiceItems("appropriation", add.getBizId(), bo.getInvoiceItems());
|
||||
}
|
||||
|
||||
if (ok && "pending".equalsIgnoreCase(add.getStatus())) {
|
||||
// 获取流程启动人ID
|
||||
Long startUserId = LoginHelper.getUserId();
|
||||
@@ -108,7 +125,11 @@ public class HrmAppropriationReqServiceImpl implements IHrmAppropriationReqServi
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateByBo(HrmAppropriationReqBo bo) {
|
||||
HrmAppropriationReq update = BeanUtil.toBean(bo, HrmAppropriationReq.class);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
boolean updated = baseMapper.updateById(update) > 0;
|
||||
if (updated && bo.getInvoiceItems() != null) {
|
||||
saveInvoiceItems("appropriation", bo.getBizId(), bo.getInvoiceItems());
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,6 +146,24 @@ public class HrmAppropriationReqServiceImpl implements IHrmAppropriationReqServi
|
||||
return baseMapper.updateById(req) > 0;
|
||||
}
|
||||
|
||||
private void saveInvoiceItems(String bizType, Long bizId, List<HrmInvoiceItemBo> boList) {
|
||||
invoiceItemMapper.delete(Wrappers.<HrmInvoiceItem>lambdaQuery()
|
||||
.eq(HrmInvoiceItem::getBizType, bizType)
|
||||
.eq(HrmInvoiceItem::getBizId, bizId));
|
||||
for (int i = 0; i < boList.size(); i++) {
|
||||
HrmInvoiceItemBo bo = boList.get(i);
|
||||
HrmInvoiceItem item = new HrmInvoiceItem();
|
||||
item.setBizType(bizType);
|
||||
item.setBizId(bizId);
|
||||
item.setOssId(bo.getOssId());
|
||||
item.setSortNo(bo.getSortNo() != null ? bo.getSortNo() : i);
|
||||
item.setItemName(bo.getItemName());
|
||||
item.setReason(bo.getReason());
|
||||
item.setAmount(bo.getAmount());
|
||||
invoiceItemMapper.insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private LambdaQueryWrapper<HrmAppropriationReq> buildQueryWrapper(HrmAppropriationReqBo bo) {
|
||||
LambdaQueryWrapper<HrmAppropriationReq> lqw = Wrappers.lambdaQuery();
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package com.ruoyi.hrm.service.impl;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.hrm.config.HrmOcrProperties;
|
||||
import com.ruoyi.hrm.domain.vo.HrmInvoiceOcrResultVo;
|
||||
import com.ruoyi.hrm.service.IHrmInvoiceOcrService;
|
||||
import com.ruoyi.oss.factory.OssFactory;
|
||||
import com.ruoyi.system.mapper.SysOssMapper;
|
||||
import com.ruoyi.system.domain.vo.SysOssVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 发票OCR识别服务实现(调用Python OCR微服务)
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class HrmInvoiceOcrServiceImpl implements IHrmInvoiceOcrService {
|
||||
|
||||
private final HrmOcrProperties ocrProperties;
|
||||
private final SysOssMapper sysOssMapper;
|
||||
|
||||
@Override
|
||||
public HrmInvoiceOcrResultVo recognizeByOssId(Long ossId) {
|
||||
SysOssVo oss = sysOssMapper.selectVoById(ossId);
|
||||
if (oss == null) {
|
||||
throw new ServiceException("附件不存在: " + ossId);
|
||||
}
|
||||
|
||||
byte[] fileBytes;
|
||||
try (InputStream in = OssFactory.instance().getObjectContent(oss.getUrl())) {
|
||||
fileBytes = IoUtil.readBytes(in);
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("读取附件失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
String fileName = StringUtils.defaultIfBlank(oss.getOriginalName(), oss.getFileName());
|
||||
return callOcrService(fileBytes, fileName, oss.getFileSuffix());
|
||||
}
|
||||
|
||||
private HrmInvoiceOcrResultVo callOcrService(byte[] fileBytes, String fileName, String fileSuffix) {
|
||||
String ocrUrl = ocrProperties.getUrl();
|
||||
String apiKey = ocrProperties.getApiKey();
|
||||
|
||||
if (StringUtils.isBlank(ocrUrl)) {
|
||||
throw new ServiceException("OCR服务地址未配置,请检查 fad.ocr.url");
|
||||
}
|
||||
|
||||
// 推断 content-type
|
||||
String suffix = StringUtils.defaultIfBlank(fileSuffix, "").toLowerCase().replace(".", "");
|
||||
String contentType;
|
||||
switch (suffix) {
|
||||
case "pdf":
|
||||
contentType = "application/pdf";
|
||||
break;
|
||||
case "png":
|
||||
contentType = "image/png";
|
||||
break;
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
contentType = "image/jpeg";
|
||||
break;
|
||||
default:
|
||||
contentType = "application/octet-stream";
|
||||
}
|
||||
|
||||
// 构建 multipart 请求
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
if (StringUtils.isNotBlank(apiKey)) {
|
||||
headers.set("X-API-Key", apiKey);
|
||||
}
|
||||
|
||||
final String finalContentType = contentType;
|
||||
final String finalFileName = StringUtils.defaultIfBlank(fileName, "invoice" + "." + suffix);
|
||||
ByteArrayResource fileResource = new ByteArrayResource(fileBytes) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return finalFileName;
|
||||
}
|
||||
};
|
||||
|
||||
HttpHeaders fileHeaders = new HttpHeaders();
|
||||
fileHeaders.setContentType(MediaType.parseMediaType(finalContentType));
|
||||
HttpEntity<ByteArrayResource> filePart = new HttpEntity<>(fileResource, fileHeaders);
|
||||
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("file", filePart);
|
||||
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
ResponseEntity<String> response;
|
||||
try {
|
||||
response = restTemplate.postForEntity(ocrUrl + "/v1/invoice/ocr", requestEntity, String.class);
|
||||
} catch (Exception e) {
|
||||
log.error("[OCR] 调用OCR服务失败 url={} error={}", ocrUrl, e.getMessage());
|
||||
throw new ServiceException("OCR服务调用失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
|
||||
throw new ServiceException("OCR服务返回异常: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
return parseOcrResponse(response.getBody());
|
||||
}
|
||||
|
||||
private HrmInvoiceOcrResultVo parseOcrResponse(String responseBody) {
|
||||
HrmInvoiceOcrResultVo result = new HrmInvoiceOcrResultVo();
|
||||
try {
|
||||
JSONObject root = JSON.parseObject(responseBody);
|
||||
JSONObject data = root.getJSONObject("data");
|
||||
if (data == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.setInvoiceType(getFieldValue(data, "invoice_type"));
|
||||
result.setSellerName(getFieldValue(data, "seller_name"));
|
||||
result.setInvoiceDate(getFieldValue(data, "invoice_date"));
|
||||
|
||||
String totalAmountStr = getFieldValue(data, "amount_with_tax");
|
||||
if (StringUtils.isBlank(totalAmountStr)) {
|
||||
totalAmountStr = getFieldValue(data, "total_amount");
|
||||
}
|
||||
result.setTotalAmount(parseBigDecimal(totalAmountStr));
|
||||
|
||||
// 解析明细行
|
||||
List<HrmInvoiceOcrResultVo.Item> items = new ArrayList<>();
|
||||
JSONArray lineItems = data.getJSONArray("line_items");
|
||||
if (lineItems != null) {
|
||||
for (int i = 0; i < lineItems.size(); i++) {
|
||||
JSONObject li = lineItems.getJSONObject(i);
|
||||
if (li == null) continue;
|
||||
String name = getStringOrFieldValue(li, "project_name");
|
||||
if (StringUtils.isBlank(name)) continue;
|
||||
|
||||
HrmInvoiceOcrResultVo.Item item = new HrmInvoiceOcrResultVo.Item();
|
||||
item.setItemName(name);
|
||||
item.setAmount(parseBigDecimal(getStringOrFieldValue(li, "amount")));
|
||||
item.setTaxRate(getStringOrFieldValue(li, "tax_rate"));
|
||||
items.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有明细行但有总金额,生成一个汇总条目
|
||||
if (items.isEmpty() && result.getTotalAmount() != null) {
|
||||
HrmInvoiceOcrResultVo.Item item = new HrmInvoiceOcrResultVo.Item();
|
||||
String seller = StringUtils.defaultIfBlank(result.getSellerName(), "发票款项");
|
||||
item.setItemName(seller);
|
||||
item.setAmount(result.getTotalAmount());
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
result.setItems(items);
|
||||
} catch (Exception e) {
|
||||
log.warn("[OCR] 解析OCR响应失败: {}", e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 从 {value: "...", confidence: 0.9} 结构取值,或直接取字符串 */
|
||||
private String getFieldValue(JSONObject obj, String key) {
|
||||
Object val = obj.get(key);
|
||||
if (val == null) return null;
|
||||
if (val instanceof JSONObject) {
|
||||
return ((JSONObject) val).getString("value");
|
||||
}
|
||||
return val.toString();
|
||||
}
|
||||
|
||||
private String getStringOrFieldValue(JSONObject obj, String key) {
|
||||
Object val = obj.get(key);
|
||||
if (val == null) return null;
|
||||
if (val instanceof JSONObject) {
|
||||
return ((JSONObject) val).getString("value");
|
||||
}
|
||||
return val.toString();
|
||||
}
|
||||
|
||||
private BigDecimal parseBigDecimal(String raw) {
|
||||
if (StringUtils.isBlank(raw)) return null;
|
||||
try {
|
||||
return new BigDecimal(raw.replace(",", "").replace("¥", "").replace("¥", "").trim());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,14 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.hrm.domain.HrmFlowTemplate;
|
||||
import com.ruoyi.hrm.domain.HrmInvoiceItem;
|
||||
import com.ruoyi.hrm.domain.HrmReimburseReq;
|
||||
import com.ruoyi.hrm.domain.bo.HrmFlowStartBo;
|
||||
import com.ruoyi.hrm.domain.bo.HrmInvoiceItemBo;
|
||||
import com.ruoyi.hrm.domain.bo.HrmReimburseReqBo;
|
||||
import com.ruoyi.hrm.domain.vo.HrmReimburseReqVo;
|
||||
import com.ruoyi.hrm.mapper.HrmFlowTemplateMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmInvoiceItemMapper;
|
||||
import com.ruoyi.hrm.mapper.HrmReimburseReqMapper;
|
||||
import com.ruoyi.hrm.service.IHrmFlowInstanceService;
|
||||
import com.ruoyi.hrm.service.IHrmReimburseReqService;
|
||||
@@ -30,10 +33,18 @@ public class HrmReimburseReqServiceImpl implements IHrmReimburseReqService {
|
||||
private final HrmReimburseReqMapper baseMapper;
|
||||
private final HrmFlowTemplateMapper flowTemplateMapper;
|
||||
private final IHrmFlowInstanceService flowInstanceService;
|
||||
private final HrmInvoiceItemMapper invoiceItemMapper;
|
||||
|
||||
@Override
|
||||
public HrmReimburseReqVo queryById(Long bizId) {
|
||||
return baseMapper.selectVoWithProjectById(bizId);
|
||||
HrmReimburseReqVo vo = baseMapper.selectVoWithProjectById(bizId);
|
||||
if (vo != null) {
|
||||
vo.setInvoiceItems(invoiceItemMapper.selectList(Wrappers.<HrmInvoiceItem>lambdaQuery()
|
||||
.eq(HrmInvoiceItem::getBizType, "reimburse")
|
||||
.eq(HrmInvoiceItem::getBizId, bizId)
|
||||
.orderByAsc(HrmInvoiceItem::getSortNo)));
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -61,6 +72,12 @@ public class HrmReimburseReqServiceImpl implements IHrmReimburseReqService {
|
||||
boolean ok = baseMapper.insert(add) > 0;
|
||||
|
||||
HrmReimburseReqVo bean = BeanUtil.toBean(add, HrmReimburseReqVo.class);
|
||||
|
||||
// 保存发票条目
|
||||
if (ok && bo.getInvoiceItems() != null && !bo.getInvoiceItems().isEmpty()) {
|
||||
saveInvoiceItems("reimburse", add.getBizId(), bo.getInvoiceItems());
|
||||
}
|
||||
|
||||
if (ok && "pending".equalsIgnoreCase(add.getStatus())) {
|
||||
Long startUserId = LoginHelper.getUserId();
|
||||
|
||||
@@ -105,7 +122,11 @@ public class HrmReimburseReqServiceImpl implements IHrmReimburseReqService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateByBo(HrmReimburseReqBo bo) {
|
||||
HrmReimburseReq update = BeanUtil.toBean(bo, HrmReimburseReq.class);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
boolean updated = baseMapper.updateById(update) > 0;
|
||||
if (updated && bo.getInvoiceItems() != null) {
|
||||
saveInvoiceItems("reimburse", bo.getBizId(), bo.getInvoiceItems());
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -124,6 +145,25 @@ public class HrmReimburseReqServiceImpl implements IHrmReimburseReqService {
|
||||
return lqw;
|
||||
}
|
||||
|
||||
private void saveInvoiceItems(String bizType, Long bizId, List<HrmInvoiceItemBo> boList) {
|
||||
// 先清除旧数据,再插入新数据(更新场景兼容)
|
||||
invoiceItemMapper.delete(Wrappers.<HrmInvoiceItem>lambdaQuery()
|
||||
.eq(HrmInvoiceItem::getBizType, bizType)
|
||||
.eq(HrmInvoiceItem::getBizId, bizId));
|
||||
for (int i = 0; i < boList.size(); i++) {
|
||||
HrmInvoiceItemBo bo = boList.get(i);
|
||||
HrmInvoiceItem item = new HrmInvoiceItem();
|
||||
item.setBizType(bizType);
|
||||
item.setBizId(bizId);
|
||||
item.setOssId(bo.getOssId());
|
||||
item.setSortNo(bo.getSortNo() != null ? bo.getSortNo() : i);
|
||||
item.setItemName(bo.getItemName());
|
||||
item.setReason(bo.getReason());
|
||||
item.setAmount(bo.getAmount());
|
||||
invoiceItemMapper.insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
private String defaultStatus(String status) {
|
||||
return status == null ? "draft" : status;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user