feat: add fail-open shadow planning boundary
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
public record InboundShadowObservation(
|
||||
boolean observed,
|
||||
String warehouseId,
|
||||
String requestId,
|
||||
String resultCode,
|
||||
String proposedSpaceId,
|
||||
String reason) {
|
||||
|
||||
public static InboundShadowObservation empty() {
|
||||
return new InboundShadowObservation(false, null, null, "NOT_OBSERVED", null, null);
|
||||
}
|
||||
|
||||
public static InboundShadowObservation unavailable(String warehouseId, String requestId,
|
||||
String resultCode, String reason) {
|
||||
return new InboundShadowObservation(true, warehouseId, requestId, resultCode, null, reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
public record OutboundShadowObservation(
|
||||
boolean observed,
|
||||
String warehouseId,
|
||||
String requestId,
|
||||
String resultCode,
|
||||
String proposedStockId,
|
||||
String proposedSpaceId,
|
||||
String reason) {
|
||||
|
||||
public static OutboundShadowObservation empty() {
|
||||
return new OutboundShadowObservation(false, null, null, "NOT_OBSERVED", null, null, null);
|
||||
}
|
||||
|
||||
public static OutboundShadowObservation unavailable(String warehouseId, String requestId,
|
||||
String resultCode, String reason) {
|
||||
return new OutboundShadowObservation(true, warehouseId, requestId, resultCode, null, null, reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
public class ShadowInboundReference {
|
||||
private String requestId;
|
||||
private String trayNo;
|
||||
private String projectNo;
|
||||
private String productId;
|
||||
private String lotNo;
|
||||
|
||||
public String getRequestId() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
public void setRequestId(String requestId) {
|
||||
this.requestId = requestId;
|
||||
}
|
||||
|
||||
public String getTrayNo() {
|
||||
return trayNo;
|
||||
}
|
||||
|
||||
public void setTrayNo(String trayNo) {
|
||||
this.trayNo = trayNo;
|
||||
}
|
||||
|
||||
public String getProjectNo() {
|
||||
return projectNo;
|
||||
}
|
||||
|
||||
public void setProjectNo(String projectNo) {
|
||||
this.projectNo = projectNo;
|
||||
}
|
||||
|
||||
public String getProductId() {
|
||||
return productId;
|
||||
}
|
||||
|
||||
public void setProductId(String productId) {
|
||||
this.productId = productId;
|
||||
}
|
||||
|
||||
public String getLotNo() {
|
||||
return lotNo;
|
||||
}
|
||||
|
||||
public void setLotNo(String lotNo) {
|
||||
this.lotNo = lotNo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
public class ShadowObservationGateway {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ShadowObservationGateway.class);
|
||||
|
||||
private final ShadowObservationProperties properties;
|
||||
private final ShadowPlanningEngine engine;
|
||||
|
||||
public ShadowObservationGateway(ShadowObservationProperties properties, ShadowPlanningEngine engine) {
|
||||
this.properties = Objects.requireNonNull(properties, "properties");
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
public InboundShadowObservation planInbound(String taskNo, String trayNo) {
|
||||
if (!properties.isEnabled() || engine == null) {
|
||||
return InboundShadowObservation.empty();
|
||||
}
|
||||
try {
|
||||
return engine.planInbound(taskNo, trayNo);
|
||||
} catch (RuntimeException exception) {
|
||||
LOG.warn("Shadow inbound planning failed open for task {}: {}", taskNo, exception.getMessage());
|
||||
return InboundShadowObservation.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public OutboundShadowObservation planProjectOutbound(String stockoutDetailId) {
|
||||
if (!properties.isEnabled() || engine == null) {
|
||||
return OutboundShadowObservation.empty();
|
||||
}
|
||||
try {
|
||||
return engine.planProjectOutbound(stockoutDetailId);
|
||||
} catch (RuntimeException exception) {
|
||||
LOG.warn("Shadow outbound planning failed open for detail {}: {}",
|
||||
stockoutDetailId, exception.getMessage());
|
||||
return OutboundShadowObservation.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public OutboundShadowObservation planExactStockOutbound(String stockoutDetailId, String stockId) {
|
||||
if (!properties.isEnabled() || engine == null) {
|
||||
return OutboundShadowObservation.empty();
|
||||
}
|
||||
try {
|
||||
return engine.planExactStockOutbound(stockoutDetailId, stockId);
|
||||
} catch (RuntimeException exception) {
|
||||
LOG.warn("Shadow exact-stock planning failed open for detail {} stock {}: {}",
|
||||
stockoutDetailId, stockId, exception.getMessage());
|
||||
return OutboundShadowObservation.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public void compareInbound(InboundShadowObservation observation, String legacyResultCode,
|
||||
String legacySpaceId) {
|
||||
if (!properties.isEnabled() || engine == null || observation == null || !observation.observed()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
engine.compareInbound(observation, legacyResultCode, legacySpaceId);
|
||||
} catch (RuntimeException exception) {
|
||||
LOG.warn("Shadow inbound comparison failed open for request {}: {}",
|
||||
observation.requestId(), exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void compareOutbound(OutboundShadowObservation observation, String legacyResultCode) {
|
||||
if (!properties.isEnabled() || engine == null || observation == null || !observation.observed()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
engine.compareOutbound(observation, legacyResultCode);
|
||||
} catch (RuntimeException exception) {
|
||||
LOG.warn("Shadow outbound comparison failed open for request {}: {}",
|
||||
observation.requestId(), exception.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "fortucky.shadow")
|
||||
public class ShadowObservationProperties {
|
||||
private boolean enabled;
|
||||
private String warehouseId = "";
|
||||
private int timeoutSeconds = 3;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getWarehouseId() {
|
||||
return warehouseId;
|
||||
}
|
||||
|
||||
public void setWarehouseId(String warehouseId) {
|
||||
this.warehouseId = warehouseId == null ? "" : warehouseId.trim();
|
||||
}
|
||||
|
||||
public int getTimeoutSeconds() {
|
||||
return timeoutSeconds;
|
||||
}
|
||||
|
||||
public void setTimeoutSeconds(int timeoutSeconds) {
|
||||
this.timeoutSeconds = timeoutSeconds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import com.ruoyi.algorithm.inbound.InboundPlan;
|
||||
import com.ruoyi.algorithm.inbound.InboundRequest;
|
||||
import com.ruoyi.algorithm.integration.SchedulingFacade;
|
||||
import com.ruoyi.algorithm.outbound.OutboundPlan;
|
||||
import com.ruoyi.algorithm.outbound.OutboundRequest;
|
||||
import com.ruoyi.algorithm.service.InboundPlanningResult;
|
||||
import com.ruoyi.algorithm.shadow.mapper.ShadowInputMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class ShadowPlanningEngine {
|
||||
private final SchedulingFacade schedulingFacade;
|
||||
private final ShadowInputMapper inputMapper;
|
||||
private final ShadowObservationProperties properties;
|
||||
private final TransactionTemplate transactions;
|
||||
|
||||
public ShadowPlanningEngine(SchedulingFacade schedulingFacade, ShadowInputMapper inputMapper,
|
||||
ShadowObservationProperties properties, PlatformTransactionManager transactionManager) {
|
||||
this.schedulingFacade = Objects.requireNonNull(schedulingFacade, "schedulingFacade");
|
||||
this.inputMapper = Objects.requireNonNull(inputMapper, "inputMapper");
|
||||
this.properties = Objects.requireNonNull(properties, "properties");
|
||||
this.transactions = new TransactionTemplate(Objects.requireNonNull(transactionManager, "transactionManager"));
|
||||
this.transactions.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
this.transactions.setTimeout(properties.getTimeoutSeconds());
|
||||
}
|
||||
|
||||
public InboundShadowObservation planInbound(String taskNo, String trayNo) {
|
||||
return transactions.execute(status -> planInboundInsideTransaction(taskNo, trayNo));
|
||||
}
|
||||
|
||||
public OutboundShadowObservation planProjectOutbound(String stockoutDetailId) {
|
||||
return transactions.execute(status -> {
|
||||
ShadowStockReference reference = inputMapper.selectProjectOutboundReference(stockoutDetailId);
|
||||
String warehouseId = warehouseId();
|
||||
if (reference == null || isBlank(reference.getProjectNo())) {
|
||||
return OutboundShadowObservation.unavailable(warehouseId, stockoutDetailId,
|
||||
"SKIPPED_MISSING_PROJECT", "No project number was available for shadow planning");
|
||||
}
|
||||
OutboundRequest request = new OutboundRequest(stockoutDetailId, null,
|
||||
reference.getProjectNo(), reference.getProductId(), null, null, null, true);
|
||||
return toOutboundObservation(warehouseId, reference, schedulingFacade.planOutbound(warehouseId, request));
|
||||
});
|
||||
}
|
||||
|
||||
public OutboundShadowObservation planExactStockOutbound(String stockoutDetailId, String stockId) {
|
||||
return transactions.execute(status -> {
|
||||
ShadowStockReference reference = inputMapper.selectStockReference(stockId);
|
||||
String warehouseId = warehouseId();
|
||||
if (reference == null || isBlank(reference.getTrayNo())) {
|
||||
return OutboundShadowObservation.unavailable(warehouseId, stockoutDetailId,
|
||||
"SKIPPED_MISSING_STOCK", "Selected stock was unavailable to shadow planning");
|
||||
}
|
||||
OutboundRequest request = new OutboundRequest(stockoutDetailId, reference.getTrayNo(),
|
||||
reference.getProjectNo(), reference.getProductId(), null, null, null, true);
|
||||
return toOutboundObservation(warehouseId, reference, schedulingFacade.planOutbound(warehouseId, request));
|
||||
});
|
||||
}
|
||||
|
||||
public void compareInbound(InboundShadowObservation observation, String legacyResultCode,
|
||||
String legacySpaceId) {
|
||||
transactions.executeWithoutResult(status -> inputMapper.insertComparison(
|
||||
UUID.randomUUID().toString(), observation.warehouseId(), "INBOUND_COMPARE",
|
||||
comparisonCode(observation.proposedSpaceId(), legacySpaceId),
|
||||
"request=" + observation.requestId() + "; shadow=" + observation.resultCode()
|
||||
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
||||
+ "; legacy=" + nullSafe(legacyResultCode)
|
||||
+ "; legacySpace=" + nullSafe(legacySpaceId)));
|
||||
}
|
||||
|
||||
public void compareOutbound(OutboundShadowObservation observation, String legacyResultCode) {
|
||||
transactions.executeWithoutResult(status -> inputMapper.insertComparison(
|
||||
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
|
||||
"LEGACY_" + nullSafe(legacyResultCode),
|
||||
"request=" + observation.requestId() + "; shadow=" + observation.resultCode()
|
||||
+ "; proposedStock=" + nullSafe(observation.proposedStockId())
|
||||
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
||||
+ "; legacy=" + nullSafe(legacyResultCode)));
|
||||
}
|
||||
|
||||
private InboundShadowObservation planInboundInsideTransaction(String taskNo, String trayNo) {
|
||||
String warehouseId = warehouseId();
|
||||
ShadowInboundReference reference = inputMapper.selectInboundInput(taskNo, trayNo);
|
||||
if (reference == null || isBlank(reference.getRequestId()) || isBlank(reference.getTrayNo())) {
|
||||
return InboundShadowObservation.unavailable(warehouseId, taskNo,
|
||||
"SKIPPED_MISSING_INPUT", "Inbound task details were unavailable to shadow planning");
|
||||
}
|
||||
InboundRequest request = new InboundRequest(reference.getRequestId(), reference.getTrayNo(),
|
||||
reference.getProjectNo(), reference.getProductId(), reference.getLotNo(),
|
||||
null, null, null, Set.of(), null);
|
||||
InboundPlanningResult result = schedulingFacade.planInbound(warehouseId, request);
|
||||
InboundPlan plan = result.plan();
|
||||
if (plan.assignment().isPresent()) {
|
||||
var assignment = plan.assignment().orElseThrow();
|
||||
return new InboundShadowObservation(true, warehouseId, reference.getRequestId(),
|
||||
"READY", assignment.spaceId(), assignment.reason());
|
||||
}
|
||||
var rejection = plan.rejection().orElseThrow();
|
||||
return InboundShadowObservation.unavailable(warehouseId, reference.getRequestId(),
|
||||
"REJECTED_" + rejection.code().name(), rejection.reason());
|
||||
}
|
||||
|
||||
private OutboundShadowObservation toOutboundObservation(String warehouseId,
|
||||
ShadowStockReference reference, OutboundPlan plan) {
|
||||
String requestId = reference.getRequestId();
|
||||
if (isBlank(requestId)) {
|
||||
requestId = plan.assignment().map(assignment -> assignment.requestId()).orElse(null);
|
||||
}
|
||||
if (plan.assignment().isPresent()) {
|
||||
var assignment = plan.assignment().orElseThrow();
|
||||
return new OutboundShadowObservation(true, warehouseId, requestId, "READY",
|
||||
reference.getStockId(), assignment.sourceSpaceId(), assignment.reason());
|
||||
}
|
||||
if (plan.waiting().isPresent()) {
|
||||
var waiting = plan.waiting().orElseThrow();
|
||||
return OutboundShadowObservation.unavailable(warehouseId, requestId,
|
||||
"WAITING_" + waiting.code().name(), waiting.reason());
|
||||
}
|
||||
var error = plan.error().orElseThrow();
|
||||
return OutboundShadowObservation.unavailable(warehouseId, requestId,
|
||||
"ERROR_" + error.code().name(), error.message());
|
||||
}
|
||||
|
||||
private String warehouseId() {
|
||||
return isBlank(properties.getWarehouseId())
|
||||
? schedulingFacade.defaultWarehouseId()
|
||||
: properties.getWarehouseId();
|
||||
}
|
||||
|
||||
private String comparisonCode(String shadowSpaceId, String legacySpaceId) {
|
||||
if (isBlank(shadowSpaceId) || isBlank(legacySpaceId)) {
|
||||
return "NOT_COMPARABLE";
|
||||
}
|
||||
return shadowSpaceId.equals(legacySpaceId) ? "MATCH" : "DIFFERENT";
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
|
||||
private String nullSafe(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import com.ruoyi.algorithm.integration.AlgorithmProperties;
|
||||
import com.ruoyi.algorithm.integration.SchedulingMode;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
public class ShadowSafetyGuard implements InitializingBean {
|
||||
private final ShadowObservationProperties shadowProperties;
|
||||
private final AlgorithmProperties algorithmProperties;
|
||||
|
||||
public ShadowSafetyGuard(ShadowObservationProperties shadowProperties,
|
||||
AlgorithmProperties algorithmProperties) {
|
||||
this.shadowProperties = Objects.requireNonNull(shadowProperties, "shadowProperties");
|
||||
this.algorithmProperties = Objects.requireNonNull(algorithmProperties, "algorithmProperties");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
validate();
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
if (!shadowProperties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (algorithmProperties.getMode() != SchedulingMode.SHADOW) {
|
||||
throw new IllegalStateException("Shadow observation requires FORTUCKY_ALGORITHM_MODE=SHADOW");
|
||||
}
|
||||
if (!algorithmProperties.isDatabaseWritesEnabled()) {
|
||||
throw new IllegalStateException("Shadow observation requires algorithm database writes for alg.* read models");
|
||||
}
|
||||
if (shadowProperties.getTimeoutSeconds() <= 0) {
|
||||
throw new IllegalStateException("Shadow observation timeout must be positive");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
public class ShadowStockReference {
|
||||
private String requestId;
|
||||
private String stockId;
|
||||
private String trayNo;
|
||||
private String spaceId;
|
||||
private String warehouseId;
|
||||
private String projectNo;
|
||||
private String productId;
|
||||
|
||||
public String getRequestId() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
public void setRequestId(String requestId) {
|
||||
this.requestId = requestId;
|
||||
}
|
||||
|
||||
public String getStockId() {
|
||||
return stockId;
|
||||
}
|
||||
|
||||
public void setStockId(String stockId) {
|
||||
this.stockId = stockId;
|
||||
}
|
||||
|
||||
public String getTrayNo() {
|
||||
return trayNo;
|
||||
}
|
||||
|
||||
public void setTrayNo(String trayNo) {
|
||||
this.trayNo = trayNo;
|
||||
}
|
||||
|
||||
public String getSpaceId() {
|
||||
return spaceId;
|
||||
}
|
||||
|
||||
public void setSpaceId(String spaceId) {
|
||||
this.spaceId = spaceId;
|
||||
}
|
||||
|
||||
public String getWarehouseId() {
|
||||
return warehouseId;
|
||||
}
|
||||
|
||||
public void setWarehouseId(String warehouseId) {
|
||||
this.warehouseId = warehouseId;
|
||||
}
|
||||
|
||||
public String getProjectNo() {
|
||||
return projectNo;
|
||||
}
|
||||
|
||||
public void setProjectNo(String projectNo) {
|
||||
this.projectNo = projectNo;
|
||||
}
|
||||
|
||||
public String getProductId() {
|
||||
return productId;
|
||||
}
|
||||
|
||||
public void setProductId(String productId) {
|
||||
this.productId = productId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.algorithm.shadow.mapper;
|
||||
|
||||
import com.ruoyi.algorithm.shadow.ShadowInboundReference;
|
||||
import com.ruoyi.algorithm.shadow.ShadowStockReference;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface ShadowInputMapper {
|
||||
ShadowInboundReference selectInboundInput(@Param("taskNo") String taskNo,
|
||||
@Param("trayNo") String trayNo);
|
||||
|
||||
ShadowStockReference selectProjectOutboundReference(
|
||||
@Param("stockoutDetailId") String stockoutDetailId);
|
||||
|
||||
ShadowStockReference selectStockReference(@Param("stockId") String stockId);
|
||||
|
||||
int insertComparison(@Param("decisionId") String decisionId,
|
||||
@Param("warehouseId") String warehouseId,
|
||||
@Param("decisionType") String decisionType,
|
||||
@Param("resultCode") String resultCode,
|
||||
@Param("reason") String reason);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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="com.ruoyi.algorithm.shadow.mapper.ShadowInputMapper">
|
||||
|
||||
<select id="selectInboundInput" resultType="com.ruoyi.algorithm.shadow.ShadowInboundReference">
|
||||
select top (1)
|
||||
job.job_id as requestId,
|
||||
job.tray_code as trayNo,
|
||||
coalesce(nullif(ltrim(rtrim(detail.spare4)), ''),
|
||||
nullif(ltrim(rtrim(stock.spare4)), '')) as projectNo,
|
||||
jobDetail.product_id as productId,
|
||||
jobDetail.lot_no as lotNo
|
||||
from mydo_job job
|
||||
left join mydo_job_detail jobDetail on jobDetail.job_id = job.job_id
|
||||
left join in_stockin_detail detail on detail.stockin_detail_id = jobDetail.stockin_detail_id
|
||||
left join bas_stock stock on stock.stock_id = jobDetail.stock_id
|
||||
where job.task_no = #{taskNo}
|
||||
and job.tray_code = #{trayNo}
|
||||
order by jobDetail.job_detail_id
|
||||
</select>
|
||||
|
||||
<select id="selectProjectOutboundReference" resultType="com.ruoyi.algorithm.shadow.ShadowStockReference">
|
||||
select detail.stockout_detail_id as requestId,
|
||||
detail.warehouse_id as warehouseId,
|
||||
nullif(ltrim(rtrim(detail.spare4)), '') as projectNo,
|
||||
detail.product_id as productId
|
||||
from out_stockout_detail detail
|
||||
where detail.stockout_detail_id = #{stockoutDetailId}
|
||||
</select>
|
||||
|
||||
<select id="selectStockReference" resultType="com.ruoyi.algorithm.shadow.ShadowStockReference">
|
||||
select top (1)
|
||||
stock.stock_id as stockId,
|
||||
stock.tray_code as trayNo,
|
||||
stock.cargo_space_id as spaceId,
|
||||
stock.warehouse_id as warehouseId,
|
||||
nullif(ltrim(rtrim(stock.spare4)), '') as projectNo,
|
||||
stock.product_id as productId
|
||||
from bas_stock stock
|
||||
where stock.stock_id = #{stockId}
|
||||
and stock.stock_status = '1'
|
||||
and stock.product_status = '1'
|
||||
and stock.quantity > stock.out_quantity
|
||||
</select>
|
||||
|
||||
<insert id="insertComparison">
|
||||
insert into alg.decision_log (
|
||||
decision_id, warehouse_id, decision_type, input_version, result_code, reason)
|
||||
values (
|
||||
try_convert(uniqueidentifier, #{decisionId}), #{warehouseId}, #{decisionType},
|
||||
null, #{resultCode}, #{reason})
|
||||
</insert>
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user