新增凭证管理

This commit is contained in:
2026-08-12 12:42:07 +08:00
parent 7f31952bff
commit 4ad3108c4d
28 changed files with 732 additions and 2 deletions
@@ -59,4 +59,10 @@ public class FileTaskCreateDTO implements Serializable {
*/
@Schema(description = "是否强制创建新任务,默认 false,如果存在相同md5的任务,会创建失败,true则新创建一个任务")
private boolean force;
@Schema(description = "业务类型")
private String businessType;
@Schema(description = "业务标识")
private String businessId;
}
@@ -91,6 +91,12 @@ public class FileTask implements Serializable {
@Schema(description = "状态 uploading:正在上传 completed:上传完成 failed:上传失败 paused:已暂停")
private String status;
@Schema(description = "业务类型")
private String businessType;
@Schema(description = "业务标识")
private String businessId;
/**
* 创建人-用户名称
*/
@@ -87,6 +87,12 @@ public class FileTaskVO implements Serializable {
@Schema(description = "状态 uploading:正在上传 completed:上传完成 failed:上传失败 paused:已暂停")
private String status;
@Schema(description = "业务类型")
private String businessType;
@Schema(description = "业务标识")
private String businessId;
/**
* 创建人-用户名称
*/
@@ -0,0 +1,17 @@
package org.springblade.transport.pojo.dto;
import lombok.Data;
import java.util.List;
@Data
public class VoucherManageSubmitRequest {
private Long id;
private String voucherBatchNo;
private Long projectId;
private String projectName;
private String fileName;
private String fileUrl;
private Long fileTaskId;
private List<Long> waybillImportBatchIds;
}
@@ -0,0 +1,10 @@
package org.springblade.transport.pojo.dto;
import lombok.Data;
/** 创建执行凭证上传草稿。 */
@Data
public class VoucherUploadDraftRequest {
private Long projectId;
private String fileName;
}
@@ -53,6 +53,9 @@ public class CustomerInvoiceInfo extends TenantEntity {
@Schema(description = "受票方名称")
private String invoiceTitle;
@Schema(description = "发票类型")
private String invoiceType;
@Schema(description = "纳税人识别号")
private String taxNo;
@@ -0,0 +1,34 @@
package org.springblade.transport.pojo.entity;
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;
/** 凭证批次。 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("blade_voucher_manage")
@Schema(description = "凭证管理")
public class VoucherManage extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
private Long projectId;
private String projectName;
private String voucherBatchNo;
private String waybillBatchNo;
private String fileName;
private String fileUrl;
private Long fileTaskId;
private String uploadSource;
private String carrierName;
private String processStatus;
private Integer voucherCount;
private Integer relatedWaybillCount;
private Integer unRelatedWaybillCount;
private String auditStatus;
}
@@ -0,0 +1,21 @@
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import java.io.Serial;
/** 凭证与运单批次关联。 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("blade_voucher_waybill_batch")
public class VoucherWaybillBatch extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
private Long voucherId;
private Long waybillImportBatchId;
private String waybillBatchNo;
private Integer waybillCount;
}
@@ -0,0 +1,19 @@
package org.springblade.transport.pojo.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.transport.pojo.entity.VoucherManage;
import java.io.Serial;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
public class VoucherManageVO extends VoucherManage {
@Serial private static final long serialVersionUID = 1L;
@TableField(exist = false) private String createUserName;
@TableField(exist = false) private String updateUserName;
@TableField(exist = false) private LocalDateTime createTimeStart;
@TableField(exist = false) private LocalDateTime createTimeEnd;
}
@@ -13,11 +13,14 @@ 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.R;
import org.springblade.core.mp.support.Query;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.file.pojo.dto.FileTaskCreateDTO;
import org.springblade.file.pojo.dto.FileTaskUpdateDTO;
import org.springblade.file.pojo.dto.IdsDTO;
import org.springblade.file.pojo.vo.FileTaskUpdateVO;
import org.springblade.file.service.IFileTaskService;
import org.springblade.file.service.IFileService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -35,6 +38,7 @@ import org.springframework.web.bind.annotation.*;
public class FileTaskController extends BladeController {
private final IFileTaskService fileTaskService;
private final IFileService fileService;
@GetMapping("/queryFileTask")
@Operation(summary = "根据文件的MD5值查询文件任务", description = "文件的MD5值检查分段,参数名:md5")
@@ -43,6 +47,24 @@ public class FileTaskController extends BladeController {
return R.data(fileTask);
}
@GetMapping("/file-url")
@Operation(summary = "获取已完成上传任务文件地址")
public R<String> fileUrl(@RequestParam Long id) {
org.springblade.file.pojo.entity.FileTask task = fileTaskService.getById(id);
if (task == null || !org.springblade.file.pojo.enums.FileTaskStatus.COMPLETED.getCode().equals(task.getStatus())) {
return R.fail("文件尚未上传完成");
}
return R.data(fileService.getFileUrl(task.getObjectKey(), null));
}
@GetMapping("/list")
@Operation(summary = "上传任务分页")
public R<IPage<FileTaskUpdateVO>> list(Query query, @RequestParam(required = false) String businessType,
@RequestParam(required = false) String businessId, @RequestParam(required = false) String attachmentName,
@RequestParam(required = false) String status) {
return R.data(fileTaskService.selectPage(query, businessType, businessId, attachmentName, status));
}
@ApiLog("上传任务-创建文件任务")
@PostMapping("/createFileTask")
@Operation(summary = "创建文件任务", description = "创建文件任务")
@@ -26,6 +26,8 @@
package org.springblade.file.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.support.Query;
import org.springblade.file.pojo.dto.FileTaskCreateDTO;
import org.springblade.file.pojo.dto.FileTaskUpdateDTO;
import org.springblade.file.pojo.entity.FileTask;
@@ -91,4 +93,6 @@ public interface IFileTaskService extends IService<FileTask> {
* @return
*/
boolean updatePaused(List<Long> param);
IPage<FileTaskUpdateVO> selectPage(Query query, String businessType, String businessId, String attachmentName, String status);
}
@@ -28,12 +28,15 @@ package org.springblade.file.service.impl;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.utils.CollectionUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.file.config.ObsProperties;
import org.springblade.file.convert.FileTaskConvert;
import org.springblade.file.listener.FileUploadedEvent;
@@ -125,6 +128,8 @@ public class FileTaskServiceImpl extends ServiceImpl<FileTaskMapper, FileTask> i
addParam.setUploadId(uploadId);
// 正在上传
addParam.setStatus(FileTaskStatus.UPLOADING.getCode());
// 兼容历史表未配置 is_deleted 默认值的场景。
addParam.setIsDeleted(0);
this.save(addParam);
return addParam.getId();
@@ -317,6 +322,17 @@ public class FileTaskServiceImpl extends ServiceImpl<FileTaskMapper, FileTask> i
);
}
@Override
public IPage<FileTaskUpdateVO> selectPage(org.springblade.core.mp.support.Query query, String businessType, String businessId, String attachmentName, String status) {
IPage<FileTask> page = page(Condition.getPage(query), Wrappers.<FileTask>lambdaQuery()
.eq(StringUtil.isNotBlank(businessType), FileTask::getBusinessType, businessType)
.eq(StringUtil.isNotBlank(businessId), FileTask::getBusinessId, businessId)
.like(StringUtil.isNotBlank(attachmentName), FileTask::getAttachmentName, attachmentName)
.eq(StringUtil.isNotBlank(status), FileTask::getStatus, status)
.eq(FileTask::getCreateUser, AuthUtil.getUserId()).orderByDesc(FileTask::getCreateTime));
return page.convert(this::getFileTaskUpdateVO);
}
/**
* 查询文件任务
* @param id
@@ -28,7 +28,10 @@ package org.springblade.file.service.impl;
import io.minio.GetObjectArgs;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.ComposeObjectArgs;
import io.minio.ComposeSource;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.UploadObjectArgs;
import io.minio.http.Method;
import lombok.RequiredArgsConstructor;
@@ -43,7 +46,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* MinIO文件存储
@@ -59,6 +65,44 @@ public class MinioFileServiceImpl extends AbstractFileService {
private final FileStorageProperties.StoreProperties properties;
private final MinioClient minioClient;
@Override
public String initiateMultipartUpload(String objectKey) {
return UUID.randomUUID().toString();
}
@Override
public String getUploadPartUrl(String objectKey, String uploadId, Integer partNumber, Long expires) {
try {
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.PUT).bucket(properties.getBucketName()).object(partObjectKey(objectKey, uploadId, partNumber))
.expiry(Math.toIntExact(resolveExpires(expires))).build());
} catch (Exception exception) {
log.error("MinIO获取分片上传链接异常 objectKey:{}", objectKey, exception);
throw new ServiceException("获取分片上传链接异常");
}
}
@Override
public void completeMultipartUpload(String objectKey, String uploadId, List<org.springblade.file.pojo.vo.PartEtagVO> partEtags) {
try {
List<ComposeSource> sources = new ArrayList<>();
for (org.springblade.file.pojo.vo.PartEtagVO part : partEtags) {
sources.add(ComposeSource.builder().bucket(properties.getBucketName()).object(partObjectKey(objectKey, uploadId, part.getPartNumber())).build());
}
minioClient.composeObject(ComposeObjectArgs.builder().bucket(properties.getBucketName()).object(resolveObjectKey(objectKey)).sources(sources).build());
for (org.springblade.file.pojo.vo.PartEtagVO part : partEtags) {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(properties.getBucketName()).object(partObjectKey(objectKey, uploadId, part.getPartNumber())).build());
}
} catch (Exception exception) {
log.error("MinIO合并分片异常 objectKey{}", objectKey, exception);
throw new ServiceException("MinIO合并分片异常");
}
}
private String partObjectKey(String objectKey, String uploadId, Integer partNumber) {
return resolveObjectKey(".multipart/" + uploadId + "/" + objectKey + ".part-" + partNumber);
}
@Override
public void uploadFile(String objectKey, InputStream inputStream) {
try {
@@ -0,0 +1,73 @@
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.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
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.transport.pojo.dto.VoucherManageSubmitRequest;
import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
import org.springblade.transport.pojo.entity.VoucherManage;
import org.springblade.transport.pojo.vo.VoucherManageVO;
import org.springblade.transport.service.IVoucherManageService;
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;
import java.util.List;
import java.util.Map;
@RestController
@AllArgsConstructor
@PreAuth(menu = "voucher_manage")
@RequestMapping("/voucher-manage")
@Tag(name = "凭证管理", description = "执行凭证批次管理")
public class VoucherManageController extends BladeController {
private final IVoucherManageService voucherManageService;
@GetMapping("/list")
@ApiOperationSupport(order = 1)
@Operation(summary = "凭证管理分页")
public R<IPage<VoucherManageVO>> list(VoucherManageVO voucherManage, Query query) {
return R.data(voucherManageService.selectPage(Condition.getPage(query), voucherManage));
}
@GetMapping("/detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "凭证详情")
public R<VoucherManageVO> detail(@RequestParam Long id) { return R.data(voucherManageService.detail(id)); }
@PostMapping("/submit")
@ApiOperationSupport(order = 3)
@Operation(summary = "上传凭证或更换运输批次")
public R submit(@RequestBody VoucherManageSubmitRequest request) { voucherManageService.submit(request); return R.success("提交成功"); }
@PostMapping("/upload-draft")
@ApiOperationSupport(order = 4)
@Operation(summary = "创建凭证上传草稿")
public R<VoucherManage> createUploadDraft(@RequestBody VoucherUploadDraftRequest request) {
return R.data(voucherManageService.createUploadDraft(request));
}
@PostMapping("/remove")
@ApiOperationSupport(order = 5)
@Operation(summary = "删除凭证")
public R remove(@RequestParam Long id) { voucherManageService.removeVoucher(id); return R.success("删除成功"); }
@GetMapping("/waybill-batches")
@ApiOperationSupport(order = 6)
@Operation(summary = "可关联运输批次")
public R<List<Map<String, Object>>> waybillBatches(@RequestParam(required = false) String batchNo,
@RequestParam(required = false) String createUser, @RequestParam(required = false) Integer waybillCount,
@RequestParam(required = false) String createTimeStart, @RequestParam(required = false) String createTimeEnd) {
return R.data(voucherManageService.selectableWaybillBatches(batchNo, createUser, waybillCount, createTimeStart, createTimeEnd));
}
}
@@ -0,0 +1,6 @@
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.transport.pojo.entity.VoucherManage;
public interface VoucherManageMapper extends BaseMapper<VoucherManage> { }
@@ -0,0 +1,15 @@
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.transport.pojo.entity.VoucherWaybillBatch;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
public interface VoucherWaybillBatchMapper extends BaseMapper<VoucherWaybillBatch> {
List<Map<String, Object>> selectWaybillBatches(@Param("tenantId") String tenantId, @Param("batchNo") String batchNo, @Param("createUser") String createUser,
@Param("waybillCount") Integer waybillCount, @Param("createTimeStart") String createTimeStart, @Param("createTimeEnd") String createTimeEnd);
List<Map<String, Object>> selectWaybillBatchesByIds(@Param("tenantId") String tenantId, @Param("ids") List<Long> ids);
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.transport.mapper.VoucherWaybillBatchMapper">
<select id="selectWaybillBatches" resultType="java.util.LinkedHashMap">
SELECT b.id, b.batch_no AS batchNo, b.waybill_count AS waybillCount, b.create_time AS createTime,
b.create_user AS createUser, u.real_name AS createUserName
FROM blade_waybill_import_batch b
LEFT JOIN blade_user u ON u.id = b.create_user
WHERE b.is_deleted = 0 AND b.tenant_id = #{tenantId}
<if test="batchNo != null and batchNo != ''">AND b.batch_no LIKE CONCAT('%', #{batchNo}, '%')</if>
<if test="createUser != null and createUser != ''">AND b.create_user = #{createUser}</if>
<if test="waybillCount != null">AND b.waybill_count = #{waybillCount}</if>
<if test="createTimeStart != null and createTimeStart != ''">AND b.create_time <![CDATA[>=]]> #{createTimeStart}</if>
<if test="createTimeEnd != null and createTimeEnd != ''">AND b.create_time <![CDATA[<=]]> #{createTimeEnd}</if>
ORDER BY b.create_time DESC
</select>
<select id="selectWaybillBatchesByIds" resultType="java.util.LinkedHashMap">
SELECT b.id, b.batch_no AS batchNo, b.waybill_count AS waybillCount FROM blade_waybill_import_batch b
INNER JOIN blade_waybill w ON w.import_batch_id = b.id AND w.is_deleted = 0
WHERE b.is_deleted = 0 AND b.tenant_id = #{tenantId} AND w.id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
GROUP BY b.id, b.batch_no, b.waybill_count
</select>
</mapper>
@@ -0,0 +1,20 @@
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.VoucherManageSubmitRequest;
import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
import org.springblade.transport.pojo.entity.VoucherManage;
import org.springblade.transport.pojo.vo.VoucherManageVO;
import java.util.List;
import java.util.Map;
public interface IVoucherManageService extends BaseService<VoucherManage> {
IPage<VoucherManageVO> selectPage(IPage<VoucherManage> page, VoucherManageVO query);
VoucherManageVO detail(Long id);
void submit(VoucherManageSubmitRequest request);
VoucherManage createUploadDraft(VoucherUploadDraftRequest request);
void removeVoucher(Long id);
List<Map<String, Object>> selectableWaybillBatches(String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd);
}
@@ -216,7 +216,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
CustomerArchiveVO detail = detail(id);
prepare(detail);
validateBase(detail);
validateApproval(detail);
// 临时客商审核通过时由流程转为正式客商,不要求补齐正式客商材料。
validateApproval(detail, Objects.equals(detail.getAccessType(), ACCESS_TEMPORARY));
CustomerArchive before = Objects.requireNonNull(BeanUtil.copyProperties(detail, CustomerArchive.class));
CustomerArchive after = copyCustomer(before);
after.setAccessType(ACCESS_FORMAL);
@@ -382,7 +383,11 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
}
private void validateApproval(CustomerArchiveVO customer) {
if (Objects.equals(customer.getAccessType(), ACCESS_FORMAL)) {
validateApproval(customer, false);
}
private void validateApproval(CustomerArchiveVO customer, boolean temporaryToFormal) {
if (Objects.equals(customer.getAccessType(), ACCESS_FORMAL) && !temporaryToFormal) {
if (Func.isEmpty(customer.getQualificationAttachments())) {
throw new ServiceException("正式客商提交审核前必须上传客商材料");
}
@@ -459,6 +464,12 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
if (Func.isEmpty(invoiceVO.getInvoiceTitle()) && Func.isEmpty(invoiceVO.getTaxNo())) {
continue;
}
if (Func.isEmpty(invoiceVO.getInvoiceType())) {
throw new ServiceException("发票类型不能为空");
}
if (!List.of("增值税专用发票", "普通发票").contains(invoiceVO.getInvoiceType())) {
throw new ServiceException("发票类型不合法");
}
invoiceVO.setRegisteredAddress(buildAddress(invoiceVO.getRegisteredRegionName(), invoiceVO.getRegisteredDetailAddress(), invoiceVO.getRegisteredAddress()));
invoiceVO.setReceiverAddress(buildAddress(invoiceVO.getReceiverRegionName(), invoiceVO.getReceiverDetailAddress(), invoiceVO.getReceiverAddress()));
CustomerInvoiceInfo invoice = Objects.requireNonNull(BeanUtil.copyProperties(invoiceVO, CustomerInvoiceInfo.class));
@@ -0,0 +1,159 @@
package org.springblade.transport.service.impl;
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.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.mapper.VoucherManageMapper;
import org.springblade.transport.mapper.VoucherWaybillBatchMapper;
import org.springblade.transport.pojo.dto.VoucherManageSubmitRequest;
import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
import org.springblade.transport.pojo.entity.VoucherManage;
import org.springblade.transport.pojo.entity.VoucherWaybillBatch;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.vo.VoucherManageVO;
import org.springblade.transport.service.IVoucherManageService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.wrapper.VoucherManageWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMapper, VoucherManage> implements IVoucherManageService {
private final VoucherWaybillBatchMapper voucherWaybillBatchMapper;
private final IProjectApplyService projectApplyService;
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService) {
this.voucherWaybillBatchMapper = voucherWaybillBatchMapper;
this.projectApplyService = projectApplyService;
}
@Override
public IPage<VoucherManageVO> selectPage(IPage<VoucherManage> page, VoucherManageVO query) {
return VoucherManageWrapper.build().pageVO(page(page, buildQuery(query)));
}
@Override
public VoucherManageVO detail(Long id) {
VoucherManage record = getById(id);
if (record == null || Objects.equals(record.getIsDeleted(), 1)) throw new ServiceException("凭证批次不存在");
return VoucherManageWrapper.build().entityVO(record);
}
@Override
@Transactional(rollbackFor = Exception.class)
public VoucherManage createUploadDraft(VoucherUploadDraftRequest request) {
if (Func.isEmpty(request.getProjectId())) throw new ServiceException("请先选择项目名称");
if (Func.isEmpty(request.getFileName())) throw new ServiceException("文件名称不能为空");
ProjectApply project = projectApplyService.getById(request.getProjectId());
if (project == null || !List.of("approved", "change_approved").contains(project.getApprovalStatus())) {
throw new ServiceException("仅允许选择审核通过或变更审核通过的项目");
}
VoucherManage voucher = new VoucherManage();
voucher.setProjectId(project.getId());
voucher.setProjectName(project.getProjectName());
voucher.setVoucherBatchNo(nextCode());
voucher.setFileName(request.getFileName());
voucher.setFileUrl("");
voucher.setUploadSource("内部");
voucher.setProcessStatus("上传中");
voucher.setAuditStatus("-");
voucher.setVoucherCount(0);
voucher.setRelatedWaybillCount(0);
voucher.setUnRelatedWaybillCount(0);
save(voucher);
return voucher;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submit(VoucherManageSubmitRequest request) {
if (Func.isEmpty(request.getProjectId())) throw new ServiceException("请选择项目名称");
if (Func.isEmpty(request.getFileName()) || Func.isEmpty(request.getFileUrl())) throw new ServiceException("请上传执行凭证压缩包");
if (Func.isEmpty(request.getWaybillImportBatchIds())) throw new ServiceException("请至少关联一个运输批次");
VoucherManage voucher = Func.isEmpty(request.getId()) ? new VoucherManage() : getById(request.getId());
if (voucher == null) throw new ServiceException("凭证批次不存在");
ProjectApply project = projectApplyService.getById(request.getProjectId());
if (project == null || !List.of("approved", "change_approved").contains(project.getApprovalStatus())) throw new ServiceException("仅允许选择审核通过或变更审核通过的项目");
voucher.setProjectId(project.getId());
voucher.setProjectName(project.getProjectName());
voucher.setFileName(request.getFileName());
voucher.setFileUrl(request.getFileUrl());
voucher.setFileTaskId(request.getFileTaskId());
voucher.setUploadSource("内部");
voucher.setProcessStatus("上传中");
voucher.setAuditStatus("-");
voucher.setVoucherCount(0);
voucher.setRelatedWaybillCount(0);
voucher.setUnRelatedWaybillCount(0);
if (Func.isEmpty(voucher.getVoucherBatchNo())) voucher.setVoucherBatchNo(nextCode());
saveOrUpdate(voucher);
voucherWaybillBatchMapper.delete(Wrappers.<VoucherWaybillBatch>lambdaQuery().eq(VoucherWaybillBatch::getVoucherId, voucher.getId()));
List<Map<String, Object>> batches = selectableWaybillBatchesByIds(request.getWaybillImportBatchIds());
if (batches.size() != request.getWaybillImportBatchIds().size()) throw new ServiceException("存在无效的运输批次");
List<VoucherWaybillBatch> relations = new ArrayList<>();
for (Map<String, Object> batch : batches) {
VoucherWaybillBatch relation = new VoucherWaybillBatch();
relation.setVoucherId(voucher.getId());
relation.setWaybillImportBatchId(((Number) batch.get("id")).longValue());
relation.setWaybillBatchNo((String) batch.get("batchNo"));
relation.setWaybillCount(((Number) batch.get("waybillCount")).intValue());
relations.add(relation);
}
relations.forEach(voucherWaybillBatchMapper::insert);
voucher.setWaybillBatchNo(relations.stream().map(VoucherWaybillBatch::getWaybillBatchNo).collect(Collectors.joining(",")));
int total = relations.stream().mapToInt(VoucherWaybillBatch::getWaybillCount).sum();
voucher.setRelatedWaybillCount(total);
voucher.setUnRelatedWaybillCount(0);
updateById(voucher);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeVoucher(Long id) {
VoucherManage voucher = getById(id);
if (voucher == null) throw new ServiceException("凭证批次不存在");
deleteLogic(List.of(id));
voucherWaybillBatchMapper.delete(Wrappers.<VoucherWaybillBatch>lambdaQuery().eq(VoucherWaybillBatch::getVoucherId, id));
}
@Override
public List<Map<String, Object>> selectableWaybillBatches(String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd) {
return voucherWaybillBatchMapper.selectWaybillBatches(AuthUtil.getTenantId(), batchNo, createUser, waybillCount, createTimeStart, createTimeEnd);
}
private List<Map<String, Object>> selectableWaybillBatchesByIds(List<Long> ids) {
if (Func.isEmpty(ids)) return List.of();
return voucherWaybillBatchMapper.selectWaybillBatchesByIds(AuthUtil.getTenantId(), ids);
}
private LambdaQueryWrapper<VoucherManage> buildQuery(VoucherManageVO query) {
LambdaQueryWrapper<VoucherManage> wrapper = Wrappers.<VoucherManage>lambdaQuery().eq(VoucherManage::getIsDeleted, 0)
.like(Func.isNotEmpty(query.getVoucherBatchNo()), VoucherManage::getVoucherBatchNo, query.getVoucherBatchNo())
.eq(Func.isNotEmpty(query.getAuditStatus()), VoucherManage::getAuditStatus, query.getAuditStatus())
.eq(Func.isNotEmpty(query.getProcessStatus()), VoucherManage::getProcessStatus, query.getProcessStatus())
.like(Func.isNotEmpty(query.getWaybillBatchNo()), VoucherManage::getWaybillBatchNo, query.getWaybillBatchNo())
.eq(Func.isNotEmpty(query.getUploadSource()), VoucherManage::getUploadSource, query.getUploadSource())
.like(Func.isNotEmpty(query.getCarrierName()), VoucherManage::getCarrierName, query.getCarrierName());
if (query.getCreateTimeStart() != null) wrapper.ge(VoucherManage::getCreateTime, query.getCreateTimeStart());
if (query.getCreateTimeEnd() != null) wrapper.le(VoucherManage::getCreateTime, query.getCreateTimeEnd());
return wrapper.orderByDesc(VoucherManage::getCreateTime);
}
private synchronized String nextCode() {
String prefix = "PZ" + java.time.LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-";
long count = count(Wrappers.<VoucherManage>lambdaQuery().likeRight(VoucherManage::getVoucherBatchNo, prefix));
return prefix + String.format("%03d", count + 1);
}
}
@@ -0,0 +1,20 @@
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.VoucherManage;
import org.springblade.transport.pojo.vo.VoucherManageVO;
import java.util.Objects;
public class VoucherManageWrapper extends BaseEntityWrapper<VoucherManage, VoucherManageVO> {
public static VoucherManageWrapper build() { return new VoucherManageWrapper(); }
@Override
public VoucherManageVO entityVO(VoucherManage entity) {
VoucherManageVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, VoucherManageVO.class));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
return vo;
}
}
@@ -0,0 +1,42 @@
-- 文件服务数据库:分片上传任务主表。
-- 本脚本需与 blade_file_task_part 表配套执行;请在 blade-file 服务配置的数据源中运行。
CREATE TABLE IF NOT EXISTS `blade_file_task` (
`id` bigint NOT NULL COMMENT '主键ID',
`md5` varchar(128) NOT NULL COMMENT '文件MD5',
`object_key` varchar(1000) NOT NULL COMMENT '对象存储对象键',
`attachment_name` varchar(500) NOT NULL COMMENT '附件名称',
`suffix` varchar(50) DEFAULT NULL COMMENT '文件后缀',
`size` bigint NOT NULL COMMENT '文件大小,单位:字节',
`chunk_size` int NOT NULL COMMENT '分片大小,单位:字节',
`chunk_total` int NOT NULL COMMENT '分片总数',
`current_index` int NOT NULL DEFAULT 0 COMMENT '已完成分片数',
`upload_id` varchar(255) DEFAULT NULL COMMENT '对象存储上传任务ID',
`status` varchar(20) NOT NULL DEFAULT 'uploading' COMMENT '状态:uploading、completed、failed、paused',
`business_type` varchar(50) DEFAULT NULL COMMENT '业务类型,例如 voucher',
`business_id` varchar(64) DEFAULT NULL COMMENT '业务标识,例如凭证批次号',
`create_user_name` varchar(50) DEFAULT NULL COMMENT '创建人名称',
`create_user` bigint DEFAULT NULL COMMENT '创建人ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user_name` varchar(50) DEFAULT NULL COMMENT '更新人名称',
`update_user` bigint DEFAULT NULL COMMENT '更新人ID',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除:0否、1是',
PRIMARY KEY (`id`),
KEY `idx_file_task_md5` (`md5`),
KEY `idx_file_task_business` (`business_type`, `business_id`),
KEY `idx_file_task_create_user_time` (`create_user`, `create_time`),
KEY `idx_file_task_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件分片上传任务';
CREATE TABLE IF NOT EXISTS `blade_file_task_part` (
`id` bigint NOT NULL COMMENT '主键ID',
`file_task_id` bigint NOT NULL COMMENT '文件上传任务ID',
`part_number` int NOT NULL COMMENT '分片编号,从1开始',
`etag` varchar(255) NOT NULL COMMENT '分片ETag',
`create_user` bigint DEFAULT NULL COMMENT '创建人ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_file_task_part` (`file_task_id`, `part_number`),
KEY `idx_file_task_part_task` (`file_task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件分片上传任务分片记录';
@@ -0,0 +1,4 @@
-- 修复旧版 blade_file_task 的逻辑删除字段默认值。
-- 请在 blade-file 服务配置的数据源执行。
UPDATE `blade_file_task` SET `is_deleted` = 0 WHERE `is_deleted` IS NULL;
ALTER TABLE `blade_file_task` MODIFY COLUMN `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除:0否、1是';
@@ -0,0 +1,8 @@
-- 请在 blade_file 服务所使用的数据源执行本脚本,不要在 transport 库执行。
-- MySQL 8.0.29+ 支持 IF NOT EXISTS;若版本较低,请先确认字段和索引不存在再执行。
ALTER TABLE `blade_file_task`
ADD COLUMN IF NOT EXISTS `business_type` varchar(50) DEFAULT NULL COMMENT '业务类型',
ADD COLUMN IF NOT EXISTS `business_id` varchar(64) DEFAULT NULL COMMENT '业务标识';
CREATE INDEX IF NOT EXISTS `idx_file_task_business`
ON `blade_file_task` (`business_type`, `business_id`);
@@ -117,6 +117,7 @@ CREATE TABLE `blade_customer_invoice_info` (
`tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '000000' COMMENT '租户ID',
`customer_id` bigint NOT NULL COMMENT '客商ID',
`invoice_title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '受票方名称',
`invoice_type` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发票类型',
`tax_no` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '纳税人识别号',
`bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开户行名称',
`registered_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '注册电话',
@@ -0,0 +1,4 @@
-- 客商发票信息:新增发票类型
ALTER TABLE `blade_customer_invoice_info`
ADD COLUMN `invoice_type` varchar(30) DEFAULT NULL COMMENT '发票类型' AFTER `invoice_title`;
@@ -0,0 +1,42 @@
CREATE TABLE IF NOT EXISTS `blade_voucher_manage` (
`id` bigint NOT NULL,
`tenant_id` varchar(12) NOT NULL DEFAULT '000000',
`project_id` bigint NOT NULL COMMENT '项目ID',
`project_name` varchar(200) NOT NULL COMMENT '项目名称',
`voucher_batch_no` varchar(30) NOT NULL COMMENT '凭证批次号',
`waybill_batch_no` varchar(1000) DEFAULT NULL COMMENT '关联运单批次号',
`file_name` varchar(100) NOT NULL COMMENT '凭证压缩包文件名',
`file_url` varchar(1000) NOT NULL COMMENT '凭证压缩包地址',
`file_task_id` bigint DEFAULT NULL COMMENT '分片上传任务ID',
`upload_source` varchar(20) NOT NULL DEFAULT '内部' COMMENT '上传来源:内部、承运商',
`carrier_name` varchar(50) DEFAULT NULL COMMENT '承运商名称',
`process_status` varchar(20) NOT NULL DEFAULT '上传中' COMMENT '上传中、处理中、处理完成',
`voucher_count` int NOT NULL DEFAULT 0 COMMENT '凭证数量',
`related_waybill_count` int NOT NULL DEFAULT 0 COMMENT '已关联运单数',
`un_related_waybill_count` int NOT NULL DEFAULT 0 COMMENT '未关联运单数',
`audit_status` varchar(20) NOT NULL DEFAULT '-' COMMENT '待审核、审核通过、审核驳回',
`create_user` bigint DEFAULT NULL, `create_dept` bigint DEFAULT NULL,
`create_time` datetime DEFAULT NULL, `update_user` bigint DEFAULT NULL, `update_time` datetime DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT 1, `is_deleted` tinyint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`), UNIQUE KEY `uk_voucher_manage_no` (`tenant_id`, `voucher_batch_no`),
KEY `idx_voucher_manage_create_time` (`create_time`), KEY `idx_voucher_manage_process_status` (`process_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='凭证管理';
CREATE TABLE IF NOT EXISTS `blade_voucher_waybill_batch` (
`id` bigint NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000',
`voucher_id` bigint NOT NULL COMMENT '凭证批次ID', `waybill_import_batch_id` bigint NOT NULL COMMENT '运单导入批次ID',
`waybill_batch_no` varchar(64) NOT NULL COMMENT '运单批次号', `waybill_count` int NOT NULL DEFAULT 0 COMMENT '运单数',
`create_user` bigint DEFAULT NULL, `create_dept` bigint DEFAULT NULL, `create_time` datetime DEFAULT NULL,
`update_user` bigint DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` tinyint NOT NULL DEFAULT 1, `is_deleted` tinyint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`), UNIQUE KEY `uk_voucher_waybill_batch` (`voucher_id`, `waybill_import_batch_id`),
KEY `idx_voucher_waybill_batch_voucher` (`voucher_id`), KEY `idx_voucher_waybill_batch_waybill` (`waybill_import_batch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='凭证关联运单批次';
INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`)
VALUES
(2090000000000000900, 2090000000000000000, 'voucher_manage', '凭证管理', 'voucher_manage', '/business/voucher-manage', 'iconfont icon-caidanguanli', 90, 1, 0, 1, NULL, '', 0),
(2090000000000000901, 2090000000000000900, 'voucher_manage_view', '查看', 'voucher_manage_view', '', '', 1, 2, 0, 1, NULL, '', 0),
(2090000000000000902, 2090000000000000900, 'voucher_manage_add', '批量导入凭证', 'voucher_manage_add', '', '', 2, 2, 0, 1, NULL, '', 0),
(2090000000000000903, 2090000000000000900, 'voucher_manage_edit', '更换运单批次', 'voucher_manage_edit', '', '', 3, 2, 0, 1, NULL, '', 0),
(2090000000000000904, 2090000000000000900, 'voucher_manage_delete', '删除', 'voucher_manage_delete', '', '', 4, 2, 0, 1, NULL, '', 0)
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `path` = VALUES(`path`), `is_deleted` = 0;
@@ -0,0 +1,93 @@
-- MySQL 5.7+ 兼容:为已存在的 blade_voucher_manage 旧表补齐字段和索引。
-- 请在 transport 数据库执行。本脚本使用 information_schema 判断,不依赖 IF NOT EXISTS 语法。
DELIMITER $$
DROP PROCEDURE IF EXISTS `upgrade_blade_voucher_manage_20260812`$$
CREATE PROCEDURE `upgrade_blade_voucher_manage_20260812`()
BEGIN
DECLARE db_name varchar(128);
SET db_name = DATABASE();
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'tenant_id') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID' AFTER `id`;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'project_id') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `project_id` bigint NOT NULL DEFAULT 0 COMMENT '项目ID';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'project_name') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `project_name` varchar(200) NOT NULL DEFAULT '' COMMENT '项目名称';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'voucher_batch_no') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `voucher_batch_no` varchar(30) NOT NULL DEFAULT '' COMMENT '凭证批次号';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'waybill_batch_no') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `waybill_batch_no` varchar(1000) DEFAULT NULL COMMENT '关联运单批次号';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'file_name') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `file_name` varchar(100) NOT NULL DEFAULT '' COMMENT '凭证压缩包文件名';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'file_url') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `file_url` varchar(1000) NOT NULL DEFAULT '' COMMENT '凭证压缩包地址';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'file_task_id') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `file_task_id` bigint DEFAULT NULL COMMENT '分片上传任务ID';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'upload_source') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `upload_source` varchar(20) NOT NULL DEFAULT '内部' COMMENT '上传来源:内部、承运商';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'carrier_name') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `carrier_name` varchar(50) DEFAULT NULL COMMENT '承运商名称';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'process_status') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `process_status` varchar(20) NOT NULL DEFAULT '上传中' COMMENT '上传中、处理中、处理完成';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'voucher_count') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `voucher_count` int NOT NULL DEFAULT 0 COMMENT '凭证数量';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'related_waybill_count') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `related_waybill_count` int NOT NULL DEFAULT 0 COMMENT '已关联运单数';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'un_related_waybill_count') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `un_related_waybill_count` int NOT NULL DEFAULT 0 COMMENT '未关联运单数';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'audit_status') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `audit_status` varchar(20) NOT NULL DEFAULT '-' COMMENT '待审核、审核通过、审核驳回';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'create_user') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `create_user` bigint DEFAULT NULL COMMENT '创建人';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'create_dept') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `create_dept` bigint DEFAULT NULL COMMENT '创建部门';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'create_time') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `create_time` datetime DEFAULT NULL COMMENT '创建时间';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'update_user') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `update_user` bigint DEFAULT NULL COMMENT '更新人';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'update_time') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `update_time` datetime DEFAULT NULL COMMENT '更新时间';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'status') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'is_deleted') THEN
ALTER TABLE `blade_voucher_manage` ADD COLUMN `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND index_name = 'idx_voucher_manage_create_time') THEN
CREATE INDEX `idx_voucher_manage_create_time` ON `blade_voucher_manage` (`create_time`);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND index_name = 'idx_voucher_manage_process_status') THEN
CREATE INDEX `idx_voucher_manage_process_status` ON `blade_voucher_manage` (`process_status`);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND index_name = 'idx_voucher_manage_tenant_batch_no') THEN
CREATE INDEX `idx_voucher_manage_tenant_batch_no` ON `blade_voucher_manage` (`tenant_id`, `voucher_batch_no`);
END IF;
END$$
CALL `upgrade_blade_voucher_manage_20260812`()$$
DROP PROCEDURE `upgrade_blade_voucher_manage_20260812`$$
DELIMITER ;