1、新增百度OCR
2、新增应收应付
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
*/
|
||||
package org.springblade.transport.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 应收应付费用调整请求
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "应收应付费用调整请求")
|
||||
public class ReceivablePayableAdjustFeeRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "应收应付明细ID")
|
||||
private Long detailId;
|
||||
|
||||
@Schema(description = "调整原因")
|
||||
private String adjustReason;
|
||||
|
||||
@Schema(description = "费用调整行")
|
||||
private List<AdjustRow> rows;
|
||||
|
||||
@Data
|
||||
@Schema(description = "费用调整行")
|
||||
public static class AdjustRow implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "费用行ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "运输量")
|
||||
private BigDecimal transportQuantity;
|
||||
|
||||
@Schema(description = "里程")
|
||||
private BigDecimal mileage;
|
||||
|
||||
@Schema(description = "运输费")
|
||||
private BigDecimal freightAmount;
|
||||
|
||||
@Schema(description = "动态费用项目")
|
||||
private Map<String, BigDecimal> feeItems;
|
||||
}
|
||||
}
|
||||
+3
@@ -46,6 +46,9 @@ public class ReceivablePayableGenerateRequest implements Serializable {
|
||||
@Schema(description = "合同ID")
|
||||
private Long contractId;
|
||||
|
||||
@Schema(description = "结算类型:receivable/payable")
|
||||
private String settlementType;
|
||||
|
||||
@Schema(description = "计费方案ID")
|
||||
private String billingPlanId;
|
||||
|
||||
|
||||
+10
@@ -28,6 +28,7 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 应收应付更新费用请求
|
||||
@@ -47,12 +48,21 @@ public class ReceivablePayableUpdateFeeRequest implements Serializable {
|
||||
@Schema(description = "合同ID")
|
||||
private Long contractId;
|
||||
|
||||
@Schema(description = "结算类型:receivable/payable")
|
||||
private String settlementType;
|
||||
|
||||
@Schema(description = "计费方案ID")
|
||||
private String billingPlanId;
|
||||
|
||||
@Schema(description = "调整原因")
|
||||
private String adjustReason;
|
||||
|
||||
@Schema(description = "手工调差金额,可正可负")
|
||||
private BigDecimal adjustAmount;
|
||||
|
||||
@Schema(description = "手工调差费用项")
|
||||
private String adjustFeeItem;
|
||||
|
||||
@Schema(description = "仅关闭")
|
||||
private Boolean closeOnly;
|
||||
|
||||
|
||||
+12
@@ -134,6 +134,9 @@ public class ContractManage extends TenantEntity {
|
||||
@Schema(description = "计费信息开关")
|
||||
private Integer billingEnabled;
|
||||
|
||||
@Schema(description = "费用生成模式:system系统生成,manual手动生成")
|
||||
private String feeGenerationMode;
|
||||
|
||||
@Schema(description = "合同主文件JSON")
|
||||
private String contractFileJson;
|
||||
|
||||
@@ -146,6 +149,15 @@ public class ContractManage extends TenantEntity {
|
||||
@Schema(description = "结算生成规则JSON")
|
||||
private String settlementRuleJson;
|
||||
|
||||
@Schema(description = "预结算配置JSON")
|
||||
private String preSettlementConfigJson;
|
||||
|
||||
@Schema(description = "正式结算配置JSON")
|
||||
private String formalSettlementConfigJson;
|
||||
|
||||
@Schema(description = "付款比例设置JSON")
|
||||
private String paymentRatioJson;
|
||||
|
||||
@Schema(description = "对账配置JSON")
|
||||
private String reconciliationJson;
|
||||
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.core.tenant.mp.TenantEntity;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 保险OCR识别模板实体类。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("blade_insurance_ocr_template")
|
||||
@Schema(description = "保险OCR识别模板")
|
||||
public class InsuranceOcrTemplate extends TenantEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 模板名称。 */
|
||||
@Schema(description = "模板名称")
|
||||
private String name;
|
||||
|
||||
/** 字段映射配置JSON。 */
|
||||
@TableField("mapping_config")
|
||||
@Schema(description = "字段映射配置JSON")
|
||||
private String mappingConfig;
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 百度 OCR 识别结果。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "百度OCR识别结果")
|
||||
public class BaiduOcrResultVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 业务证件类型。 */
|
||||
@Schema(description = "证件类型")
|
||||
private String type;
|
||||
|
||||
/** 正副面,非卡证类型为空。 */
|
||||
@Schema(description = "证件面,front为正面或主页,back为反面或副页")
|
||||
private String side;
|
||||
|
||||
/** 百度 OCR 原始返回结果,包含 words_result 等字段。 */
|
||||
@Schema(description = "百度OCR原始返回结果")
|
||||
private Map<String, Object> result;
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.transport.pojo.entity.InsuranceOcrTemplate;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 保险OCR识别模板视图实体类。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "保险OCR识别模板")
|
||||
public class InsuranceOcrTemplateVO extends InsuranceOcrTemplate {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 创建人姓名。 */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "创建人姓名")
|
||||
private String createUserName;
|
||||
|
||||
/** 更新人姓名。 */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "更新人姓名")
|
||||
private String updateUserName;
|
||||
|
||||
}
|
||||
+21
-20
@@ -11,6 +11,7 @@ import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.log.annotation.ApiLog;
|
||||
import org.springblade.core.tenant.annotation.NonDS;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.file.listener.FileEvent;
|
||||
@@ -57,29 +58,29 @@ public class FileController extends BladeController {
|
||||
@ApiLog("附件管理-批量上传")
|
||||
@Operation(summary = "OBS批量上传", description = "OBS批量上传,参数名:files")
|
||||
@PostMapping("/ossUpload")
|
||||
public R ossUpload(MultipartFile[] files, @RequestParam(value = "fileName", required = false) String fileName) {
|
||||
public FR ossUpload(MultipartFile[] files, @RequestParam(value = "fileName", required = false) String fileName) {
|
||||
return attachmentService.batchOssUpload(files, fileName);
|
||||
}
|
||||
|
||||
@Operation(summary = "上传", description = "上传,参数名:file")
|
||||
@PostMapping("/upload")
|
||||
public R upload(MultipartFile file) {
|
||||
public FR upload(MultipartFile file) {
|
||||
MultipartFile[] files = {file};
|
||||
R<List<Attachment>> result = attachmentService.batchOssUpload(files, null);
|
||||
List<Attachment> data = result.getData();
|
||||
if(data != null && !data.isEmpty())return R.data(data.get(0));
|
||||
return R.fail("上传失败");
|
||||
if(data != null && !data.isEmpty())return FR.data(data.get(0));
|
||||
return FR.fail("上传失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "获取附件,多个附件id用逗号分割", description = "获取附件,多个附件id用逗号分割")
|
||||
@GetMapping("/getAttachment")
|
||||
public R getAttachment(@RequestParam(value = "id") String id) {
|
||||
return R.data(attachmentService.getAttachment(id));
|
||||
public FR getAttachment(@RequestParam(value = "id") String id) {
|
||||
return FR.data(attachmentService.getAttachment(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取文件url", description = "获取文件url,参数:objectKey")
|
||||
@GetMapping("/getFileUrl")
|
||||
public R getFileUrl(@RequestParam(value = "objectKey") String objectKey, @RequestParam(value = "attachmentName", required = false) String attachmentName) {
|
||||
public FR getFileUrl(@RequestParam(value = "objectKey") String objectKey, @RequestParam(value = "attachmentName", required = false) String attachmentName) {
|
||||
if (StringUtil.isBlank(attachmentName)) {
|
||||
// 附件名为空,查询附件名
|
||||
Attachment attachment = attachmentService.getOne(Wrappers.<Attachment>lambdaQuery()
|
||||
@@ -94,7 +95,7 @@ public class FileController extends BladeController {
|
||||
attachmentName = attachmentName.replaceAll(",", "_");
|
||||
}
|
||||
}
|
||||
return R.data(fileService.getFileUrl(objectKey, attachmentName, null));
|
||||
return FR.data(fileService.getFileUrl(objectKey, attachmentName, null));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,9 +105,9 @@ public class FileController extends BladeController {
|
||||
*/
|
||||
@Operation(summary = "获取wps文件预览url", description = "获取wps文件预览url")
|
||||
@GetMapping("/getWpsFilePreviewUrl")
|
||||
public R getWpsFilePreviewUrl(@NotNull(message = "附件id不能为空") @RequestParam(value = "attachmentId", required = false) String attachmentId,
|
||||
public FR getWpsFilePreviewUrl(@NotNull(message = "附件id不能为空") @RequestParam(value = "attachmentId", required = false) String attachmentId,
|
||||
@NotBlank(message = "附件名称不能为空") @RequestParam(value = "attachmentName", required = false) String attachmentName) {
|
||||
return R.data(wpsService.getFilePreviewUrl(attachmentId, attachmentName));
|
||||
return FR.data(wpsService.getFilePreviewUrl(attachmentId, attachmentName));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,41 +118,41 @@ public class FileController extends BladeController {
|
||||
*/
|
||||
@Operation(summary = "获取wps文件编辑url", description = "获取wps文件编辑url")
|
||||
@GetMapping("/getWpsFileEditUrl")
|
||||
public R getWpsFileEditUrl(@NotNull(message = "附件id不能为空") @RequestParam(value = "attachmentId", required = false) String attachmentId,
|
||||
public FR getWpsFileEditUrl(@NotNull(message = "附件id不能为空") @RequestParam(value = "attachmentId", required = false) String attachmentId,
|
||||
@NotBlank(message = "附件名称不能为空") @RequestParam(value = "attachmentName", required = false) String attachmentName) {
|
||||
return R.data(wpsService.getWpsFileEditUrl(attachmentId, attachmentName));
|
||||
return FR.data(wpsService.getWpsFileEditUrl(attachmentId, attachmentName));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量获取文件url", description = "批量获取文件url,参数:objectKey数组")
|
||||
@PostMapping("/getFileUrls")
|
||||
public R getFileUrls(@RequestBody List<String> objectKeys) {
|
||||
return R.data(fileService.getFileUrls(objectKeys, null));
|
||||
public FR getFileUrls(@RequestBody List<String> objectKeys) {
|
||||
return FR.data(fileService.getFileUrls(objectKeys, null));
|
||||
}
|
||||
|
||||
@ApiLog("OCR识别-识别身份证")
|
||||
@Operation(summary = "识别身份证信息支持正反面", description = "参数:url")
|
||||
@GetMapping("/recognitionIDCard")
|
||||
public R recognitionIDCard(@RequestParam(value = "url", required = false) String url,
|
||||
@RequestParam(value = "objectKey", required = false) String objectKey) {
|
||||
public FR recognitionIDCard(@RequestParam(value = "url", required = false) String url,
|
||||
@RequestParam(value = "objectKey", required = false) String objectKey) {
|
||||
String imageUrl = StringUtil.isNotBlank(url) ? url : objectKey;
|
||||
if (StringUtil.isBlank(imageUrl)) {
|
||||
return R.fail("图片地址不能为空");
|
||||
return FR.fail("图片地址不能为空");
|
||||
}
|
||||
if (!imageUrl.startsWith("http://") && !imageUrl.startsWith("https://")) {
|
||||
imageUrl = fileService.getFileUrl(imageUrl, null);
|
||||
}
|
||||
return R.data(ocrService.recognitionIDCard(List.of(imageUrl)));
|
||||
return FR.data(ocrService.recognitionIDCard(List.of(imageUrl)));
|
||||
}
|
||||
|
||||
@SentinelResource("ocr:batchCards")
|
||||
@ApiLog("OCR识别-识别车辆运输凭证(不知道类型)")
|
||||
@Operation(summary = "识别车辆运输凭证,不知道类型", description = "参数:objectKeylist")
|
||||
@PostMapping("/recognitionTransportCertificates")
|
||||
public R<TransportCertificateVO> recognitionTransportCertificates(
|
||||
public FR<TransportCertificateVO> recognitionTransportCertificates(
|
||||
@RequestBody List<Object> attachments,
|
||||
@RequestParam(value = "projectAbbreviation", required = false) String projectAbbreviation,
|
||||
@RequestParam(value = "code", required = false) String code) {
|
||||
return R.data(ocrConvertService.recognitionTransportCertificate(
|
||||
return FR.data(ocrConvertService.recognitionTransportCertificate(
|
||||
buildCertificateBatchRecognitionDTO(attachments, projectAbbreviation, code)
|
||||
));
|
||||
}
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@
|
||||
package org.springblade.file.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.file.pojo.entity.Attachment;
|
||||
import org.springblade.file.pojo.vo.AttachmentDetailVO;
|
||||
@@ -49,7 +50,7 @@ public interface IAttachmentService extends IService<Attachment> {
|
||||
* @param fileName 文件名,如果 files只有1个,且fileName不为空,设置文件名为 fileName
|
||||
* @return
|
||||
*/
|
||||
R<List<Attachment>> batchOssUpload(MultipartFile[] files, String fileName);
|
||||
FR<List<Attachment>> batchOssUpload(MultipartFile[] files, String fileName);
|
||||
|
||||
/**
|
||||
* 获取附件,多个附件id用逗号分割
|
||||
|
||||
+4
-3
@@ -32,6 +32,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.CollectionUtil;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
@@ -78,7 +79,7 @@ public class AttachmentServiceImpl extends ServiceImpl<AttachmentMapper, Attachm
|
||||
private IAttachmentService self;
|
||||
|
||||
@Override
|
||||
public R<List<Attachment>> batchOssUpload(MultipartFile[] files, String overWriteFileName){
|
||||
public FR<List<Attachment>> batchOssUpload(MultipartFile[] files, String overWriteFileName){
|
||||
try {
|
||||
List<Attachment> attachmentList = new ArrayList<>();
|
||||
// 文件数量为1个,且重写的文件名不为空,使用重写的文件名,给uniapp上传使用,uniapp上传的文件名不是原始文件名
|
||||
@@ -98,11 +99,11 @@ public class AttachmentServiceImpl extends ServiceImpl<AttachmentMapper, Attachm
|
||||
}
|
||||
this.saveBatch(attachmentList);
|
||||
log.info("OBS上传 successfully");
|
||||
return R.data(attachmentList,"OBS上传 successfully");
|
||||
return FR.data(attachmentList,"OBS上传 successfully");
|
||||
} catch (Exception e) {
|
||||
log.error("OBS上传 failed,Exception", e);
|
||||
}
|
||||
return R.fail("OBS上传 failed");
|
||||
return FR.fail("OBS上传 failed");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||
import org.springblade.transport.ocr.service.IBaiduOcrService;
|
||||
import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 百度 OCR 控制器。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
@RequestMapping("/baidu-ocr")
|
||||
@Tag(name = "百度OCR", description = "百度OCR证件识别")
|
||||
public class BaiduOcrController extends BladeController {
|
||||
|
||||
private final IBaiduOcrService baiduOcrService;
|
||||
|
||||
/**
|
||||
* 识别上传的图片。
|
||||
*
|
||||
* @param file 图片文件
|
||||
* @param type 证件类型:id_card、business_license、vehicle_license、driving_license、road_transport_certificate、general
|
||||
* @param side 正副面:front或back,适用于身份证、行驶证、驾驶证,默认front
|
||||
* @return OCR结果
|
||||
*/
|
||||
@PostMapping(value = "/recognize", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "上传图片OCR识别", description = "支持身份证、营业执照、行驶证、驾驶证、道路运输证和通用文字识别")
|
||||
public R<BaiduOcrResultVO> recognize(
|
||||
@RequestPart("file") MultipartFile file,
|
||||
@Parameter(description = "OCR证件类型", required = true) @RequestParam String type,
|
||||
@Parameter(description = "正副面:front或back,适用于身份证、行驶证、驾驶证,默认front") @RequestParam(required = false) String side) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("OCR图片不能为空");
|
||||
}
|
||||
BaiduOcrType ocrType = resolveType(type);
|
||||
if (file.getSize() > ocrType.getMaxRawSize()) {
|
||||
throw new ServiceException("OCR图片过大,请压缩后重新上传");
|
||||
}
|
||||
try {
|
||||
return R.data(baiduOcrService.recognize(ocrType, side, file.getBytes()));
|
||||
} catch (IOException exception) {
|
||||
log.error("读取OCR图片失败,type={},size={}", ocrType.name(), file.getSize(), exception);
|
||||
throw new ServiceException("读取OCR图片失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别图片地址。
|
||||
*
|
||||
* @param imageUrl 图片地址
|
||||
* @param type 证件类型
|
||||
* @param side 正副面
|
||||
* @return OCR结果
|
||||
*/
|
||||
@PostMapping("/recognize-url")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "图片地址OCR识别", description = "图片地址必须是百度可访问的HTTP或HTTPS地址")
|
||||
public R<BaiduOcrResultVO> recognizeUrl(
|
||||
@Parameter(description = "图片地址", required = true) @RequestParam String imageUrl,
|
||||
@Parameter(description = "OCR证件类型", required = true) @RequestParam String type,
|
||||
@Parameter(description = "正副面:front或back,适用于身份证、行驶证、驾驶证,默认front") @RequestParam(required = false) String side) {
|
||||
return R.data(baiduOcrService.recognizeUrl(resolveType(type), side, imageUrl));
|
||||
}
|
||||
|
||||
private BaiduOcrType resolveType(String type) {
|
||||
try {
|
||||
return BaiduOcrType.from(type);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new ServiceException(exception.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -136,6 +136,12 @@ public class ContractManageController extends BladeController {
|
||||
return R.status(contractManageService.startChange(id, changeContent, changeReason));
|
||||
}
|
||||
|
||||
@PostMapping("/submit-change")
|
||||
@Operation(summary = "提交合同变更")
|
||||
public R submitChange(@RequestBody ContractManage contractManage) {
|
||||
return R.status(contractManageService.submitChange(contractManage));
|
||||
}
|
||||
|
||||
@PostMapping("/terminate")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "终止合同", description = "传入id和reason")
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.entity.InsuranceOcrTemplate;
|
||||
import org.springblade.transport.pojo.vo.InsuranceOcrTemplateVO;
|
||||
import org.springblade.transport.service.IInsuranceOcrTemplateService;
|
||||
import org.springblade.transport.wrapper.InsuranceOcrTemplateWrapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 保险OCR识别模板控制器。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@PreAuth(menu = "insurance_ocr_template")
|
||||
@RequestMapping("/insurance-ocr-template")
|
||||
@Tag(name = "保险OCR识别模板", description = "保险OCR识别模板")
|
||||
public class InsuranceOcrTemplateController extends BladeController {
|
||||
|
||||
private final IInsuranceOcrTemplateService insuranceOcrTemplateService;
|
||||
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "详情", description = "传入id")
|
||||
public R<InsuranceOcrTemplateVO> detail(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
InsuranceOcrTemplate insuranceOcrTemplate = insuranceOcrTemplateService.getById(id);
|
||||
if (insuranceOcrTemplate == null || insuranceOcrTemplate.getIsDeleted() == 1) {
|
||||
throw new ServiceException("保险OCR识别模板不存在");
|
||||
}
|
||||
return R.data(InsuranceOcrTemplateWrapper.build().entityVO(insuranceOcrTemplate));
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "分页", description = "传入insuranceOcrTemplate")
|
||||
public R<IPage<InsuranceOcrTemplateVO>> list(InsuranceOcrTemplateVO insuranceOcrTemplate, Query query) {
|
||||
return R.data(insuranceOcrTemplateService.selectInsuranceOcrTemplatePage(Condition.getPage(query), insuranceOcrTemplate));
|
||||
}
|
||||
|
||||
@PostMapping("/submit")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "新增或修改", description = "传入insuranceOcrTemplate")
|
||||
public R submit(@RequestBody InsuranceOcrTemplate insuranceOcrTemplate) {
|
||||
return R.status(insuranceOcrTemplateService.submit(insuranceOcrTemplate));
|
||||
}
|
||||
|
||||
@PostMapping("/remove")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "逻辑删除", description = "传入ids")
|
||||
public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
|
||||
return R.status(insuranceOcrTemplateService.deleteLogic(Func.toLongList(ids)));
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -166,7 +166,7 @@ public class InsuranceRecordController extends BladeController {
|
||||
*/
|
||||
@PostMapping("/recognize")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "OCR识别保单", description = "上传保单图片或PDF")
|
||||
@Operation(summary = "OCR识别保单", description = "上传保单图片")
|
||||
public R<InsuranceRecord> recognize(MultipartFile file,
|
||||
@RequestParam(required = false) String vehicleType,
|
||||
@RequestParam(required = false) String ocrTemplate) {
|
||||
|
||||
+19
-1
@@ -35,6 +35,7 @@ import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest;
|
||||
@@ -49,6 +50,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -94,8 +96,24 @@ public class ReceivablePayableDetailController extends BladeController {
|
||||
return R.success("更新成功");
|
||||
}
|
||||
|
||||
@GetMapping("/transfer-candidates")
|
||||
@GetMapping("/update-fee-contracts")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "更新费用可选合同")
|
||||
public R<List<Map<String, Object>>> updateFeeContracts(
|
||||
@RequestParam(required = false) String settlementType) {
|
||||
return R.data(detailService.updateFeeContracts(settlementType));
|
||||
}
|
||||
|
||||
@PostMapping("/adjust-fee")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "调整费用")
|
||||
public R adjustFee(@RequestBody ReceivablePayableAdjustFeeRequest request) {
|
||||
detailService.adjustFee(request);
|
||||
return R.success("保存成功");
|
||||
}
|
||||
|
||||
@GetMapping("/transfer-candidates")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "转结算候选明细")
|
||||
public R<IPage<Map<String, Object>>> transferCandidates(Query query,
|
||||
@RequestParam(required = false) String contractName,
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.springblade.transport.pojo.entity.InsuranceOcrTemplate;
|
||||
|
||||
/**
|
||||
* 保险OCR识别模板Mapper接口。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface InsuranceOcrTemplateMapper extends BaseMapper<InsuranceOcrTemplate> {
|
||||
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.ocr.config;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
|
||||
/**
|
||||
* 百度 OCR HTTP 客户端配置。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(BaiduOcrProperties.class)
|
||||
public class BaiduOcrConfiguration {
|
||||
|
||||
/**
|
||||
* 创建百度 OCR HTTP 客户端。
|
||||
*
|
||||
* @param properties 百度 OCR 配置
|
||||
* @return HTTP 客户端
|
||||
*/
|
||||
@Bean(name = "baiduOcrHttpClient")
|
||||
public HttpClient baiduOcrHttpClient(BaiduOcrProperties properties) {
|
||||
return HttpClient.newBuilder()
|
||||
.connectTimeout(properties.getConnectTimeout())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.ocr.config;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 百度 OCR 配置。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "baidu.ocr")
|
||||
public class BaiduOcrProperties {
|
||||
|
||||
/**
|
||||
* 是否启用百度 OCR。
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* 百度智能云应用 API Key。
|
||||
*/
|
||||
@ToString.Exclude
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* 百度智能云应用 Secret Key。
|
||||
*/
|
||||
@ToString.Exclude
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* 百度 OCR 服务地址。
|
||||
*/
|
||||
private String endpoint = "https://aip.baidubce.com";
|
||||
|
||||
/**
|
||||
* 建立百度接口连接的超时时间。
|
||||
*/
|
||||
private Duration connectTimeout = Duration.ofSeconds(5);
|
||||
|
||||
/**
|
||||
* 百度接口请求超时时间。
|
||||
*/
|
||||
private Duration requestTimeout = Duration.ofSeconds(30);
|
||||
|
||||
/**
|
||||
* 提前刷新 access_token 的时间。
|
||||
*/
|
||||
private Duration tokenRefreshAdvance = Duration.ofMinutes(1);
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.ocr.constant;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 百度 OCR 支持的证件类型。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum BaiduOcrType {
|
||||
|
||||
/** 身份证。 */
|
||||
ID_CARD("身份证", "/rest/2.0/ocr/v1/idcard", "id_card_side", 8 * 1024 * 1024, 8192),
|
||||
/** 营业执照。 */
|
||||
BUSINESS_LICENSE("营业执照", "/rest/2.0/ocr/v1/business_license", null, 10 * 1024 * 1024, 8192),
|
||||
/** 行驶证。 */
|
||||
VEHICLE_LICENSE("行驶证", "/rest/2.0/ocr/v1/vehicle_license", "vehicle_license_side", 4 * 1024 * 1024, 4096),
|
||||
/** 驾驶证。 */
|
||||
DRIVING_LICENSE("驾驶证", "/rest/2.0/ocr/v1/driving_license", "driving_license_side", 4 * 1024 * 1024, 4096),
|
||||
/** 道路运输证。 */
|
||||
ROAD_TRANSPORT_CERTIFICATE("道路运输证", "/rest/2.0/ocr/v1/road_transport_certificate", null, 4 * 1024 * 1024, 4096),
|
||||
/** 通用文字识别(标准版)。 */
|
||||
GENERAL("通用证件", "/rest/2.0/ocr/v1/general_basic", null, 8 * 1024 * 1024, 4096);
|
||||
|
||||
private final String description;
|
||||
private final String path;
|
||||
private final String sideParameter;
|
||||
private final int maxEncodedSize;
|
||||
private final int maxDimension;
|
||||
|
||||
/**
|
||||
* 将请求参数转换为证件类型。
|
||||
*
|
||||
* @param value 类型名称、枚举名或常用别名
|
||||
* @return 证件类型
|
||||
*/
|
||||
public static BaiduOcrType from(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("OCR证件类型不能为空");
|
||||
}
|
||||
String normalized = value.trim().replace('-', '_').replace(' ', '_').toUpperCase(Locale.ROOT);
|
||||
return switch (normalized) {
|
||||
case "ID_CARD", "IDCARD", "ID" -> ID_CARD;
|
||||
case "BUSINESS_LICENSE", "BUSINESSLICENSE", "LICENSE", "营业执照" -> BUSINESS_LICENSE;
|
||||
case "VEHICLE_LICENSE", "VEHICLELICENSE", "DRIVING_VEHICLE", "行驶证" -> VEHICLE_LICENSE;
|
||||
case "DRIVING_LICENSE", "DRIVINGLICENSE", "驾驶证" -> DRIVING_LICENSE;
|
||||
case "ROAD_TRANSPORT_CERTIFICATE", "ROADTRANSPORTCERTIFICATE", "ROAD_TRANSPORT", "道路运输证" -> ROAD_TRANSPORT_CERTIFICATE;
|
||||
case "GENERAL", "GENERAL_BASIC", "COMMON", "COMMON_CARD", "通用证件", "通用文字识别" -> GENERAL;
|
||||
default -> throw new IllegalArgumentException("不支持的OCR证件类型:" + value);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断该类型是否支持正副面参数。
|
||||
*
|
||||
* @return 是否支持正副面
|
||||
*/
|
||||
public boolean supportsSide() {
|
||||
return sideParameter != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上传图片原始大小的理论上限。
|
||||
*
|
||||
* @return 原始大小上限
|
||||
*/
|
||||
public long getMaxRawSize() {
|
||||
return (long) maxEncodedSize * 3 / 4;
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.ocr.service;
|
||||
|
||||
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||
import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
|
||||
|
||||
/**
|
||||
* 百度 OCR 服务。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface IBaiduOcrService {
|
||||
|
||||
/**
|
||||
* 识别上传的图片。
|
||||
*
|
||||
* @param type 证件类型
|
||||
* @param side 正副面
|
||||
* @param image 图片二进制
|
||||
* @return 识别结果
|
||||
*/
|
||||
BaiduOcrResultVO recognize(BaiduOcrType type, String side, byte[] image);
|
||||
|
||||
/**
|
||||
* 识别图片地址。
|
||||
*
|
||||
* @param type 证件类型
|
||||
* @param side 正副面
|
||||
* @param imageUrl 图片地址
|
||||
* @return 识别结果
|
||||
*/
|
||||
BaiduOcrResultVO recognizeUrl(BaiduOcrType type, String side, String imageUrl);
|
||||
}
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.ocr.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.transport.ocr.config.BaiduOcrProperties;
|
||||
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||
import org.springblade.transport.ocr.service.IBaiduOcrService;
|
||||
import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 百度 OCR 服务实现。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BaiduOcrServiceImpl implements IBaiduOcrService {
|
||||
|
||||
private static final String TOKEN_PATH = "/oauth/2.0/token";
|
||||
private static final String TOKEN_GRANT_TYPE = "client_credentials";
|
||||
private static final int ACCESS_TOKEN_INVALID = 110;
|
||||
private static final int ACCESS_TOKEN_EXPIRED = 111;
|
||||
private static final int MIN_IMAGE_DIMENSION = 15;
|
||||
private static final Set<String> SUPPORTED_IMAGE_FORMATS = Set.of("JPEG", "JPG", "PNG", "BMP");
|
||||
private static final TypeReference<Map<String, Object>> RESULT_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
private final BaiduOcrProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HttpClient httpClient;
|
||||
private final Object tokenMonitor = new Object();
|
||||
|
||||
private volatile AccessToken accessToken;
|
||||
|
||||
@Override
|
||||
public BaiduOcrResultVO recognize(BaiduOcrType type, String side, byte[] image) {
|
||||
validateType(type);
|
||||
if (image == null || image.length == 0) {
|
||||
throw new ServiceException("OCR图片不能为空");
|
||||
}
|
||||
if (image.length > type.getMaxRawSize()) {
|
||||
throw new ServiceException("OCR图片过大,请压缩后重新上传");
|
||||
}
|
||||
validateImage(type, image);
|
||||
String imageBase64 = Base64.getEncoder().encodeToString(image);
|
||||
String encodedImage = URLEncoder.encode(imageBase64, StandardCharsets.UTF_8);
|
||||
if (encodedImage.length() > type.getMaxEncodedSize()) {
|
||||
throw new ServiceException(type.getDescription() + "图片经Base64和URL编码后不能超过"
|
||||
+ (type.getMaxEncodedSize() / 1024 / 1024) + "M");
|
||||
}
|
||||
Map<String, String> form = new LinkedHashMap<>();
|
||||
form.put("image", imageBase64);
|
||||
return request(type, side, form);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaiduOcrResultVO recognizeUrl(BaiduOcrType type, String side, String imageUrl) {
|
||||
validateType(type);
|
||||
if (StringUtil.isBlank(imageUrl)) {
|
||||
throw new ServiceException("OCR图片地址不能为空");
|
||||
}
|
||||
String normalizedUrl = imageUrl.trim();
|
||||
validateImageUrl(normalizedUrl);
|
||||
Map<String, String> form = new LinkedHashMap<>();
|
||||
form.put("url", normalizedUrl);
|
||||
return request(type, side, form);
|
||||
}
|
||||
|
||||
private BaiduOcrResultVO request(BaiduOcrType type, String side, Map<String, String> form) {
|
||||
validateType(type);
|
||||
if (!properties.isEnabled()) {
|
||||
throw new ServiceException("百度OCR服务未启用");
|
||||
}
|
||||
if (StringUtil.isBlank(properties.getApiKey()) || StringUtil.isBlank(properties.getSecretKey())) {
|
||||
throw new ServiceException("百度OCR的API Key和Secret Key未配置");
|
||||
}
|
||||
String normalizedSide = normalizeSide(type, side);
|
||||
if (normalizedSide != null) {
|
||||
form.put(type.getSideParameter(), normalizedSide);
|
||||
}
|
||||
JsonNode result = sendOcrRequest(type, form);
|
||||
int errorCode = result.path("error_code").asInt(0);
|
||||
if (errorCode != 0) {
|
||||
String message = result.path("error_msg").asText("未知错误");
|
||||
log.warn("百度OCR识别失败,type={},side={},errorCode={},logId={}",
|
||||
type.name(), normalizedSide, errorCode, result.path("log_id").asText(""));
|
||||
throw new ServiceException("百度OCR识别失败(" + errorCode + "):" + message);
|
||||
}
|
||||
BaiduOcrResultVO response = new BaiduOcrResultVO();
|
||||
response.setType(type.name());
|
||||
response.setSide(normalizedSide);
|
||||
response.setResult(objectMapper.convertValue(result, RESULT_TYPE));
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonNode sendOcrRequest(BaiduOcrType type, Map<String, String> form) {
|
||||
String token = getAccessToken();
|
||||
JsonNode result = executeOcrRequest(type, form, token);
|
||||
if (isAccessTokenInvalid(result)) {
|
||||
invalidateAccessToken(token);
|
||||
log.warn("百度OCR access_token 已失效,重新获取后重试,type={},errorCode={}",
|
||||
type.name(), result.path("error_code").asInt());
|
||||
result = executeOcrRequest(type, form, getAccessToken());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonNode executeOcrRequest(BaiduOcrType type, Map<String, String> form, String token) {
|
||||
try {
|
||||
URI requestUri = URI.create(buildEndpoint(type.getPath()) + "?access_token="
|
||||
+ URLEncoder.encode(token, StandardCharsets.UTF_8));
|
||||
HttpRequest request = HttpRequest.newBuilder(requestUri)
|
||||
.timeout(properties.getRequestTimeout())
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(toFormBody(form), StandardCharsets.UTF_8))
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
log.error("百度OCR接口响应异常,type={}, status={}", type.name(), response.statusCode());
|
||||
throw new ServiceException("百度OCR接口调用失败,HTTP状态码:" + response.statusCode());
|
||||
}
|
||||
JsonNode result = objectMapper.readTree(response.body());
|
||||
if (result == null || !result.isObject()) {
|
||||
throw new ServiceException("百度OCR响应格式异常");
|
||||
}
|
||||
return result;
|
||||
} catch (ServiceException exception) {
|
||||
throw exception;
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("调用百度OCR接口被中断,type={}", type.name(), exception);
|
||||
throw new ServiceException("百度OCR接口调用被中断");
|
||||
} catch (IllegalArgumentException exception) {
|
||||
log.error("百度OCR接口地址配置不正确,type={}", type.name());
|
||||
throw new ServiceException("百度OCR接口地址配置不正确");
|
||||
} catch (IOException exception) {
|
||||
log.error("调用百度OCR接口失败,type={}", type.name(), exception);
|
||||
throw new ServiceException("百度OCR接口调用失败");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAccessTokenInvalid(JsonNode result) {
|
||||
int errorCode = result.path("error_code").asInt(0);
|
||||
return errorCode == ACCESS_TOKEN_INVALID || errorCode == ACCESS_TOKEN_EXPIRED;
|
||||
}
|
||||
|
||||
private void invalidateAccessToken(String rejectedToken) {
|
||||
synchronized (tokenMonitor) {
|
||||
if (accessToken != null && Objects.equals(accessToken.value(), rejectedToken)) {
|
||||
accessToken = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getAccessToken() {
|
||||
AccessToken currentToken = accessToken;
|
||||
if (currentToken != null && currentToken.isValid(properties.getTokenRefreshAdvance())) {
|
||||
return currentToken.value();
|
||||
}
|
||||
synchronized (tokenMonitor) {
|
||||
currentToken = accessToken;
|
||||
if (currentToken != null && currentToken.isValid(properties.getTokenRefreshAdvance())) {
|
||||
return currentToken.value();
|
||||
}
|
||||
return requestAccessToken();
|
||||
}
|
||||
}
|
||||
|
||||
private String requestAccessToken() {
|
||||
try {
|
||||
String query = "grant_type=" + URLEncoder.encode(TOKEN_GRANT_TYPE, StandardCharsets.UTF_8)
|
||||
+ "&client_id=" + URLEncoder.encode(properties.getApiKey(), StandardCharsets.UTF_8)
|
||||
+ "&client_secret=" + URLEncoder.encode(properties.getSecretKey(), StandardCharsets.UTF_8);
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(buildEndpoint(TOKEN_PATH) + "?" + query))
|
||||
.timeout(properties.getRequestTimeout())
|
||||
.header("Accept", "application/json")
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
JsonNode result = objectMapper.readTree(response.body());
|
||||
if (result == null || !result.isObject()) {
|
||||
throw new ServiceException("百度OCR鉴权响应格式异常");
|
||||
}
|
||||
String token = result.path("access_token").asText(null);
|
||||
long expiresIn = result.path("expires_in").asLong(0);
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300 || StringUtil.isBlank(token) || expiresIn <= 0) {
|
||||
String error = result.path("error").asText("");
|
||||
String message = result.path("error_description").asText("未知错误");
|
||||
log.error("百度OCR鉴权失败,status={},error={}", response.statusCode(), error);
|
||||
throw new ServiceException("百度OCR鉴权失败:" + message);
|
||||
}
|
||||
accessToken = new AccessToken(token, Instant.now().plusSeconds(expiresIn));
|
||||
return token;
|
||||
} catch (ServiceException exception) {
|
||||
throw exception;
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ServiceException("百度OCR鉴权请求被中断");
|
||||
} catch (IllegalArgumentException exception) {
|
||||
log.error("百度OCR鉴权地址配置不正确");
|
||||
throw new ServiceException("百度OCR鉴权地址配置不正确");
|
||||
} catch (IOException exception) {
|
||||
log.error("获取百度OCR access_token 失败", exception);
|
||||
throw new ServiceException("百度OCR鉴权失败");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeSide(BaiduOcrType type, String side) {
|
||||
if (StringUtil.isBlank(side)) {
|
||||
return type.supportsSide() ? "front" : null;
|
||||
}
|
||||
if (!type.supportsSide()) {
|
||||
throw new ServiceException(type.getDescription() + "不支持正副面参数");
|
||||
}
|
||||
String normalized = side.trim().toLowerCase(Locale.ROOT);
|
||||
if ("front".equals(normalized) || "main".equals(normalized) || "正面".equals(normalized) || "主页".equals(normalized)) {
|
||||
return "front";
|
||||
}
|
||||
if ("back".equals(normalized) || "side".equals(normalized) || "副面".equals(normalized) || "副页".equals(normalized) || "反面".equals(normalized)) {
|
||||
return "back";
|
||||
}
|
||||
throw new ServiceException("OCR证件面参数只能是front或back");
|
||||
}
|
||||
|
||||
private void validateType(BaiduOcrType type) {
|
||||
if (type == null) {
|
||||
throw new ServiceException("OCR证件类型不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateImageUrl(String imageUrl) {
|
||||
if (imageUrl.getBytes(StandardCharsets.UTF_8).length > 1024) {
|
||||
throw new ServiceException("OCR图片地址长度不能超过1024字节");
|
||||
}
|
||||
try {
|
||||
URI imageUri = URI.create(imageUrl);
|
||||
String scheme = imageUri.getScheme();
|
||||
if (StringUtil.isBlank(scheme) || StringUtil.isBlank(imageUri.getHost())
|
||||
|| imageUri.getUserInfo() != null
|
||||
|| !("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme))) {
|
||||
throw new ServiceException("OCR图片地址必须是有效的HTTP或HTTPS地址");
|
||||
}
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new ServiceException("OCR图片地址格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateImage(BaiduOcrType type, byte[] image) {
|
||||
try (ImageInputStream imageInputStream = ImageIO.createImageInputStream(new ByteArrayInputStream(image))) {
|
||||
if (imageInputStream == null) {
|
||||
throw new ServiceException("OCR图片格式不正确");
|
||||
}
|
||||
Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream);
|
||||
if (!imageReaders.hasNext()) {
|
||||
throw new ServiceException("OCR仅支持JPG、JPEG、PNG、BMP图片");
|
||||
}
|
||||
ImageReader imageReader = imageReaders.next();
|
||||
try {
|
||||
imageReader.setInput(imageInputStream, true, true);
|
||||
String formatName = imageReader.getFormatName().toUpperCase(Locale.ROOT);
|
||||
if (!SUPPORTED_IMAGE_FORMATS.contains(formatName)) {
|
||||
throw new ServiceException("OCR仅支持JPG、JPEG、PNG、BMP图片");
|
||||
}
|
||||
int width = imageReader.getWidth(0);
|
||||
int height = imageReader.getHeight(0);
|
||||
if (Math.min(width, height) < MIN_IMAGE_DIMENSION || Math.max(width, height) > type.getMaxDimension()) {
|
||||
throw new ServiceException(type.getDescription() + "图片最短边不能小于15px,最长边不能超过"
|
||||
+ type.getMaxDimension() + "px");
|
||||
}
|
||||
} finally {
|
||||
imageReader.dispose();
|
||||
}
|
||||
} catch (ServiceException exception) {
|
||||
throw exception;
|
||||
} catch (IOException exception) {
|
||||
log.error("解析OCR图片失败,type={},size={}", type.name(), image.length, exception);
|
||||
throw new ServiceException("OCR图片格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private String toFormBody(Map<String, String> form) {
|
||||
return form.entrySet().stream()
|
||||
.map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "="
|
||||
+ URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8))
|
||||
.reduce((left, right) -> left + "&" + right)
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
private String buildEndpoint(String path) {
|
||||
String endpoint = properties.getEndpoint();
|
||||
if (StringUtil.isBlank(endpoint)) {
|
||||
throw new ServiceException("百度OCR服务地址未配置");
|
||||
}
|
||||
return endpoint.replaceAll("/+$", "") + path;
|
||||
}
|
||||
|
||||
private record AccessToken(String value, Instant expiresAt) {
|
||||
private boolean isValid(Duration advance) {
|
||||
Duration refreshAdvance = advance == null || advance.isNegative() ? Duration.ZERO : advance;
|
||||
return StringUtil.isNotBlank(value) && expiresAt.isAfter(Instant.now().plus(refreshAdvance));
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -48,6 +48,7 @@ public interface IContractManageService extends BaseService<ContractManage> {
|
||||
boolean reject(Long id);
|
||||
boolean withdraw(Long id);
|
||||
boolean startChange(Long id, String changeContent, String changeReason);
|
||||
boolean submitChange(ContractManage contractManage);
|
||||
boolean updateAttachments(ContractManage contractManage);
|
||||
boolean terminate(Long id, String reason);
|
||||
boolean removeDraft(String ids);
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import org.springblade.core.mp.base.BaseService;
|
||||
import org.springblade.transport.pojo.entity.InsuranceOcrTemplate;
|
||||
import org.springblade.transport.pojo.vo.InsuranceOcrTemplateVO;
|
||||
|
||||
/**
|
||||
* 保险OCR识别模板服务类。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface IInsuranceOcrTemplateService extends BaseService<InsuranceOcrTemplate> {
|
||||
|
||||
/**
|
||||
* 分页查询模板。
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param insuranceOcrTemplate 查询条件
|
||||
* @return 模板分页
|
||||
*/
|
||||
IPage<InsuranceOcrTemplateVO> selectInsuranceOcrTemplatePage(IPage<InsuranceOcrTemplate> page, InsuranceOcrTemplateVO insuranceOcrTemplate);
|
||||
|
||||
/**
|
||||
* 保存模板。
|
||||
*
|
||||
* @param insuranceOcrTemplate 模板
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean submit(InsuranceOcrTemplate insuranceOcrTemplate);
|
||||
|
||||
}
|
||||
+9
@@ -24,6 +24,7 @@ package org.springblade.transport.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import org.springblade.core.mp.base.BaseService;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest;
|
||||
@@ -32,6 +33,7 @@ import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO;
|
||||
import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO;
|
||||
import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -49,6 +51,10 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
|
||||
|
||||
void updateFee(ReceivablePayableUpdateFeeRequest request);
|
||||
|
||||
List<Map<String, Object>> updateFeeContracts(String settlementType);
|
||||
|
||||
void adjustFee(ReceivablePayableAdjustFeeRequest request);
|
||||
|
||||
void transferSettlement(ReceivablePayableTransferRequest request);
|
||||
|
||||
IPage<Map<String, Object>> transferCandidates(IPage<?> page, String contractName, String batchNo,
|
||||
@@ -59,4 +65,7 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
|
||||
ReceivablePayableFeeDetailVO generatePreview(IPage<?> page, ReceivablePayableGenerateRequest request);
|
||||
|
||||
void generateFee(ReceivablePayableGenerateRequest request);
|
||||
|
||||
/** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */
|
||||
void generateForCompletedWaybills(List<Long> waybillIds);
|
||||
}
|
||||
|
||||
+44
@@ -44,6 +44,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -205,6 +206,25 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
return updateById(contractManage);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submitChange(ContractManage request) {
|
||||
ContractManage source = loadExists(request.getId());
|
||||
if (!Objects.equals(source.getContractStage(), STAGE_FORMAL)
|
||||
|| !List.of(STATUS_APPROVED, STATUS_CHANGE_APPROVED, STATUS_CHANGE_REJECTED).contains(source.getApprovalStatus())) {
|
||||
throw new ServiceException("当前合同状态不允许发起变更");
|
||||
}
|
||||
validatePaymentRatios(request.getPaymentRatioJson());
|
||||
source.setContractName(request.getContractName()); source.setPartyB(request.getPartyB());
|
||||
source.setStartDate(request.getStartDate()); source.setEndDate(request.getEndDate()); source.setContractFormat(request.getContractFormat());
|
||||
source.setSettlementMode(request.getSettlementMode()); source.setLegalSealFlag(request.getLegalSealFlag()); source.setCopyCount(request.getCopyCount());
|
||||
source.setPaymentDays(request.getPaymentDays()); source.setRemark(request.getRemark()); source.setBillingEnabled(request.getBillingEnabled()); source.setFeeGenerationMode(request.getFeeGenerationMode());
|
||||
source.setBillingPlanJson(request.getBillingPlanJson()); source.setSettlementRuleJson(request.getSettlementRuleJson()); source.setPreSettlementConfigJson(request.getPreSettlementConfigJson()); source.setFormalSettlementConfigJson(request.getFormalSettlementConfigJson()); source.setPaymentRatioJson(request.getPaymentRatioJson());
|
||||
source.setContractFileJson(request.getContractFileJson()); source.setAttachmentsJson(request.getAttachmentsJson());
|
||||
updateById(source);
|
||||
return startChange(source.getId(), request.getChangeContent(), request.getChangeReason());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateAttachments(ContractManage contractManage) {
|
||||
@@ -414,6 +434,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
contractManage.setAttachmentsJson(TransportBusinessSupport.trimToNull(contractManage.getAttachmentsJson()));
|
||||
contractManage.setBillingPlanJson(TransportBusinessSupport.trimToNull(contractManage.getBillingPlanJson()));
|
||||
contractManage.setSettlementRuleJson(TransportBusinessSupport.trimToNull(contractManage.getSettlementRuleJson()));
|
||||
contractManage.setPaymentRatioJson(TransportBusinessSupport.trimToNull(contractManage.getPaymentRatioJson()));
|
||||
contractManage.setReconciliationJson(TransportBusinessSupport.trimToNull(contractManage.getReconciliationJson()));
|
||||
contractManage.setChangeRecordJson(TransportBusinessSupport.trimToNull(contractManage.getChangeRecordJson()));
|
||||
normalizeOptionalIntegerFields(contractManage);
|
||||
@@ -433,6 +454,29 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
TransportBusinessSupport.validateRequired(contractManage.getContractCategory(), "请选择合同类别");
|
||||
validateContractNameUnique(contractManage);
|
||||
TransportBusinessSupport.validateLength(contractManage.getRemark(), 2000, "备注不能超过2000字");
|
||||
validatePaymentRatios(contractManage.getPaymentRatioJson());
|
||||
}
|
||||
|
||||
private void validatePaymentRatios(String paymentRatioJson) {
|
||||
if (Func.isEmpty(paymentRatioJson)) return;
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(paymentRatioJson, List.class);
|
||||
if (!(parsed instanceof List<?> rows) || rows.isEmpty()) return;
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (Object row : rows) {
|
||||
if (row instanceof Map<?, ?> values && values.get("ratioLimit") != null
|
||||
&& !String.valueOf(values.get("ratioLimit")).isBlank()) {
|
||||
total = total.add(new BigDecimal(String.valueOf(values.get("ratioLimit"))));
|
||||
}
|
||||
}
|
||||
if (total.compareTo(BigDecimal.valueOf(100)) != 0) {
|
||||
throw new ServiceException("付款比例上限合计必须等于100%");
|
||||
}
|
||||
} catch (ServiceException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("付款比例设置格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateContractNameUnique(ContractManage contractManage) {
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.transport.mapper.InsuranceOcrTemplateMapper;
|
||||
import org.springblade.transport.pojo.entity.InsuranceOcrTemplate;
|
||||
import org.springblade.transport.pojo.vo.InsuranceOcrTemplateVO;
|
||||
import org.springblade.transport.service.IInsuranceOcrTemplateService;
|
||||
import org.springblade.transport.wrapper.InsuranceOcrTemplateWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 保险OCR识别模板服务实现类。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Service
|
||||
public class InsuranceOcrTemplateServiceImpl extends BaseServiceImpl<InsuranceOcrTemplateMapper, InsuranceOcrTemplate> implements IInsuranceOcrTemplateService {
|
||||
|
||||
private static final int NAME_MAX_LENGTH = 100;
|
||||
private static final int MAPPING_VALUE_MAX_LENGTH = 100;
|
||||
private static final List<String> INSURANCE_FIELD_KEYS = List.of(
|
||||
"保险类型", "保单号", "开始日期", "结束日期", "保额", "保费", "发票号", "开票日期", "备注"
|
||||
);
|
||||
|
||||
@Override
|
||||
public IPage<InsuranceOcrTemplateVO> selectInsuranceOcrTemplatePage(IPage<InsuranceOcrTemplate> page, InsuranceOcrTemplateVO insuranceOcrTemplate) {
|
||||
LambdaQueryWrapper<InsuranceOcrTemplate> queryWrapper = Wrappers.<InsuranceOcrTemplate>lambdaQuery()
|
||||
.eq(InsuranceOcrTemplate::getIsDeleted, 0)
|
||||
.like(StringUtil.isNotBlank(insuranceOcrTemplate.getName()), InsuranceOcrTemplate::getName, insuranceOcrTemplate.getName())
|
||||
.orderByDesc(InsuranceOcrTemplate::getCreateTime);
|
||||
return InsuranceOcrTemplateWrapper.build().pageVO(page(page, queryWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean submit(InsuranceOcrTemplate insuranceOcrTemplate) {
|
||||
boolean created = insuranceOcrTemplate.getId() == null;
|
||||
if (!created) {
|
||||
InsuranceOcrTemplate oldTemplate = getById(insuranceOcrTemplate.getId());
|
||||
if (oldTemplate == null || oldTemplate.getIsDeleted() == 1) {
|
||||
throw new ServiceException("保险OCR识别模板不存在");
|
||||
}
|
||||
insuranceOcrTemplate.setTenantId(oldTemplate.getTenantId());
|
||||
}
|
||||
insuranceOcrTemplate.setName(trimToNull(insuranceOcrTemplate.getName()));
|
||||
insuranceOcrTemplate.setMappingConfig(trimToNull(insuranceOcrTemplate.getMappingConfig()));
|
||||
if (created && insuranceOcrTemplate.getStatus() == null) {
|
||||
insuranceOcrTemplate.setStatus(1);
|
||||
}
|
||||
validate(insuranceOcrTemplate);
|
||||
return saveOrUpdate(insuranceOcrTemplate);
|
||||
}
|
||||
|
||||
private void validate(InsuranceOcrTemplate insuranceOcrTemplate) {
|
||||
if (StringUtil.isBlank(insuranceOcrTemplate.getName())) {
|
||||
throw new ServiceException("模板名称不能为空");
|
||||
}
|
||||
if (insuranceOcrTemplate.getName().length() > NAME_MAX_LENGTH) {
|
||||
throw new ServiceException("模板名称不能超过100个字符");
|
||||
}
|
||||
Long nameCount = count(Wrappers.<InsuranceOcrTemplate>lambdaQuery()
|
||||
.eq(InsuranceOcrTemplate::getIsDeleted, 0)
|
||||
.eq(InsuranceOcrTemplate::getName, insuranceOcrTemplate.getName())
|
||||
.ne(insuranceOcrTemplate.getId() != null, InsuranceOcrTemplate::getId, insuranceOcrTemplate.getId()));
|
||||
if (nameCount > 0) {
|
||||
throw new ServiceException("模板名称已存在");
|
||||
}
|
||||
validateMappingConfig(insuranceOcrTemplate.getMappingConfig());
|
||||
}
|
||||
|
||||
private void validateMappingConfig(String mappingConfig) {
|
||||
if (StringUtil.isBlank(mappingConfig)) {
|
||||
throw new ServiceException("字段映射配置不能为空");
|
||||
}
|
||||
JSONArray mappingArray;
|
||||
try {
|
||||
mappingArray = JSON.parseArray(mappingConfig);
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("字段映射配置格式不正确");
|
||||
}
|
||||
if (mappingArray == null || mappingArray.size() != INSURANCE_FIELD_KEYS.size()) {
|
||||
throw new ServiceException("字段映射配置必须包含全部保险字段");
|
||||
}
|
||||
Set<String> mappingKeySet = new LinkedHashSet<>();
|
||||
boolean hasMappingValue = false;
|
||||
for (Object item : mappingArray) {
|
||||
if (!(item instanceof JSONObject mapping)) {
|
||||
throw new ServiceException("字段映射配置格式不正确");
|
||||
}
|
||||
String key = trimToNull(mapping.getString("key"));
|
||||
String value = trimToNull(mapping.getString("value"));
|
||||
if (!INSURANCE_FIELD_KEYS.contains(key)) {
|
||||
throw new ServiceException("字段映射包含不支持的键名");
|
||||
}
|
||||
if (!mappingKeySet.add(key)) {
|
||||
throw new ServiceException("字段映射键名不能重复");
|
||||
}
|
||||
if (value != null && value.length() > MAPPING_VALUE_MAX_LENGTH) {
|
||||
throw new ServiceException("字段映射值不能超过100个字符");
|
||||
}
|
||||
hasMappingValue = hasMappingValue || value != null;
|
||||
}
|
||||
if (!mappingKeySet.containsAll(INSURANCE_FIELD_KEYS)) {
|
||||
throw new ServiceException("字段映射配置必须包含全部保险字段");
|
||||
}
|
||||
if (!hasMappingValue) {
|
||||
throw new ServiceException("请至少填写一个字段映射值");
|
||||
}
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
}
|
||||
+147
-5
@@ -25,9 +25,13 @@
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
@@ -36,18 +40,30 @@ import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.excel.InsuranceRecordExcel;
|
||||
import org.springblade.transport.excel.InsuranceRecordExportExcel;
|
||||
import org.springblade.transport.mapper.InsuranceRecordMapper;
|
||||
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||
import org.springblade.transport.ocr.service.IBaiduOcrService;
|
||||
import org.springblade.transport.pojo.entity.InsuranceRecord;
|
||||
import org.springblade.transport.pojo.entity.InsuranceOcrTemplate;
|
||||
import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
|
||||
import org.springblade.transport.pojo.vo.InsuranceRecordVO;
|
||||
import org.springblade.transport.service.IInsuranceOcrTemplateService;
|
||||
import org.springblade.transport.service.IInsuranceRecordService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 保险记录 服务实现类
|
||||
@@ -55,6 +71,7 @@ import java.util.Set;
|
||||
* @author Chill
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordMapper, InsuranceRecord> implements IInsuranceRecordService {
|
||||
|
||||
private static final int VEHICLE_NO_MAX_LENGTH = 50;
|
||||
@@ -64,7 +81,11 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
|
||||
private static final int OCR_TEMPLATE_MAX_LENGTH = 100;
|
||||
private static final int POLICY_FILE_MAX_LENGTH = 1000;
|
||||
private static final int REMARK_MAX_LENGTH = 200;
|
||||
private static final Set<String> SUPPORT_FILE_TYPES = Set.of("jpg", "jpeg", "png", "pdf");
|
||||
private static final Set<String> SUPPORT_FILE_TYPES = Set.of("jpg", "jpeg", "png", "bmp");
|
||||
private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})[-/.年](\\d{1,2})[-/.月](\\d{1,2})日?");
|
||||
|
||||
private final IBaiduOcrService baiduOcrService;
|
||||
private final IInsuranceOcrTemplateService insuranceOcrTemplateService;
|
||||
|
||||
@Override
|
||||
public IPage<InsuranceRecordVO> selectInsuranceRecordPage(IPage<InsuranceRecordVO> page, InsuranceRecordVO insuranceRecord) {
|
||||
@@ -116,10 +137,131 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
|
||||
@Override
|
||||
public InsuranceRecord recognizePolicy(MultipartFile file, String vehicleType, String ocrTemplate) {
|
||||
validatePolicyFile(file);
|
||||
String templateName = trimToNull(ocrTemplate);
|
||||
if (templateName == null) {
|
||||
throw new ServiceException("请选择OCR识别模板");
|
||||
}
|
||||
InsuranceOcrTemplate insuranceOcrTemplate = insuranceOcrTemplateService.getOne(Wrappers.<InsuranceOcrTemplate>lambdaQuery()
|
||||
.eq(InsuranceOcrTemplate::getIsDeleted, 0)
|
||||
.eq(InsuranceOcrTemplate::getStatus, 1)
|
||||
.eq(InsuranceOcrTemplate::getName, templateName));
|
||||
if (insuranceOcrTemplate == null) {
|
||||
throw new ServiceException("OCR识别模板不存在或已停用");
|
||||
}
|
||||
InsuranceRecord insuranceRecord = new InsuranceRecord();
|
||||
insuranceRecord.setVehicleType(normalizeVehicleType(vehicleType));
|
||||
insuranceRecord.setOcrTemplate(trimToNull(ocrTemplate));
|
||||
throw new ServiceException("当前未配置OCR识别服务,请手动填写保单信息");
|
||||
insuranceRecord.setOcrTemplate(templateName);
|
||||
try {
|
||||
BaiduOcrResultVO ocrResult = baiduOcrService.recognize(BaiduOcrType.GENERAL, null, file.getBytes());
|
||||
applyTemplateMapping(insuranceRecord, insuranceOcrTemplate.getMappingConfig(), ocrResult.getResult());
|
||||
return insuranceRecord;
|
||||
} catch (IOException exception) {
|
||||
throw new ServiceException("读取保单图片失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyTemplateMapping(InsuranceRecord insuranceRecord, String mappingConfig, Map<String, Object> ocrResult) {
|
||||
Map<String, String> mappingMap = parseMappingConfig(mappingConfig);
|
||||
List<String> words = extractOcrWords(ocrResult);
|
||||
insuranceRecord.setInsuranceType(findMappedValue(words, mappingMap.get("保险类型")));
|
||||
insuranceRecord.setPolicyNo(findMappedValue(words, mappingMap.get("保单号")));
|
||||
insuranceRecord.setStartDate(parseDate(findMappedValue(words, mappingMap.get("开始日期"))));
|
||||
insuranceRecord.setEndDate(parseDate(findMappedValue(words, mappingMap.get("结束日期"))));
|
||||
insuranceRecord.setInsuredAmount(parseAmount(findMappedValue(words, mappingMap.get("保额"))));
|
||||
insuranceRecord.setPremium(parseAmount(findMappedValue(words, mappingMap.get("保费"))));
|
||||
insuranceRecord.setInvoiceNo(findMappedValue(words, mappingMap.get("发票号")));
|
||||
insuranceRecord.setInvoiceDate(parseDate(findMappedValue(words, mappingMap.get("开票日期"))));
|
||||
insuranceRecord.setRemark(findMappedValue(words, mappingMap.get("备注")));
|
||||
}
|
||||
|
||||
private Map<String, String> parseMappingConfig(String mappingConfig) {
|
||||
try {
|
||||
JSONArray mappingArray = JSON.parseArray(mappingConfig);
|
||||
Map<String, String> mappingMap = new LinkedHashMap<>();
|
||||
for (Object item : mappingArray) {
|
||||
if (item instanceof JSONObject mapping) {
|
||||
String key = trimToNull(mapping.getString("key"));
|
||||
String value = trimToNull(mapping.getString("value"));
|
||||
if (key != null && value != null) {
|
||||
mappingMap.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mappingMap;
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("OCR识别模板字段映射配置格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> extractOcrWords(Map<String, Object> ocrResult) {
|
||||
if (ocrResult == null || ocrResult.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Object wordsResult = ocrResult.get("words_result");
|
||||
if (wordsResult instanceof Collection<?> wordCollection) {
|
||||
return wordCollection.stream().map(this::extractWord).filter(Objects::nonNull).toList();
|
||||
}
|
||||
if (wordsResult instanceof Map<?, ?> wordMap) {
|
||||
return wordMap.values().stream().map(this::extractWord).filter(Objects::nonNull).toList();
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private String extractWord(Object wordItem) {
|
||||
if (wordItem instanceof Map<?, ?> wordMap) {
|
||||
Object words = wordMap.get("words");
|
||||
return words == null ? null : trimToNull(String.valueOf(words));
|
||||
}
|
||||
return wordItem == null ? null : trimToNull(String.valueOf(wordItem));
|
||||
}
|
||||
|
||||
private String findMappedValue(List<String> words, String mappingValue) {
|
||||
if (mappingValue == null || words.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Pattern standalonePattern = Pattern.compile("^" + Pattern.quote(mappingValue) + "\\s*[::]?$");
|
||||
Pattern inlinePattern = Pattern.compile(Pattern.quote(mappingValue) + "\\s*[::]?\\s*(.+)$");
|
||||
for (int index = 0; index < words.size(); index++) {
|
||||
String word = words.get(index).trim();
|
||||
Matcher inlineMatcher = inlinePattern.matcher(word);
|
||||
if (inlineMatcher.find()) {
|
||||
return trimToNull(inlineMatcher.group(1));
|
||||
}
|
||||
if (standalonePattern.matcher(word).matches() && index + 1 < words.size()) {
|
||||
return trimToNull(words.get(index + 1));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private LocalDate parseDate(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
Matcher matcher = DATE_PATTERN.matcher(value);
|
||||
if (!matcher.find()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.of(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)), Integer.parseInt(matcher.group(3)));
|
||||
} catch (RuntimeException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal parseAmount(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String amount = value.replaceAll("[^\\d.]", "");
|
||||
if (amount.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(amount);
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void prepare(InsuranceRecord insuranceRecord) {
|
||||
@@ -186,7 +328,7 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
|
||||
|
||||
private void validatePolicyFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("请上传保单图片或PDF文件");
|
||||
throw new ServiceException("请上传保单图片");
|
||||
}
|
||||
String filename = file.getOriginalFilename();
|
||||
if (Func.isEmpty(filename) || !filename.contains(".")) {
|
||||
@@ -194,7 +336,7 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
|
||||
}
|
||||
String extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
if (!SUPPORT_FILE_TYPES.contains(extension)) {
|
||||
throw new ServiceException("仅支持JPG、PNG、PDF文件");
|
||||
throw new ServiceException("仅支持JPG、PNG、BMP文件");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+486
-42
@@ -36,10 +36,12 @@ import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
|
||||
import org.springblade.transport.mapper.ReceivablePayableChangeRecordMapper;
|
||||
import org.springblade.transport.mapper.ReceivablePayableDetailMapper;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.CommonAddress;
|
||||
import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee;
|
||||
import org.springblade.transport.pojo.entity.ReceivablePayableChangeRecord;
|
||||
import org.springblade.transport.pojo.entity.ReceivablePayableDetail;
|
||||
@@ -49,11 +51,13 @@ import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO;
|
||||
import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO;
|
||||
import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.ICommonAddressService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.wrapper.ReceivablePayableDetailWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
@@ -61,12 +65,15 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 应收应付明细服务实现类
|
||||
@@ -74,6 +81,7 @@ import java.util.Set;
|
||||
* @author Chill
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ReceivablePayableDetailServiceImpl
|
||||
extends BaseServiceImpl<ReceivablePayableDetailMapper, ReceivablePayableDetail>
|
||||
implements IReceivablePayableDetailService {
|
||||
@@ -82,15 +90,18 @@ public class ReceivablePayableDetailServiceImpl
|
||||
private final ReceivablePayableChangeRecordMapper changeRecordMapper;
|
||||
private final IWaybillService waybillService;
|
||||
private final IContractManageService contractManageService;
|
||||
private final ICommonAddressService commonAddressService;
|
||||
|
||||
public ReceivablePayableDetailServiceImpl(ReceivablePayableCargoFeeMapper cargoFeeMapper,
|
||||
ReceivablePayableChangeRecordMapper changeRecordMapper,
|
||||
IWaybillService waybillService,
|
||||
IContractManageService contractManageService) {
|
||||
ReceivablePayableChangeRecordMapper changeRecordMapper,
|
||||
IWaybillService waybillService,
|
||||
IContractManageService contractManageService,
|
||||
ICommonAddressService commonAddressService) {
|
||||
this.cargoFeeMapper = cargoFeeMapper;
|
||||
this.changeRecordMapper = changeRecordMapper;
|
||||
this.waybillService = waybillService;
|
||||
this.contractManageService = contractManageService;
|
||||
this.commonAddressService = commonAddressService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,7 +116,11 @@ public class ReceivablePayableDetailServiceImpl
|
||||
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId())
|
||||
.eq(ReceivablePayableCargoFee::getIsDeleted, 0)
|
||||
.orderByAsc(ReceivablePayableCargoFee::getCreateTime));
|
||||
return buildFeeDetail(rows);
|
||||
ReceivablePayableFeeDetailVO result = buildFeeDetail(rows);
|
||||
LinkedHashSet<String> feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId()));
|
||||
feeItemNames.addAll(result.getFeeItemNames());
|
||||
result.setFeeItemNames(new ArrayList<>(feeItemNames));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,11 +140,11 @@ public class ReceivablePayableDetailServiceImpl
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateFee(ReceivablePayableUpdateFeeRequest request) {
|
||||
if (Boolean.TRUE.equals(request.getCloseOnly())) {
|
||||
closeDetails(request.getIds());
|
||||
closeDetails(request.getIds(), request.getSettlementType());
|
||||
return;
|
||||
}
|
||||
if (Func.isEmpty(request.getIds()) && Func.isEmpty(request.getContractId())) {
|
||||
throw new ServiceException("请选择需要更新的费用明细或合同");
|
||||
if (Func.isEmpty(request.getContractId())) {
|
||||
throw new ServiceException("请选择需要更新费用的合同");
|
||||
}
|
||||
List<ReceivablePayableDetail> details = list(buildUpdateQuery(request));
|
||||
if (Func.isEmpty(details)) {
|
||||
@@ -140,12 +155,107 @@ public class ReceivablePayableDetailServiceImpl
|
||||
continue;
|
||||
}
|
||||
BigDecimal before = money(detail.getTotalAmount());
|
||||
rebuildDetailFee(detail);
|
||||
rebuildDetailFee(detail, request.getBillingPlanId());
|
||||
if (request.getAdjustAmount() != null && request.getAdjustAmount().compareTo(BigDecimal.ZERO) != 0) {
|
||||
applyManualAdjustment(detail, request.getAdjustAmount(), request.getAdjustFeeItem(), request.getAdjustReason());
|
||||
}
|
||||
saveChangeRecord(detail, "【费用合计】从[" + formatMoney(before) + "]调整为[" + formatMoney(detail.getTotalAmount()) + "]",
|
||||
request.getAdjustReason());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> updateFeeContracts(String settlementType) {
|
||||
List<Long> contractIds = list(Wrappers.<ReceivablePayableDetail>lambdaQuery()
|
||||
.select(ReceivablePayableDetail::getContractId)
|
||||
.eq(ReceivablePayableDetail::getIsDeleted, 0)
|
||||
.eq(ReceivablePayableDetail::getSettlementStatus, "pending")
|
||||
.eq(Func.isNotEmpty(settlementType), ReceivablePayableDetail::getSettlementType,
|
||||
settlementType(settlementType))
|
||||
.isNotNull(ReceivablePayableDetail::getContractId)
|
||||
.groupBy(ReceivablePayableDetail::getContractId))
|
||||
.stream().map(ReceivablePayableDetail::getContractId).toList();
|
||||
if (Func.isEmpty(contractIds)) {
|
||||
return List.of();
|
||||
}
|
||||
Map<Long, ContractManage> contracts = contractManageService.listByIds(contractIds).stream()
|
||||
.collect(java.util.stream.Collectors.toMap(ContractManage::getId, contract -> contract));
|
||||
return contractIds.stream().map(contracts::get).filter(Objects::nonNull).map(contract -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", contract.getId());
|
||||
item.put("contractNo", contract.getContractNo());
|
||||
item.put("contractName", contract.getContractName());
|
||||
item.put("billingPlanJson", contract.getBillingPlanJson());
|
||||
return item;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void adjustFee(ReceivablePayableAdjustFeeRequest request) {
|
||||
if (request == null || request.getDetailId() == null || Func.isEmpty(request.getRows())) {
|
||||
throw new ServiceException("费用调整数据不能为空");
|
||||
}
|
||||
ReceivablePayableDetail detail = getExisting(request.getDetailId());
|
||||
if (!"pending".equals(detail.getSettlementStatus())) {
|
||||
throw new ServiceException("仅待结算明细允许调整");
|
||||
}
|
||||
List<ReceivablePayableCargoFee> existingRows = cargoFeeMapper.selectList(
|
||||
Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId())
|
||||
.eq(ReceivablePayableCargoFee::getIsDeleted, 0));
|
||||
Map<Long, ReceivablePayableCargoFee> existingMap = existingRows.stream()
|
||||
.collect(java.util.stream.Collectors.toMap(ReceivablePayableCargoFee::getId, row -> row));
|
||||
if (request.getRows().size() != existingRows.size()) {
|
||||
throw new ServiceException("费用调整行数据不完整");
|
||||
}
|
||||
Set<String> allowedFeeItems = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId()));
|
||||
existingRows.forEach(row -> allowedFeeItems.addAll(parseMap(row.getFeeItemsJson()).keySet()));
|
||||
List<String> changes = new ArrayList<>();
|
||||
for (ReceivablePayableAdjustFeeRequest.AdjustRow adjusted : request.getRows()) {
|
||||
ReceivablePayableCargoFee existing = existingMap.get(adjusted.getId());
|
||||
if (existing == null) {
|
||||
throw new ServiceException("存在无效的费用调整行");
|
||||
}
|
||||
validateNonNegative(adjusted.getTransportQuantity(), "运输量");
|
||||
validateNonNegative(adjusted.getMileage(), "里程");
|
||||
validateNonNegative(adjusted.getFreightAmount(), "运输费");
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
if (adjusted.getFeeItems() != null) {
|
||||
adjusted.getFeeItems().forEach((name, amount) -> {
|
||||
if (!allowedFeeItems.contains(name)) {
|
||||
throw new ServiceException("费用项目不存在:" + name);
|
||||
}
|
||||
validateNonNegative(amount, name);
|
||||
feeItems.put(name, money(amount));
|
||||
});
|
||||
}
|
||||
appendChange(changes, "计费数量", existing.getTransportQuantity(), adjusted.getTransportQuantity());
|
||||
appendChange(changes, "里程", existing.getMileage(), adjusted.getMileage());
|
||||
appendChange(changes, "运输费", existing.getFreightAmount(), adjusted.getFreightAmount());
|
||||
Map<String, Object> oldFeeItems = parseMap(existing.getFeeItemsJson());
|
||||
for (String name : allowedFeeItems) {
|
||||
appendChange(changes, name, decimal(oldFeeItems.get(name)), money(feeItems.get(name)));
|
||||
}
|
||||
BigDecimal freightAmount = money(adjusted.getFreightAmount());
|
||||
BigDecimal afterAmount = adjustedAfterAmount(freightAmount, feeItems);
|
||||
existing.setTransportQuantity(money(adjusted.getTransportQuantity()));
|
||||
existing.setMileage(money(adjusted.getMileage()));
|
||||
existing.setFreightAmount(freightAmount);
|
||||
existing.setFeeItemsJson(JsonUtil.toJson(feeItems));
|
||||
existing.setAdjustAmount(afterAmount.subtract(money(existing.getOriginalAmount())));
|
||||
existing.setAfterAmount(afterAmount);
|
||||
cargoFeeMapper.updateById(existing);
|
||||
}
|
||||
if (changes.isEmpty()) {
|
||||
throw new ServiceException("未修改任何费用数据");
|
||||
}
|
||||
for (int i = 0; i < changes.size(); i++) {
|
||||
saveChangeRecord(detail, changes.get(i), request.getAdjustReason(), String.format("%04d", i + 1));
|
||||
}
|
||||
refreshAdjustedDetail(detail, existingRows);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void transferSettlement(ReceivablePayableTransferRequest request) {
|
||||
@@ -203,11 +313,10 @@ public class ReceivablePayableDetailServiceImpl
|
||||
public ReceivablePayableFeeDetailVO generatePreview(IPage<?> page, ReceivablePayableGenerateRequest request) {
|
||||
validateGenerateRequest(request, true);
|
||||
List<Waybill> waybills = waybillService.list(buildWaybillQuery(request));
|
||||
ContractManage contract = contractManageService.getById(request.getContractId());
|
||||
List<ReceivablePayableCargoFee> fees = waybills.stream()
|
||||
.skip((page.getCurrent() - 1) * page.getSize())
|
||||
.limit(page.getSize())
|
||||
.map(waybill -> buildCargoFee(null, waybill))
|
||||
.toList();
|
||||
.skip((page.getCurrent() - 1) * page.getSize()).limit(page.getSize())
|
||||
.flatMap(waybill -> calculatedFees(waybill, contract, request.getBillingPlanId()).stream()).toList();
|
||||
ReceivablePayableFeeDetailVO vo = buildFeeDetail(fees);
|
||||
vo.setTotal((long) waybills.size());
|
||||
return vo;
|
||||
@@ -223,16 +332,123 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
ContractManage contract = contractManageService.getById(request.getContractId());
|
||||
for (Waybill waybill : waybills) {
|
||||
if (existsByWaybill(waybill.getId())) {
|
||||
if (existsByWaybill(waybill.getId(), settlementType(request.getSettlementType()))) {
|
||||
continue;
|
||||
}
|
||||
ReceivablePayableDetail detail = buildDetail(waybill, contract);
|
||||
ReceivablePayableDetail detail = buildDetail(waybill, contract, request.getBillingPlanId(), settlementType(request.getSettlementType()));
|
||||
save(detail);
|
||||
ReceivablePayableCargoFee cargoFee = buildCargoFee(detail.getId(), waybill);
|
||||
cargoFeeMapper.insert(cargoFee);
|
||||
calculatedFees(waybill, contract, request.getBillingPlanId()).forEach(fee -> {
|
||||
fee.setDetailId(detail.getId());
|
||||
cargoFeeMapper.insert(fee);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
|
||||
rollbackFor = Exception.class)
|
||||
public void generateForCompletedWaybills(List<Long> waybillIds) {
|
||||
if (Func.isEmpty(waybillIds)) return;
|
||||
for (Long waybillId : waybillIds) {
|
||||
Waybill waybill = waybillService.getById(waybillId);
|
||||
if (waybill == null || Func.isEmpty(waybill.getContractId())) continue;
|
||||
ContractManage contract = contractManageService.getById(waybill.getContractId());
|
||||
if (contract == null || !isSystemGeneration(contract)) continue;
|
||||
try {
|
||||
String planId = matchedPlanId(waybill, contract);
|
||||
if (Func.isEmpty(planId)) continue;
|
||||
List<ReceivablePayableCargoFee> matchedFees = calculatedFees(waybill, contract, planId, true);
|
||||
if (matchedFees.isEmpty()) continue;
|
||||
for (String settlementType : List.of("payable", "receivable")) {
|
||||
if (existsByWaybill(waybill.getId(), settlementType)) continue;
|
||||
ReceivablePayableDetail detail = buildDetail(waybill, contract, settlementType, matchedFees);
|
||||
save(detail);
|
||||
for (ReceivablePayableCargoFee fee : matchedFees) {
|
||||
ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class);
|
||||
copy.setId(null);
|
||||
copy.setDetailId(detail.getId());
|
||||
cargoFeeMapper.insert(copy);
|
||||
}
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, contractId:{}, failureReason:{}",
|
||||
waybill.getId(), waybill.getWaybillNo(), waybill.getContractId(), exception.getMessage(), exception);
|
||||
if (exception instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSystemGeneration(ContractManage contract) {
|
||||
if (Func.isNotEmpty(contract.getFeeGenerationMode())) {
|
||||
return "system".equalsIgnoreCase(contract.getFeeGenerationMode()) || "系统生成".equals(contract.getFeeGenerationMode());
|
||||
}
|
||||
return !Integer.valueOf(0).equals(contract.getBillingEnabled());
|
||||
}
|
||||
|
||||
private String matchedPlanId(Waybill waybill, ContractManage contract) {
|
||||
List<Map<String, Object>> plans = parseList(contract.getBillingPlanJson());
|
||||
if (plans.isEmpty()) return null;
|
||||
Map<String, Object> plan = plans.stream().filter(item -> Boolean.TRUE.equals(item.get("defaultPlan"))).findFirst()
|
||||
.orElse(plans.get(plans.size() - 1));
|
||||
if (!(plan.get("rules") instanceof List<?> rules)) return null;
|
||||
boolean matched = rules.stream().anyMatch(value -> value instanceof Map<?, ?> raw && matchesRule(raw, waybill));
|
||||
if (!matched) return null;
|
||||
return "__matched__";
|
||||
}
|
||||
|
||||
private boolean matchesRule(Map<?, ?> raw, Waybill waybill) {
|
||||
Object conditionValue = raw.get("matchCondition");
|
||||
if (!(conditionValue instanceof Map<?, ?> condition)) return true;
|
||||
return matchesLocation(condition, "origin", waybill.getDepartureAddressId(), waybill.getDepartureName(), waybill.getDepartureAddress())
|
||||
&& matchesLocation(condition, "destination", waybill.getArrivalAddressId(), waybill.getArrivalName(), waybill.getArrivalAddress())
|
||||
&& matchesCondition(condition.get("cargoType"), waybill.getCargoType());
|
||||
}
|
||||
|
||||
private boolean matchesLocation(Map<?, ?> condition, String location, Long addressId, String addressName,
|
||||
String detailAddress) {
|
||||
Object expectedName = condition.get(location);
|
||||
Object expectedCode = condition.get(location + "Code");
|
||||
if (isBlank(expectedName) && isBlank(expectedCode)) return true;
|
||||
String actualCode = resolveRegionCode(addressId);
|
||||
if (!isBlank(expectedCode) && !isBlank(actualCode)) {
|
||||
return matchesCondition(expectedCode, actualCode);
|
||||
}
|
||||
return matchesCondition(expectedName, addressName)
|
||||
|| matchesCondition(expectedName, detailAddress);
|
||||
}
|
||||
|
||||
private String resolveRegionCode(Long addressId) {
|
||||
if (addressId == null) return "";
|
||||
try {
|
||||
CommonAddress address = commonAddressService.getById(addressId);
|
||||
return address == null || address.getRegionCode() == null ? "" : address.getRegionCode().trim();
|
||||
} catch (Exception exception) {
|
||||
log.warn("解析运单行政区编码失败,addressId:{}", addressId, exception);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBlank(Object value) {
|
||||
return value == null || String.valueOf(value).trim().isEmpty();
|
||||
}
|
||||
|
||||
private boolean matchesCondition(Object expected, String actual) {
|
||||
if (expected == null || String.valueOf(expected).isBlank()) return true;
|
||||
if (expected instanceof List<?> values) {
|
||||
return values.stream().anyMatch(value -> matchesCondition(value, actual));
|
||||
}
|
||||
String expectedText = String.valueOf(expected).trim();
|
||||
String actualText = String.valueOf(actual == null ? "" : actual).trim();
|
||||
if (expectedText.isEmpty() || actualText.isEmpty()) return false;
|
||||
if (expectedText.equals(actualText) || actualText.contains(expectedText) || expectedText.contains(actualText)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ReceivablePayableDetail> buildQuery(ReceivablePayableDetailVO query) {
|
||||
LambdaQueryWrapper<ReceivablePayableDetail> wrapper = Wrappers.<ReceivablePayableDetail>lambdaQuery()
|
||||
.eq(ReceivablePayableDetail::getIsDeleted, 0)
|
||||
@@ -251,6 +467,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
.like(Func.isNotEmpty(query.getBatchNo()), ReceivablePayableDetail::getBatchNo, query.getBatchNo())
|
||||
.like(Func.isNotEmpty(query.getVehicleNo()), ReceivablePayableDetail::getVehicleNo, query.getVehicleNo())
|
||||
.eq(Func.isNotEmpty(query.getSettlementStatus()), ReceivablePayableDetail::getSettlementStatus, query.getSettlementStatus());
|
||||
wrapper.eq(Func.isNotEmpty(query.getSettlementType()), ReceivablePayableDetail::getSettlementType, query.getSettlementType());
|
||||
return wrapper.orderByDesc(ReceivablePayableDetail::getCreateTime);
|
||||
}
|
||||
|
||||
@@ -258,12 +475,10 @@ public class ReceivablePayableDetailServiceImpl
|
||||
LambdaQueryWrapper<ReceivablePayableDetail> wrapper = Wrappers.<ReceivablePayableDetail>lambdaQuery()
|
||||
.eq(ReceivablePayableDetail::getIsDeleted, 0)
|
||||
.eq(ReceivablePayableDetail::getSettlementStatus, "pending");
|
||||
if (Func.isNotEmpty(request.getIds())) {
|
||||
wrapper.in(ReceivablePayableDetail::getId, request.getIds());
|
||||
}
|
||||
if (Func.isNotEmpty(request.getContractId())) {
|
||||
wrapper.eq(ReceivablePayableDetail::getContractId, request.getContractId());
|
||||
if (Func.isNotEmpty(request.getSettlementType())) {
|
||||
wrapper.eq(ReceivablePayableDetail::getSettlementType, settlementType(request.getSettlementType()));
|
||||
}
|
||||
wrapper.eq(ReceivablePayableDetail::getContractId, request.getContractId());
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@@ -272,7 +487,8 @@ public class ReceivablePayableDetailServiceImpl
|
||||
.eq(Waybill::getIsDeleted, 0)
|
||||
.eq(Waybill::getContractId, request.getContractId())
|
||||
.eq(Waybill::getBusinessStatus, "completed")
|
||||
.notInSql(Waybill::getId, "select waybill_id from blade_receivable_payable_detail where is_deleted = 0");
|
||||
.notInSql(Waybill::getId, "select waybill_id from blade_receivable_payable_detail where is_deleted = 0"
|
||||
+ (Func.isNotEmpty(request.getSettlementType()) ? " and settlement_type = '" + settlementType(request.getSettlementType()) + "'" : ""));
|
||||
if (Func.isNotEmpty(request.getBatchNo())) {
|
||||
wrapper.like(Waybill::getBatchNo, request.getBatchNo());
|
||||
}
|
||||
@@ -288,20 +504,29 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return wrapper.orderByDesc(Waybill::getCreateTime);
|
||||
}
|
||||
|
||||
private ReceivablePayableDetail buildDetail(Waybill waybill, ContractManage contract) {
|
||||
ReceivablePayableCargoFee cargoFee = buildCargoFee(null, waybill);
|
||||
private ReceivablePayableDetail buildDetail(Waybill waybill, ContractManage contract, String billingPlanId, String settlementType) {
|
||||
List<ReceivablePayableCargoFee> fees = calculatedFees(waybill, contract, billingPlanId);
|
||||
return buildDetail(waybill, contract, settlementType, fees);
|
||||
}
|
||||
|
||||
private ReceivablePayableDetail buildDetail(Waybill waybill, ContractManage contract, String settlementType, List<ReceivablePayableCargoFee> fees) {
|
||||
BigDecimal freight = fees.stream().filter(this::isFreight).map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal other = total.subtract(freight).setScale(2, RoundingMode.HALF_UP);
|
||||
ReceivablePayableDetail detail = new ReceivablePayableDetail();
|
||||
detail.setDocumentNo(nextDocumentNo());
|
||||
detail.setSettlementType("receivable");
|
||||
detail.setSettlementType(settlementType);
|
||||
detail.setProjectId(waybill.getProjectId());
|
||||
detail.setProjectName(waybill.getProjectName());
|
||||
detail.setDeptId(waybill.getDeptId());
|
||||
detail.setDeptName(waybill.getDeptName());
|
||||
detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate());
|
||||
detail.setCustomerName(waybill.getCustomerName());
|
||||
detail.setContractId(waybill.getContractId());
|
||||
detail.setCustomerName("payable".equals(settlementType)
|
||||
? (contract == null ? waybill.getCarrierName() : contract.getPartyA())
|
||||
: (contract == null ? waybill.getCustomerName() : contract.getPartyB()));
|
||||
detail.setContractId(contract == null ? waybill.getContractId() : contract.getId());
|
||||
detail.setContractNo(contract == null ? null : contract.getContractNo());
|
||||
detail.setContractName(waybill.getContractName());
|
||||
detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName());
|
||||
detail.setSourceType("系统生成");
|
||||
detail.setWaybillId(waybill.getId());
|
||||
detail.setWaybillNo(waybill.getWaybillNo());
|
||||
@@ -315,11 +540,12 @@ public class ReceivablePayableDetailServiceImpl
|
||||
detail.setBatchNo(waybill.getBatchNo());
|
||||
detail.setUnitPrice(waybill.getUnitPrice());
|
||||
detail.setCurrency("RMB");
|
||||
detail.setFreightAmount(cargoFee.getFreightAmount());
|
||||
detail.setOtherFeeAmount(waybill.getOtherFeeTotal());
|
||||
detail.setTotalAmount(cargoFee.getAfterAmount());
|
||||
detail.setFreightAmount(freight);
|
||||
detail.setOtherFeeAmount(other);
|
||||
detail.setTotalAmount(total);
|
||||
detail.setSettlementStatus("pending");
|
||||
detail.setFeeItemsJson(cargoFee.getFeeItemsJson());
|
||||
detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap<String, BigDecimal>::new,
|
||||
(map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll)));
|
||||
return detail;
|
||||
}
|
||||
|
||||
@@ -358,6 +584,133 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return cargoFee;
|
||||
}
|
||||
|
||||
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId) {
|
||||
return calculatedFees(waybill, contract, planId, false);
|
||||
}
|
||||
|
||||
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId, boolean matchOnly) {
|
||||
List<Map<String, Object>> plans = parseList(contract == null ? null : contract.getBillingPlanJson());
|
||||
Map<String, Object> plan = "__matched__".equals(planId) ? plans.stream().filter(this::isDefaultPlan).findFirst().orElseGet(() -> plans.isEmpty() ? null : plans.get(plans.size() - 1)) : plans.stream().filter(item -> Objects.equals(stringValue(item, "id"), planId)
|
||||
|| Objects.equals(stringValue(item, "planId"), planId)).findFirst()
|
||||
.orElseGet(() -> plans.stream().filter(this::isDefaultPlan).findFirst().orElse(null));
|
||||
if (plan == null || !(plan.get("rules") instanceof List<?>)) return matchOnly ? List.of() : List.of(buildCargoFee(null, waybill));
|
||||
List<ReceivablePayableCargoFee> result = new ArrayList<>();
|
||||
int line = 1;
|
||||
for (Object value : (List<?>) plan.get("rules")) {
|
||||
if (!(value instanceof Map<?, ?> raw)) continue;
|
||||
Map<String, Object> rule = new LinkedHashMap<>();
|
||||
raw.forEach((key, item) -> rule.put(String.valueOf(key), item));
|
||||
if (matchOnly && !matchesRule(raw, waybill)) continue;
|
||||
BigDecimal amount = calculateRule(rule, waybill);
|
||||
if (amount == null) continue;
|
||||
ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee();
|
||||
fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++));
|
||||
fee.setCargoName(stringValue(rule, "feeItem", "费用")); fee.setCargoType(waybill.getCargoType());
|
||||
fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", ""));
|
||||
fee.setTransportQuantity(measure(rule, waybill)); fee.setQuantityUnit(stringValue(rule, "billingUnit", waybill.getQuantityUnit()));
|
||||
fee.setPriceUnit(stringValue(rule, "billingUnit", waybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice")));
|
||||
fee.setMileage(waybill.getMileage()); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO);
|
||||
fee.setFeeItemsJson(JsonUtil.toJson(Map.of(fee.getCargoName(), amount)));
|
||||
fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", waybill.getRemark()));
|
||||
result.add(fee);
|
||||
}
|
||||
return result.isEmpty() && !matchOnly ? List.of(buildCargoFee(null, waybill)) : result;
|
||||
}
|
||||
|
||||
private boolean isDefaultPlan(Map<String, Object> plan) {
|
||||
Object value = plan.get("defaultPlan");
|
||||
return Boolean.TRUE.equals(value) || "true".equalsIgnoreCase(String.valueOf(value));
|
||||
}
|
||||
|
||||
private BigDecimal calculateRule(Map<String, Object> rule, Waybill waybill) {
|
||||
String element = stringValue(rule, "billingElement", ""); String type = stringValue(rule, "billingType", "");
|
||||
BigDecimal base = measure(rule, waybill); BigDecimal unit = decimal(rule.get("unitPrice"));
|
||||
if ("按重量".equals(element) || "按吨·公里".equals(element)) {
|
||||
BigDecimal minimum = decimal(rule.get("minimumBillingWeight"));
|
||||
if (minimum.signum() > 0 && "按重量".equals(element)) base = base.max(minimum);
|
||||
if (minimum.signum() > 0 && "按吨·公里".equals(element)) base = base.divide(money(waybill.getQuantity()).max(BigDecimal.ONE), 6, RoundingMode.HALF_UP).multiply(minimum).multiply(money(waybill.getMileage()));
|
||||
}
|
||||
if ("固定一口价".equals(type)) return unit.setScale(2, RoundingMode.HALF_UP);
|
||||
List<Map<String, Object>> ranges = ranges(rule);
|
||||
if (ranges.isEmpty() || "固定单价".equals(type)) return base.multiply(unit).setScale(2, RoundingMode.HALF_UP);
|
||||
if (type.contains("区间") && type.contains("一口价")) return range(ranges, base).map(r -> decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice"))).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
|
||||
if ("区间单价".equals(type)) { BigDecimal rangeBase = base; return range(ranges, rangeBase).map(r -> decimal(r.get("unitPrice")).multiply(rangeBase)).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); }
|
||||
if ("阶梯单价".equals(type)) {
|
||||
BigDecimal total = BigDecimal.ZERO, previous = BigDecimal.ZERO;
|
||||
for (Map<String, Object> r : ranges.stream().sorted(Comparator.comparing(x -> decimal(x.get("lowerLimit")))).toList()) {
|
||||
BigDecimal upper = decimal(r.get("upperLimit")); BigDecimal part = base.min(upper).subtract(previous).max(BigDecimal.ZERO);
|
||||
total = total.add(part.multiply(decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice")))); previous = upper;
|
||||
if (base.compareTo(upper) <= 0) break;
|
||||
}
|
||||
return total.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
return base.multiply(unit).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private boolean isFreight(ReceivablePayableCargoFee fee) {
|
||||
return fee.getFreightAmount() != null && fee.getFreightAmount().compareTo(BigDecimal.ZERO) > 0;
|
||||
}
|
||||
|
||||
private boolean isFreightRule(Map<String, Object> rule) {
|
||||
String type = stringValue(rule, "feeType", "") + stringValue(rule, "feeItem", "");
|
||||
return type.contains("运费") || type.contains("运输费");
|
||||
}
|
||||
|
||||
private BigDecimal measure(Map<String, Object> rule, Waybill waybill) {
|
||||
return switch (stringValue(rule, "billingElement", "按重量")) {
|
||||
case "按体积" -> volume(waybill);
|
||||
case "按车辆", "固定金额(整单一口价)" -> BigDecimal.ONE;
|
||||
case "按里程" -> money(waybill.getMileage());
|
||||
case "按吨·公里" -> money(waybill.getQuantity()).multiply(money(waybill.getMileage()));
|
||||
case "按数量" -> money(waybill.getQuantity());
|
||||
default -> money(waybill.getQuantity());
|
||||
};
|
||||
}
|
||||
|
||||
private BigDecimal volume(Waybill waybill) {
|
||||
Map<String, Object> goods = parseMap(waybill.getGoodsJson());
|
||||
BigDecimal value = decimal(goods.get("volume"));
|
||||
if (value.signum() == 0) value = decimal(goods.get("cargoVolume"));
|
||||
return value;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> ranges(Map<String, Object> rule) {
|
||||
Object source = rule.get("limitRanges");
|
||||
if (!(source instanceof List<?>)) { Map<String, Object> fallback = new LinkedHashMap<>(); fallback.put("lowerLimit", rule.get("lowerLimit")); fallback.put("upperLimit", rule.get("upperLimit")); fallback.put("unitPrice", rule.get("unitPrice")); source = List.of(fallback); }
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object value : (List<?>) source) if (value instanceof Map<?, ?> raw) {
|
||||
Map<String, Object> item = new LinkedHashMap<>(); raw.forEach((key, val) -> item.put(String.valueOf(key), val));
|
||||
if (item.get("lowerLimit") != null && item.get("upperLimit") != null) result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Optional<Map<String, Object>> range(List<Map<String, Object>> ranges, BigDecimal value) {
|
||||
List<Map<String, Object>> sorted = ranges.stream().sorted(Comparator.comparing(x -> decimal(x.get("lowerLimit")))).toList();
|
||||
for (int i = 0; i < sorted.size(); i++) {
|
||||
Map<String, Object> r = sorted.get(i);
|
||||
boolean upperMatched = i == sorted.size() - 1 ? value.compareTo(decimal(r.get("upperLimit"))) <= 0 : value.compareTo(decimal(r.get("upperLimit"))) < 0;
|
||||
if (value.compareTo(decimal(r.get("lowerLimit"))) >= 0 && upperMatched) return Optional.of(r);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> parseList(String json) {
|
||||
if (Func.isEmpty(json)) return List.of();
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(json, List.class);
|
||||
if (parsed instanceof List<?> list) return list.stream().filter(Map.class::isInstance).map(item -> {
|
||||
Map<String, Object> result = new LinkedHashMap<>(); ((Map<?, ?>) item).forEach((k, v) -> result.put(String.valueOf(k), v)); return result;
|
||||
}).toList();
|
||||
} catch (Exception ignored) { }
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private String stringValue(Map<String, Object> map, String key) { return stringValue(map, key, ""); }
|
||||
private String stringValue(Map<String, Object> map, String key, String fallback) {
|
||||
Object value = map.get(key); return value == null || String.valueOf(value).isBlank() ? fallback : String.valueOf(value);
|
||||
}
|
||||
|
||||
private ReceivablePayableFeeDetailVO buildFeeDetail(List<ReceivablePayableCargoFee> rows) {
|
||||
Set<String> feeItemNames = new LinkedHashSet<>();
|
||||
List<ReceivablePayableCargoFeeVO> records = rows.stream().map(row -> {
|
||||
@@ -379,26 +732,93 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return vo;
|
||||
}
|
||||
|
||||
private void rebuildDetailFee(ReceivablePayableDetail detail) {
|
||||
private List<String> contractFeeItemNames(Long contractId) {
|
||||
if (contractId == null) return List.of();
|
||||
ContractManage contract = contractManageService.getById(contractId);
|
||||
if (contract == null) return List.of();
|
||||
LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||
for (Map<String, Object> plan : parseList(contract.getBillingPlanJson())) {
|
||||
if (!(plan.get("rules") instanceof List<?> rules)) continue;
|
||||
for (Object value : rules) {
|
||||
if (!(value instanceof Map<?, ?> raw)) continue;
|
||||
Object feeItem = raw.get("feeItem");
|
||||
if (!isBlank(feeItem)) names.add(String.valueOf(feeItem));
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(names);
|
||||
}
|
||||
|
||||
private void validateNonNegative(BigDecimal value, String field) {
|
||||
if (value != null && value.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new ServiceException(field + "不能小于0");
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal adjustedAfterAmount(BigDecimal freightAmount, Map<String, BigDecimal> feeItems) {
|
||||
BigDecimal feeItemTotal = feeItems.values().stream().map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
|
||||
return (containsFreight ? feeItemTotal : freightAmount.add(feeItemTotal)).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private boolean isFreightFeeItem(String name) {
|
||||
return name != null && (name.contains("运费") || name.contains("运输费"));
|
||||
}
|
||||
|
||||
private void appendChange(List<String> changes, String field, BigDecimal before, BigDecimal after) {
|
||||
BigDecimal oldValue = money(before);
|
||||
BigDecimal newValue = money(after);
|
||||
if (oldValue.compareTo(newValue) != 0) {
|
||||
changes.add("【" + field + "】从[" + formatValue(oldValue) + "]调整为[" + formatValue(newValue) + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private String formatValue(BigDecimal value) {
|
||||
return money(value).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
private void refreshAdjustedDetail(ReceivablePayableDetail detail, List<ReceivablePayableCargoFee> rows) {
|
||||
BigDecimal freight = rows.stream().map(row -> money(row.getFreightAmount())).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal total = rows.stream().map(row -> money(row.getAfterAmount())).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
rows.forEach(row -> parseMap(row.getFeeItemsJson()).forEach((name, value) ->
|
||||
feeItems.merge(name, decimal(value), BigDecimal::add)));
|
||||
if (!rows.isEmpty()) {
|
||||
detail.setTransportQuantity(rows.get(0).getTransportQuantity());
|
||||
detail.setMileage(rows.get(0).getMileage());
|
||||
}
|
||||
detail.setFreightAmount(freight);
|
||||
detail.setOtherFeeAmount(total.subtract(freight));
|
||||
detail.setTotalAmount(total);
|
||||
detail.setFeeItemsJson(JsonUtil.toJson(feeItems));
|
||||
updateById(detail);
|
||||
}
|
||||
|
||||
private void rebuildDetailFee(ReceivablePayableDetail detail, String billingPlanId) {
|
||||
Waybill waybill = waybillService.getById(detail.getWaybillId());
|
||||
if (waybill == null) {
|
||||
throw new ServiceException("关联运单不存在");
|
||||
}
|
||||
ReceivablePayableCargoFee cargoFee = buildCargoFee(detail.getId(), waybill);
|
||||
ContractManage contract = contractManageService.getById(detail.getContractId());
|
||||
List<ReceivablePayableCargoFee> fees = calculatedFees(waybill, contract, billingPlanId);
|
||||
cargoFeeMapper.delete(Wrappers.<ReceivablePayableCargoFee>lambdaQuery().eq(ReceivablePayableCargoFee::getDetailId, detail.getId()));
|
||||
cargoFeeMapper.insert(cargoFee);
|
||||
detail.setFreightAmount(cargoFee.getFreightAmount());
|
||||
detail.setOtherFeeAmount(money(waybill.getOtherFeeTotal()));
|
||||
detail.setTotalAmount(cargoFee.getAfterAmount());
|
||||
detail.setFeeItemsJson(cargoFee.getFeeItemsJson());
|
||||
fees.forEach(fee -> { fee.setDetailId(detail.getId()); cargoFeeMapper.insert(fee); });
|
||||
BigDecimal freight = fees.stream().filter(this::isFreight).map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
detail.setFreightAmount(freight);
|
||||
detail.setOtherFeeAmount(total.subtract(freight));
|
||||
detail.setTotalAmount(total);
|
||||
detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap<String, BigDecimal>::new, (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll)));
|
||||
updateById(detail);
|
||||
}
|
||||
|
||||
private void closeDetails(List<Long> ids) {
|
||||
private void closeDetails(List<Long> ids, String settlementType) {
|
||||
if (Func.isEmpty(ids)) {
|
||||
throw new ServiceException("请选择需要关闭的明细");
|
||||
}
|
||||
for (ReceivablePayableDetail detail : listByIds(ids)) {
|
||||
if (Func.isNotEmpty(settlementType) && !Objects.equals(detail.getSettlementType(), settlementType(settlementType))) {
|
||||
throw new ServiceException("费用明细结算类型不匹配");
|
||||
}
|
||||
if (!"pending".equals(detail.getSettlementStatus())) {
|
||||
throw new ServiceException("仅待结算明细允许关闭");
|
||||
}
|
||||
@@ -408,9 +828,13 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
|
||||
private void saveChangeRecord(ReceivablePayableDetail detail, String content, String reason) {
|
||||
saveChangeRecord(detail, content, reason, "0001");
|
||||
}
|
||||
|
||||
private void saveChangeRecord(ReceivablePayableDetail detail, String content, String reason, String lineNo) {
|
||||
ReceivablePayableChangeRecord record = new ReceivablePayableChangeRecord();
|
||||
record.setDetailId(detail.getId());
|
||||
record.setLineNo("0001");
|
||||
record.setLineNo(lineNo);
|
||||
record.setCargoName(detail.getCargoName());
|
||||
record.setChangeContent(content);
|
||||
record.setAdjustUser(AuthUtil.getUserId());
|
||||
@@ -420,6 +844,16 @@ public class ReceivablePayableDetailServiceImpl
|
||||
changeRecordMapper.insert(record);
|
||||
}
|
||||
|
||||
private void applyManualAdjustment(ReceivablePayableDetail detail, BigDecimal amount, String feeItem, String reason) {
|
||||
ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee();
|
||||
fee.setDetailId(detail.getId()); fee.setWaybillId(detail.getWaybillId()); fee.setLineNo("ADJ-" + System.currentTimeMillis());
|
||||
fee.setCargoName(Func.isEmpty(feeItem) ? "手工调差" : feeItem); fee.setBillingFactor("手工调整"); fee.setBillingType("手工调差");
|
||||
fee.setOriginalAmount(BigDecimal.ZERO); fee.setAdjustAmount(money(amount)); fee.setAfterAmount(money(amount)); fee.setFeeItemsJson(JsonUtil.toJson(Map.of(fee.getCargoName(), money(amount)))); fee.setRemark(reason);
|
||||
cargoFeeMapper.insert(fee);
|
||||
detail.setOtherFeeAmount(money(detail.getOtherFeeAmount()).add(money(amount)));
|
||||
detail.setTotalAmount(money(detail.getTotalAmount()).add(money(amount))); updateById(detail);
|
||||
}
|
||||
|
||||
private ReceivablePayableDetail getExisting(Long id) {
|
||||
ReceivablePayableDetail detail = getById(id);
|
||||
if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) {
|
||||
@@ -428,12 +862,21 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return detail;
|
||||
}
|
||||
|
||||
private boolean existsByWaybill(Long waybillId) {
|
||||
private boolean existsByWaybill(Long waybillId, String settlementType) {
|
||||
return count(Wrappers.<ReceivablePayableDetail>lambdaQuery()
|
||||
.eq(ReceivablePayableDetail::getWaybillId, waybillId)
|
||||
.eq(ReceivablePayableDetail::getSettlementType, settlementType)
|
||||
.eq(ReceivablePayableDetail::getIsDeleted, 0)) > 0;
|
||||
}
|
||||
|
||||
private String settlementType(String value) {
|
||||
if (Func.isEmpty(value)) return "receivable";
|
||||
if (!List.of("receivable", "payable").contains(value)) {
|
||||
throw new ServiceException("结算类型不正确");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private void validateGenerateRequest(ReceivablePayableGenerateRequest request, boolean requireWaybill) {
|
||||
if (Func.isEmpty(request.getContractId())) {
|
||||
throw new ServiceException("请选择运单合同");
|
||||
@@ -441,6 +884,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
if (Func.isEmpty(request.getBillingPlanId())) {
|
||||
throw new ServiceException("请选择计费方案");
|
||||
}
|
||||
settlementType(request.getSettlementType());
|
||||
if (requireWaybill && Func.isEmpty(request.getWaybillIds())) {
|
||||
throw new ServiceException("请选择需要生成费用的运单");
|
||||
}
|
||||
|
||||
+14
-1
@@ -42,11 +42,13 @@ import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillVO;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.wrapper.WaybillWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
@@ -62,6 +64,7 @@ import java.util.stream.Collectors;
|
||||
* @author Chill
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill> implements IWaybillService {
|
||||
|
||||
private static final String STATUS_DRAFT = "draft";
|
||||
@@ -69,6 +72,10 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
@jakarta.annotation.Resource
|
||||
private ILoadingManageService loadingManageService;
|
||||
|
||||
@jakarta.annotation.Resource
|
||||
@org.springframework.context.annotation.Lazy
|
||||
private IReceivablePayableDetailService receivablePayableDetailService;
|
||||
|
||||
@Override
|
||||
public IPage<WaybillVO> selectWaybillPage(IPage<Waybill> page, WaybillVO waybill) {
|
||||
IPage<Waybill> entityPage = page(page, buildQuery(waybill));
|
||||
@@ -284,7 +291,11 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
throw new ServiceException("当前状态不允许完成");
|
||||
}
|
||||
waybill.setBusinessStatus("completed");
|
||||
return updateById(waybill);
|
||||
boolean updated = updateById(waybill);
|
||||
if (updated) {
|
||||
receivablePayableDetailService.generateForCompletedWaybills(List.of(waybill.getId()));
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -300,6 +311,8 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
complete(waybill.getId());
|
||||
result.setSuccessCount(result.getSuccessCount() + 1);
|
||||
} catch (Exception exception) {
|
||||
log.error("批量完成运单失败,waybillId:{}, waybillNo:{}, failureReason:{}",
|
||||
waybill.getId(), waybill.getWaybillNo(), exception.getMessage(), exception);
|
||||
result.setSkippedCount(result.getSkippedCount() + 1);
|
||||
result.getSkippedCodes().add(waybill.getWaybillNo());
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.wrapper;
|
||||
|
||||
import org.springblade.core.mp.support.BaseEntityWrapper;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.pojo.entity.InsuranceOcrTemplate;
|
||||
import org.springblade.transport.pojo.vo.InsuranceOcrTemplateVO;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 保险OCR识别模板包装类。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class InsuranceOcrTemplateWrapper extends BaseEntityWrapper<InsuranceOcrTemplate, InsuranceOcrTemplateVO> {
|
||||
|
||||
public static InsuranceOcrTemplateWrapper build() {
|
||||
return new InsuranceOcrTemplateWrapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsuranceOcrTemplateVO entityVO(InsuranceOcrTemplate insuranceOcrTemplate) {
|
||||
InsuranceOcrTemplateVO insuranceOcrTemplateVO = Objects.requireNonNull(BeanUtil.copyProperties(insuranceOcrTemplate, InsuranceOcrTemplateVO.class));
|
||||
insuranceOcrTemplateVO.setCreateUserName(UserCache.getUserRealName(insuranceOcrTemplate.getCreateUser()));
|
||||
insuranceOcrTemplateVO.setUpdateUserName(UserCache.getUserRealName(insuranceOcrTemplate.getUpdateUser()));
|
||||
return insuranceOcrTemplateVO;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -94,6 +94,17 @@ thirdParty:
|
||||
# MK开放接口地址
|
||||
baseUrl: http://127.0.0.1:8080
|
||||
|
||||
#百度OCR配置,API Key和Secret Key请通过环境变量注入
|
||||
baidu:
|
||||
ocr:
|
||||
enabled: ${BAIDU_OCR_ENABLED:false}
|
||||
api-key: ${BAIDU_OCR_API_KEY:}
|
||||
secret-key: ${BAIDU_OCR_SECRET_KEY:}
|
||||
endpoint: ${BAIDU_OCR_ENDPOINT:https://aip.baidubce.com}
|
||||
connect-timeout: 5s
|
||||
request-timeout: 30s
|
||||
token-refresh-advance: 1m
|
||||
|
||||
|
||||
powerjob:
|
||||
worker:
|
||||
|
||||
@@ -66,6 +66,17 @@ thirdParty:
|
||||
# MK开放接口地址
|
||||
baseUrl: http://127.0.0.1:8080
|
||||
|
||||
#百度OCR配置,API Key和Secret Key请通过环境变量注入
|
||||
baidu:
|
||||
ocr:
|
||||
enabled: ${BAIDU_OCR_ENABLED:false}
|
||||
api-key: ${BAIDU_OCR_API_KEY:}
|
||||
secret-key: ${BAIDU_OCR_SECRET_KEY:}
|
||||
endpoint: ${BAIDU_OCR_ENDPOINT:https://aip.baidubce.com}
|
||||
connect-timeout: 5s
|
||||
request-timeout: 30s
|
||||
token-refresh-advance: 1m
|
||||
|
||||
powerjob:
|
||||
worker:
|
||||
server-address: 172.16.203.228:7700
|
||||
|
||||
@@ -43,3 +43,14 @@ blade:
|
||||
url: jdbc:mysql://192.168.0.188:3306/bladex?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
|
||||
#百度OCR配置,API Key和Secret Key请通过环境变量注入
|
||||
baidu:
|
||||
ocr:
|
||||
enabled: ${BAIDU_OCR_ENABLED:false}
|
||||
api-key: ${BAIDU_OCR_API_KEY:}
|
||||
secret-key: ${BAIDU_OCR_SECRET_KEY:}
|
||||
endpoint: ${BAIDU_OCR_ENDPOINT:https://aip.baidubce.com}
|
||||
connect-timeout: 5s
|
||||
request-timeout: 30s
|
||||
token-refresh-advance: 1m
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 合同费用生成、结算配置及付款比例设置
|
||||
ALTER TABLE blade_contract_manage
|
||||
ADD COLUMN fee_generation_mode varchar(20) DEFAULT 'system' COMMENT '费用生成模式:system系统生成,manual手动生成' AFTER billing_enabled,
|
||||
ADD COLUMN pre_settlement_config_json text DEFAULT NULL COMMENT '预结算配置JSON' AFTER settlement_rule_json,
|
||||
ADD COLUMN formal_settlement_config_json text DEFAULT NULL COMMENT '正式结算配置JSON' AFTER pre_settlement_config_json,
|
||||
ADD COLUMN payment_ratio_json text DEFAULT NULL COMMENT '付款比例设置JSON' AFTER formal_settlement_config_json;
|
||||
@@ -0,0 +1,32 @@
|
||||
-- ----------------------------
|
||||
-- Table structure for blade_insurance_ocr_template
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `blade_insurance_ocr_template`;
|
||||
CREATE TABLE `blade_insurance_ocr_template` (
|
||||
`id` bigint(20) NOT NULL COMMENT '主键',
|
||||
`tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID',
|
||||
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板名称',
|
||||
`mapping_config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '字段映射配置JSON',
|
||||
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
|
||||
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_user` bigint(20) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`status` int(11) NOT NULL DEFAULT '1' COMMENT '状态',
|
||||
`is_deleted` int(11) NOT NULL DEFAULT '0' COMMENT '是否已删除',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_insurance_ocr_template_name` (`tenant_id`, `name`) USING BTREE,
|
||||
KEY `idx_insurance_ocr_template_status` (`status`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='保险OCR识别模板';
|
||||
|
||||
-- ----------------------------
|
||||
-- Menu data for insurance OCR template
|
||||
-- parent_id:基础配置 1164733399668962201
|
||||
-- ----------------------------
|
||||
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) VALUES
|
||||
(2086000000000000001, 1164733399668962201, 'insurance_ocr_template', '保险OCR识别模板', 'insurance_ocr_template', '/base/insurance-ocr-template', 'iconfont iconicon_doc', 90, 1, 0, 1, NULL, '', 0),
|
||||
(2086000000000000002, 2086000000000000001, 'insurance_ocr_template_add', '新增', 'insurance_ocr_template_add', '', '', 1, 2, 0, 1, NULL, '', 0),
|
||||
(2086000000000000003, 2086000000000000001, 'insurance_ocr_template_edit', '编辑', 'insurance_ocr_template_edit', '', '', 2, 2, 0, 1, NULL, '', 0),
|
||||
(2086000000000000004, 2086000000000000001, 'insurance_ocr_template_delete', '删除', 'insurance_ocr_template_delete', '', '', 3, 2, 0, 1, NULL, '', 0),
|
||||
(2086000000000000005, 2086000000000000001, 'insurance_ocr_template_view', '查看', 'insurance_ocr_template_view', '', '', 4, 2, 0, 1, NULL, '', 0),
|
||||
(2086000000000000006, 2086000000000000001, 'insurance_ocr_template_list', '列表', 'insurance_ocr_template_list', '/blade-transport/insurance-ocr-template/list', '', 5, 2, 0, 1, NULL, '', 0);
|
||||
Reference in New Issue
Block a user