修改订单同步

This commit is contained in:
wuweihua
2026-09-21 10:05:16 +08:00
parent 18ea834a59
commit 974e88715b
37 changed files with 1576 additions and 24 deletions
+8
View File
@@ -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>
@@ -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
@@ -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;
@@ -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;
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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));
}
}
@@ -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);
}
}
@@ -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 &gt; 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;
}
}
@@ -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();
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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());
@@ -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 &lt;= 1
</select>
<select id="selectByOrderId" resultType="java.util.HashMap">
SELECT * FROM (
SELECT * FROM ${orderTable} WHERE ORDER_ID = #{orderNo}
) WHERE ROWNUM &lt;= 1
</select>
<select id="selectByOrgOrderId" resultType="java.util.HashMap">
SELECT * FROM (
SELECT * FROM ${orderTable} WHERE ORG_ORDER_ID = #{orderNo}
) WHERE ROWNUM &lt;= 1
</select>
<select id="selectByTrackId" resultType="java.util.HashMap">
SELECT * FROM (
SELECT * FROM ${orderTable} WHERE TRACK_ID = #{trackingNo}
) WHERE ROWNUM &lt;= 1
</select>
<select id="selectByBarcode" resultType="java.util.HashMap">
SELECT * FROM (
SELECT * FROM ${orderTable} WHERE BARCODE_NUMBER = #{trackingNo}
) WHERE ROWNUM &lt;= 1
</select>
<select id="selectByCustomerTrackId" resultType="java.util.HashMap">
SELECT * FROM (
SELECT * FROM ${orderTable} WHERE CUSTOMER_TRACK_ID = #{trackingNo}
) WHERE ROWNUM &lt;= 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}
+27
View File
@@ -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 || '-'