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:
2026-05-08 17:34:08 +08:00
parent 28a37f4105
commit c412f73b80
23 changed files with 1043 additions and 278 deletions

View File

@@ -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 = "";
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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> {
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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;
}
}
}

View File

@@ -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;
}