This commit is contained in:
wuweihua
2026-09-22 10:40:09 +08:00
10 changed files with 317 additions and 1 deletions
@@ -0,0 +1,59 @@
package com.ruoyi.web.controller.api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
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.TmsOrderBillStatusResult;
import com.ruoyi.price.service.IPrcOrderBillService;
/**
* TMS 订单客户状态查询接口。
*
* <p>该接口不要求登录 Token,但必须携带 X-Api-Code。
* identifier 会按订单号、运单号、转单号进行精确匹配。</p>
*/
@Anonymous
@RestController
@RequestMapping("/api/tms/order-bill")
public class TmsOrderBillStatusController extends BaseController {
private static final String HEADER_API_CODE = "X-Api-Code";
@Autowired
private IPrcOrderBillService prcOrderBillService;
@Autowired
private TmsApiProperties tmsApiProperties;
/**
* 查询订单对应客户是否正常、额度是否欠费。
*
* <p>接口只返回查询事实和状态字段,不判断上游是否继续处理。</p>
*/
@GetMapping("/status")
public AjaxResult status(
@RequestHeader(value = HEADER_API_CODE, required = false) String headerApiCode,
@RequestParam(value = "identifier", required = false) String identifier) {
assertApiCode(headerApiCode);
TmsOrderBillStatusResult result = prcOrderBillService.queryTmsOrderBillStatus(identifier);
return success(result);
}
private void assertApiCode(String headerApiCode) {
String expected = tmsApiProperties.getApiCode();
if (StringUtils.isEmpty(expected)) {
throw new ServiceException("API_CODE_NOT_CONFIGURED: bms.tms.api-code is empty");
}
if (StringUtils.isEmpty(headerApiCode) || !expected.equals(headerApiCode.trim())) {
throw new ServiceException("API_CODE_INVALID: missing or invalid X-Api-Code");
}
}
}
@@ -0,0 +1,47 @@
package com.ruoyi.price.domain.vo;
import lombok.Data;
/**
* TMS 查询订单计费状态的业务结果。
*
* <p>该结果不直接返回完整订单计费数据,只返回订单匹配结果和相关状态事实。</p>
*/
@Data
public class TmsOrderBillStatusResult {
/**
* 是否查询到计费订单。
*/
private boolean found;
/**
* 匹配到的记录数,最多返回 2 条用于判断是否存在歧义。
*/
private int matchedCount;
/**
* 命中类型:ORDER_NO/ TRACKING_NO/TRANSFER_NO/MULTIPLE。
*/
private String matchType;
/**
* 计费订单标识,仅在唯一命中时返回。
*/
private String billId;
private String orderNo;
private String trackingNo;
private String transferNo;
private Long customerId;
private String customerCode;
/**
* 客户生效状态:ACTIVE/MISSING/DISABLED。
*/
private String customerActiveStatus;
/**
* 额度欠费状态:NORMAL/ARREARS。
*/
private String creditArrearsStatus;
}
@@ -20,4 +20,21 @@ public enum BillCreditArrearsStatus
{
return code;
}
/** 将数据库或接口中的状态值转换为枚举;未知值返回 null。 */
public static BillCreditArrearsStatus of(String code)
{
if (code == null)
{
return null;
}
for (BillCreditArrearsStatus status : values())
{
if (status.code.equalsIgnoreCase(code.trim()))
{
return status;
}
}
return null;
}
}
@@ -21,4 +21,21 @@ public enum BillCustomerActiveStatus
{
return code;
}
/** 将数据库或接口中的状态值转换为枚举;未知值返回 null。 */
public static BillCustomerActiveStatus of(String code)
{
if (code == null)
{
return null;
}
for (BillCustomerActiveStatus status : values())
{
if (status.code.equalsIgnoreCase(code.trim()))
{
return status;
}
}
return null;
}
}
@@ -0,0 +1,39 @@
package com.ruoyi.price.enums;
/**
* TMS 查询订单计费数据时的标识匹配类型。
*/
public enum TmsOrderBillMatchType {
/**
* 按订单号匹配。
*/
ORDER_NO("ORDER_NO", "订单号"),
/**
* 按运单号匹配。
*/
TRACKING_NO("TRACKING_NO", "运单号"),
/**
* 按转单号匹配。
*/
TRANSFER_NO("TRANSFER_NO", "转单号"),
/**
* 同一标识匹配到多条记录,无法唯一确定订单。
*/
MULTIPLE("MULTIPLE", "多条匹配");
private final String code;
private final String label;
TmsOrderBillMatchType(String code, String label) {
this.code = code;
this.label = label;
}
public String getCode() {
return code;
}
public String getLabel() {
return label;
}
}
@@ -13,6 +13,9 @@ public interface PrcOrderBillMapper
public PrcOrderBill selectPrcOrderBillByTransferNo(String transferNo);
/** TMS 状态查询:按订单号、运单号或转单号精确匹配,最多返回两条用于识别歧义。 */
List<PrcOrderBill> selectPrcOrderBillByTmsIdentifier(@Param("identifier") String identifier);
public PrcOrderBill selectPrcOrderBillById(String billId);
public List<PrcOrderBill> selectPrcOrderBillList(PrcOrderBill bill);
@@ -4,6 +4,7 @@ import java.util.List;
import com.ruoyi.price.domain.PrcOrderBill;
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
import com.ruoyi.price.domain.vo.TmsOrderBillStatusResult;
/** 订单计费:落库 prc_order_bill + prc_income_bill/+fee + prc_pay_bill/+fee。 */
public interface IPrcOrderBillService
@@ -24,4 +25,7 @@ public interface IPrcOrderBillService
TmsPriceQuoteResult getBill(String billId);
TmsPriceQuoteResult getBillByOrderNo(String orderNo);
/** 提供给 TMS 判断订单是否可以继续处理的客户状态查询。 */
TmsOrderBillStatusResult queryTmsOrderBillStatus(String identifier);
}
@@ -27,10 +27,12 @@ import com.ruoyi.price.domain.PrcOrderBillExt;
import com.ruoyi.price.domain.PrcPayBill;
import com.ruoyi.price.domain.PrcPayBillFee;
import com.ruoyi.price.domain.vo.TmsPriceBillRequest;
import com.ruoyi.price.domain.vo.TmsOrderBillStatusResult;
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.enums.TmsOrderBillMatchType;
import com.ruoyi.price.mapper.PrcIncomeBillFeeMapper;
import com.ruoyi.price.mapper.PrcIncomeBillMapper;
import com.ruoyi.price.mapper.PrcOrderBillExtMapper;
@@ -199,6 +201,85 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
payBillFeeMapper.selectByBillId(bill.getBillId()));
}
/** 查询 TMS 后续流程所需的客户状态,只返回订单数据和状态事实,不代替上游做业务决策。 */
@Override
@Transactional(readOnly = true)
public TmsOrderBillStatusResult queryTmsOrderBillStatus(String identifier)
{
String value = StringUtils.trim(identifier);
if (StringUtils.isEmpty(value))
{
throw new ServiceException("订单号、运单号或转单号不能为空");
}
if (value.length() > 64)
{
throw new ServiceException("订单号、运单号或转单号长度不能超过64个字符");
}
List<PrcOrderBill> bills = orderBillMapper.selectPrcOrderBillByTmsIdentifier(value);
if (bills == null || bills.isEmpty())
{
TmsOrderBillStatusResult result = new TmsOrderBillStatusResult();
result.setMatchedCount(0);
return result;
}
if (bills.size() > 1)
{
TmsOrderBillStatusResult result = new TmsOrderBillStatusResult();
result.setFound(true);
result.setMatchedCount(bills.size());
result.setMatchType(TmsOrderBillMatchType.MULTIPLE.getCode());
return result;
}
PrcOrderBill bill = bills.get(0);
TmsOrderBillStatusResult result = new TmsOrderBillStatusResult();
result.setFound(true);
result.setMatchedCount(1);
result.setMatchType(resolveTmsMatchType(value, bill).getCode());
result.setBillId(bill.getBillId());
result.setOrderNo(bill.getOrderNo());
result.setTrackingNo(bill.getTrackingNo());
result.setTransferNo(bill.getTransferNo());
result.setCustomerId(bill.getCustomerId());
result.setCustomerCode(bill.getCustomerCode());
result.setCustomerActiveStatus(normalizeCustomerActiveStatus(bill.getCustomerActiveStatus()));
result.setCreditArrearsStatus(normalizeCreditArrearsStatus(bill.getCreditArrearsStatus()));
return result;
}
private TmsOrderBillMatchType resolveTmsMatchType(String identifier, PrcOrderBill bill)
{
if (identifier.equalsIgnoreCase(StringUtils.trim(bill.getOrderNo())))
{
return TmsOrderBillMatchType.ORDER_NO;
}
if (identifier.equalsIgnoreCase(StringUtils.trim(bill.getTrackingNo())))
{
return TmsOrderBillMatchType.TRACKING_NO;
}
return TmsOrderBillMatchType.TRANSFER_NO;
}
private String normalizeCustomerActiveStatus(String status)
{
BillCustomerActiveStatus statusEnum = BillCustomerActiveStatus.of(status);
return statusEnum == null ? normalizeUnknownStatus(status) : statusEnum.getCode();
}
private String normalizeCreditArrearsStatus(String status)
{
BillCreditArrearsStatus statusEnum = BillCreditArrearsStatus.of(status);
return statusEnum == null ? normalizeUnknownStatus(status) : statusEnum.getCode();
}
/** 保留未知状态的原始值(统一为大写),便于上游识别新状态或脏数据。 */
private String normalizeUnknownStatus(String status)
{
String normalized = StringUtils.trim(status);
return StringUtils.isEmpty(normalized) ? null : normalized.toUpperCase();
}
private void persistBill(TmsPriceBillRequest request, BigDecimal weight, Date bizDate,
TmsPriceQuoteResult result)
{
@@ -390,7 +471,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
cmd.setAmount(delta.abs());
customerAccountApi.releaseCredit(cmd);
}
saveCreditEsLog(bill, cmd, action, true, markArrears ? "ARREARS" : null);
saveCreditEsLog(bill, cmd, action, true,
markArrears ? BillCreditArrearsStatus.ARREARS.getCode() : null);
return markArrears ? arrears : normal;
}
catch (RuntimeException e)
@@ -104,6 +104,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
limit 1
</select>
<!-- TMS 状态查询使用精确匹配,避免模糊查询把多个订单误判为同一条。 -->
<select id="selectPrcOrderBillByTmsIdentifier" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
where order_no = #{identifier}
or tracking_no = #{identifier}
or transfer_no = #{identifier}
order by
case
when order_no = #{identifier} then 1
when tracking_no = #{identifier} then 2
when transfer_no = #{identifier} then 3
else 4
end,
create_time desc,
bill_id desc
limit 2
</select>
<select id="selectPrcOrderBillById" parameterType="String" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
where bill_id = #{billId}
@@ -0,0 +1,30 @@
-- TMS 订单状态查询接口:为运单号精确查询补充索引。
-- 订单号已有唯一索引,转单号已有索引;当前表结构通常缺少 tracking_no 索引。
-- 脚本可重复执行。
SET NAMES utf8mb4;
SET @db_name := DATABASE();
SET @has_tracking_index := (
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'prc_order_bill'
AND INDEX_NAME = 'idx_order_bill_tracking_no'
);
SET @sql := IF(@has_tracking_index = 0,
'ALTER TABLE prc_order_bill ADD INDEX idx_order_bill_tracking_no (tracking_no)',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SELECT
TABLE_NAME,
INDEX_NAME,
COLUMN_NAME,
NON_UNIQUE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'prc_order_bill'
AND INDEX_NAME IN ('uk_bill_order', 'idx_order_bill_tracking_no', 'idx_order_bill_transfer_no')
ORDER BY INDEX_NAME, SEQ_IN_INDEX;