修改订单同步
This commit is contained in:
@@ -36,6 +36,14 @@
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Oracle driver for SLAVE upstream order query (enable slave DS to use) -->
|
||||
<dependency>
|
||||
<groupId>com.oracle.database.jdbc</groupId>
|
||||
<artifactId>ojdbc11</artifactId>
|
||||
<version>23.5.0.24.07</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 核心模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
|
||||
+35
@@ -16,12 +16,15 @@ import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.framework.config.properties.TmsApiProperties;
|
||||
import com.ruoyi.price.domain.vo.BillingTaskSubmitRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
|
||||
import com.ruoyi.price.domain.PrcBillingTask;
|
||||
import com.ruoyi.price.search.EsBizLogSupport;
|
||||
import com.ruoyi.price.search.EsLogType;
|
||||
import com.ruoyi.price.search.IBillSearchStore;
|
||||
import com.ruoyi.price.service.IPrcBillingTaskService;
|
||||
import com.ruoyi.price.service.IPrcOrderBillService;
|
||||
import com.ruoyi.price.service.ITmsPriceQuoteService;
|
||||
|
||||
@@ -46,6 +49,9 @@ public class TmsPriceController extends BaseController
|
||||
@Autowired
|
||||
private IPrcOrderBillService prcOrderBillService;
|
||||
|
||||
@Autowired
|
||||
private IPrcBillingTaskService prcBillingTaskService;
|
||||
|
||||
@Autowired
|
||||
private TmsApiProperties tmsApiProperties;
|
||||
|
||||
@@ -99,6 +105,35 @@ public class TmsPriceController extends BaseController
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Warehouse inbound: create billing task (PENDING/REBILL/REFUND) and publish MQ.
|
||||
* Consumer queries Oracle by transferNo/orderNo then inserts prc_order_bill.
|
||||
* POST /api/tms/price/billing-task
|
||||
*/
|
||||
@PostMapping("/billing-task")
|
||||
public AjaxResult submitBillingTask(
|
||||
@RequestHeader(value = HEADER_API_CODE, required = false) String headerApiCode,
|
||||
@RequestBody BillingTaskSubmitRequest request)
|
||||
{
|
||||
assertApiCode(headerApiCode, request == null ? null : request.getApiCode());
|
||||
PrcBillingTask task = prcBillingTaskService.submitInbound(request);
|
||||
return success(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query billing task writeback status.
|
||||
* GET /api/tms/price/billing-task/{taskId}
|
||||
*/
|
||||
@GetMapping("/billing-task/{taskId}")
|
||||
public AjaxResult getBillingTask(
|
||||
@RequestHeader(value = HEADER_API_CODE, required = false) String headerApiCode,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@RequestParam(value = "apiCode", required = false) String apiCode)
|
||||
{
|
||||
assertApiCode(headerApiCode, apiCode);
|
||||
return success(prcBillingTaskService.getById(taskId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Query order bill by orderNo after sync.
|
||||
*/
|
||||
|
||||
@@ -10,13 +10,17 @@ spring:
|
||||
username: bms
|
||||
password: D3wsHf2ZMfFtWtKX
|
||||
connectionInitSqls: SET NAMES utf8mb4
|
||||
# 从库数据源
|
||||
# Slave DS: Oracle upstream order (TAB_ORDER)
|
||||
slave:
|
||||
# 从数据源开关/默认关闭
|
||||
enabled: false
|
||||
url:
|
||||
username:
|
||||
password:
|
||||
enabled: true
|
||||
driverClassName: oracle.jdbc.OracleDriver
|
||||
url: jdbc:oracle:thin:@//120.79.189.203:15281/WSDB
|
||||
username: readonly_report
|
||||
password: k5x15WP1hE
|
||||
# Oracle remote query may need longer read timeout than MySQL
|
||||
connectTimeout: 30000
|
||||
socketTimeout: 120000
|
||||
validationQuery: SELECT 1 FROM DUAL
|
||||
# 初始连接数
|
||||
initialSize: 5
|
||||
# 最小连接池数量
|
||||
|
||||
@@ -151,6 +151,17 @@ bms:
|
||||
tms:
|
||||
# Agreed API identification code for TMS callers
|
||||
api-code: BMS.TMS.ZYaOvlQzpcfau8TUaCFfO3Hp8ieHIqGw
|
||||
billing:
|
||||
# MQ mode: redis (default list queue) | local (Spring event)
|
||||
mq-mode: redis
|
||||
mq-queue-key: bms:billing:task
|
||||
consumer-enabled: true
|
||||
consumer-delay-ms: 1000
|
||||
oracle:
|
||||
# SLAVE datasource Oracle main-order table (schema.table allowed)
|
||||
order-table: WEISHI_USER.TAB_ORDER
|
||||
# Fallback product when Oracle row has no PRODUCT_CODE
|
||||
default-product-code: ""
|
||||
elasticsearch:
|
||||
enabled: true
|
||||
uris: http://127.0.0.1:9200
|
||||
|
||||
+2
-1
@@ -607,7 +607,8 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
flow.setEffectiveDate(effectiveDate);
|
||||
flow.setExpireDate(expireDate);
|
||||
flow.setReason("账户初始化");
|
||||
flow.setOperatorId(SecurityUtils.getUserId());
|
||||
// No login (open API / MQ / job): operatorId null, same as occupyCredit
|
||||
flow.setOperatorId(currentUserId());
|
||||
flow.setCreateBy(username);
|
||||
flow.setRemark(remark);
|
||||
flow.setDelFlag("0");
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.ruoyi.price.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Billing task MQ + Oracle upstream order settings.
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "bms.billing")
|
||||
public class BillingProperties
|
||||
{
|
||||
/** local = Spring event; redis = Redis list queue */
|
||||
private String mqMode = "redis";
|
||||
private String mqQueueKey = "bms:billing:task";
|
||||
private boolean consumerEnabled = true;
|
||||
private Oracle oracle = new Oracle();
|
||||
|
||||
public String getMqMode()
|
||||
{
|
||||
return mqMode;
|
||||
}
|
||||
|
||||
public void setMqMode(String mqMode)
|
||||
{
|
||||
this.mqMode = mqMode;
|
||||
}
|
||||
|
||||
public String getMqQueueKey()
|
||||
{
|
||||
return mqQueueKey;
|
||||
}
|
||||
|
||||
public void setMqQueueKey(String mqQueueKey)
|
||||
{
|
||||
this.mqQueueKey = mqQueueKey;
|
||||
}
|
||||
|
||||
public boolean isConsumerEnabled()
|
||||
{
|
||||
return consumerEnabled;
|
||||
}
|
||||
|
||||
public void setConsumerEnabled(boolean consumerEnabled)
|
||||
{
|
||||
this.consumerEnabled = consumerEnabled;
|
||||
}
|
||||
|
||||
public Oracle getOracle()
|
||||
{
|
||||
return oracle;
|
||||
}
|
||||
|
||||
public void setOracle(Oracle oracle)
|
||||
{
|
||||
this.oracle = oracle;
|
||||
}
|
||||
|
||||
public static class Oracle
|
||||
{
|
||||
/** Upstream main-order table name on SLAVE/Oracle DS */
|
||||
private String orderTable = "WEISHI_USER.TAB_ORDER";
|
||||
/** Optional default product when Oracle row has no PRODUCT_CODE */
|
||||
private String defaultProductCode = "";
|
||||
|
||||
public String getOrderTable()
|
||||
{
|
||||
return orderTable;
|
||||
}
|
||||
|
||||
public void setOrderTable(String orderTable)
|
||||
{
|
||||
this.orderTable = orderTable;
|
||||
}
|
||||
|
||||
public String getDefaultProductCode()
|
||||
{
|
||||
return defaultProductCode;
|
||||
}
|
||||
|
||||
public void setDefaultProductCode(String defaultProductCode)
|
||||
{
|
||||
this.defaultProductCode = defaultProductCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.ruoyi.price.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/** Billing task row prc_billing_task */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PrcBillingTask extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long taskId;
|
||||
private String transferNo;
|
||||
private String trackingNo;
|
||||
private String orderNo;
|
||||
/** PENDING / REBILL / REFUND */
|
||||
private String billType;
|
||||
/** WAITING / SUCCESS / FAILED */
|
||||
private String successStatus;
|
||||
/** Billing time from mobile (not Oracle order time; not overwritten on writeback) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date billTime;
|
||||
private String billId;
|
||||
private String errorMessage;
|
||||
}
|
||||
@@ -21,6 +21,8 @@ public class PrcOrderBill extends BaseEntity
|
||||
/** 批量查询:多个订单号精确匹配(仅查询用,非表字段) */
|
||||
private List<String> orderNos;
|
||||
private String trackingNo;
|
||||
/** transfer order no */
|
||||
private String transferNo;
|
||||
private Long customerId;
|
||||
private String customerCode;
|
||||
private String customerName;
|
||||
@@ -79,6 +81,10 @@ public class PrcOrderBill extends BaseEntity
|
||||
private String currency;
|
||||
/** 应收账单占用状态:UNBILLED/BILLED。 */
|
||||
private String arBillStatus;
|
||||
/** ACTIVE / MISSING / DISABLED */
|
||||
private String customerActiveStatus;
|
||||
/** NORMAL / ARREARS */
|
||||
private String creditArrearsStatus;
|
||||
private String roundMode;
|
||||
|
||||
private List<PrcIncomeBillFee> incomeFees;
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.price.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Warehouse inbound request: create billing task then MQ consume.
|
||||
* Provide at least one of transferNo / trackingNo / orderNo.
|
||||
* billTime is billing time from mobile (stored in prc_billing_task.bill_time).
|
||||
*/
|
||||
@Data
|
||||
public class BillingTaskSubmitRequest implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String transferNo;
|
||||
private String trackingNo;
|
||||
private String orderNo;
|
||||
/** Billing time from mobile; persisted to prc_billing_task.bill_time */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date billTime;
|
||||
/** Optional: PENDING / REBILL / REFUND. Empty = auto PENDING or REBILL. */
|
||||
private String billType;
|
||||
/** Optional body apiCode when header X-Api-Code missing */
|
||||
private String apiCode;
|
||||
}
|
||||
@@ -20,6 +20,8 @@ public class TmsPriceBillRequest extends TmsPriceQuoteRequest
|
||||
private String orderNo;
|
||||
|
||||
private String trackingNo;
|
||||
/** transfer order no */
|
||||
private String transferNo;
|
||||
private String customerName;
|
||||
private Integer pieces;
|
||||
private BigDecimal actualWeight;
|
||||
|
||||
@@ -73,8 +73,14 @@ public class TmsPriceQuoteResult implements Serializable
|
||||
private String billId;
|
||||
private String orderNo;
|
||||
private String trackingNo;
|
||||
/** transfer order no */
|
||||
private String transferNo;
|
||||
private String customerCode;
|
||||
private String customerName;
|
||||
/** ACTIVE / MISSING / DISABLED */
|
||||
private String customerActiveStatus;
|
||||
/** NORMAL / ARREARS */
|
||||
private String creditArrearsStatus;
|
||||
private Integer pieces;
|
||||
private BigDecimal actualWeight;
|
||||
private BigDecimal volume;
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.price.enums;
|
||||
|
||||
/**
|
||||
* Credit arrears status snapshot on prc_order_bill when billing.
|
||||
* Billing continues even when ARREARS; occupy still runs with checkAvailable=false.
|
||||
*/
|
||||
public enum BillCreditArrearsStatus
|
||||
{
|
||||
NORMAL("NORMAL"),
|
||||
ARREARS("ARREARS");
|
||||
|
||||
private final String code;
|
||||
|
||||
BillCreditArrearsStatus(String code)
|
||||
{
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode()
|
||||
{
|
||||
return code;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.ruoyi.price.enums;
|
||||
|
||||
/**
|
||||
* Customer effective status snapshot on prc_order_bill when billing.
|
||||
* Billing continues even when MISSING / DISABLED.
|
||||
*/
|
||||
public enum BillCustomerActiveStatus
|
||||
{
|
||||
ACTIVE("ACTIVE"),
|
||||
MISSING("MISSING"),
|
||||
DISABLED("DISABLED");
|
||||
|
||||
private final String code;
|
||||
|
||||
BillCustomerActiveStatus(String code)
|
||||
{
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode()
|
||||
{
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ruoyi.price.enums;
|
||||
|
||||
/** Billing task success flag on prc_billing_task.success_status */
|
||||
public enum BillTaskSuccessStatus
|
||||
{
|
||||
WAITING("WAITING"),
|
||||
SUCCESS("SUCCESS"),
|
||||
FAILED("FAILED");
|
||||
|
||||
private final String code;
|
||||
|
||||
BillTaskSuccessStatus(String code)
|
||||
{
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode()
|
||||
{
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ruoyi.price.enums;
|
||||
|
||||
/** Billing task type on prc_billing_task.bill_type */
|
||||
public enum BillTaskType
|
||||
{
|
||||
/** first-time billing */
|
||||
PENDING("PENDING"),
|
||||
/** same transfer/order already billed */
|
||||
REBILL("REBILL"),
|
||||
/** refund */
|
||||
REFUND("REFUND");
|
||||
|
||||
private final String code;
|
||||
|
||||
BillTaskType(String code)
|
||||
{
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode()
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
||||
public static BillTaskType fromCode(String code)
|
||||
{
|
||||
if (code == null || code.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
for (BillTaskType t : values())
|
||||
{
|
||||
if (t.code.equalsIgnoreCase(code.trim()))
|
||||
{
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.price.mapper;
|
||||
|
||||
import java.util.Map;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/** Read upstream main order from Oracle (SLAVE datasource). */
|
||||
public interface OracleUpstreamOrderMapper
|
||||
{
|
||||
Map<String, Object> selectByTransferNo(@Param("orderTable") String orderTable, @Param("transferNo") String transferNo);
|
||||
|
||||
Map<String, Object> selectByOrderId(@Param("orderTable") String orderTable, @Param("orderNo") String orderNo);
|
||||
|
||||
Map<String, Object> selectByOrgOrderId(@Param("orderTable") String orderTable, @Param("orderNo") String orderNo);
|
||||
|
||||
Map<String, Object> selectByTrackId(@Param("orderTable") String orderTable, @Param("trackingNo") String trackingNo);
|
||||
|
||||
Map<String, Object> selectByBarcode(@Param("orderTable") String orderTable, @Param("trackingNo") String trackingNo);
|
||||
|
||||
Map<String, Object> selectByCustomerTrackId(@Param("orderTable") String orderTable, @Param("trackingNo") String trackingNo);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ruoyi.price.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.price.domain.PrcBillingTask;
|
||||
|
||||
public interface PrcBillingTaskMapper
|
||||
{
|
||||
PrcBillingTask selectPrcBillingTaskById(Long taskId);
|
||||
|
||||
List<PrcBillingTask> selectPrcBillingTaskList(PrcBillingTask query);
|
||||
|
||||
int insertPrcBillingTask(PrcBillingTask task);
|
||||
|
||||
int updatePrcBillingTask(PrcBillingTask task);
|
||||
|
||||
/** CAS: WAITING -> processing claim not needed; writeback SUCCESS/FAILED */
|
||||
int updateBillingResult(PrcBillingTask task);
|
||||
}
|
||||
@@ -11,6 +11,8 @@ public interface PrcOrderBillMapper
|
||||
{
|
||||
public PrcOrderBill selectPrcOrderBillByOrderNo(String orderNo);
|
||||
|
||||
public PrcOrderBill selectPrcOrderBillByTransferNo(String transferNo);
|
||||
|
||||
public PrcOrderBill selectPrcOrderBillById(String billId);
|
||||
|
||||
public List<PrcOrderBill> selectPrcOrderBillList(PrcOrderBill bill);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ruoyi.price.mq;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/** MQ payload: only taskId; consumer loads full row from MySQL. */
|
||||
public class BillingTaskMessage implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long taskId;
|
||||
|
||||
public BillingTaskMessage()
|
||||
{
|
||||
}
|
||||
|
||||
public BillingTaskMessage(Long taskId)
|
||||
{
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public Long getTaskId()
|
||||
{
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(Long taskId)
|
||||
{
|
||||
this.taskId = taskId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.ruoyi.price.mq;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.price.config.BillingProperties;
|
||||
import com.ruoyi.price.service.IPrcBillingTaskService;
|
||||
|
||||
/**
|
||||
* Consume billing task MQ messages and call billing interface.
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(prefix = "bms.billing", name = "consumer-enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class BillingTaskMqConsumer
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(BillingTaskMqConsumer.class);
|
||||
|
||||
@Autowired
|
||||
private IPrcBillingTaskService billingTaskService;
|
||||
|
||||
@Autowired
|
||||
private BillingProperties billingProperties;
|
||||
|
||||
@Autowired(required = false)
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/** Local MQ mode: async event listener. */
|
||||
@Async
|
||||
@EventListener
|
||||
public void onLocalEvent(BillingTaskMqEvent event)
|
||||
{
|
||||
if (!isMode("local"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (event == null || event.getMessage() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
billingTaskService.processTask(event.getMessage().getTaskId());
|
||||
}
|
||||
|
||||
/** Redis MQ mode: poll list every second. */
|
||||
@Scheduled(fixedDelayString = "${bms.billing.consumer-delay-ms:1000}")
|
||||
public void pollRedisQueue()
|
||||
{
|
||||
if (!isMode("redis"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (stringRedisTemplate == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
String key = billingProperties.getMqQueueKey();
|
||||
String payload = stringRedisTemplate.opsForList().rightPop(key);
|
||||
if (payload == null || payload.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
BillingTaskMessage msg = JSON.parseObject(payload, BillingTaskMessage.class);
|
||||
if (msg == null || msg.getTaskId() == null)
|
||||
{
|
||||
log.warn("invalid billing mq payload={}", payload);
|
||||
return;
|
||||
}
|
||||
billingTaskService.processTask(msg.getTaskId());
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
log.error("billing mq consume failed payload={}", payload, e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMode(String expected)
|
||||
{
|
||||
String mode = billingProperties.getMqMode();
|
||||
if (StringUtils.isEmpty(mode))
|
||||
{
|
||||
mode = "redis";
|
||||
}
|
||||
return expected.equalsIgnoreCase(mode.trim());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ruoyi.price.mq;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
public class BillingTaskMqEvent extends ApplicationEvent
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final BillingTaskMessage message;
|
||||
|
||||
public BillingTaskMqEvent(Object source, BillingTaskMessage message)
|
||||
{
|
||||
super(source);
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public BillingTaskMessage getMessage()
|
||||
{
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.ruoyi.price.mq;
|
||||
|
||||
/** Publish billing task id to MQ after warehouse inbound insert. */
|
||||
public interface IBillingTaskMqPublisher
|
||||
{
|
||||
void publish(BillingTaskMessage message);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.ruoyi.price.mq;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Local MQ: Spring application event (dev / no Redis broker).
|
||||
* Enable with bms.billing.mq-mode=local
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(prefix = "bms.billing", name = "mq-mode", havingValue = "local")
|
||||
public class LocalBillingTaskMqPublisher implements IBillingTaskMqPublisher
|
||||
{
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
public LocalBillingTaskMqPublisher(ApplicationEventPublisher eventPublisher)
|
||||
{
|
||||
this.eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(BillingTaskMessage message)
|
||||
{
|
||||
eventPublisher.publishEvent(new BillingTaskMqEvent(this, message));
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.ruoyi.price.mq;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.ruoyi.price.config.BillingProperties;
|
||||
|
||||
/**
|
||||
* Redis list as MQ queue (default). LPUSH on publish; consumer BRPOP/RPOP.
|
||||
* Enable with bms.billing.mq-mode=redis (default).
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(prefix = "bms.billing", name = "mq-mode", havingValue = "redis", matchIfMissing = true)
|
||||
public class RedisBillingTaskMqPublisher implements IBillingTaskMqPublisher
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(RedisBillingTaskMqPublisher.class);
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final BillingProperties billingProperties;
|
||||
|
||||
public RedisBillingTaskMqPublisher(StringRedisTemplate stringRedisTemplate, BillingProperties billingProperties)
|
||||
{
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
this.billingProperties = billingProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(BillingTaskMessage message)
|
||||
{
|
||||
String key = billingProperties.getMqQueueKey();
|
||||
String payload = JSON.toJSONString(message);
|
||||
stringRedisTemplate.opsForList().leftPush(key, payload);
|
||||
log.info("billing task mq published taskId={} queue={}", message.getTaskId(), key);
|
||||
}
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
package com.ruoyi.price.oracle;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
|
||||
|
||||
/**
|
||||
* Map Oracle main-order row (FIELD_MAP style columns) to TmsPriceBillRequest.
|
||||
*/
|
||||
@Component
|
||||
public class OracleOrderBillMapper
|
||||
{
|
||||
public TmsPriceBillRequest toBillRequest(Map<String, Object> row, String defaultProductCode)
|
||||
{
|
||||
if (row == null || row.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
TmsPriceBillRequest req = new TmsPriceBillRequest();
|
||||
req.setOrderNo(firstStr(row, "ORG_ORDER_ID", "ORDER_ID", "TRANSFER_ORDER_ID"));
|
||||
req.setTrackingNo(firstStr(row, "TRANSFER_ORDER_ID", "TRACK_ID", "BARCODE_NUMBER", "CUSTOMER_TRACK_ID"));
|
||||
req.setTransferNo(firstStr(row, "TRANSFER_ORDER_ID"));
|
||||
req.setCustomerCode(firstStr(row, "CUSTOMER_CODE"));
|
||||
req.setCustomerName(firstStr(row, "CUSTOMER_NAME"));
|
||||
String product = firstStr(row, "PRODUCT_CODE", "GOODS_CODE", "CHANNEL_CODE");
|
||||
if (StringUtils.isEmpty(product))
|
||||
{
|
||||
product = defaultProductCode;
|
||||
}
|
||||
req.setProductCode(product);
|
||||
req.setCountryCode(firstStr(row, "COUNTRY"));
|
||||
String countryName = firstStr(row, "COUNTRY_NAME", "COUNTRY_EINGTH_NAME", "COUNTRY");
|
||||
req.setCountryName(countryName);
|
||||
req.setDestPostcode(firstStr(row, "CONSIGNEE_POSTCODE"));
|
||||
// Prefer positive weight: Oracle WEIGHT is often 0 while ACTUAL_WEIGHT/PUSH_WEIGHT holds real value.
|
||||
BigDecimal weight = firstPositiveDecimal(row, "ACTUAL_WEIGHT", "PUSH_WEIGHT", "WEIGHT", "VOLUME_WEIGHT");
|
||||
req.setWeight(weight);
|
||||
req.setActualWeight(firstPositiveDecimal(row, "ACTUAL_WEIGHT", "PUSH_WEIGHT", "WEIGHT"));
|
||||
req.setPieces(firstInt(row, "ORDER_PIECE", "PACKAGE_NUM", "PRODUCT_SUM"));
|
||||
req.setVolume(toVolumeM3(firstDecimal(row, "VOLUME")));
|
||||
req.setVolumeWeight(firstDecimal(row, "VOLUME_WEIGHT"));
|
||||
req.setPkgLength(firstDecimal(row, "LENGTH"));
|
||||
req.setPkgWidth(firstDecimal(row, "WIDTH"));
|
||||
req.setPkgHeight(firstDecimal(row, "HEIGHT"));
|
||||
req.setShipperName(firstStr(row, "SHIPPER_NAME", "SHIPPER_COMPANYNAME"));
|
||||
req.setConsigneeName(firstStr(row, "CONSIGNEE_NAME"));
|
||||
req.setWarehouseCode(firstStr(row, "REGISTER_SITE_CODE"));
|
||||
req.setOrderRemark(firstStr(row, "TRACK_REMARK", "EXCEPTION_REMARK", "ORDER_CUSTOMNOTE"));
|
||||
// CREATE_DATE is order place time only; bizDate (billing date) comes from mobile inbound task.
|
||||
Date createDate = firstDate(row, "CREATE_DATE");
|
||||
if (createDate != null)
|
||||
{
|
||||
req.setOrderTime(createDate);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
private static String firstStr(Map<String, Object> row, String... keys)
|
||||
{
|
||||
Object v = firstValue(row, keys);
|
||||
if (v == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String s = String.valueOf(v).trim();
|
||||
return s.isEmpty() ? null : s;
|
||||
}
|
||||
|
||||
private static BigDecimal firstDecimal(Map<String, Object> row, String... keys)
|
||||
{
|
||||
Object v = firstValue(row, keys);
|
||||
if (v == null || "".equals(String.valueOf(v).trim()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (v instanceof BigDecimal)
|
||||
{
|
||||
return (BigDecimal) v;
|
||||
}
|
||||
if (v instanceof Number)
|
||||
{
|
||||
return BigDecimal.valueOf(((Number) v).doubleValue());
|
||||
}
|
||||
try
|
||||
{
|
||||
return new BigDecimal(String.valueOf(v).trim());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** First decimal that is strictly > 0 among keys (skip Oracle zero placeholders). */
|
||||
private static BigDecimal firstPositiveDecimal(Map<String, Object> row, String... keys)
|
||||
{
|
||||
if (row == null || keys == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
for (String key : keys)
|
||||
{
|
||||
Object raw = getIgnoreCase(row, key);
|
||||
if (raw == null || "".equals(String.valueOf(raw).trim()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BigDecimal d;
|
||||
if (raw instanceof BigDecimal)
|
||||
{
|
||||
d = (BigDecimal) raw;
|
||||
}
|
||||
else if (raw instanceof Number)
|
||||
{
|
||||
d = BigDecimal.valueOf(((Number) raw).doubleValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
d = new BigDecimal(String.valueOf(raw).trim());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (d != null && d.signum() > 0)
|
||||
{
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Integer firstInt(Map<String, Object> row, String... keys)
|
||||
{
|
||||
BigDecimal d = firstDecimal(row, keys);
|
||||
return d == null ? null : Integer.valueOf(d.intValue());
|
||||
}
|
||||
|
||||
private static Date firstDate(Map<String, Object> row, String... keys)
|
||||
{
|
||||
Object v = firstValue(row, keys);
|
||||
if (v == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (v instanceof Date)
|
||||
{
|
||||
return (Date) v;
|
||||
}
|
||||
String s = String.valueOf(v).trim();
|
||||
String[] formats = {
|
||||
"dd/MM/yyyy HH:mm:ss", "dd/MM/yyyy HH:mm", "yyyy-MM-dd HH:mm:ss",
|
||||
"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd"
|
||||
};
|
||||
for (String fmt : formats)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new SimpleDateFormat(fmt, Locale.ENGLISH).parse(s);
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static BigDecimal toVolumeM3(BigDecimal v)
|
||||
{
|
||||
if (v == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (v.doubleValue() > 10d)
|
||||
{
|
||||
return v.divide(BigDecimal.valueOf(1_000_000L), 6, java.math.RoundingMode.HALF_UP);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
private static Object firstValue(Map<String, Object> row, String... keys)
|
||||
{
|
||||
if (row == null || keys == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
for (String key : keys)
|
||||
{
|
||||
Object v = getIgnoreCase(row, key);
|
||||
if (v != null && !"".equals(String.valueOf(v).trim()))
|
||||
{
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object getIgnoreCase(Map<String, Object> row, String key)
|
||||
{
|
||||
if (row.containsKey(key))
|
||||
{
|
||||
return row.get(key);
|
||||
}
|
||||
String upper = key.toUpperCase(Locale.ROOT);
|
||||
if (row.containsKey(upper))
|
||||
{
|
||||
return row.get(upper);
|
||||
}
|
||||
String lower = key.toLowerCase(Locale.ROOT);
|
||||
if (row.containsKey(lower))
|
||||
{
|
||||
return row.get(lower);
|
||||
}
|
||||
for (Map.Entry<String, Object> e : row.entrySet())
|
||||
{
|
||||
if (e.getKey() != null && e.getKey().equalsIgnoreCase(key))
|
||||
{
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package com.ruoyi.price.oracle;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.ruoyi.common.annotation.DataSource;
|
||||
import com.ruoyi.common.enums.DataSourceType;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.price.config.BillingProperties;
|
||||
import com.ruoyi.price.mapper.OracleUpstreamOrderMapper;
|
||||
|
||||
/**
|
||||
* Query Oracle main order via SLAVE datasource.
|
||||
* Requires spring.datasource.druid.slave.enabled=true and Oracle URL.
|
||||
*/
|
||||
@Service
|
||||
public class OracleUpstreamOrderService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(OracleUpstreamOrderService.class);
|
||||
|
||||
@Autowired
|
||||
private OracleUpstreamOrderMapper oracleUpstreamOrderMapper;
|
||||
|
||||
@Autowired
|
||||
private BillingProperties billingProperties;
|
||||
|
||||
@DataSource(DataSourceType.SLAVE)
|
||||
public Map<String, Object> findMainOrder(String transferNo, String orderNo, String trackingNo)
|
||||
{
|
||||
if (StringUtils.isEmpty(transferNo) && StringUtils.isEmpty(orderNo) && StringUtils.isEmpty(trackingNo))
|
||||
{
|
||||
throw new ServiceException("transferNo / orderNo / trackingNo required for Oracle order query");
|
||||
}
|
||||
String table = billingProperties.getOracle().getOrderTable();
|
||||
if (StringUtils.isEmpty(table))
|
||||
{
|
||||
throw new ServiceException("bms.billing.oracle.order-table is empty");
|
||||
}
|
||||
if (!table.matches("[A-Za-z0-9_\\.]+"))
|
||||
{
|
||||
throw new ServiceException("invalid oracle order table name");
|
||||
}
|
||||
|
||||
String tf = emptyToNull(transferNo);
|
||||
String ono = emptyToNull(orderNo);
|
||||
String track = emptyToNull(trackingNo);
|
||||
try
|
||||
{
|
||||
// Avoid PageHelper wrapping this Oracle query (can slow / break SQL).
|
||||
PageHelper.clearPage();
|
||||
|
||||
// 1) Prefer exact match by transfer no (TRANSFER_ORDER_ID)
|
||||
if (tf != null)
|
||||
{
|
||||
Map<String, Object> byTransfer = query("TRANSFER_ORDER_ID",
|
||||
() -> oracleUpstreamOrderMapper.selectByTransferNo(table, tf));
|
||||
if (!isEmpty(byTransfer))
|
||||
{
|
||||
return byTransfer;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Fallback: order no
|
||||
Map<String, Object> row = null;
|
||||
if (ono != null)
|
||||
{
|
||||
row = query("ORDER_ID", () -> oracleUpstreamOrderMapper.selectByOrderId(table, ono));
|
||||
}
|
||||
if (isEmpty(row) && ono != null)
|
||||
{
|
||||
row = query("ORG_ORDER_ID", () -> oracleUpstreamOrderMapper.selectByOrgOrderId(table, ono));
|
||||
}
|
||||
|
||||
// 3) Fallback: tracking no
|
||||
if (isEmpty(row) && track != null)
|
||||
{
|
||||
row = query("TRACK_ID", () -> oracleUpstreamOrderMapper.selectByTrackId(table, track));
|
||||
}
|
||||
if (isEmpty(row) && track != null)
|
||||
{
|
||||
row = query("BARCODE_NUMBER", () -> oracleUpstreamOrderMapper.selectByBarcode(table, track));
|
||||
}
|
||||
if (isEmpty(row) && track != null)
|
||||
{
|
||||
row = query("CUSTOMER_TRACK_ID", () -> oracleUpstreamOrderMapper.selectByCustomerTrackId(table, track));
|
||||
}
|
||||
return row;
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
throw new ServiceException("Oracle order query failed: " + e.getMessage());
|
||||
}
|
||||
finally
|
||||
{
|
||||
PageHelper.clearPage();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> query(String by, Supplier<Map<String, Object>> supplier)
|
||||
{
|
||||
long t0 = System.currentTimeMillis();
|
||||
Map<String, Object> row = supplier.get();
|
||||
log.info("oracle order lookup by={} hit={} costMs={}", by, !isEmpty(row), System.currentTimeMillis() - t0);
|
||||
return row;
|
||||
}
|
||||
|
||||
private static boolean isEmpty(Map<String, Object> row)
|
||||
{
|
||||
return row == null || row.isEmpty();
|
||||
}
|
||||
|
||||
private static String emptyToNull(String s)
|
||||
{
|
||||
return StringUtils.isEmpty(s) ? null : s.trim();
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.price.service;
|
||||
|
||||
import com.ruoyi.price.domain.PrcBillingTask;
|
||||
import com.ruoyi.price.domain.vo.BillingTaskSubmitRequest;
|
||||
|
||||
public interface IPrcBillingTaskService
|
||||
{
|
||||
/** Warehouse inbound: create task + publish MQ. */
|
||||
PrcBillingTask submitInbound(BillingTaskSubmitRequest request);
|
||||
|
||||
/** MQ consumer entry: Oracle query -> bill -> writeback success status. */
|
||||
void processTask(Long taskId);
|
||||
|
||||
PrcBillingTask getById(Long taskId);
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
package com.ruoyi.price.service.impl;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.price.config.BillingProperties;
|
||||
import com.ruoyi.price.domain.PrcBillingTask;
|
||||
import com.ruoyi.price.domain.PrcOrderBill;
|
||||
import com.ruoyi.price.domain.vo.BillingTaskSubmitRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
|
||||
import com.ruoyi.price.enums.BillTaskSuccessStatus;
|
||||
import com.ruoyi.price.enums.BillTaskType;
|
||||
import com.ruoyi.price.mapper.PrcBillingTaskMapper;
|
||||
import com.ruoyi.price.mapper.PrcOrderBillMapper;
|
||||
import com.ruoyi.price.mq.BillingTaskMessage;
|
||||
import com.ruoyi.price.mq.IBillingTaskMqPublisher;
|
||||
import com.ruoyi.price.oracle.OracleOrderBillMapper;
|
||||
import com.ruoyi.price.oracle.OracleUpstreamOrderService;
|
||||
import com.ruoyi.price.service.IPrcBillingTaskService;
|
||||
import com.ruoyi.price.service.IPrcOrderBillService;
|
||||
|
||||
@Service
|
||||
public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(PrcBillingTaskServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private PrcBillingTaskMapper billingTaskMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcOrderBillMapper orderBillMapper;
|
||||
|
||||
@Autowired
|
||||
private IBillingTaskMqPublisher billingTaskMqPublisher;
|
||||
|
||||
@Autowired
|
||||
private OracleUpstreamOrderService oracleUpstreamOrderService;
|
||||
|
||||
@Autowired
|
||||
private OracleOrderBillMapper oracleOrderBillMapper;
|
||||
|
||||
@Autowired
|
||||
private IPrcOrderBillService prcOrderBillService;
|
||||
|
||||
@Autowired
|
||||
private BillingProperties billingProperties;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PrcBillingTask submitInbound(BillingTaskSubmitRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new ServiceException("billing task request required");
|
||||
}
|
||||
String transferNo = trim(request.getTransferNo());
|
||||
String trackingNo = trim(request.getTrackingNo());
|
||||
String orderNo = trim(request.getOrderNo());
|
||||
Date billTime = request.getBillTime();
|
||||
if (StringUtils.isEmpty(transferNo) && StringUtils.isEmpty(trackingNo) && StringUtils.isEmpty(orderNo))
|
||||
{
|
||||
throw new ServiceException("transferNo or trackingNo or orderNo required");
|
||||
}
|
||||
if (billTime == null)
|
||||
{
|
||||
throw new ServiceException("billTime required (billing time from mobile)");
|
||||
}
|
||||
|
||||
BillTaskType type = BillTaskType.fromCode(request.getBillType());
|
||||
if (type == null)
|
||||
{
|
||||
type = resolveAutoType(transferNo, trackingNo, orderNo);
|
||||
}
|
||||
|
||||
PrcBillingTask task = new PrcBillingTask();
|
||||
task.setTransferNo(transferNo);
|
||||
task.setTrackingNo(trackingNo);
|
||||
task.setOrderNo(orderNo);
|
||||
task.setBillTime(billTime);
|
||||
task.setBillType(type.getCode());
|
||||
task.setSuccessStatus(BillTaskSuccessStatus.WAITING.getCode());
|
||||
task.setCreateBy("tms");
|
||||
billingTaskMapper.insertPrcBillingTask(task);
|
||||
|
||||
billingTaskMqPublisher.publish(new BillingTaskMessage(task.getTaskId()));
|
||||
log.info("billing task submitted taskId={} type={} billTime={} transferNo={} trackingNo={} orderNo={}",
|
||||
task.getTaskId(), task.getBillType(), billTime, transferNo, trackingNo, orderNo);
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processTask(Long taskId)
|
||||
{
|
||||
if (taskId == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
PrcBillingTask task = billingTaskMapper.selectPrcBillingTaskById(taskId);
|
||||
if (task == null)
|
||||
{
|
||||
log.warn("billing task not found taskId={}", taskId);
|
||||
return;
|
||||
}
|
||||
if (BillTaskSuccessStatus.SUCCESS.getCode().equals(task.getSuccessStatus()))
|
||||
{
|
||||
log.info("billing task already success taskId={}", taskId);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
Map<String, Object> row = oracleUpstreamOrderService.findMainOrder(
|
||||
task.getTransferNo(), task.getOrderNo(), task.getTrackingNo());
|
||||
if (row == null || row.isEmpty())
|
||||
{
|
||||
throw new ServiceException("Oracle main order not found");
|
||||
}
|
||||
TmsPriceBillRequest billReq = oracleOrderBillMapper.toBillRequest(row,
|
||||
billingProperties.getOracle().getDefaultProductCode());
|
||||
if (billReq == null || StringUtils.isEmpty(billReq.getOrderNo()))
|
||||
{
|
||||
throw new ServiceException("Oracle order mapped orderNo empty");
|
||||
}
|
||||
if (StringUtils.isEmpty(billReq.getCustomerCode()))
|
||||
{
|
||||
throw new ServiceException("Oracle order mapped customerCode empty");
|
||||
}
|
||||
if (StringUtils.isEmpty(billReq.getProductCode()))
|
||||
{
|
||||
throw new ServiceException("Oracle order mapped productCode empty");
|
||||
}
|
||||
if (billReq.getWeight() == null || billReq.getWeight().signum() <= 0)
|
||||
{
|
||||
throw new ServiceException("Oracle order mapped weight invalid");
|
||||
}
|
||||
// Prefer task keys when Oracle mapping missed them
|
||||
if (StringUtils.isEmpty(billReq.getTransferNo()) && StringUtils.isNotEmpty(task.getTransferNo()))
|
||||
{
|
||||
billReq.setTransferNo(task.getTransferNo());
|
||||
}
|
||||
if (StringUtils.isEmpty(billReq.getTrackingNo()) && StringUtils.isNotEmpty(task.getTrackingNo()))
|
||||
{
|
||||
billReq.setTrackingNo(task.getTrackingNo());
|
||||
}
|
||||
// Billing date from task.bill_time (mobile), not Oracle CREATE_DATE.
|
||||
if (task.getBillTime() == null)
|
||||
{
|
||||
throw new ServiceException("task billTime empty (billing time from mobile required)");
|
||||
}
|
||||
billReq.setBizDate(new SimpleDateFormat("yyyy-MM-dd").format(task.getBillTime()));
|
||||
|
||||
BillTaskType type = BillTaskType.fromCode(task.getBillType());
|
||||
if (BillTaskType.REFUND == type)
|
||||
{
|
||||
// V1: refund still syncs latest Oracle snapshot into bill (business refine later).
|
||||
log.info("billing task REFUND path uses bill upsert taskId={}", taskId);
|
||||
}
|
||||
|
||||
TmsPriceQuoteResult result = prcOrderBillService.bill(billReq);
|
||||
writeback(task, BillTaskSuccessStatus.SUCCESS, result == null ? null : result.getBillId(),
|
||||
billReq, null);
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
log.error("billing task failed taskId={}", taskId, e);
|
||||
writeback(task, BillTaskSuccessStatus.FAILED, null, null, cut(e.getMessage(), 900));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrcBillingTask getById(Long taskId)
|
||||
{
|
||||
return billingTaskMapper.selectPrcBillingTaskById(taskId);
|
||||
}
|
||||
|
||||
private BillTaskType resolveAutoType(String transferNo, String trackingNo, String orderNo)
|
||||
{
|
||||
PrcOrderBill existed = null;
|
||||
if (StringUtils.isNotEmpty(orderNo))
|
||||
{
|
||||
existed = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo);
|
||||
}
|
||||
if (existed == null && StringUtils.isNotEmpty(transferNo))
|
||||
{
|
||||
existed = orderBillMapper.selectPrcOrderBillByTransferNo(transferNo);
|
||||
}
|
||||
if (existed == null && StringUtils.isNotEmpty(trackingNo))
|
||||
{
|
||||
PrcOrderBill q = new PrcOrderBill();
|
||||
q.setTrackingNo(trackingNo);
|
||||
java.util.List<PrcOrderBill> list = orderBillMapper.selectPrcOrderBillList(q);
|
||||
if (list != null && !list.isEmpty())
|
||||
{
|
||||
existed = list.get(0);
|
||||
}
|
||||
}
|
||||
return existed == null ? BillTaskType.PENDING : BillTaskType.REBILL;
|
||||
}
|
||||
|
||||
private void writeback(PrcBillingTask task, BillTaskSuccessStatus status, String billId,
|
||||
TmsPriceBillRequest billReq, String errorMessage)
|
||||
{
|
||||
PrcBillingTask upd = new PrcBillingTask();
|
||||
upd.setTaskId(task.getTaskId());
|
||||
upd.setSuccessStatus(status.getCode());
|
||||
// Keep bill_time as mobile billing time; do not overwrite on writeback.
|
||||
upd.setBillId(billId);
|
||||
upd.setErrorMessage(errorMessage);
|
||||
if (billReq != null)
|
||||
{
|
||||
upd.setOrderNo(billReq.getOrderNo());
|
||||
upd.setTrackingNo(billReq.getTrackingNo());
|
||||
upd.setTransferNo(billReq.getTransferNo());
|
||||
}
|
||||
billingTaskMapper.updateBillingResult(upd);
|
||||
}
|
||||
|
||||
private static String trim(String s)
|
||||
{
|
||||
return s == null ? null : s.trim();
|
||||
}
|
||||
|
||||
private static String cut(String s, int max)
|
||||
{
|
||||
if (s == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return s.length() <= max ? s : s.substring(0, max);
|
||||
}
|
||||
}
|
||||
+75
-10
@@ -27,6 +27,8 @@ import com.ruoyi.price.domain.PrcPayBillFee;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteFeeLine;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
|
||||
import com.ruoyi.price.enums.BillCreditArrearsStatus;
|
||||
import com.ruoyi.price.enums.BillCustomerActiveStatus;
|
||||
import com.ruoyi.price.mapper.PrcIncomeBillFeeMapper;
|
||||
import com.ruoyi.price.mapper.PrcOrderBillExtMapper;
|
||||
import com.ruoyi.price.mapper.PrcOrderBillMapper;
|
||||
@@ -37,6 +39,7 @@ import com.ruoyi.price.search.IBillSearchStore;
|
||||
import com.ruoyi.customer.customermoney.api.CreditUsageCommand;
|
||||
import com.ruoyi.customer.customermoney.api.CustomerFundBizType;
|
||||
import com.ruoyi.customer.customermoney.api.ICustomerAccountApi;
|
||||
import com.ruoyi.customer.customermoney.vo.CustomerAccountStatusVO;
|
||||
import com.ruoyi.price.service.IPrcOrderBillService;
|
||||
import com.ruoyi.price.service.ITmsPriceQuoteService;
|
||||
|
||||
@@ -192,6 +195,13 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
bill.setCustomerCode(customerCode);
|
||||
bill.setCustomerName(request.getCustomerName());
|
||||
bill.setTrackingNo(request.getTrackingNo());
|
||||
bill.setTransferNo(request.getTransferNo());
|
||||
String customerActiveStatus = result == null ? null : result.getCustomerActiveStatus();
|
||||
if (StringUtils.isEmpty(customerActiveStatus))
|
||||
{
|
||||
customerActiveStatus = BillCustomerActiveStatus.ACTIVE.getCode();
|
||||
}
|
||||
bill.setCustomerActiveStatus(customerActiveStatus);
|
||||
bill.setPieces(request.getPieces());
|
||||
bill.setActualWeight(request.getActualWeight());
|
||||
bill.setVolume(request.getVolume());
|
||||
@@ -236,10 +246,16 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
bill.setPayTotal(result.getPayTotal() == null ? BigDecimal.ZERO : result.getPayTotal());
|
||||
bill.setCurrency(result.getCurrency());
|
||||
bill.setRoundMode(result.getRoundMode());
|
||||
// Credit soft-fail before persist so insert/update stores arrears flag in one write.
|
||||
String creditArrearsStatus = syncOrderCredit(bill, previousIncome, customerActiveStatus);
|
||||
bill.setCreditArrearsStatus(creditArrearsStatus);
|
||||
result.setBillId(bill.getBillId());
|
||||
result.setOrderNo(orderNo);
|
||||
result.setTransferNo(bill.getTransferNo());
|
||||
result.setCustomerId(bill.getCustomerId());
|
||||
result.setCustomerCode(bill.getCustomerCode());
|
||||
result.setCustomerActiveStatus(customerActiveStatus);
|
||||
result.setCreditArrearsStatus(creditArrearsStatus);
|
||||
result.setIncomeTotal(bill.getIncomeTotal());
|
||||
result.setPayTotal(bill.getPayTotal());
|
||||
result.setPayableTotal(bill.getIncomeTotal());
|
||||
@@ -263,33 +279,40 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
String syncJson = toRequestJson(request);
|
||||
persistBillExt(bill, syncJson);
|
||||
result.setRequestJson(syncJson);
|
||||
syncOrderCredit(bill, previousIncome);
|
||||
billSearchStore.save(EsLogType.BILL, orderNo, syncJson, toResponseJson(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Occupy/release credit by income total delta (bizType=ORDER).
|
||||
* Always write CREDIT log to ES (SUCCESS / FAIL), then rethrow on failure.
|
||||
* Missing/disabled customer: skip occupy, return NORMAL.
|
||||
* Insufficient credit: still occupy with checkAvailable=false, return ARREARS.
|
||||
* Always write CREDIT log to ES (SUCCESS / FAIL / SKIP).
|
||||
*/
|
||||
private void syncOrderCredit(PrcOrderBill bill, BigDecimal previousIncome)
|
||||
private String syncOrderCredit(PrcOrderBill bill, BigDecimal previousIncome, String customerActiveStatus)
|
||||
{
|
||||
String normal = BillCreditArrearsStatus.NORMAL.getCode();
|
||||
String arrears = BillCreditArrearsStatus.ARREARS.getCode();
|
||||
if (bill == null)
|
||||
{
|
||||
return;
|
||||
return normal;
|
||||
}
|
||||
if (!BillCustomerActiveStatus.ACTIVE.getCode().equals(customerActiveStatus))
|
||||
{
|
||||
saveCreditEsLog(bill, null, "SKIP", true, "customerActiveStatus=" + customerActiveStatus);
|
||||
return normal;
|
||||
}
|
||||
Long customerId = bill.getCustomerId();
|
||||
if (customerId == null || customerId.longValue() <= 0L)
|
||||
{
|
||||
ServiceException ex = new ServiceException(MessageUtils.message("price.bill.customerRequired"));
|
||||
saveCreditEsLog(bill, null, "SKIP", false, EsBizLogSupport.errorText(ex));
|
||||
throw ex;
|
||||
saveCreditEsLog(bill, null, "SKIP", true, "customerId missing");
|
||||
return normal;
|
||||
}
|
||||
BigDecimal oldAmt = nz(previousIncome);
|
||||
BigDecimal newAmt = nz(bill.getIncomeTotal());
|
||||
BigDecimal delta = newAmt.subtract(oldAmt);
|
||||
if (delta.compareTo(BigDecimal.ZERO) == 0)
|
||||
{
|
||||
return;
|
||||
return normal;
|
||||
}
|
||||
CreditUsageCommand cmd = new CreditUsageCommand();
|
||||
cmd.setCustomerId(customerId);
|
||||
@@ -297,8 +320,14 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
cmd.setCurrency(StringUtils.isEmpty(bill.getCurrency()) ? "CNY" : bill.getCurrency());
|
||||
cmd.setBizType(CustomerFundBizType.ORDER);
|
||||
cmd.setBizNo(bill.getOrderNo());
|
||||
cmd.setCheckAvailable(Boolean.TRUE);
|
||||
// Soft: allow used_credit to exceed available; mark ARREARS on the bill instead of failing.
|
||||
cmd.setCheckAvailable(Boolean.FALSE);
|
||||
String action = delta.compareTo(BigDecimal.ZERO) > 0 ? "OCCUPY" : "RELEASE";
|
||||
boolean markArrears = false;
|
||||
if (delta.compareTo(BigDecimal.ZERO) > 0)
|
||||
{
|
||||
markArrears = isCreditInsufficient(customerId, bill.getCustomerCode(), delta);
|
||||
}
|
||||
try
|
||||
{
|
||||
if (delta.compareTo(BigDecimal.ZERO) > 0)
|
||||
@@ -311,15 +340,44 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
cmd.setAmount(delta.abs());
|
||||
customerAccountApi.releaseCredit(cmd);
|
||||
}
|
||||
saveCreditEsLog(bill, cmd, action, true, null);
|
||||
saveCreditEsLog(bill, cmd, action, true, markArrears ? "ARREARS" : null);
|
||||
return markArrears ? arrears : normal;
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
// Unexpected occupy/release failure (lock conflict etc.): fail the bill txn.
|
||||
saveCreditEsLog(bill, cmd, action, false, EsBizLogSupport.errorText(e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** True when available credit cannot cover this occupy delta (or already overdue). */
|
||||
private boolean isCreditInsufficient(Long customerId, String customerCode, BigDecimal delta)
|
||||
{
|
||||
try
|
||||
{
|
||||
CustomerAccountStatusVO status = customerAccountApi.getAccountStatus(customerId, customerCode);
|
||||
if (status == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (status.isOverdue())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
BigDecimal available = status.getAvailableCredit();
|
||||
if (available == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return available.compareTo(nz(delta)) < 0;
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** CREDIT ES log; does not set ATTR_FAIL_LOGGED so BILL fail can still be written. */
|
||||
private void saveCreditEsLog(PrcOrderBill bill, CreditUsageCommand cmd, String action, boolean success,
|
||||
String errorMessage)
|
||||
@@ -352,6 +410,10 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
{
|
||||
resp.put("msg", errorMessage);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(errorMessage) && success)
|
||||
{
|
||||
resp.put("note", errorMessage);
|
||||
}
|
||||
String status = success ? EsBizLogSupport.STATUS_SUCCESS : EsBizLogSupport.STATUS_FAIL;
|
||||
String err = success ? null : EsBizLogSupport.cut(errorMessage);
|
||||
billSearchStore.save(EsLogType.CREDIT, bill.getOrderNo(), req.toJSONString(), resp.toJSONString(), status, err);
|
||||
@@ -529,7 +591,10 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
private void fillResultOrder(TmsPriceQuoteResult r, PrcOrderBill bill)
|
||||
{
|
||||
r.setTrackingNo(bill.getTrackingNo());
|
||||
r.setTransferNo(bill.getTransferNo());
|
||||
r.setCustomerName(bill.getCustomerName());
|
||||
r.setCustomerActiveStatus(bill.getCustomerActiveStatus());
|
||||
r.setCreditArrearsStatus(bill.getCreditArrearsStatus());
|
||||
r.setPieces(bill.getPieces());
|
||||
r.setActualWeight(bill.getActualWeight());
|
||||
r.setVolume(bill.getVolume());
|
||||
|
||||
+46
-1
@@ -39,6 +39,7 @@ import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteFeeLine;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
|
||||
import com.ruoyi.price.enums.BillCustomerActiveStatus;
|
||||
import com.ruoyi.price.mapper.BmsProductCountryMapper;
|
||||
import com.ruoyi.price.mapper.BmsProductMapper;
|
||||
import com.ruoyi.price.mapper.PrcCustomerPriceSetMapper;
|
||||
@@ -163,10 +164,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
public TmsPriceQuoteResult calculate(TmsPriceQuoteRequest request)
|
||||
{
|
||||
validateRequest(request);
|
||||
resolveCustomer(request);
|
||||
// Bill path: missing/disabled customer only marks status, pricing still continues.
|
||||
String customerActiveStatus = softResolveCustomer(request);
|
||||
Date bizDate = resolveBizDate(request.getBizDate());
|
||||
TmsPriceQuoteResult result = doQuote(request, bizDate);
|
||||
applySurchargesAndFees(result);
|
||||
result.setCustomerActiveStatus(customerActiveStatus);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -359,6 +362,48 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.quote.customerDisabled"));
|
||||
}
|
||||
applyResolvedCustomer(request, hit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft resolve for billing: never throws on missing/disabled customer.
|
||||
* Returns ACTIVE / MISSING / DISABLED. Quote path still uses hard {@link #resolveCustomer}.
|
||||
*/
|
||||
private String softResolveCustomer(TmsPriceQuoteRequest request)
|
||||
{
|
||||
if (request == null || StringUtils.isEmpty(request.getCustomerCode()))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.quote.customerCodeRequired"));
|
||||
}
|
||||
String code = request.getCustomerCode().trim();
|
||||
if (StringUtils.isEmpty(code))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.quote.customerCodeRequired"));
|
||||
}
|
||||
request.setCustomerCode(code);
|
||||
BmsCustomer query = new BmsCustomer();
|
||||
query.setCustomerCode(code);
|
||||
List<BmsCustomer> list = bmsCustomerMapper.selectBmsCustomerList(query);
|
||||
if (list == null || list.isEmpty())
|
||||
{
|
||||
request.setCustomerId(null);
|
||||
return BillCustomerActiveStatus.MISSING.getCode();
|
||||
}
|
||||
BmsCustomer hit = list.get(0);
|
||||
applyResolvedCustomer(request, hit);
|
||||
if ("1".equals(hit.getIsEnable()))
|
||||
{
|
||||
return BillCustomerActiveStatus.DISABLED.getCode();
|
||||
}
|
||||
return BillCustomerActiveStatus.ACTIVE.getCode();
|
||||
}
|
||||
|
||||
private void applyResolvedCustomer(TmsPriceQuoteRequest request, BmsCustomer hit)
|
||||
{
|
||||
if (request == null || hit == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
request.setCustomerId(hit.getCustomerId());
|
||||
if (StringUtils.isNotEmpty(hit.getCustomerCode()))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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.price.mapper.OracleUpstreamOrderMapper">
|
||||
|
||||
<!--
|
||||
Exact-match lookups only (no multi-column OR).
|
||||
Nested ROWNUM form helps Oracle use indexes on large TAB_ORDER.
|
||||
-->
|
||||
|
||||
<select id="selectByTransferNo" resultType="java.util.HashMap">
|
||||
SELECT * FROM (
|
||||
SELECT * FROM ${orderTable} WHERE TRANSFER_ORDER_ID = #{transferNo}
|
||||
) WHERE ROWNUM <= 1
|
||||
</select>
|
||||
|
||||
<select id="selectByOrderId" resultType="java.util.HashMap">
|
||||
SELECT * FROM (
|
||||
SELECT * FROM ${orderTable} WHERE ORDER_ID = #{orderNo}
|
||||
) WHERE ROWNUM <= 1
|
||||
</select>
|
||||
|
||||
<select id="selectByOrgOrderId" resultType="java.util.HashMap">
|
||||
SELECT * FROM (
|
||||
SELECT * FROM ${orderTable} WHERE ORG_ORDER_ID = #{orderNo}
|
||||
) WHERE ROWNUM <= 1
|
||||
</select>
|
||||
|
||||
<select id="selectByTrackId" resultType="java.util.HashMap">
|
||||
SELECT * FROM (
|
||||
SELECT * FROM ${orderTable} WHERE TRACK_ID = #{trackingNo}
|
||||
) WHERE ROWNUM <= 1
|
||||
</select>
|
||||
|
||||
<select id="selectByBarcode" resultType="java.util.HashMap">
|
||||
SELECT * FROM (
|
||||
SELECT * FROM ${orderTable} WHERE BARCODE_NUMBER = #{trackingNo}
|
||||
) WHERE ROWNUM <= 1
|
||||
</select>
|
||||
|
||||
<select id="selectByCustomerTrackId" resultType="java.util.HashMap">
|
||||
SELECT * FROM (
|
||||
SELECT * FROM ${orderTable} WHERE CUSTOMER_TRACK_ID = #{trackingNo}
|
||||
) WHERE ROWNUM <= 1
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -0,0 +1,95 @@
|
||||
<?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.price.mapper.PrcBillingTaskMapper">
|
||||
|
||||
<resultMap type="PrcBillingTask" id="PrcBillingTaskResult">
|
||||
<id property="taskId" column="task_id" />
|
||||
<result property="transferNo" column="transfer_no" />
|
||||
<result property="trackingNo" column="tracking_no" />
|
||||
<result property="orderNo" column="order_no" />
|
||||
<result property="billType" column="bill_type" />
|
||||
<result property="successStatus" column="success_status" />
|
||||
<result property="billTime" column="bill_time" />
|
||||
<result property="billId" column="bill_id" />
|
||||
<result property="errorMessage" column="error_message" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectPrcBillingTaskVo">
|
||||
select task_id, transfer_no, tracking_no, order_no, bill_type, success_status,
|
||||
bill_time, bill_id, error_message, create_by, create_time, update_by, update_time
|
||||
from prc_billing_task
|
||||
</sql>
|
||||
|
||||
<select id="selectPrcBillingTaskById" parameterType="Long" resultMap="PrcBillingTaskResult">
|
||||
<include refid="selectPrcBillingTaskVo"/>
|
||||
where task_id = #{taskId}
|
||||
</select>
|
||||
|
||||
<select id="selectPrcBillingTaskList" parameterType="PrcBillingTask" resultMap="PrcBillingTaskResult">
|
||||
<include refid="selectPrcBillingTaskVo"/>
|
||||
<where>
|
||||
<if test="transferNo != null and transferNo != ''">
|
||||
AND transfer_no = #{transferNo}
|
||||
</if>
|
||||
<if test="trackingNo != null and trackingNo != ''">
|
||||
AND tracking_no = #{trackingNo}
|
||||
</if>
|
||||
<if test="orderNo != null and orderNo != ''">
|
||||
AND order_no = #{orderNo}
|
||||
</if>
|
||||
<if test="billType != null and billType != ''">
|
||||
AND bill_type = #{billType}
|
||||
</if>
|
||||
<if test="successStatus != null and successStatus != ''">
|
||||
AND success_status = #{successStatus}
|
||||
</if>
|
||||
</where>
|
||||
order by task_id desc
|
||||
</select>
|
||||
|
||||
<insert id="insertPrcBillingTask" parameterType="PrcBillingTask" useGeneratedKeys="true" keyProperty="taskId">
|
||||
insert into prc_billing_task (
|
||||
transfer_no, tracking_no, order_no, bill_type, success_status,
|
||||
bill_time, bill_id, error_message, create_by, create_time
|
||||
) values (
|
||||
#{transferNo}, #{trackingNo}, #{orderNo}, #{billType}, #{successStatus},
|
||||
#{billTime}, #{billId}, #{errorMessage}, #{createBy}, sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updatePrcBillingTask" parameterType="PrcBillingTask">
|
||||
update prc_billing_task
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="transferNo != null">transfer_no = #{transferNo},</if>
|
||||
<if test="trackingNo != null">tracking_no = #{trackingNo},</if>
|
||||
<if test="orderNo != null">order_no = #{orderNo},</if>
|
||||
<if test="billType != null">bill_type = #{billType},</if>
|
||||
<if test="successStatus != null">success_status = #{successStatus},</if>
|
||||
<if test="billTime != null">bill_time = #{billTime},</if>
|
||||
<if test="billId != null">bill_id = #{billId},</if>
|
||||
<if test="errorMessage != null">error_message = #{errorMessage},</if>
|
||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||
update_time = sysdate()
|
||||
</trim>
|
||||
where task_id = #{taskId}
|
||||
</update>
|
||||
|
||||
<!-- Do not overwrite bill_time: it stores mobile billing time. -->
|
||||
<update id="updateBillingResult" parameterType="PrcBillingTask">
|
||||
update prc_billing_task
|
||||
set success_status = #{successStatus},
|
||||
bill_id = #{billId},
|
||||
error_message = #{errorMessage},
|
||||
order_no = coalesce(#{orderNo}, order_no),
|
||||
tracking_no = coalesce(#{trackingNo}, tracking_no),
|
||||
transfer_no = coalesce(#{transferNo}, transfer_no),
|
||||
update_time = sysdate()
|
||||
where task_id = #{taskId}
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -11,6 +11,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="billKey" column="bill_key" />
|
||||
<result property="orderNo" column="order_no" />
|
||||
<result property="trackingNo" column="tracking_no" />
|
||||
<result property="transferNo" column="transfer_no" />
|
||||
<result property="customerId" column="customer_id" />
|
||||
<result property="customerCode" column="customer_code" />
|
||||
<result property="customerName" column="customer_name" />
|
||||
@@ -58,6 +59,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="payTotal" column="pay_total" />
|
||||
<result property="currency" column="currency" />
|
||||
<result property="arBillStatus" column="ar_bill_status" />
|
||||
<result property="customerActiveStatus" column="customer_active_status" />
|
||||
<result property="creditArrearsStatus" column="credit_arrears_status" />
|
||||
<result property="roundMode" column="round_mode" />
|
||||
<result property="createBy" column="created_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
@@ -66,7 +69,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectPrcOrderBillVo">
|
||||
select bill_id, bill_key, order_no, tracking_no, customer_id, customer_code, customer_name,
|
||||
select bill_id, bill_key, order_no, tracking_no, transfer_no, customer_id, customer_code, customer_name,
|
||||
pieces, actual_weight, volume, volume_weight, pkg_length, pkg_width, pkg_height,
|
||||
shipper_name, consignee_name, dest_postcode, warehouse_code, order_time, order_remark,
|
||||
product_code, product_name, country_code, country_name, weight, biz_date, tms_id,
|
||||
@@ -74,7 +77,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
set_type, bind_priority, customer_priceset_id, product_country_id,
|
||||
weight_min, weight_max, weight_label, effective_date, charge_weight,
|
||||
freight_rate, base_freight_rate, discount_percent, registration_fee,
|
||||
freight_amount, surcharge_amount, income_total, pay_total, currency, ar_bill_status, round_mode,
|
||||
freight_amount, surcharge_amount, income_total, pay_total, currency, ar_bill_status,
|
||||
customer_active_status, credit_arrears_status, round_mode,
|
||||
created_by, create_time, updated_by, update_time
|
||||
from prc_order_bill
|
||||
</sql>
|
||||
@@ -84,6 +88,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where order_no = #{orderNo}
|
||||
</select>
|
||||
|
||||
<select id="selectPrcOrderBillByTransferNo" parameterType="String" resultMap="PrcOrderBillResult">
|
||||
<include refid="selectPrcOrderBillVo"/>
|
||||
where transfer_no = #{transferNo}
|
||||
order by create_time desc, bill_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectPrcOrderBillById" parameterType="String" resultMap="PrcOrderBillResult">
|
||||
<include refid="selectPrcOrderBillVo"/>
|
||||
where bill_id = #{billId}
|
||||
@@ -117,6 +128,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="trackingNo != null and trackingNo != ''">
|
||||
AND tracking_no like concat('%', #{trackingNo}, '%')
|
||||
</if>
|
||||
<if test="transferNo != null and transferNo != ''">
|
||||
AND transfer_no like concat('%', #{transferNo}, '%')
|
||||
</if>
|
||||
<if test="customerActiveStatus != null and customerActiveStatus != ''">
|
||||
AND customer_active_status = #{customerActiveStatus}
|
||||
</if>
|
||||
<if test="creditArrearsStatus != null and creditArrearsStatus != ''">
|
||||
AND credit_arrears_status = #{creditArrearsStatus}
|
||||
</if>
|
||||
<if test="customerCode != null and customerCode != ''">
|
||||
AND customer_code like concat('%', #{customerCode}, '%')
|
||||
</if>
|
||||
@@ -282,7 +302,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<insert id="insertPrcOrderBill" parameterType="PrcOrderBill">
|
||||
insert into prc_order_bill (
|
||||
bill_id, bill_key, order_no, tracking_no, customer_id, customer_code, customer_name,
|
||||
bill_id, bill_key, order_no, tracking_no, transfer_no, customer_id, customer_code, customer_name,
|
||||
pieces, actual_weight, volume, volume_weight, pkg_length, pkg_width, pkg_height,
|
||||
shipper_name, consignee_name, dest_postcode, warehouse_code, order_time, order_remark,
|
||||
product_code, product_name, country_code, country_name, weight, biz_date, tms_id,
|
||||
@@ -290,10 +310,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
set_type, bind_priority, customer_priceset_id, product_country_id,
|
||||
weight_min, weight_max, weight_label, effective_date, charge_weight,
|
||||
freight_rate, base_freight_rate, discount_percent, registration_fee,
|
||||
freight_amount, surcharge_amount, income_total, pay_total, currency, ar_bill_status, round_mode,
|
||||
freight_amount, surcharge_amount, income_total, pay_total, currency, ar_bill_status,
|
||||
customer_active_status, credit_arrears_status, round_mode,
|
||||
create_time
|
||||
) values (
|
||||
#{billId}, #{billKey}, #{orderNo}, #{trackingNo}, #{customerId}, #{customerCode}, #{customerName},
|
||||
#{billId}, #{billKey}, #{orderNo}, #{trackingNo}, #{transferNo}, #{customerId}, #{customerCode}, #{customerName},
|
||||
#{pieces}, #{actualWeight}, #{volume}, #{volumeWeight}, #{pkgLength}, #{pkgWidth}, #{pkgHeight},
|
||||
#{shipperName}, #{consigneeName}, #{destPostcode}, #{warehouseCode}, #{orderTime}, #{orderRemark},
|
||||
#{productCode}, #{productName}, #{countryCode}, #{countryName}, #{weight}, #{bizDate}, #{tmsId},
|
||||
@@ -301,7 +322,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
#{setType}, #{bindPriority}, #{customerPricesetId}, #{productCountryId},
|
||||
#{weightMin}, #{weightMax}, #{weightLabel}, #{effectiveDate}, #{chargeWeight},
|
||||
#{freightRate}, #{baseFreightRate}, #{discountPercent}, #{registrationFee},
|
||||
#{freightAmount}, #{surchargeAmount}, #{incomeTotal}, #{payTotal}, #{currency}, #{arBillStatus}, #{roundMode},
|
||||
#{freightAmount}, #{surchargeAmount}, #{incomeTotal}, #{payTotal}, #{currency}, #{arBillStatus},
|
||||
#{customerActiveStatus}, #{creditArrearsStatus}, #{roundMode},
|
||||
sysdate()
|
||||
)
|
||||
</insert>
|
||||
@@ -310,6 +332,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
update prc_order_bill
|
||||
set bill_key = #{billKey},
|
||||
tracking_no = #{trackingNo},
|
||||
transfer_no = #{transferNo},
|
||||
customer_id = #{customerId},
|
||||
customer_code = #{customerCode},
|
||||
customer_name = #{customerName},
|
||||
@@ -358,6 +381,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
income_total = #{incomeTotal},
|
||||
pay_total = #{payTotal},
|
||||
currency = #{currency},
|
||||
customer_active_status = #{customerActiveStatus},
|
||||
credit_arrears_status = #{creditArrearsStatus},
|
||||
round_mode = #{roundMode},
|
||||
update_time = sysdate()
|
||||
where bill_id = #{billId}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
-- prc_billing_task: inbound warehouse billing queue
|
||||
-- bill_type: PENDING / REBILL / REFUND
|
||||
-- success_status: WAITING / SUCCESS / FAILED
|
||||
-- bill_time: billing time from mobile (used as bill bizDate)
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prc_billing_task (
|
||||
task_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '任务ID',
|
||||
transfer_no VARCHAR(64) DEFAULT NULL COMMENT '转单号',
|
||||
tracking_no VARCHAR(64) DEFAULT NULL COMMENT '运单号',
|
||||
order_no VARCHAR(64) DEFAULT NULL COMMENT '订单号',
|
||||
bill_type VARCHAR(20) NOT NULL COMMENT '计费类型:PENDING待计费 REBILL重计费 REFUND退费',
|
||||
success_status VARCHAR(20) NOT NULL DEFAULT 'WAITING' COMMENT '是否计费成功:WAITING待处理 SUCCESS成功 FAILED失败',
|
||||
bill_time DATETIME DEFAULT NULL COMMENT '计费时间(手游/入库传入,用作计费日)',
|
||||
bill_id VARCHAR(36) DEFAULT NULL COMMENT '计费订单ID prc_order_bill.bill_id',
|
||||
error_message VARCHAR(1000) DEFAULT NULL COMMENT '失败原因',
|
||||
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间',
|
||||
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (task_id),
|
||||
KEY idx_billing_task_transfer (transfer_no),
|
||||
KEY idx_billing_task_tracking (tracking_no),
|
||||
KEY idx_billing_task_order (order_no),
|
||||
KEY idx_billing_task_status (success_status, create_time),
|
||||
KEY idx_billing_task_type (bill_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='计费任务表';
|
||||
@@ -0,0 +1,12 @@
|
||||
-- prc_order_bill: transfer_no + customer/credit status flags
|
||||
-- customer_active_status: ACTIVE / MISSING / DISABLED
|
||||
-- credit_arrears_status: NORMAL / ARREARS
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE prc_order_bill
|
||||
ADD COLUMN transfer_no VARCHAR(64) DEFAULT NULL COMMENT '转单号' AFTER tracking_no,
|
||||
ADD COLUMN customer_active_status VARCHAR(20) DEFAULT NULL COMMENT '客户生效状态:ACTIVE正常 MISSING不存在 DISABLED停用' AFTER ar_bill_status,
|
||||
ADD COLUMN credit_arrears_status VARCHAR(20) DEFAULT NULL COMMENT '额度欠费状态:NORMAL正常 ARREARS欠费' AFTER customer_active_status,
|
||||
ADD KEY idx_order_bill_transfer_no (transfer_no),
|
||||
ADD KEY idx_order_bill_customer_active (customer_active_status),
|
||||
ADD KEY idx_order_bill_credit_arrears (credit_arrears_status);
|
||||
@@ -48,6 +48,12 @@
|
||||
<template #col-matchSource="{ row }">
|
||||
<el-tag size="small" :type="sourceTagType(row.matchSource)">{{ sourceLabel(row.matchSource) }}</el-tag>
|
||||
</template>
|
||||
<template #col-customerActiveStatus="{ row }">
|
||||
<el-tag size="small" :type="customerActiveTagType(row.customerActiveStatus)">{{ customerActiveLabel(row.customerActiveStatus) }}</el-tag>
|
||||
</template>
|
||||
<template #col-creditArrearsStatus="{ row }">
|
||||
<el-tag size="small" :type="creditArrearsTagType(row.creditArrearsStatus)">{{ creditArrearsLabel(row.creditArrearsStatus) }}</el-tag>
|
||||
</template>
|
||||
<template #col-incomeTotal="{ row }">
|
||||
<span class="payable">{{ formatMoney(row.incomeTotal) }}</span>
|
||||
</template>
|
||||
@@ -91,6 +97,7 @@ const detail = ref(null)
|
||||
const columns = [
|
||||
{ label: '订单号', prop: 'orderNo', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '运单号', prop: 'trackingNo', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '转单号', prop: 'transferNo', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '客户', prop: 'customerName', minWidth: 140, showOverflowTooltip: true },
|
||||
{ label: '客户代码', prop: 'customerCode', width: 120 },
|
||||
{ label: '产品', prop: 'product', minWidth: 160, slot: 'product' },
|
||||
@@ -100,6 +107,8 @@ const columns = [
|
||||
{ label: '应收总额', prop: 'incomeTotal', align: 'right', width: 120, slot: 'incomeTotal' },
|
||||
{ label: '应付总额', prop: 'payTotal', align: 'right', width: 120, slot: 'payTotal' },
|
||||
{ label: '计价来源', prop: 'matchSource', align: 'center', width: 130, slot: 'matchSource' },
|
||||
{ label: '客户生效', prop: 'customerActiveStatus', align: 'center', width: 100, slot: 'customerActiveStatus' },
|
||||
{ label: '额度欠费', prop: 'creditArrearsStatus', align: 'center', width: 100, slot: 'creditArrearsStatus' },
|
||||
{ label: '下单时间', prop: 'orderTime', align: 'center', width: 170, parseTime: true },
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
@@ -107,6 +116,7 @@ const columns = [
|
||||
const searchColumns = [
|
||||
{ label: '订单号', prop: 'orderNos', placeholder: '订单号' },
|
||||
{ label: '运单号', prop: 'trackingNo', component: 'input', placeholder: '运单号' },
|
||||
{ label: '转单号', prop: 'transferNo', component: 'input', placeholder: '转单号' },
|
||||
{ label: '客户', prop: 'customerCodes', placeholder: '客户代码' },
|
||||
{ label: '产品代码', prop: 'productCodes', placeholder: '产品代码' }
|
||||
]
|
||||
@@ -151,6 +161,29 @@ function sourceTagType(source) {
|
||||
if (source === 'PRODUCT_DEFAULT') return 'info'
|
||||
return ''
|
||||
}
|
||||
|
||||
function customerActiveLabel(status) {
|
||||
const map = { ACTIVE: '正常', MISSING: '不存在', DISABLED: '停用' }
|
||||
return map[status] || status || '-'
|
||||
}
|
||||
|
||||
function customerActiveTagType(status) {
|
||||
if (status === 'ACTIVE') return 'success'
|
||||
if (status === 'DISABLED') return 'warning'
|
||||
if (status === 'MISSING') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function creditArrearsLabel(status) {
|
||||
const map = { NORMAL: '正常', ARREARS: '欠费' }
|
||||
return map[status] || status || '-'
|
||||
}
|
||||
|
||||
function creditArrearsTagType(status) {
|
||||
if (status === 'NORMAL') return 'success'
|
||||
if (status === 'ARREARS') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
<el-descriptions class="detail-desc" :column="2" border size="small">
|
||||
<el-descriptions-item label="订单号">{{ detail.orderNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="运单号">{{ detail.trackingNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="转单号">{{ detail.transferNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户">{{ displayCustomer(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户生效">{{ customerActiveLabel(detail.customerActiveStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="额度欠费">{{ creditArrearsLabel(detail.creditArrearsStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="仓库">{{ detail.warehouseCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发件人">{{ detail.shipperName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="收件人">{{ detail.consigneeName || '-' }}</el-descriptions-item>
|
||||
@@ -173,6 +176,16 @@ watch(() => props.detail, () => {
|
||||
feeTab.value = 'income'
|
||||
})
|
||||
|
||||
function customerActiveLabel(status) {
|
||||
const map = { ACTIVE: '正常', MISSING: '不存在', DISABLED: '停用' }
|
||||
return map[status] || status || '-'
|
||||
}
|
||||
|
||||
function creditArrearsLabel(status) {
|
||||
const map = { NORMAL: '正常', ARREARS: '欠费' }
|
||||
return map[status] || status || '-'
|
||||
}
|
||||
|
||||
function displayCustomer(row) {
|
||||
if (row.customerName && row.customerCode) return row.customerName + ' (' + row.customerCode + ')'
|
||||
return row.customerName || row.customerCode || row.customerId || '-'
|
||||
|
||||
Reference in New Issue
Block a user