From 212c64e0ed968b2e572e314e02245d0d1bef8f73 Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Mon, 21 Sep 2026 10:05:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=AE=A2=E5=8D=95=E5=90=8C?= =?UTF-8?q?=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bms-back/ruoyi-admin/pom.xml | 8 + .../controller/api/TmsPriceController.java | 35 +++ .../src/main/resources/application-druid.yml | 16 +- .../src/main/resources/application.yml | 11 + .../api/impl/CustomerAccountApiImpl.java | 3 +- .../ruoyi/price/config/BillingProperties.java | 86 +++++++ .../ruoyi/price/domain/PrcBillingTask.java | 29 +++ .../com/ruoyi/price/domain/PrcOrderBill.java | 6 + .../domain/vo/BillingTaskSubmitRequest.java | 28 +++ .../price/domain/vo/TmsPriceBillRequest.java | 2 + .../price/domain/vo/TmsPriceQuoteResult.java | 6 + .../price/enums/BillCreditArrearsStatus.java | 23 ++ .../price/enums/BillCustomerActiveStatus.java | 24 ++ .../price/enums/BillTaskSuccessStatus.java | 21 ++ .../com/ruoyi/price/enums/BillTaskType.java | 40 +++ .../mapper/OracleUpstreamOrderMapper.java | 20 ++ .../price/mapper/PrcBillingTaskMapper.java | 18 ++ .../price/mapper/PrcOrderBillMapper.java | 2 + .../ruoyi/price/mq/BillingTaskMessage.java | 30 +++ .../ruoyi/price/mq/BillingTaskMqConsumer.java | 94 +++++++ .../ruoyi/price/mq/BillingTaskMqEvent.java | 21 ++ .../price/mq/IBillingTaskMqPublisher.java | 7 + .../price/mq/LocalBillingTaskMqPublisher.java | 27 ++ .../price/mq/RedisBillingTaskMqPublisher.java | 38 +++ .../price/oracle/OracleOrderBillMapper.java | 231 +++++++++++++++++ .../oracle/OracleUpstreamOrderService.java | 125 +++++++++ .../price/service/IPrcBillingTaskService.java | 15 ++ .../impl/PrcBillingTaskServiceImpl.java | 238 ++++++++++++++++++ .../service/impl/PrcOrderBillServiceImpl.java | 85 ++++++- .../impl/TmsPriceQuoteServiceImpl.java | 47 +++- .../price/OracleUpstreamOrderMapper.xml | 47 ++++ .../mapper/price/PrcBillingTaskMapper.xml | 95 +++++++ .../mapper/price/PrcOrderBillMapper.xml | 37 ++- bms-back/sql/price/billing_task.sql | 27 ++ .../sql/price/order_bill_status_alter.sql | 12 + bms-front/src/views/price/billing/bill.vue | 33 +++ .../price/billing/components/BillDetail.vue | 13 + 37 files changed, 1576 insertions(+), 24 deletions(-) create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/config/BillingProperties.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcBillingTask.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/BillingTaskSubmitRequest.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillCreditArrearsStatus.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillCustomerActiveStatus.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillTaskSuccessStatus.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillTaskType.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/OracleUpstreamOrderMapper.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcBillingTaskMapper.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMessage.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMqConsumer.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMqEvent.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/IBillingTaskMqPublisher.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/LocalBillingTaskMqPublisher.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/RedisBillingTaskMqPublisher.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleOrderBillMapper.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleUpstreamOrderService.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcBillingTaskService.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillingTaskServiceImpl.java create mode 100644 bms-back/ruoyi-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml create mode 100644 bms-back/ruoyi-system/src/main/resources/mapper/price/PrcBillingTaskMapper.xml create mode 100644 bms-back/sql/price/billing_task.sql create mode 100644 bms-back/sql/price/order_bill_status_alter.sql diff --git a/bms-back/ruoyi-admin/pom.xml b/bms-back/ruoyi-admin/pom.xml index 4643a4d7..decfe985 100644 --- a/bms-back/ruoyi-admin/pom.xml +++ b/bms-back/ruoyi-admin/pom.xml @@ -36,6 +36,14 @@ mysql-connector-j + + + com.oracle.database.jdbc + ojdbc11 + 23.5.0.24.07 + runtime + + com.ruoyi diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java index 1bbcdb69..f2c21df2 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java @@ -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. */ diff --git a/bms-back/ruoyi-admin/src/main/resources/application-druid.yml b/bms-back/ruoyi-admin/src/main/resources/application-druid.yml index b48acc38..3148cebf 100644 --- a/bms-back/ruoyi-admin/src/main/resources/application-druid.yml +++ b/bms-back/ruoyi-admin/src/main/resources/application-druid.yml @@ -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 # 最小连接池数量 diff --git a/bms-back/ruoyi-admin/src/main/resources/application.yml b/bms-back/ruoyi-admin/src/main/resources/application.yml index ee0b02e5..2aed60c7 100644 --- a/bms-back/ruoyi-admin/src/main/resources/application.yml +++ b/bms-back/ruoyi-admin/src/main/resources/application.yml @@ -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 diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java index ccf2afe5..42929a56 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/customer/customermoney/api/impl/CustomerAccountApiImpl.java @@ -592,7 +592,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"); diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/config/BillingProperties.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/config/BillingProperties.java new file mode 100644 index 00000000..80ad4f95 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/config/BillingProperties.java @@ -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; + } + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcBillingTask.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcBillingTask.java new file mode 100644 index 00000000..e6fb3b12 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcBillingTask.java @@ -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; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcOrderBill.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcOrderBill.java index 354875aa..18eafda2 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcOrderBill.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcOrderBill.java @@ -21,6 +21,8 @@ public class PrcOrderBill extends BaseEntity /** 批量查询:多个订单号精确匹配(仅查询用,非表字段) */ private List 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 incomeFees; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/BillingTaskSubmitRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/BillingTaskSubmitRequest.java new file mode 100644 index 00000000..8086e000 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/BillingTaskSubmitRequest.java @@ -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; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceBillRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceBillRequest.java index 9f2a1ba4..dec65633 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceBillRequest.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceBillRequest.java @@ -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; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java index 715d8bba..5d11ebe0 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java @@ -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; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillCreditArrearsStatus.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillCreditArrearsStatus.java new file mode 100644 index 00000000..c7336f51 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillCreditArrearsStatus.java @@ -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; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillCustomerActiveStatus.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillCustomerActiveStatus.java new file mode 100644 index 00000000..9d4be226 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillCustomerActiveStatus.java @@ -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; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillTaskSuccessStatus.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillTaskSuccessStatus.java new file mode 100644 index 00000000..ba8b7e49 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillTaskSuccessStatus.java @@ -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; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillTaskType.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillTaskType.java new file mode 100644 index 00000000..7867e5f0 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/enums/BillTaskType.java @@ -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; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/OracleUpstreamOrderMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/OracleUpstreamOrderMapper.java new file mode 100644 index 00000000..96871b97 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/OracleUpstreamOrderMapper.java @@ -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 selectByTransferNo(@Param("orderTable") String orderTable, @Param("transferNo") String transferNo); + + Map selectByOrderId(@Param("orderTable") String orderTable, @Param("orderNo") String orderNo); + + Map selectByOrgOrderId(@Param("orderTable") String orderTable, @Param("orderNo") String orderNo); + + Map selectByTrackId(@Param("orderTable") String orderTable, @Param("trackingNo") String trackingNo); + + Map selectByBarcode(@Param("orderTable") String orderTable, @Param("trackingNo") String trackingNo); + + Map selectByCustomerTrackId(@Param("orderTable") String orderTable, @Param("trackingNo") String trackingNo); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcBillingTaskMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcBillingTaskMapper.java new file mode 100644 index 00000000..94a8a1a8 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcBillingTaskMapper.java @@ -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 selectPrcBillingTaskList(PrcBillingTask query); + + int insertPrcBillingTask(PrcBillingTask task); + + int updatePrcBillingTask(PrcBillingTask task); + + /** CAS: WAITING -> processing claim not needed; writeback SUCCESS/FAILED */ + int updateBillingResult(PrcBillingTask task); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcOrderBillMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcOrderBillMapper.java index c37804b5..6175630f 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcOrderBillMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcOrderBillMapper.java @@ -11,6 +11,8 @@ public interface PrcOrderBillMapper { public PrcOrderBill selectPrcOrderBillByOrderNo(String orderNo); + public PrcOrderBill selectPrcOrderBillByTransferNo(String transferNo); + public PrcOrderBill selectPrcOrderBillById(String billId); public List selectPrcOrderBillList(PrcOrderBill bill); diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMessage.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMessage.java new file mode 100644 index 00000000..d9f855c2 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMessage.java @@ -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; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMqConsumer.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMqConsumer.java new file mode 100644 index 00000000..b334e679 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMqConsumer.java @@ -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()); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMqEvent.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMqEvent.java new file mode 100644 index 00000000..07b23f8e --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/BillingTaskMqEvent.java @@ -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; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/IBillingTaskMqPublisher.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/IBillingTaskMqPublisher.java new file mode 100644 index 00000000..2c9be146 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/IBillingTaskMqPublisher.java @@ -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); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/LocalBillingTaskMqPublisher.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/LocalBillingTaskMqPublisher.java new file mode 100644 index 00000000..468d7752 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/LocalBillingTaskMqPublisher.java @@ -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)); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/RedisBillingTaskMqPublisher.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/RedisBillingTaskMqPublisher.java new file mode 100644 index 00000000..3ad0f1fb --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mq/RedisBillingTaskMqPublisher.java @@ -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); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleOrderBillMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleOrderBillMapper.java new file mode 100644 index 00000000..69afffe2 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleOrderBillMapper.java @@ -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 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 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 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 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 row, String... keys) + { + BigDecimal d = firstDecimal(row, keys); + return d == null ? null : Integer.valueOf(d.intValue()); + } + + private static Date firstDate(Map 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 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 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 e : row.entrySet()) + { + if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) + { + return e.getValue(); + } + } + return null; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleUpstreamOrderService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleUpstreamOrderService.java new file mode 100644 index 00000000..0b433869 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/oracle/OracleUpstreamOrderService.java @@ -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 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 byTransfer = query("TRANSFER_ORDER_ID", + () -> oracleUpstreamOrderMapper.selectByTransferNo(table, tf)); + if (!isEmpty(byTransfer)) + { + return byTransfer; + } + } + + // 2) Fallback: order no + Map 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 query(String by, Supplier> supplier) + { + long t0 = System.currentTimeMillis(); + Map row = supplier.get(); + log.info("oracle order lookup by={} hit={} costMs={}", by, !isEmpty(row), System.currentTimeMillis() - t0); + return row; + } + + private static boolean isEmpty(Map row) + { + return row == null || row.isEmpty(); + } + + private static String emptyToNull(String s) + { + return StringUtils.isEmpty(s) ? null : s.trim(); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcBillingTaskService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcBillingTaskService.java new file mode 100644 index 00000000..19c40c2d --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IPrcBillingTaskService.java @@ -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); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillingTaskServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillingTaskServiceImpl.java new file mode 100644 index 00000000..e9ac2cce --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillingTaskServiceImpl.java @@ -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 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 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); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java index 75742683..14867cad 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcOrderBillServiceImpl.java @@ -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()); diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java index 601e938b..ea9d446f 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java @@ -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 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())) { diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml new file mode 100644 index 00000000..e7b6bba9 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcBillingTaskMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcBillingTaskMapper.xml new file mode 100644 index 00000000..a6d04f3e --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcBillingTaskMapper.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + + + 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() + ) + + + + update prc_billing_task + + transfer_no = #{transferNo}, + tracking_no = #{trackingNo}, + order_no = #{orderNo}, + bill_type = #{billType}, + success_status = #{successStatus}, + bill_time = #{billTime}, + bill_id = #{billId}, + error_message = #{errorMessage}, + update_by = #{updateBy}, + update_time = sysdate() + + where task_id = #{taskId} + + + + + 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} + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml index fdd85e36..6d95011f 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml @@ -11,6 +11,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -58,6 +59,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + @@ -66,7 +69,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - 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 @@ -84,6 +88,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where order_no = #{orderNo} + +