diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcIndicatorResultServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcIndicatorResultServiceImpl.java
index c9ce52f..c6b162f 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcIndicatorResultServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcIndicatorResultServiceImpl.java
@@ -29,6 +29,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
+import java.net.URI;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -181,7 +182,7 @@ public class MesQcIndicatorResultServiceImpl implements MesQcIndicatorResultServ
* 按检测项的 resultType 校验明细值格式
*
*
FLOAT → 必须可解析为 BigDecimal;INTEGER → 必须可解析为整数;
- * TEXT / DICT / FILE → 放行
+ * DICT → 必须属于字典值域;FILE → 必须为 http/https URL;TEXT → 放行
*/
private void validateDetailValues(List items,
Map indicatorMap) {
@@ -219,7 +220,23 @@ public class MesQcIndicatorResultServiceImpl implements MesQcIndicatorResultServ
dictDataApi.validateDictDataList(dictType, Collections.singleton(item.getValue()));
}
}
- // FILE / TEXT 不做格式校验
+ if (Objects.equals(resultType, MesQcResultValueTypeEnum.FILE.getType())
+ && !isHttpUrl(item.getValue())) {
+ throw exception(QC_RESULT_VALUE_FORMAT_INVALID,
+ "检测项[" + indicator.getName() + "]要求文件 URL,实际值=" + item.getValue());
+ }
+ // TEXT 不做格式校验
+ }
+ }
+
+ private boolean isHttpUrl(String value) {
+ try {
+ URI uri = URI.create(value);
+ String scheme = uri.getScheme();
+ return StrUtil.isNotBlank(uri.getHost())
+ && ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme));
+ } catch (IllegalArgumentException e) {
+ return false;
}
}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeService.java
index 64ac10e..aaea87d 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeService.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeService.java
@@ -117,6 +117,15 @@ public interface MesWmArrivalNoticeService {
void validateArrivalNoticeAndLineExists(Long noticeId, Long lineId);
/**
+ * 校验到货通知单已就绪可被采购入库引用
+ * 校验状态为待入库 + 所有需检行都已完成 IQC
+ *
+ * @param id 到货通知单编号
+ * @return 到货通知单
+ */
+ MesWmArrivalNoticeDO validateArrivalNoticeReadyForReceipt(Long id);
+
+ /**
* 查询指定供应商的到货通知单数量
*
* @param vendorId 供应商编号
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeServiceImpl.java
index 78ae354..3b5a79e 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeServiceImpl.java
@@ -210,6 +210,23 @@ public class MesWmArrivalNoticeServiceImpl implements MesWmArrivalNoticeService
return notice;
}
+ @Override
+ public MesWmArrivalNoticeDO validateArrivalNoticeReadyForReceipt(Long id) {
+ // 1. 校验到货通知单存在且状态为待入库
+ MesWmArrivalNoticeDO notice = validateArrivalNoticeExists(id);
+ if (ObjUtil.notEqual(MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT.getStatus(), notice.getStatus())) {
+ throw exception(WM_ARRIVAL_NOTICE_STATUS_NOT_PENDING_RECEIPT);
+ }
+ // 2. 行级防御校验:确保所有需检行都已完成 IQC
+ List lines = arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(id);
+ boolean hasUnchecked = CollectionUtils.anyMatch(lines,
+ line -> Boolean.TRUE.equals(line.getIqcCheckFlag()) && line.getIqcId() == null);
+ if (hasUnchecked) {
+ throw exception(WM_ARRIVAL_NOTICE_IQC_PENDING);
+ }
+ return notice;
+ }
+
private void validateCodeUnique(Long id, String code) {
MesWmArrivalNoticeDO notice = arrivalNoticeMapper.selectByCode(code);
if (notice == null) {
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java
index aaf2b47..0875cbd 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java
@@ -14,7 +14,6 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceip
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemreceipt.MesWmItemReceiptMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
-import cn.iocoder.yudao.module.mes.enums.wm.MesWmArrivalNoticeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemReceiptStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService;
@@ -92,10 +91,7 @@ public class MesWmItemReceiptServiceImpl implements MesWmItemReceiptService {
vendorService.validateVendorExistsAndEnable(reqVO.getVendorId());
// 校验到货通知单存在
if (reqVO.getNoticeId() != null) {
- MesWmArrivalNoticeDO notice = arrivalNoticeService.validateArrivalNoticeExists(reqVO.getNoticeId());
- if (ObjUtil.notEqual(notice.getStatus(), MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT.getStatus())) {
- throw exception(WM_ARRIVAL_NOTICE_STATUS_NOT_PENDING_RECEIPT);
- }
+ MesWmArrivalNoticeDO notice = arrivalNoticeService.validateArrivalNoticeReadyForReceipt(reqVO.getNoticeId());
if (ObjUtil.notEqual(notice.getVendorId(), reqVO.getVendorId())) {
throw exception(WM_ARRIVAL_NOTICE_VENDOR_MISMATCH);
}
diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcIndicatorResultServiceImplTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcIndicatorResultServiceImplTest.java
new file mode 100644
index 0000000..ac34a74
--- /dev/null
+++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcIndicatorResultServiceImplTest.java
@@ -0,0 +1,288 @@
+package cn.iocoder.yudao.module.mes.service.qc.indicatorresult;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.indicatorresult.vo.MesQcIndicatorResultPageReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.indicatorresult.vo.MesQcIndicatorResultSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.qc.indicatorresult.MesQcIndicatorResultMapper;
+import cn.iocoder.yudao.module.mes.enums.qc.MesQcResultValueTypeEnum;
+import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
+import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
+import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService;
+import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
+import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService;
+import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcService;
+import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+
+import jakarta.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link MesQcIndicatorResultServiceImpl} 的单元测试
+ *
+ * @author 芋道源码
+ */
+@Import(MesQcIndicatorResultServiceImpl.class)
+public class MesQcIndicatorResultServiceImplTest extends BaseDbUnitTest {
+
+ @Resource
+ private MesQcIndicatorResultServiceImpl indicatorResultService;
+
+ @Resource
+ private MesQcIndicatorResultMapper resultMapper;
+
+ @MockitoBean
+ private MesQcIndicatorResultDetailService resultDetailService;
+ @MockitoBean
+ private MesQcIndicatorService indicatorService;
+ @MockitoBean
+ private MesQcIqcService iqcService;
+ @MockitoBean
+ private MesQcIpqcService ipqcService;
+ @MockitoBean
+ private MesQcOqcService oqcService;
+ @MockitoBean
+ private MesQcRqcService rqcService;
+ @MockitoBean
+ private DictDataApi dictDataApi;
+
+ @Test
+ public void testCreateIndicatorResult_success() {
+ // 准备参数
+ MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("plain text value", MesQcResultValueTypeEnum.TEXT.getType());
+ // mock 方法
+ mockIqcAndIndicator(reqVO, MesQcResultValueTypeEnum.TEXT.getType());
+
+ // 调用
+ Long resultId = indicatorResultService.createIndicatorResult(reqVO);
+
+ // 断言
+ assertNotNull(resultId);
+ MesQcIndicatorResultDO dbResult = resultMapper.selectById(resultId);
+ assertNotNull(dbResult);
+ assertEquals(reqVO.getCode(), dbResult.getCode());
+ assertEquals(reqVO.getQcId(), dbResult.getQcId());
+ assertEquals(reqVO.getQcType(), dbResult.getQcType());
+ assertEquals(100L, dbResult.getItemId()); // 从 mock IQC 获取
+ verify(resultDetailService).createDetailList(anyList());
+ }
+
+ @Test
+ public void testCreateIndicatorResult_fileValueInvalid() {
+ // 准备参数
+ MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("not-a-url", MesQcResultValueTypeEnum.FILE.getType());
+ // mock 方法
+ mockIqcAndIndicator(reqVO, MesQcResultValueTypeEnum.FILE.getType());
+
+ // 调用,并断言异常
+ assertServiceException(() -> indicatorResultService.createIndicatorResult(reqVO),
+ QC_RESULT_VALUE_FORMAT_INVALID, "检测项[检测项A]要求文件 URL,实际值=not-a-url");
+
+ // 断言:未入库
+ assertEquals(0, resultMapper.selectCount());
+ verify(resultDetailService, never()).createDetailList(anyList());
+ }
+
+ @Test
+ public void testUpdateIndicatorResult_success() {
+ // mock 数据
+ MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class, o -> {
+ o.setQcType(MesQcTypeEnum.IQC.getType());
+ });
+ resultMapper.insert(dbResult);
+ // 准备参数
+ MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("updated text", MesQcResultValueTypeEnum.TEXT.getType());
+ reqVO.setId(dbResult.getId());
+ // mock 方法
+ MesQcIndicatorDO indicator = new MesQcIndicatorDO();
+ indicator.setId(reqVO.getItems().get(0).getIndicatorId());
+ indicator.setName("检测项A");
+ indicator.setResultType(MesQcResultValueTypeEnum.TEXT.getType());
+ when(indicatorService.validateIndicatorListExists(anySet()))
+ .thenReturn(Map.of(indicator.getId(), indicator));
+
+ // 调用
+ indicatorResultService.updateIndicatorResult(reqVO);
+
+ // 断言
+ MesQcIndicatorResultDO updatedResult = resultMapper.selectById(dbResult.getId());
+ assertEquals(reqVO.getCode(), updatedResult.getCode());
+ // qcId/qcType/itemId 不允许改挂,应保持原值
+ assertEquals(dbResult.getQcId(), updatedResult.getQcId());
+ assertEquals(dbResult.getQcType(), updatedResult.getQcType());
+ assertEquals(dbResult.getItemId(), updatedResult.getItemId());
+ verify(resultDetailService).createOrUpdateDetailList(anyList());
+ }
+
+ @Test
+ public void testUpdateIndicatorResult_notExists() {
+ // 准备参数
+ MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("text", MesQcResultValueTypeEnum.TEXT.getType());
+ reqVO.setId(randomLongId());
+
+ // 调用,并断言异常
+ assertServiceException(() -> indicatorResultService.updateIndicatorResult(reqVO),
+ QC_RESULT_NOT_EXISTS);
+ }
+
+ @Test
+ public void testDeleteIndicatorResult_success() {
+ // mock 数据
+ MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class);
+ resultMapper.insert(dbResult);
+
+ // 调用
+ indicatorResultService.deleteIndicatorResult(dbResult.getId());
+
+ // 断言
+ assertNull(resultMapper.selectById(dbResult.getId()));
+ verify(resultDetailService).deleteDetailByResultId(dbResult.getId());
+ }
+
+ @Test
+ public void testDeleteIndicatorResult_notExists() {
+ // 调用,并断言异常
+ assertServiceException(() -> indicatorResultService.deleteIndicatorResult(randomLongId()),
+ QC_RESULT_NOT_EXISTS);
+ }
+
+ @Test
+ public void testGetIndicatorResult() {
+ // mock 数据
+ MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class);
+ resultMapper.insert(dbResult);
+
+ // 调用
+ MesQcIndicatorResultDO result = indicatorResultService.getIndicatorResult(dbResult.getId());
+
+ // 断言
+ assertPojoEquals(dbResult, result);
+ }
+
+ @Test
+ public void testGetIndicatorResultPage() {
+ // mock 数据
+ MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class, o -> {
+ o.setQcId(1L);
+ o.setQcType(MesQcTypeEnum.IQC.getType());
+ o.setCode("SPL-001");
+ o.setItemId(100L);
+ });
+ resultMapper.insert(dbResult);
+ // 测试 qcId 不匹配
+ resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setQcId(2L)));
+ // 测试 qcType 不匹配
+ resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setQcType(MesQcTypeEnum.IPQC.getType())));
+ // 测试 code 不匹配
+ resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setCode("SPL-999")));
+ // 测试 itemId 不匹配
+ resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setItemId(999L)));
+ // 准备参数
+ MesQcIndicatorResultPageReqVO pageReqVO = new MesQcIndicatorResultPageReqVO();
+ pageReqVO.setQcId(1L);
+ pageReqVO.setQcType(MesQcTypeEnum.IQC.getType());
+ pageReqVO.setCode("SPL-001");
+ pageReqVO.setItemId(100L);
+
+ // 调用
+ PageResult pageResult = indicatorResultService.getIndicatorResultPage(pageReqVO);
+
+ // 断言
+ assertEquals(1, pageResult.getTotal());
+ assertEquals(1, pageResult.getList().size());
+ assertPojoEquals(dbResult, pageResult.getList().get(0));
+ }
+
+ @Test
+ public void testGetIndicatorResultCountByQcIdAndType() {
+ // mock 数据
+ MesQcIndicatorResultDO result1 = randomPojo(MesQcIndicatorResultDO.class, o -> {
+ o.setQcId(1L);
+ o.setQcType(MesQcTypeEnum.IQC.getType());
+ });
+ resultMapper.insert(result1);
+ MesQcIndicatorResultDO result2 = randomPojo(MesQcIndicatorResultDO.class, o -> {
+ o.setQcId(1L);
+ o.setQcType(MesQcTypeEnum.IQC.getType());
+ });
+ resultMapper.insert(result2);
+ // 不匹配
+ resultMapper.insert(randomPojo(MesQcIndicatorResultDO.class, o -> {
+ o.setQcId(2L);
+ o.setQcType(MesQcTypeEnum.IPQC.getType());
+ }));
+
+ // 调用
+ Long count = indicatorResultService.getIndicatorResultCountByQcIdAndType(1L, MesQcTypeEnum.IQC.getType());
+
+ // 断言
+ assertEquals(2L, count);
+ }
+
+ @Test
+ public void testValidateIndicatorResultExistsByQcIdAndType_success() {
+ // mock 数据
+ resultMapper.insert(randomPojo(MesQcIndicatorResultDO.class, o -> {
+ o.setQcId(1L);
+ o.setQcType(MesQcTypeEnum.IQC.getType());
+ }));
+
+ // 调用(不抛异常即通过)
+ indicatorResultService.validateIndicatorResultExistsByQcIdAndType(1L, MesQcTypeEnum.IQC.getType());
+ }
+
+ @Test
+ public void testValidateIndicatorResultExistsByQcIdAndType_notExists() {
+ // 调用,并断言异常
+ assertServiceException(
+ () -> indicatorResultService.validateIndicatorResultExistsByQcIdAndType(randomLongId(), MesQcTypeEnum.IQC.getType()),
+ QC_FINISH_INDICATOR_RESULT_REQUIRED);
+ }
+
+ // ==================== 辅助方法 ====================
+
+ private void mockIqcAndIndicator(MesQcIndicatorResultSaveReqVO reqVO, Integer resultType) {
+ MesQcIqcDO iqc = new MesQcIqcDO();
+ iqc.setId(reqVO.getQcId());
+ iqc.setItemId(100L);
+ when(iqcService.validateIqcExists(reqVO.getQcId())).thenReturn(iqc);
+
+ MesQcIndicatorDO indicator = new MesQcIndicatorDO();
+ indicator.setId(reqVO.getItems().get(0).getIndicatorId());
+ indicator.setName("检测项A");
+ indicator.setResultType(resultType);
+ when(indicatorService.validateIndicatorListExists(anySet()))
+ .thenReturn(Map.of(indicator.getId(), indicator));
+ }
+
+ private MesQcIndicatorResultSaveReqVO buildReqVO(String value, Integer resultType) {
+ MesQcIndicatorResultSaveReqVO.Item item = new MesQcIndicatorResultSaveReqVO.Item();
+ item.setIndicatorId(10L);
+ item.setValue(value);
+
+ MesQcIndicatorResultSaveReqVO reqVO = new MesQcIndicatorResultSaveReqVO();
+ reqVO.setCode("SPL-001");
+ reqVO.setQcId(1L);
+ reqVO.setQcType(MesQcTypeEnum.IQC.getType());
+ reqVO.setItems(List.of(item));
+ return reqVO;
+ }
+
+}
diff --git a/yudao-module-mes/src/test/resources/sql/clean.sql b/yudao-module-mes/src/test/resources/sql/clean.sql
index 05b58c8..8cb28c3 100644
--- a/yudao-module-mes/src/test/resources/sql/clean.sql
+++ b/yudao-module-mes/src/test/resources/sql/clean.sql
@@ -20,3 +20,4 @@ DELETE FROM "mes_wm_batch";
DELETE FROM "mes_wm_material_stock";
DELETE FROM "mes_pro_task";
DELETE FROM "mes_pro_route_process";
+DELETE FROM "mes_qc_indicator_result";
diff --git a/yudao-module-mes/src/test/resources/sql/create_tables.sql b/yudao-module-mes/src/test/resources/sql/create_tables.sql
index 734f74e..881ac06 100644
--- a/yudao-module-mes/src/test/resources/sql/create_tables.sql
+++ b/yudao-module-mes/src/test/resources/sql/create_tables.sql
@@ -810,3 +810,23 @@ CREATE TABLE IF NOT EXISTS "mes_wm_product_sales_detail" (
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
+
+-- ----------------------------
+-- MES 检验结果记录
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS "mes_qc_indicator_result" (
+ "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+ "code" varchar(64) DEFAULT NULL,
+ "qc_id" bigint DEFAULT NULL,
+ "qc_type" int DEFAULT NULL,
+ "item_id" bigint DEFAULT NULL,
+ "sn" varchar(128) DEFAULT NULL,
+ "remark" varchar(500) DEFAULT NULL,
+ "creator" varchar(64) DEFAULT '',
+ "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updater" varchar(64) DEFAULT '',
+ "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "deleted" bit NOT NULL DEFAULT FALSE,
+ "tenant_id" bigint NOT NULL DEFAULT 0,
+ PRIMARY KEY ("id")
+);