From b50e57bf7e17d696ddb4e9785c909052da034ef4 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期二, 14 七月 2026 09:10:47 +0800
Subject: [PATCH] Merge branch '2.1' into 2.3
---
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserInfoDto.java | 12
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java | 17
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java | 1108 +++++++++++++
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java | 668 ++++++++
docs/superpowers/plans/2026-07-11-icbc-admin-fund-operations.md | 66
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderListVO.java | 8
ruoyi-service/ruoyi-payment/lib/InfosecCrypto_Java1_02_JDK14+.jar | 0
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TChargingBillExport.java | 4
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java | 2
ruoyi-service/ruoyi-payment/lib/readme.md | 14
ruoyi-service/ruoyi-other/src/main/resources/bootstrap.yml | 6
ruoyi-service/ruoyi-payment/lib/hsm-software-share-1.0.5.jar | 0
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderTimeVO.java | 4
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrderShareRecord.java | 124 +
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java | 25
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml | 164 +
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Partner.java | 18
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/IcbcAdminFundOperationReq.java | 12
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingSimulationSupportTest.java | 49
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/domain/IcbcPaymentFlowRecord.java | 31
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillVO.java | 4
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java | 225 ++
ruoyi-service/ruoyi-payment/lib/update-info.md | 2
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentResp.java | 34
ruoyi-service/ruoyi-order/src/main/resources/bootstrap.yml | 2
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundReq.java | 27
ruoyi-service/ruoyi-account/src/main/resources/bootstrap.yml | 6
ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/icbc/service/impl/IcbcNotifyParserTest.java | 27
ruoyi-api/ruoyi-api-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 3
ruoyi-service/ruoyi-payment/lib/icbc-ca.jar | 0
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/mapper/IcbcPaymentFlowRecordMapper.java | 7
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/ChargingOrderSettlementExecutor.java | 22
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillWapper.java | 4
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/ChargingSimulationVO.java | 25
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcNotifyParser.java | 54
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TApplyChargingPileController.java | 15
ruoyi-modules/ruoyi-system/src/main/resources/bootstrap.yml | 5
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitResp.java | 20
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryResp.java | 22
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/IcbcAdminSubOrderVO.java | 45
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPaymentFlowRecorder.java | 92 +
nacos-config/ruoyi-gateway-prod-whitelist-addition.md | 17
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/PartnerMapper.xml | 7
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingBillMapper.xml | 6
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java | 4
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPayService.java | 35
.gitignore | 3
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java | 39
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java | 9
ruoyi-service/ruoyi-chargingPile/src/main/resources/bootstrap.yml | 6
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PartnerListDTO.java | 8
sql/20260711_icbc_payment_flow_record.sql | 22
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundResp.java | 20
ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop-io.jar | 0
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java | 148 +
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java | 25
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/factory/IcbcPaymentFallbackFactory.java | 67
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitReq.java | 24
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java | 7
ruoyi-service/ruoyi-jianguan/src/main/resources/bootstrap.yml | 7
sql/charging_pile_service.sql | 6
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/ChargingSimulationReq.java | 12
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java | 159 ++
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryReq.java | 19
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TChargingOrderExport.java | 4
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java | 71
ruoyi-service/ruoyi-payment/lib/commons-logging-1.2.jar | 0
ruoyi-service/ruoyi-payment/pom.xml | 45
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/ChargingBillController.java | 6
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderShareRecordMapper.java | 7
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundReq.java | 19
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingSimulationSupport.java | 127 +
ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java | 20
ruoyi-auth/src/main/resources/bootstrap.yml | 2
docs/superpowers/specs/2026-07-11-icbc-admin-fund-operations-design.md | 57
ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java | 327 ++++
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/feignClient/IcbcPaymentClient.java | 68
ruoyi-service/ruoyi-integration/src/main/resources/bootstrap.yml | 7
ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop.jar | 0
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java | 68
ruoyi-service/ruoyi-payment/src/main/resources/bootstrap.yml | 6
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/ChargingListQuery.java | 2
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentReq.java | 66
ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundResp.java | 18
sql/20260529_icbc_partner_profit_sharing.sql | 7
sql/20260530_charging_order_share_record.sql | 44
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/AccountListVO.java | 4
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/FinancialSettlementController.java | 2
ruoyi-visual/ruoyi-monitor/src/main/resources/bootstrap.yml | 6
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillListVO.java | 4
90 files changed, 4,479 insertions(+), 131 deletions(-)
diff --git a/.gitignore b/.gitignore
index 09e64c6..f467dca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
# Package Files #
*.jar
+!ruoyi-service/ruoyi-payment/lib/*.jar
*.war
*.ear
@@ -17,4 +18,4 @@
*.yaml
*.yml
-*.log
\ No newline at end of file
+*.log
diff --git a/docs/superpowers/plans/2026-07-11-icbc-admin-fund-operations.md b/docs/superpowers/plans/2026-07-11-icbc-admin-fund-operations.md
new file mode 100644
index 0000000..38ccfd4
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-11-icbc-admin-fund-operations.md
@@ -0,0 +1,66 @@
+# ICBC Admin Fund Operations Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add management-console operations for querying and manually operating ICBC funds for real charging orders.
+
+**Architecture:** The order service validates local charging orders and share records, then calls the payment service through Feign. The management frontend exposes one fund-operation modal from the charging-order list.
+
+**Tech Stack:** Java 8, Spring Boot, MyBatis-Plus, OpenFeign, JUnit 5, React 18, Ant Design Pro.
+
+## Global Constraints
+
+- Only ICBC paid charging orders are eligible.
+- Refund operations affect real bank funds but do not update local business status.
+- A specific share record must be selected for sub-order refunds.
+- Existing unrelated workspace changes must remain untouched.
+
+---
+
+### Task 1: Request Resolution and Validation
+
+**Files:**
+- Create: `ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java`
+- Test: `ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java`
+
+- [ ] Write failing tests for ICBC order eligibility, positive amounts and DTO assembly.
+- [ ] Run the focused test and confirm failure because the support class is missing.
+- [ ] Implement the minimal pure support class.
+- [ ] Run the focused test and confirm success.
+
+### Task 2: Backend Management APIs
+
+**Files:**
+- Modify: `ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/feignClient/IcbcPaymentClient.java`
+- Modify: `ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/factory/IcbcPaymentFallbackFactory.java`
+- Create: `ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/IcbcAdminFundOperationReq.java`
+- Create: `ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/IcbcAdminSubOrderVO.java`
+- Modify: `ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java`
+- Modify: `ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java`
+- Modify: `ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java`
+
+- [ ] Add the missing Feign sub-order-refund operation and fallback.
+- [ ] Add request/response models for order code, amount and selected share record.
+- [ ] Add service methods that query payment, refund the main order, query all share records and refund one selected sub-order.
+- [ ] Add permission-protected controller endpoints.
+- [ ] Compile the order and payment dependency graph.
+
+### Task 3: Management Fund Operation Modal
+
+**Files:**
+- Modify: `chargingPile_management/src/pages/order/pileOrder/service.js`
+- Modify: `chargingPile_management/src/pages/order/pileOrder/index.jsx`
+- Modify: `chargingPile_management/src/pages/order/pileOrder/style.less`
+
+- [ ] Add four API wrappers.
+- [ ] Add the ICBC-only operation button.
+- [ ] Add payment query, main refund and sub-order table sections.
+- [ ] Add real-fund warnings, amount validation, loading states and confirmation dialogs.
+- [ ] Run frontend syntax/type/build checks.
+
+### Task 4: Final Verification
+
+- [ ] Run focused JUnit tests.
+- [ ] Run Maven compile for order and payment modules.
+- [ ] Run frontend static checks.
+- [ ] Run scoped `git diff --check` without touching unrelated changes.
diff --git a/docs/superpowers/specs/2026-07-11-icbc-admin-fund-operations-design.md b/docs/superpowers/specs/2026-07-11-icbc-admin-fund-operations-design.md
new file mode 100644
index 0000000..6e13df2
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-11-icbc-admin-fund-operations-design.md
@@ -0,0 +1,57 @@
+# 工行管理后台资金操作设计
+
+## 目标
+
+在“订单管理/充电桩订单列表”为正常扫码支付生成的 ICBC 充电订单提供支付查询、主订单退款、分账查询和分账退款操作。
+
+## 约束
+
+- 仅 `source_name=ICBC` 且已支付的充电订单允许操作。
+- 管理端只提交业务订单号;退款类操作额外提交金额。
+- 分账查询返回订单下全部工行分账记录,分账退款必须选择具体记录。
+- 操作只调用工行接口并返回结果,不修改充电订单、退款记录或分账记录的业务状态。
+- 工行支付审计表继续记录查询、退款、分账查询和分账退款请求及响应。
+- 管理端必须显示“仅操作银行资金,不同步本地订单状态”的风险提示。
+
+## 后端架构
+
+订单服务负责管理端编排:校验订单、读取 `t_charging_order` 和 `t_charging_order_share_record`,自动组装工行参数,再通过 `IcbcPaymentClient` 调用支付服务。
+
+接口:
+
+- `POST /order/t-charging-order/icbc/fund/payment-query`
+- `POST /order/t-charging-order/icbc/fund/refund`
+- `POST /order/t-charging-order/icbc/fund/sub-orders`
+- `POST /order/t-charging-order/icbc/fund/sub-order-refund`
+
+主订单退款使用订单 `code`、`recharge_serial_number` 和管理端金额。若本地缺少工行订单号,先调用支付查询补充本次请求上下文,但不回写订单。
+
+分账查询按订单读取全部 `pay_channel=ICBC` 且未删除的分账记录,并逐条调用工行子订单查询。单条查询失败不影响其他记录返回。
+
+分账退款以选中的分账记录为准,使用其原订单号、子订单号、商户号、协议号和原交易日期组装请求。
+
+## 管理界面
+
+充电桩订单列表中,符合条件的订单显示“资金操作”。弹窗顶部展示订单号、工行订单号和风险提示,并提供:
+
+- 支付查询按钮及原始结果。
+- 主订单退款金额输入框和二次确认。
+- 分账子订单表格、逐条查询结果和逐条退款入口。
+
+提交期间按钮禁用,退款必须再次确认真实资金影响。
+
+## 错误处理
+
+- 非 ICBC、未支付、订单不存在直接拒绝。
+- 金额必须大于零。
+- 分账记录必须属于当前订单且为 ICBC 渠道。
+- 工行调用失败时返回明确错误及原始响应,不修改本地业务状态。
+- 后端使用订单级本地锁避免同一实例上的并发重复操作;跨实例重复操作风险由工行结果和审计记录追踪。
+
+## 测试
+
+- 参数解析与订单校验单元测试。
+- 工行订单号缺失时查询补全测试。
+- 多分账记录全部返回、单条失败隔离测试。
+- 非法分账记录、非 ICBC、未支付和非法金额测试。
+- 后端模块编译及管理端构建/类型检查。
diff --git a/nacos-config/ruoyi-gateway-prod-whitelist-addition.md b/nacos-config/ruoyi-gateway-prod-whitelist-addition.md
new file mode 100644
index 0000000..405c423
--- /dev/null
+++ b/nacos-config/ruoyi-gateway-prod-whitelist-addition.md
@@ -0,0 +1,17 @@
+# ruoyi-gateway-prod whitelist addition
+
+Add this entry to `DEFAULT_GROUP/ruoyi-gateway-prod.yml` in Nacos:
+
+```yaml
+security:
+ ignore:
+ whites:
+ - /payment/icbc/callBack
+```
+
+Context:
+
+- Controller: `ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java`
+- Internal route: `@RequestMapping("/icbc")` + `@PostMapping("/callBack")`
+- Public gateway route: `/payment/icbc/callBack`
+
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserInfoDto.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserInfoDto.java
index 7ed53c5..b70ce2a 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserInfoDto.java
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserInfoDto.java
@@ -24,6 +24,18 @@
@ApiModelProperty("当前积分")
private Integer points;
+ @ApiModelProperty("微信openid")
+ private String wxOpenid;
+
+ @ApiModelProperty("支付宝openid")
+ private String aliOpenid;
+
+ @ApiModelProperty("支付宝userId")
+ private String aliUserId;
+
+ @ApiModelProperty("支付宝unionId")
+ private String aliUnionId;
+
@ApiModelProperty("当前余额")
private BigDecimal balance;
@ApiModelProperty("当前优惠卷数量")
diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PartnerListDTO.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PartnerListDTO.java
index 397bc76..9ea918b 100644
--- a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PartnerListDTO.java
+++ b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PartnerListDTO.java
@@ -31,6 +31,14 @@
private String account;
@ApiModelProperty(value = "备注")
private String remark;
+ @ApiModelProperty(value = "工行合作商ID")
+ private String icbcPartnerId;
+ @ApiModelProperty(value = "工行二级商户编号")
+ private String icbcSubMerId;
+ @ApiModelProperty(value = "工行二级商户协议编号")
+ private String icbcSubMerPrtclNo;
+ @ApiModelProperty(value = "工行直接分账商户清算账号")
+ private String icbcMerAcct;
@ApiModelProperty(value = "电费分成比例")
private BigDecimal electricityShareRatio;
@ApiModelProperty(value = "服务费分成比例")
diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Partner.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Partner.java
index c6c2ea0..2411274 100644
--- a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Partner.java
+++ b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Partner.java
@@ -75,6 +75,24 @@
@ApiModelProperty(value = "工行合作商ID")
private String icbcPartnerId;
/**
+ * 工行二级商户编号
+ */
+ @TableField("icbc_sub_mer_id")
+ @ApiModelProperty(value = "工行二级商户编号")
+ private String icbcSubMerId;
+ /**
+ * 工行二级商户协议编号
+ */
+ @TableField("icbc_sub_mer_prtcl_no")
+ @ApiModelProperty(value = "工行二级商户协议编号")
+ private String icbcSubMerPrtclNo;
+ /**
+ * 工行直接分账商户清算账号
+ */
+ @TableField("icbc_mer_acct")
+ @ApiModelProperty(value = "工行直接分账商户清算账号")
+ private String icbcMerAcct;
+ /**
* 电费分成比例
*/
@TableField("electricity_share_ratio")
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/ChargingListQuery.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/ChargingListQuery.java
index e62a390..24ae53d 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/ChargingListQuery.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/ChargingListQuery.java
@@ -24,6 +24,8 @@
private Integer state;
@ApiModelProperty("状态(1日结 2月结)")
private Integer orderState;
+ @ApiModelProperty("账户类型(1微信商户 2支付宝商户 3工商银行商户)")
+ private Integer payType;
@ApiModelProperty("uid 前端忽略")
private String uid;
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrderShareRecord.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrderShareRecord.java
new file mode 100644
index 0000000..24f7d91
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TChargingOrderShareRecord.java
@@ -0,0 +1,124 @@
+package com.ruoyi.order.api.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.ruoyi.common.core.web.domain.BasePojo;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 充电订单分账记录,覆盖余额、微信、支付宝、工行等实际支付渠道。
+ */
+@Data
+@TableName("t_charging_order_share_record")
+public class TChargingOrderShareRecord extends BasePojo {
+ @TableId(value = "id", type = IdType.AUTO)
+ private Long id;
+
+ @TableField("charging_order_id")
+ private Long chargingOrderId;
+
+ @TableField("charging_order_code")
+ private String chargingOrderCode;
+
+ @TableField("site_id")
+ private Integer siteId;
+
+ @TableField("partner_id")
+ private Integer partnerId;
+
+ @TableField("pay_channel")
+ private String payChannel;
+
+ @TableField("channel_pay_amount")
+ private BigDecimal channelPayAmount;
+
+ @TableField("channel_gift_amount")
+ private BigDecimal channelGiftAmount;
+
+ @TableField("channel_pay_ratio")
+ private BigDecimal channelPayRatio;
+
+ @TableField("channel_electricity_amount")
+ private BigDecimal channelElectricityAmount;
+
+ @TableField("channel_service_amount")
+ private BigDecimal channelServiceAmount;
+
+ @TableField("electricity_share_ratio")
+ private BigDecimal electricityShareRatio;
+
+ @TableField("service_share_ratio")
+ private BigDecimal serviceShareRatio;
+
+ @TableField("electricity_share_amount")
+ private BigDecimal electricityShareAmount;
+
+ @TableField("service_share_amount")
+ private BigDecimal serviceShareAmount;
+
+ @TableField("share_amount")
+ private BigDecimal shareAmount;
+
+ @TableField("need_online_share")
+ private Boolean needOnlineShare;
+
+ @TableField("online_share_status")
+ private String onlineShareStatus;
+
+ @TableField("seq_no")
+ private String seqNo;
+
+ @TableField("busi_type")
+ private String busiType;
+
+ @TableField("ori_mer_id")
+ private String oriMerId;
+
+ @TableField("ori_trx_date")
+ private LocalDate oriTrxDate;
+
+ @TableField("sub_order_id")
+ private String subOrderId;
+
+ @TableField("sub_mer_id")
+ private String subMerId;
+
+ @TableField("sub_mer_prtcl_no")
+ private String subMerPrtclNo;
+
+ @TableField("mer_acct")
+ private String merAcct;
+
+ @TableField("icbc_order_id")
+ private String icbcOrderId;
+
+ @TableField("sub_order_seq")
+ private String subOrderSeq;
+
+ @TableField("sub_order_trxid")
+ private String subOrderTrxid;
+
+ @TableField("rec_result")
+ private String recResult;
+
+ @TableField("request_json")
+ private String requestJson;
+
+ @TableField("response_json")
+ private String responseJson;
+
+ @TableField("fail_reason")
+ private String failReason;
+
+ @TableField("retry_count")
+ private Integer retryCount;
+
+ @TableField("last_retry_time")
+ private LocalDateTime lastRetryTime;
+}
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/AccountListVO.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/AccountListVO.java
index b4913d0..9d65389 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/AccountListVO.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/AccountListVO.java
@@ -26,6 +26,10 @@
private BigDecimal commissionAmount;
@ApiModelProperty(value = "分佣")
private BigDecimal sharingAmount;
+ @ApiModelProperty(value = "平台收入")
+ private BigDecimal platformIncome;
+ @ApiModelProperty(value = "合作商收入")
+ private BigDecimal partnerIncome;
@ApiModelProperty(value = "列表数据")
private PageInfo<ChargingBillListVO> list;
@ApiModelProperty(value = "导出数据")
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillListVO.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillListVO.java
index 5814a48..5b6338e 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillListVO.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillListVO.java
@@ -37,6 +37,10 @@
private BigDecimal serviceCharge;
@ApiModelProperty(value = "平台手续费")
private BigDecimal commissionAmount;
+ @ApiModelProperty(value = "平台收入")
+ private BigDecimal platformIncome;
+ @ApiModelProperty(value = "合作商收入")
+ private BigDecimal partnerIncome;
@ApiModelProperty(value = "充电电流 度")
private BigDecimal chargingCapacity;
@ApiModelProperty(value = "入账金额")
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillVO.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillVO.java
index 2f68bfb..6ef06fc 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillVO.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillVO.java
@@ -30,6 +30,10 @@
private BigDecimal commissionAmount;
@ApiModelProperty(value = "三方平台分佣")
private BigDecimal sharingAmount;
+ @ApiModelProperty(value = "平台收入")
+ private BigDecimal platformIncome;
+ @ApiModelProperty(value = "合作商收入")
+ private BigDecimal partnerIncome;
@ApiModelProperty(value = "优惠抵扣")
private BigDecimal discount;
@ApiModelProperty(value = "账单分类 1全站 2各个站点 前端忽略")
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillWapper.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillWapper.java
index da4f1f3..3b84807 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillWapper.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingBillWapper.java
@@ -31,6 +31,10 @@
private BigDecimal commissionAmount;
@ApiModelProperty(value = "三方平台分佣")
private BigDecimal sharingAmount;
+ @ApiModelProperty(value = "平台收入")
+ private BigDecimal platformIncome;
+ @ApiModelProperty(value = "合作商收入")
+ private BigDecimal partnerIncome;
@ApiModelProperty(value = "优惠抵扣")
private BigDecimal discount;
@ApiModelProperty(value = "账单分类 1全站 2各个站点 前端忽略")
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderListVO.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderListVO.java
index 25092cd..e46ece6 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderListVO.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderListVO.java
@@ -38,6 +38,10 @@
private Integer chargingGunId;
@ApiModelProperty(value = "订单来源(1=自己平台,2=三方平台)")
private Integer orderSource;
+ @ApiModelProperty(value = "支付渠道来源")
+ private String sourceName;
+ @ApiModelProperty(value = "第三方支付流水号或工行订单号")
+ private String rechargeSerialNumber;
@ApiModelProperty(value = "是否白名单用户")
private Integer isWhitelist;
@ApiModelProperty(value = "白名单用户退款金额")
@@ -80,6 +84,10 @@
private BigDecimal balanceIncome;
@ApiModelProperty(value = "余额赠送收入")
private BigDecimal balanceGiftIncome;
+ @ApiModelProperty(value = "平台收入")
+ private BigDecimal platformIncome;
+ @ApiModelProperty(value = "合作商收入")
+ private BigDecimal partnerIncome;
@ApiModelProperty(value = "充电后剩余金额")
private BigDecimal residualAmount;
@ApiModelProperty(value = "累计电费")
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderTimeVO.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderTimeVO.java
index 02a5021..10c8eba 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderTimeVO.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderTimeVO.java
@@ -23,6 +23,10 @@
private BigDecimal electrovalence;
@ApiModelProperty(value = "累计服务费")
private BigDecimal serviceCharge;
+ @ApiModelProperty(value = "平台收入")
+ private BigDecimal platformIncome;
+ @ApiModelProperty(value = "合作商收入")
+ private BigDecimal partnerIncome;
@ApiModelProperty(value = "尖峰平谷充电量")
private String capacityProportion;
@ApiModelProperty(value = "尖峰平谷服务费")
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
index c998fb4..687897a 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
@@ -27,6 +27,10 @@
private BigDecimal balanceIncome;
@ApiModelProperty(value = "赠送费用")
private BigDecimal balanceGiftIncome;
+ @ApiModelProperty(value = "平台收入")
+ private BigDecimal platformIncome;
+ @ApiModelProperty(value = "合作商收入")
+ private BigDecimal partnerIncome;
@ApiModelProperty(value = "白名单用户退款金额")
private BigDecimal whitelistRefundAmount;
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/factory/IcbcPaymentFallbackFactory.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/factory/IcbcPaymentFallbackFactory.java
new file mode 100644
index 0000000..e8d70f6
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/factory/IcbcPaymentFallbackFactory.java
@@ -0,0 +1,67 @@
+package com.ruoyi.payment.api.factory;
+
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.payment.api.feignClient.IcbcPaymentClient;
+import com.ruoyi.payment.api.vo.IcbcPaymentReq;
+import com.ruoyi.payment.api.vo.IcbcPaymentResp;
+import com.ruoyi.payment.api.vo.IcbcRefundReq;
+import com.ruoyi.payment.api.vo.IcbcRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.openfeign.FallbackFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * 工行支付调用降级处理。
+ */
+@Component
+public class IcbcPaymentFallbackFactory implements FallbackFactory<IcbcPaymentClient> {
+ private static final Logger log = LoggerFactory.getLogger(IcbcPaymentFallbackFactory.class);
+
+ @Override
+ public IcbcPaymentClient create(Throwable throwable) {
+ log.error("工行支付调用失败:{}", throwable.getMessage());
+ return new IcbcPaymentClient() {
+ @Override
+ public R<IcbcPaymentResp> payment(IcbcPaymentReq req) {
+ return R.fail("调起工行支付失败:" + throwable.getMessage());
+ }
+
+ @Override
+ public R<IcbcPaymentResp> query(String outTradeNo) {
+ return R.fail("查询工行支付订单失败:" + throwable.getMessage());
+ }
+
+ @Override
+ public R<IcbcPaymentResp> close(String outTradeNo) {
+ return R.fail("关闭工行支付订单失败:" + throwable.getMessage());
+ }
+
+ @Override
+ public R<IcbcRefundResp> refund(IcbcRefundReq req) {
+ return R.fail("工行退款失败:" + throwable.getMessage());
+ }
+
+ @Override
+ public R<IcbcSubOrderSplitResp> splitSubOrder(IcbcSubOrderSplitReq req) {
+ return R.fail("工行小订单拆分失败:" + throwable.getMessage());
+ }
+
+ @Override
+ public R<IcbcSubOrderRefundResp> refundSubOrder(IcbcSubOrderRefundReq req) {
+ return R.fail("工行小订单拆分退款失败:" + throwable.getMessage());
+ }
+
+ @Override
+ public R<IcbcSubOrderQueryResp> querySubOrder(IcbcSubOrderQueryReq req) {
+ return R.fail("查询工行子订单失败:" + throwable.getMessage());
+ }
+ };
+ }
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/feignClient/IcbcPaymentClient.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/feignClient/IcbcPaymentClient.java
new file mode 100644
index 0000000..4cd1d87
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/feignClient/IcbcPaymentClient.java
@@ -0,0 +1,68 @@
+package com.ruoyi.payment.api.feignClient;
+
+import com.ruoyi.common.core.constant.ServiceNameConstants;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.payment.api.factory.IcbcPaymentFallbackFactory;
+import com.ruoyi.payment.api.vo.IcbcPaymentReq;
+import com.ruoyi.payment.api.vo.IcbcPaymentResp;
+import com.ruoyi.payment.api.vo.IcbcRefundReq;
+import com.ruoyi.payment.api.vo.IcbcRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+
+/**
+ * 工行统一支付服务。
+ */
+@FeignClient(contextId = "IcbcPaymentClient", value = ServiceNameConstants.PAYMENT_SERVICE, fallbackFactory = IcbcPaymentFallbackFactory.class)
+public interface IcbcPaymentClient {
+
+ /**
+ * 调起工行聚合支付。
+ */
+ @PostMapping("/icbc/payment")
+ R<IcbcPaymentResp> payment(@RequestBody IcbcPaymentReq req);
+
+ /**
+ * 查询工行支付订单。
+ */
+ @PostMapping("/icbc/query")
+ R<IcbcPaymentResp> query(@RequestParam("outTradeNo") String outTradeNo);
+
+ /**
+ * 关闭工行待支付订单。
+ */
+ @PostMapping("/icbc/close")
+ R<IcbcPaymentResp> close(@RequestParam("outTradeNo") String outTradeNo);
+
+ /**
+ * 工行聚合支付退款。
+ */
+ @PostMapping("/icbc/refund")
+ R<IcbcRefundResp> refund(@RequestBody IcbcRefundReq req);
+
+ /**
+ * 工行小订单拆分。
+ */
+ @PostMapping("/icbc/sub-order/split")
+ R<IcbcSubOrderSplitResp> splitSubOrder(@RequestBody IcbcSubOrderSplitReq req);
+
+ /**
+ * 工行小订单拆分退款。
+ */
+ @PostMapping("/icbc/sub-order/refund")
+ R<IcbcSubOrderRefundResp> refundSubOrder(@RequestBody IcbcSubOrderRefundReq req);
+
+ /**
+ * 查询工行子订单。
+ */
+ @PostMapping("/icbc/sub-order/query")
+ R<IcbcSubOrderQueryResp> querySubOrder(@RequestBody IcbcSubOrderQueryReq req);
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentReq.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentReq.java
new file mode 100644
index 0000000..4b5eb0f
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentReq.java
@@ -0,0 +1,66 @@
+package com.ruoyi.payment.api.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 工行统一支付请求。
+ */
+@Data
+public class IcbcPaymentReq {
+ /**
+ * 业务订单号。
+ */
+ private String outTradeNo;
+
+ /**
+ * 支付金额,单位元。
+ */
+ private BigDecimal amount;
+
+ /**
+ * 订单标题。
+ */
+ private String subject;
+
+ /**
+ * 订单描述。
+ */
+ private String body;
+
+ /**
+ * 工行 open_id。微信小程序传微信 openId;支付宝小程序传支付宝 16 位 userId。
+ */
+ private String openId;
+
+ /**
+ * 工行 union_id,预留给支付宝生活号等 access_type=8 场景。
+ */
+ private String unionId;
+
+ /**
+ * 小程序设备标识,用于工行 deviceInfo/decive_info。
+ */
+ private String deviceInfo;
+
+ /**
+ * 兼容原支付方式:1=微信,2=支付宝,3=支付宝H5。
+ */
+ private Integer paymentType;
+
+ /**
+ * 支付结果通知地址。
+ */
+ private String notifyUrl;
+
+ /**
+ * H5 支付回跳地址。
+ */
+ private String returnUrl;
+
+ /**
+ * 分账预留字段,后续接入站点、运营商、平台分账规则后映射到工行请求。
+ */
+ private String profitSharing;
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentResp.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentResp.java
new file mode 100644
index 0000000..380cd4d
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentResp.java
@@ -0,0 +1,34 @@
+package com.ruoyi.payment.api.vo;
+
+import lombok.Data;
+
+/**
+ * 工行统一支付响应。
+ */
+@Data
+public class IcbcPaymentResp {
+ /**
+ * 业务订单号。
+ */
+ private String orderId;
+
+ /**
+ * 工行侧交易号或支付流水。
+ */
+ private String tradeNo;
+
+ /**
+ * 工行支付状态或本地骨架状态。
+ */
+ private String status;
+
+ /**
+ * 给前端调起支付使用的数据。
+ */
+ private String payData;
+
+ /**
+ * 工行 SDK 原始响应摘要。
+ */
+ private String rawResponse;
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundReq.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundReq.java
new file mode 100644
index 0000000..7fa761f
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundReq.java
@@ -0,0 +1,19 @@
+package com.ruoyi.payment.api.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 工行聚合支付退款请求。
+ */
+@Data
+public class IcbcRefundReq {
+ private String outTradeNo;
+ private String orderId;
+ private String outRefundNo;
+ private BigDecimal refundAmount;
+ private BigDecimal totalAmount;
+ private String reason;
+ private String merAcct;
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundResp.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundResp.java
new file mode 100644
index 0000000..777a594
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcRefundResp.java
@@ -0,0 +1,18 @@
+package com.ruoyi.payment.api.vo;
+
+import lombok.Data;
+
+/**
+ * 工行聚合支付退款响应。
+ */
+@Data
+public class IcbcRefundResp {
+ private String outTradeNo;
+ private String orderId;
+ private String outRefundNo;
+ private String refundId;
+ private String refundTime;
+ private String refundAmount;
+ private String status;
+ private String rawResponse;
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryReq.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryReq.java
new file mode 100644
index 0000000..1f24419
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryReq.java
@@ -0,0 +1,19 @@
+package com.ruoyi.payment.api.vo;
+
+import lombok.Data;
+
+/**
+ * 工行聚合支付子订单查询请求。
+ */
+@Data
+public class IcbcSubOrderQueryReq {
+ private String serNo;
+ private String orderNoType;
+ private String orderId;
+ private String merOrderId;
+ private String prtiId;
+ private String queryType;
+ private String subOrderNo;
+ private String merTrnscSerno;
+ private String subPrtiId;
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryResp.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryResp.java
new file mode 100644
index 0000000..a2c8ebc
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderQueryResp.java
@@ -0,0 +1,22 @@
+package com.ruoyi.payment.api.vo;
+
+import lombok.Data;
+
+/**
+ * 工行聚合支付子订单查询响应。
+ */
+@Data
+public class IcbcSubOrderQueryResp {
+ private String orderStatus;
+ private String subOutMerNo;
+ private String subOutAgreeNo;
+ private String outTradeNo;
+ private String orderId;
+ private String subOrderNo;
+ private String subMerOrderId;
+ private String orderAmnt;
+ private String orderRefundAmnt;
+ private String payDate;
+ private String status;
+ private String rawResponse;
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundReq.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundReq.java
new file mode 100644
index 0000000..ac948a1
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundReq.java
@@ -0,0 +1,27 @@
+package com.ruoyi.payment.api.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 工行聚合支付小订单拆分退款请求。
+ */
+@Data
+public class IcbcSubOrderRefundReq {
+ private String chargingOrderCode;
+ private String seqNo;
+ private String busiType;
+ private String oriMerId;
+ private String oriTrxDate;
+ private String subOrderId;
+ private String retSubOrderId;
+ private String outtrxSerialNo;
+ private String subMerId;
+ private String subMerPrtclNo;
+ private BigDecimal refundAmount;
+ private String refundSource;
+ private String merAcct;
+ private String merSpInfor;
+ private String termId;
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundResp.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundResp.java
new file mode 100644
index 0000000..882871d
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundResp.java
@@ -0,0 +1,20 @@
+package com.ruoyi.payment.api.vo;
+
+import lombok.Data;
+
+/**
+ * 工行聚合支付小订单拆分退款响应。
+ */
+@Data
+public class IcbcSubOrderRefundResp {
+ private String chargingOrderCode;
+ private String subOrderId;
+ private String retSubOrderId;
+ private String outtrxSerialNo;
+ private String orderId;
+ private String subOrderSeq;
+ private String subOrderTrxid;
+ private String refundAmount;
+ private String status;
+ private String rawResponse;
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitReq.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitReq.java
new file mode 100644
index 0000000..86465d5
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitReq.java
@@ -0,0 +1,24 @@
+package com.ruoyi.payment.api.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 工行聚合支付小订单拆分请求。
+ */
+@Data
+public class IcbcSubOrderSplitReq {
+ private String chargingOrderCode;
+ private String seqNo;
+ private String busiType;
+ private String oriMerId;
+ private String oriTrxDate;
+ private String subOrderId;
+ private String subMerId;
+ private String subMerPrtclNo;
+ private BigDecimal classifyAmt;
+ private String merAcct;
+ private String merSpInfor;
+ private String termId;
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitResp.java b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitResp.java
new file mode 100644
index 0000000..0d0322a
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderSplitResp.java
@@ -0,0 +1,20 @@
+package com.ruoyi.payment.api.vo;
+
+import lombok.Data;
+
+/**
+ * 工行聚合支付小订单拆分响应。
+ */
+@Data
+public class IcbcSubOrderSplitResp {
+ private String chargingOrderCode;
+ private String subOrderId;
+ private String orderId;
+ private String subOrderSeq;
+ private String subOrderTrxid;
+ private String recResult;
+ private String classifyAmt;
+ private String subMerId;
+ private String status;
+ private String rawResponse;
+}
diff --git a/ruoyi-api/ruoyi-api-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-api/ruoyi-api-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 1e04370..c1a0848 100644
--- a/ruoyi-api/ruoyi-api-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/ruoyi-api/ruoyi-api-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,4 +1,5 @@
com.ruoyi.payment.api.factory.WxPaymentFallbackFactory
com.ruoyi.payment.api.factory.AliPaymentFallbackFactory
com.ruoyi.payment.api.factory.AppAliPaymentFallbackFactory
-com.ruoyi.payment.api.factory.H5AliPaymentFallbackFactory
\ No newline at end of file
+com.ruoyi.payment.api.factory.H5AliPaymentFallbackFactory
+com.ruoyi.payment.api.factory.IcbcPaymentFallbackFactory
diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java
index be73042..23b185b 100644
--- a/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java
+++ b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java
@@ -54,4 +54,24 @@
* 地址
*/
private String address;
+
+ /**
+ * 微信小程序 openId
+ */
+ private String wxOpenid;
+
+ /**
+ * 支付宝小程序 openId
+ */
+ private String aliOpenid;
+
+ /**
+ * 支付宝 userId。
+ */
+ private String aliUserId;
+
+ /**
+ * 支付宝 unionId,工行支付宝小程序支付 union_id 使用。
+ */
+ private String aliUnionId;
}
diff --git a/ruoyi-auth/src/main/resources/bootstrap.yml b/ruoyi-auth/src/main/resources/bootstrap.yml
index 5b28ed3..de780b5 100644
--- a/ruoyi-auth/src/main/resources/bootstrap.yml
+++ b/ruoyi-auth/src/main/resources/bootstrap.yml
@@ -6,8 +6,8 @@
spring:
profiles:
# 环境配置
+# active: dev
active: dev
-# active: prod
application:
# 应用名称
name: ruoyi-auth
diff --git a/ruoyi-modules/ruoyi-system/src/main/resources/bootstrap.yml b/ruoyi-modules/ruoyi-system/src/main/resources/bootstrap.yml
index 4de4316..dea794e 100644
--- a/ruoyi-modules/ruoyi-system/src/main/resources/bootstrap.yml
+++ b/ruoyi-modules/ruoyi-system/src/main/resources/bootstrap.yml
@@ -3,7 +3,7 @@
tomcat:
basedir: /data/tomcat
# Spring
-spring:
+spring:
application:
# 应用名称
name: ruoyi-system
@@ -11,9 +11,8 @@
allow-bean-definition-overriding: true
profiles:
# 环境配置
-# active: local
+# active: dev
active: dev
-# active: prod
---
spring:
config:
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java
index 6db885e..1c775d9 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java
@@ -9,6 +9,7 @@
import com.alipay.api.response.AlipaySystemOauthTokenResponse;
import com.alipay.api.response.AlipayUserInfoShareResponse;
import com.alipay.api.response.AlipayUserUserinfoShareResponse;
+import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.account.ali.Constant.AliConstant;
@@ -28,6 +29,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
+import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@@ -56,8 +58,16 @@
// 登录
AlipaySystemOauthTokenResponse response = new AliAppletTools(aliProperties).login(appletUserLogin.getCode());
if (response.isSuccess()) {
- // 获取用户信息
-// AlipayUserInfoShareResponse userInfo = new AliAppletTools(aliProperties).getUserInfo(response.getAccessToken());
+ String aliUnionId = firstNotBlank(readStringProperty(response, "getUnionId"), readStringProperty(response, "getUnion_id"),
+ getResponseBodyValue(response, "union_id"), getResponseBodyValue(response, "unionId"));
+ String aliUserId = firstNotBlank(response.getUserId(), getResponseBodyValue(response, "user_id"), getResponseBodyValue(response, "userId"));
+ log.info("支付宝授权换取用户标识成功,openId:{},userId:{},unionId:{}", response.getOpenId(), aliUserId, aliUnionId);
+ AlipayUserInfoShareResponse userInfo = new AliAppletTools(aliProperties).getUserInfo(response.getAccessToken());
+ log.info("支付宝用户信息接口返回:{}", JSON.toJSONString(userInfo));
+ aliUserId = firstNotBlank(aliUserId, userInfo == null ? null : userInfo.getUserId(),
+ getResponseBodyValue(userInfo, "user_id"), getResponseBodyValue(userInfo, "userId"));
+ aliUnionId = firstNotBlank(aliUnionId, readStringProperty(userInfo, "getUnionId"), readStringProperty(userInfo, "getUnion_id"),
+ getResponseBodyValue(userInfo, "union_id"), getResponseBodyValue(userInfo, "unionId"));
if(StringUtils.isEmpty(appletUserLogin.getToken())){
return AjaxResult.success("已拒绝授权");
}
@@ -65,7 +75,8 @@
String phoneNumber = new AliAppletTools(aliProperties).getPhoneNumber(appletUserLogin.getToken());
log.info("获取支付宝用户信息:{}", phoneNumber);
// 用户信息封装
- return AjaxResult.success(appUserService.aliLogin(response,phoneNumber,appletUserLogin.getInviteUserId()));
+ return AjaxResult.success(appUserService.aliLogin(response,phoneNumber,appletUserLogin.getInviteUserId(),
+ aliUserId, aliUnionId));
} else {
String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);
log.warn("诊断结果:{}",diagnosisUrl);
@@ -73,4 +84,55 @@
}
}
+ private String getResponseBodyValue(Object response, String key) {
+ String body = readStringProperty(response, "getBody");
+ if (StringUtils.isEmpty(body)) {
+ return null;
+ }
+ try {
+ com.alibaba.fastjson.JSONObject bodyJson = JSON.parseObject(body);
+ String value = bodyJson.getString(key);
+ if (StringUtils.isNotEmpty(value)) {
+ return value;
+ }
+ for (String nestedKey : bodyJson.keySet()) {
+ com.alibaba.fastjson.JSONObject nested = bodyJson.getJSONObject(nestedKey);
+ if (nested != null) {
+ value = nested.getString(key);
+ if (StringUtils.isNotEmpty(value)) {
+ return value;
+ }
+ }
+ }
+ return null;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private String readStringProperty(Object target, String methodName) {
+ if (target == null) {
+ return null;
+ }
+ try {
+ Method method = target.getClass().getMethod(methodName);
+ Object value = method.invoke(target);
+ return value == null ? null : String.valueOf(value);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private String firstNotBlank(String... values) {
+ if (values == null) {
+ return null;
+ }
+ for (String value : values) {
+ if (StringUtils.isNotEmpty(value)) {
+ return value;
+ }
+ }
+ return null;
+ }
+
}
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
index 6274d4b..814bab6 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
@@ -889,7 +889,8 @@
@ApiOperation(value = "个人中心信息", tags = {"小程序-个人中心"})
@GetMapping(value = "/user/info")
public R<AppUserInfoDto> info() {
- Long userId = tokenService.getLoginUserApplet().getUserId();
+ LoginUserApplet loginUserApplet = tokenService.getLoginUserApplet();
+ Long userId = loginUserApplet.getUserId();
TAppUser byId = appUserService.getById(userId);
AppUserInfoDto appUserInfoDto = new AppUserInfoDto();
TVip data = vipClient.getVipInfoByType(2).getData();
@@ -924,6 +925,10 @@
appUserInfoDto.setUserCars(list);
appUserInfoDto.setFirstAdd(byId.getFirstAdd()!=null?byId.getFirstAdd():0);
appUserInfoDto.setPoints(byId.getPoints()==null?0:byId.getPoints());
+ appUserInfoDto.setWxOpenid(byId.getWxOpenid());
+ appUserInfoDto.setAliOpenid(byId.getAliOpenid());
+ appUserInfoDto.setAliUserId(loginUserApplet.getAliUserId());
+ appUserInfoDto.setAliUnionId(loginUserApplet.getAliUnionId());
appUserInfoDto.setBalance(byId.getBalance()==null?BigDecimal.ZERO:byId.getBalance());
Partner boundPartner = null;
try {
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java
index 2e29c49..60e5059 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java
@@ -32,7 +32,7 @@
* @return
*/
// Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response, AlipayUserInfoShareResponse userInfo);
- Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId);
+ Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId,String aliUserId,String aliUnionId);
/**
* 封装用户信息和token
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java
index 879ca16..36a7866 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java
@@ -88,7 +88,7 @@
}
@Override
- public Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId) {
+ public Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId,String aliUserId,String aliUnionId) {
// 通过手机号查询用户,是否已存在手动导入用户,包含微信用户
TAppUser appUser = this.getOne(Wrappers.lambdaQuery(TAppUser.class)
.eq(TAppUser::getPhone, phone)
@@ -129,7 +129,10 @@
if(Objects.nonNull(inviteUserId)){
inviteUserService.saveInviteUser(appUser.getId(), inviteUserId);
}
- return this.getUserInfo(appUser);
+ LoginUserApplet loginUserApplet = buildLoginUserApplet(appUser);
+ loginUserApplet.setAliUserId(aliUserId);
+ loginUserApplet.setAliUnionId(aliUnionId);
+ return buildTokenInfo(loginUserApplet);
}
// @Override
// public Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response, AlipayUserInfoShareResponse userInfo) {
@@ -163,6 +166,10 @@
@Override
public Map<String, Object> getUserInfo(TAppUser appUser) {
+ return buildTokenInfo(buildLoginUserApplet(appUser));
+ }
+
+ private LoginUserApplet buildLoginUserApplet(TAppUser appUser) {
LoginUserApplet loginUserApplet = new LoginUserApplet();
if(ObjectUtils.isNotNull(appUser)){
loginUserApplet.setUserId(appUser.getId());
@@ -171,7 +178,13 @@
loginUserApplet.setPhone(appUser.getPhone());
loginUserApplet.setAvatar(appUser.getAvatar());
loginUserApplet.setAddress(appUser.getProvince()+appUser.getCity());
+ loginUserApplet.setWxOpenid(appUser.getWxOpenid());
+ loginUserApplet.setAliOpenid(appUser.getAliOpenid());
}
+ return loginUserApplet;
+ }
+
+ private Map<String, Object> buildTokenInfo(LoginUserApplet loginUserApplet) {
Map<String, Object> tokenInfos = new HashMap<>();
tokenInfos.put("token",tokenService.createTokenApplet(loginUserApplet));
tokenInfos.put("info",loginUserApplet);
diff --git a/ruoyi-service/ruoyi-account/src/main/resources/bootstrap.yml b/ruoyi-service/ruoyi-account/src/main/resources/bootstrap.yml
index c820626..a7ff26b 100644
--- a/ruoyi-service/ruoyi-account/src/main/resources/bootstrap.yml
+++ b/ruoyi-service/ruoyi-account/src/main/resources/bootstrap.yml
@@ -13,7 +13,7 @@
profiles:
# 环境配置
# active: dev
- active: prod
+ active: dev
---
spring:
config:
@@ -23,7 +23,7 @@
nacos:
discovery:
# 服务注册地址
- server-addr: 192.168.110.169:8848
+ server-addr: 192.168.110.80:8848
service: ${spring.application.name}
group: DEFAULT_GROUP
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
@@ -31,7 +31,7 @@
password: nacos
config:
# 配置中心地址
- server-addr: 192.168.110.169:8848
+ server-addr: 192.168.110.80:8848
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
group: DEFAULT_GROUP
name: ${spring.application.name}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TApplyChargingPileController.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TApplyChargingPileController.java
index 5120ee2..faed8a8 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TApplyChargingPileController.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/TApplyChargingPileController.java
@@ -337,8 +337,11 @@
BigDecimal bigDecimal = new BigDecimal("0");
for (TChargingOrder chargingOrder : collect) {
- bigDecimal = bigDecimal.add(chargingOrder.getRefundAmount());
- if (chargingOrder.getRechargePaymentType()!=null){
+ bigDecimal = bigDecimal.add(chargingOrder.getRefundAmount() == null ? BigDecimal.ZERO : chargingOrder.getRefundAmount());
+ if ("ICBC".equals(chargingOrder.getSourceName())) {
+ chargingOrder.setPayPlatform("工商银行");
+ chargingOrder.setRechargePaymentType1("工商银行支付");
+ } else if (chargingOrder.getRechargePaymentType()!=null){
switch (chargingOrder.getRechargePaymentType()){
case 1:
chargingOrder.setPayPlatform("微信");
@@ -348,12 +351,18 @@
chargingOrder.setPayPlatform("支付宝");
chargingOrder.setRechargePaymentType1("支付宝小程序支付");
break;
+ case 3:
+ chargingOrder.setPayPlatform("支付宝");
+ chargingOrder.setRechargePaymentType1("支付宝H5支付");
+ break;
}
}
chargingOrder.setXuhao(i);
i++;
}
- collect.get(0).setTotal(bigDecimal);
+ if (!collect.isEmpty()) {
+ collect.get(0).setTotal(bigDecimal);
+ }
try {
response.setCharacterEncoding(Constants.UTF8);
response.setContentType("application/vnd.ms-excel");
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java
index 1fafeec..36844e2 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java
@@ -25,6 +25,7 @@
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
+import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
@@ -151,8 +152,32 @@
if(null == partner.getId() && StringUtils.isEmpty(partner.getPassword())){
return AjaxResult.error("登录密码不能为空");
}
+ if (isInvalidShareRatio(partner.getElectricityShareRatio())) {
+ return AjaxResult.error("电费分成比例必须为0-100且最多两位小数");
+ }
+ if (isInvalidShareRatio(partner.getServiceShareRatio())) {
+ return AjaxResult.error("服务费分成比例必须为0-100且最多两位小数");
+ }
+ if (partner.getElectricityShareRatio() != null || partner.getServiceShareRatio() != null) {
+ if (StringUtils.isEmpty(partner.getIcbcSubMerId())) {
+ return AjaxResult.error("填写分成比例时工行二级商户编号不能为空");
+ }
+ if (StringUtils.isEmpty(partner.getIcbcSubMerPrtclNo())) {
+ return AjaxResult.error("填写分成比例时工行二级商户协议编号不能为空");
+ }
+ }
return AjaxResult.success();
}
+
+ private boolean isInvalidShareRatio(BigDecimal ratio) {
+ if (ratio == null) {
+ return false;
+ }
+ if (ratio.compareTo(BigDecimal.ZERO) < 0 || ratio.compareTo(new BigDecimal("100")) > 0) {
+ return true;
+ }
+ return Math.max(ratio.stripTrailingZeros().scale(), 0) > 2;
+ }
/**
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/resources/bootstrap.yml b/ruoyi-service/ruoyi-chargingPile/src/main/resources/bootstrap.yml
index eefa2d5..48a10b0 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/resources/bootstrap.yml
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/resources/bootstrap.yml
@@ -13,7 +13,7 @@
profiles:
# 环境配置
# active: dev
- active: prod
+ active: dev
---
spring:
config:
@@ -23,7 +23,7 @@
nacos:
discovery:
# 服务注册地址
- server-addr: 192.168.110.169:8848
+ server-addr: 192.168.110.80:8848
service: ${spring.application.name}
group: DEFAULT_GROUP
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
@@ -31,7 +31,7 @@
password: nacos
config:
# 配置中心地址
- server-addr: 192.168.110.169:8848
+ server-addr: 192.168.110.80:8848
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
group: DEFAULT_GROUP
name: ${spring.application.name}
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/PartnerMapper.xml b/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/PartnerMapper.xml
index 9af10a6..92706a4 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/PartnerMapper.xml
+++ b/ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/PartnerMapper.xml
@@ -12,6 +12,9 @@
<result column="logo_url" property="logoUrl" />
<result column="rate" property="rate" />
<result column="icbc_partner_id" property="icbcPartnerId" />
+ <result column="icbc_sub_mer_id" property="icbcSubMerId" />
+ <result column="icbc_sub_mer_prtcl_no" property="icbcSubMerPrtclNo" />
+ <result column="icbc_mer_acct" property="icbcMerAcct" />
<result column="electricity_share_ratio" property="electricityShareRatio" />
<result column="service_share_ratio" property="serviceShareRatio" />
<result column="venue_share_amount" property="venueShareAmount" />
@@ -38,6 +41,10 @@
p.address,
p.account,
p.remark,
+ p.icbc_partner_id as icbcPartnerId,
+ p.icbc_sub_mer_id as icbcSubMerId,
+ p.icbc_sub_mer_prtcl_no as icbcSubMerPrtclNo,
+ p.icbc_mer_acct as icbcMerAcct,
p.electricity_share_ratio as electricityShareRatio,
p.service_share_ratio as serviceShareRatio,
p.venue_share_amount as venueShareAmount
diff --git a/ruoyi-service/ruoyi-integration/src/main/resources/bootstrap.yml b/ruoyi-service/ruoyi-integration/src/main/resources/bootstrap.yml
index ea6e110..31199bf 100644
--- a/ruoyi-service/ruoyi-integration/src/main/resources/bootstrap.yml
+++ b/ruoyi-service/ruoyi-integration/src/main/resources/bootstrap.yml
@@ -13,8 +13,7 @@
profiles:
# 环境配置
# active: dev
- active: prod
-
+ active: dev
---
spring:
config:
@@ -24,7 +23,7 @@
nacos:
discovery:
# 服务注册地址
- server-addr: 192.168.110.169:8848
+ server-addr: 192.168.110.80:8848
service: ${spring.application.name}
group: DEFAULT_GROUP
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
@@ -32,7 +31,7 @@
password: nacos
config:
# 配置中心地址
- server-addr: 192.168.110.169:8848
+ server-addr: 192.168.110.80:8848
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
group: DEFAULT_GROUP
name: ${spring.application.name}
diff --git a/ruoyi-service/ruoyi-jianguan/src/main/resources/bootstrap.yml b/ruoyi-service/ruoyi-jianguan/src/main/resources/bootstrap.yml
index ff76b29..2fc0648 100644
--- a/ruoyi-service/ruoyi-jianguan/src/main/resources/bootstrap.yml
+++ b/ruoyi-service/ruoyi-jianguan/src/main/resources/bootstrap.yml
@@ -13,8 +13,7 @@
profiles:
# 环境配置
# active: dev
- active: prod
-
+ active: dev
---
spring:
config:
@@ -24,7 +23,7 @@
nacos:
discovery:
# 服务注册地址
- server-addr: 192.168.110.169:8848
+ server-addr: 192.168.110.80:8848
service: ${spring.application.name}
group: DEFAULT_GROUP
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
@@ -32,7 +31,7 @@
password: nacos
config:
# 配置中心地址
- server-addr: 192.168.110.169:8848
+ server-addr: 192.168.110.80:8848
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
group: DEFAULT_GROUP
name: ${spring.application.name}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/ChargingBillController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/ChargingBillController.java
index 4b26ca7..a6fda40 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/ChargingBillController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/ChargingBillController.java
@@ -154,6 +154,8 @@
accountListVO.setRefundAmount(res.getRefundAmount().setScale(2, BigDecimal.ROUND_DOWN));
accountListVO.setCommissionAmount(res.getCommissionAmount().setScale(2, BigDecimal.ROUND_DOWN));
accountListVO.setSharingAmount(res.getSharingAmount().setScale(2, BigDecimal.ROUND_DOWN));
+ accountListVO.setPlatformIncome(res.getPlatformIncome().setScale(2, BigDecimal.ROUND_DOWN));
+ accountListVO.setPartnerIncome(res.getPartnerIncome().setScale(2, BigDecimal.ROUND_DOWN));
accountListVO.setList(res.getList());
accountListVO.setOrderList(res.getExportList());
return R.ok(accountListVO);
@@ -203,6 +205,8 @@
tChargingBillExport.setBillWeek(orderInvoiceVO.getBillWeek());
tChargingBillExport.setSiteName(orderInvoiceVO.getSiteName());
tChargingBillExport.setPaymentAmount(orderInvoiceVO.getPaymentAmount());
+ tChargingBillExport.setPlatformIncome(orderInvoiceVO.getPlatformIncome());
+ tChargingBillExport.setPartnerIncome(orderInvoiceVO.getPartnerIncome());
tChargingBillExport.setElectrovalence(orderInvoiceVO.getElectrovalence());
tChargingBillExport.setServiceCharge(orderInvoiceVO.getServiceCharge());
tChargingBillExport.setChargingCapacity(orderInvoiceVO.getChargingCapacity());
@@ -276,6 +280,8 @@
tChargingBillExport.setBillWeek(orderInvoiceVO.getBillWeek());
tChargingBillExport.setSiteName(orderInvoiceVO.getSiteName());
tChargingBillExport.setPaymentAmount(orderInvoiceVO.getPaymentAmount());
+ tChargingBillExport.setPlatformIncome(orderInvoiceVO.getPlatformIncome());
+ tChargingBillExport.setPartnerIncome(orderInvoiceVO.getPartnerIncome());
tChargingBillExport.setElectrovalence(orderInvoiceVO.getElectrovalence());
tChargingBillExport.setServiceCharge(orderInvoiceVO.getServiceCharge());
tChargingBillExport.setChargingCapacity(orderInvoiceVO.getChargingCapacity());
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/FinancialSettlementController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/FinancialSettlementController.java
index 3e32e86..aa91cec 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/FinancialSettlementController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/FinancialSettlementController.java
@@ -250,6 +250,8 @@
}
tChargingOrderExport.setBalanceIncome(formatMoney(chargingOrderListVO.getBalanceIncome()));
tChargingOrderExport.setBalanceGiftIncome(formatMoney(chargingOrderListVO.getBalanceGiftIncome()));
+ tChargingOrderExport.setPlatformIncome(formatMoney(chargingOrderListVO.getPlatformIncome()));
+ tChargingOrderExport.setPartnerIncome(formatMoney(chargingOrderListVO.getPartnerIncome()));
tChargingOrderExport.setTripartiteCommission(formatMoney(chargingOrderListVO.getSharingAmount()));
tChargingOrderExport.setOrderCode(chargingOrderListVO.getStartChargeSeq());
tChargingOrderExport.setIsSingle("是");
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
index 3a23f90..c6323f7 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -58,6 +58,8 @@
import com.ruoyi.order.util.PreviousSixMonths;
import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
import com.ruoyi.order.vo.EndOfChargePageInfo;
+import com.ruoyi.order.vo.ChargingSimulationVO;
+import com.ruoyi.order.vo.IcbcAdminSubOrderVO;
import com.ruoyi.order.vo.StatisticsOfBatteryVO;
import com.ruoyi.order.vo.UpdateOrderPaymentAmountDto;
import com.ruoyi.order.vo.WatchChargingOrderVo;
@@ -65,6 +67,9 @@
import com.ruoyi.other.api.feignClient.UserSiteClient;
import com.ruoyi.payment.api.feignClient.AliPaymentClient;
import com.ruoyi.payment.api.feignClient.WxPaymentClient;
+import com.ruoyi.payment.api.vo.IcbcPaymentResp;
+import com.ruoyi.payment.api.vo.IcbcRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.feignClient.SysUserClient;
import com.ruoyi.system.api.feignClient.SysUserRoleClient;
@@ -948,6 +953,48 @@
public AjaxResult paymentChargingOrder(@RequestBody AddChargingOrder addChargingOrder) {
return chargingOrderService.paymentChargingOrder(addChargingOrder);
}
+
+ @RequiresPermissions(value = {"/chargingPileOrder", "/paymentOrder/refund"}, logical = Logical.AND)
+ @PostMapping("/icbc/fund/payment-query")
+ @ApiOperation(value = "管理后台查询工行支付订单", tags = {"后台-充电桩订单"})
+ public AjaxResult<IcbcPaymentResp> queryIcbcAdminPayment(@RequestBody IcbcAdminFundOperationReq request) {
+ return AjaxResult.success(chargingOrderService.queryIcbcAdminPayment(request == null ? null : request.getOrderCode()));
+ }
+
+ @RequiresPermissions(value = {"/chargingPileOrder", "/paymentOrder/refund"}, logical = Logical.AND)
+ @PostMapping("/icbc/fund/refund")
+ @ApiOperation(value = "管理后台工行主订单退款", tags = {"后台-充电桩订单"})
+ public AjaxResult<IcbcRefundResp> refundIcbcAdminPayment(@RequestBody IcbcAdminFundOperationReq request) {
+ return AjaxResult.success(chargingOrderService.refundIcbcAdminPayment(request));
+ }
+
+ @RequiresPermissions(value = {"/chargingPileOrder", "/paymentOrder/refund"}, logical = Logical.AND)
+ @PostMapping("/icbc/fund/sub-orders")
+ @ApiOperation(value = "管理后台查询工行分账子订单", tags = {"后台-充电桩订单"})
+ public AjaxResult<List<IcbcAdminSubOrderVO>> queryIcbcAdminSubOrders(@RequestBody IcbcAdminFundOperationReq request) {
+ return AjaxResult.success(chargingOrderService.queryIcbcAdminSubOrders(request == null ? null : request.getOrderCode()));
+ }
+
+ @RequiresPermissions(value = {"/chargingPileOrder", "/paymentOrder/refund"}, logical = Logical.AND)
+ @PostMapping("/icbc/fund/sub-order-refund")
+ @ApiOperation(value = "管理后台工行分账子订单退款", tags = {"后台-充电桩订单"})
+ public AjaxResult<IcbcSubOrderRefundResp> refundIcbcAdminSubOrder(@RequestBody IcbcAdminFundOperationReq request) {
+ return AjaxResult.success(chargingOrderService.refundIcbcAdminSubOrder(request));
+ }
+
+ @ResponseBody
+ @GetMapping("/simulation/current")
+ @ApiOperation(value = "查询当前枪可模拟结算的工行订单", tags = {"小程序-模拟充电"})
+ public AjaxResult<ChargingSimulationVO> getCurrentSimulationOrder(@RequestParam("gunId") Integer gunId) {
+ return AjaxResult.success(chargingOrderService.getCurrentSimulationOrder(gunId));
+ }
+
+ @ResponseBody
+ @PostMapping("/simulation/complete")
+ @ApiOperation(value = "模拟完成充电并执行真实结算", tags = {"小程序-模拟充电"})
+ public AjaxResult<ChargingSimulationVO> completeSimulation(@RequestBody ChargingSimulationReq request) {
+ return AjaxResult.success(chargingOrderService.completeSimulation(request));
+ }
/**
* 充电充值支付回调
@@ -1123,11 +1170,12 @@
public R<List<SixChargingDto>> charge(Integer siteId) {
Long userId = tokenService.getLoginUser().getUserid();
SysUser sysUser = sysUserClient.getSysUser(userId).getData();
+ boolean partnerView = isPartnerView(sysUser);
List<Integer> siteIds = resolvePlatformRevenueSiteIds(userId, sysUser, siteId);
LocalDate sixBefore = PreviousSixMonths.get();
//通过siteIds进行sql查询统计
List<SixChargingDto> sixChargingDtos = generateLastSixMonths();
- List<SixChargingDto> chargingDtos = chargingOrderService.charge(sixBefore, siteIds);
+ List<SixChargingDto> chargingDtos = chargingOrderService.charge(sixBefore, siteIds, partnerView);
for (SixChargingDto sixChargingDto : sixChargingDtos) {
for (SixChargingDto chargingDto : chargingDtos) {
if (sixChargingDto.getMonth().equals(chargingDto.getMonth())){
@@ -1165,9 +1213,10 @@
public R<List<SixCircleDto>> circle() {
Long userId = tokenService.getLoginUser().getUserid();
SysUser sysUser = sysUserClient.getSysUser(userId).getData();
+ boolean partnerView = isPartnerView(sysUser);
List<Integer> siteIds = resolvePlatformRevenueSiteIds(userId, sysUser, null);
LocalDate sixBefore = PreviousSixMonths.get();
- List<SixCircleDto> sixCircleDtos = chargingOrderService.circle(siteIds,sixBefore);
+ List<SixCircleDto> sixCircleDtos = chargingOrderService.circle(siteIds,sixBefore, partnerView);
for (SixCircleDto sixCircleDto : sixCircleDtos) {
Site site = siteClient.getSiteByIds(Arrays.asList(sixCircleDto.getSiteId())).getData().get(0);
sixCircleDto.setSiteName(site.getName());
@@ -1252,14 +1301,15 @@
//count近6个月的数据
Long userId = tokenService.getLoginUser().getUserid();
SysUser sysUser = sysUserClient.getSysUser(userId).getData();
+ boolean partnerView = isPartnerView(sysUser);
List<Integer> siteIds = resolvePlatformRevenueSiteIds(userId, sysUser, null);
- Map<String,Object> map = chargingOrderService.countAll(siteIds);
- boolean platformVisible = 1 == sysUser.getObjectId();
+ Map<String,Object> map = chargingOrderService.countAll(siteIds, partnerView);
+ if (map == null) {
+ map = new HashMap<String,Object>();
+ }
+ boolean platformVisible = !partnerView;
if(platformVisible){
BigDecimal data = parkingLotClient.getRecordAmount().getData();
- if (map ==null){
- map = new HashMap<String,Object>();
- }
map.put("parkingAmount",data);
BigDecimal data1 = shoppingOrderService.getSumAmount();
map.put("shopAmount",data1);
@@ -1281,9 +1331,14 @@
public R<List<SixIncomeCircleDto>> payTypeCircle() {
Long userId = tokenService.getLoginUser().getUserid();
SysUser sysUser = sysUserClient.getSysUser(userId).getData();
+ boolean partnerView = isPartnerView(sysUser);
List<Integer> siteIds = resolvePlatformRevenueSiteIds(userId, sysUser, null);
LocalDate sixBefore = PreviousSixMonths.get();
- return R.ok(chargingOrderService.payTypeCircle(sixBefore, siteIds));
+ return R.ok(chargingOrderService.payTypeCircle(sixBefore, siteIds, partnerView));
+ }
+
+ private boolean isPartnerView(SysUser sysUser) {
+ return sysUser != null && Integer.valueOf(2).equals(sysUser.getRoleType());
}
/**
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/ChargingSimulationReq.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/ChargingSimulationReq.java
new file mode 100644
index 0000000..8a978d5
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/ChargingSimulationReq.java
@@ -0,0 +1,12 @@
+package com.ruoyi.order.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class ChargingSimulationReq {
+ private Integer gunId;
+ private BigDecimal electricityFee;
+ private BigDecimal serviceFee;
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/IcbcAdminFundOperationReq.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/IcbcAdminFundOperationReq.java
new file mode 100644
index 0000000..83f571d
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/IcbcAdminFundOperationReq.java
@@ -0,0 +1,12 @@
+package com.ruoyi.order.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class IcbcAdminFundOperationReq {
+ private String orderCode;
+ private Long shareRecordId;
+ private BigDecimal amount;
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TChargingBillExport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TChargingBillExport.java
index 39e61c9..caa518d 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TChargingBillExport.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TChargingBillExport.java
@@ -24,6 +24,10 @@
private String siteName;
@Excel(name = "总金额",width = 30)
private BigDecimal paymentAmount;
+ @Excel(name = "平台收入",width = 30)
+ private BigDecimal platformIncome;
+ @Excel(name = "合作商收入",width = 30)
+ private BigDecimal partnerIncome;
@Excel(name = "总电费",width = 30)
private BigDecimal electrovalence;
@Excel(name = "总服务费",width = 30)
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TChargingOrderExport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TChargingOrderExport.java
index 5e19b08..7214c5b 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TChargingOrderExport.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/TChargingOrderExport.java
@@ -96,6 +96,10 @@
private String balanceIncome;
@Excel(name = "余额赠送收入",width = 30)
private String balanceGiftIncome;
+ @Excel(name = "平台收入",width = 30)
+ private String platformIncome;
+ @Excel(name = "合作商收入",width = 30)
+ private String partnerIncome;
@Excel(name = "引流平台分佣",width = 30)
private String tripartiteCommission;
@Excel(name = "车架号来源",width = 30)
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
index 3ca1971..5f19cc7 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderMapper.java
@@ -65,6 +65,8 @@
Map<String, Object> exchangeCodeBalanceSummary(@Param("orderIds") List<Long> orderIds);
List<Map<String, Object>> exchangeCodeBalanceIncomeByOrderIds(@Param("orderIds") List<Long> orderIds);
List<ChargingOrderBalanceIncomeDetailVO> exchangeCodeBalanceIncomeDetail(@Param("orderIds") List<Long> orderIds);
+ List<Map<String, Object>> chargingOrderShareIncomeByOrderIds(@Param("orderIds") List<Long> orderIds);
+ Map<String, Object> chargingOrderShareIncomeSummaryByOrderIds(@Param("orderIds") List<Long> orderIds);
List<PayOrderDto> payOrderQuery(@Param("pageInfo")PageInfo<PayOrderDto> pageInfo,@Param("data") PayOrderQueryDto payOrderQueryDto);
List<EnterpriseChargingAnalysisVO> enterpriseChargingAnalysis(@Param("pageInfo") PageInfo<EnterpriseChargingAnalysisVO> pageInfo,
@@ -83,11 +85,12 @@
List<ChargingOrderGroup> chargingOrderGroup(@Param("chargingPercentProvinceDto")ChargingPercentProvinceDto chargingPercentProvinceDto);
- List<SixChargingDto> charge(@Param("sixBefore")LocalDate sixBefore,@Param("siteIds") List<Integer> siteIds);
+ List<SixChargingDto> charge(@Param("sixBefore")LocalDate sixBefore,@Param("siteIds") List<Integer> siteIds, @Param("partnerView") Boolean partnerView);
- List<SixCircleDto> circle(@Param("siteIds")List<Integer> siteIds,@Param("sixBefore")LocalDate sixBefore);
+ List<SixCircleDto> circle(@Param("siteIds")List<Integer> siteIds,@Param("sixBefore")LocalDate sixBefore, @Param("partnerView") Boolean partnerView);
- Map<String, Object> countAll(@Param("siteIds")List<Integer> siteIds);
+ Map<String, Object> countAll(@Param("siteIds")List<Integer> siteIds, @Param("partnerView") Boolean partnerView);
+ List<Map<String, Object>> chargingOrderShareIncomeByPayChannel(@Param("sixBefore")LocalDate sixBefore,@Param("siteIds") List<Integer> siteIds);
List<Map<String, Object>> getSumByType(@Param("start")LocalDateTime start,@Param("end")LocalDateTime end,@Param("siteIds")List<Integer> siteIds);
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderShareRecordMapper.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderShareRecordMapper.java
new file mode 100644
index 0000000..29085e8
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/mapper/TChargingOrderShareRecordMapper.java
@@ -0,0 +1,7 @@
+package com.ruoyi.order.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.order.api.model.TChargingOrderShareRecord;
+
+public interface TChargingOrderShareRecordMapper extends BaseMapper<TChargingOrderShareRecord> {
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/ChargingOrderSettlementExecutor.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/ChargingOrderSettlementExecutor.java
new file mode 100644
index 0000000..e062ac1
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/ChargingOrderSettlementExecutor.java
@@ -0,0 +1,22 @@
+package com.ruoyi.order.service;
+
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+/**
+ * 通过独立Spring Bean调用结算服务,确保@Transactional代理生效。
+ */
+@Service
+public class ChargingOrderSettlementExecutor {
+ @Lazy
+ @Resource
+ private TChargingOrderService chargingOrderService;
+
+ public R settle(TransactionRecordMessageVO record) {
+ return chargingOrderService.endChargeBillingCharge(record);
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
index 2eeade7..90878cb 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
@@ -14,6 +14,8 @@
import com.ruoyi.order.api.vo.*;
import com.ruoyi.order.dto.*;
import com.ruoyi.order.vo.ChargingOrderListInfoVO;
+import com.ruoyi.order.vo.ChargingSimulationVO;
+import com.ruoyi.order.vo.IcbcAdminSubOrderVO;
import com.ruoyi.order.vo.EndOfChargePageInfo;
import com.ruoyi.order.vo.StatisticsOfBatteryVO;
import org.springframework.web.bind.annotation.RequestBody;
@@ -24,6 +26,9 @@
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
+import com.ruoyi.payment.api.vo.IcbcPaymentResp;
+import com.ruoyi.payment.api.vo.IcbcRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp;
/**
* <p>
@@ -66,6 +71,18 @@
* @return
*/
AjaxResult paymentChargingOrder(AddChargingOrder addChargingOrder);
+
+ ChargingSimulationVO getCurrentSimulationOrder(Integer gunId);
+
+ ChargingSimulationVO completeSimulation(ChargingSimulationReq request);
+
+ IcbcPaymentResp queryIcbcAdminPayment(String orderCode);
+
+ IcbcRefundResp refundIcbcAdminPayment(IcbcAdminFundOperationReq request);
+
+ List<IcbcAdminSubOrderVO> queryIcbcAdminSubOrders(String orderCode);
+
+ IcbcSubOrderRefundResp refundIcbcAdminSubOrder(IcbcAdminFundOperationReq request);
/**
@@ -157,13 +174,13 @@
List<ChargingOrderGroup> chargingOrderGroup(ChargingPercentProvinceDto chargingPercentProvinceDto);
- List<SixChargingDto> charge(LocalDate sixBefore, List<Integer> siteIds);
+ List<SixChargingDto> charge(LocalDate sixBefore, List<Integer> siteIds, Boolean partnerView);
- List<SixCircleDto> circle(List<Integer> siteIds,LocalDate sixBefore);
+ List<SixCircleDto> circle(List<Integer> siteIds,LocalDate sixBefore, Boolean partnerView);
- Map<String, Object> countAll(List<Integer> siteIds);
+ Map<String, Object> countAll(List<Integer> siteIds, Boolean partnerView);
- List<SixIncomeCircleDto> payTypeCircle(LocalDate sixBefore, List<Integer> siteIds);
+ List<SixIncomeCircleDto> payTypeCircle(LocalDate sixBefore, List<Integer> siteIds, Boolean partnerView);
List<Map<String, Object>> getSumByType(LocalDate start,LocalDate end,List<Integer> siteIds);
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingSimulationSupport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingSimulationSupport.java
new file mode 100644
index 0000000..3b54db4
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/ChargingSimulationSupport.java
@@ -0,0 +1,127 @@
+package com.ruoyi.order.service.impl;
+
+import com.ruoyi.order.api.model.AccountingStrategyDetailOrder;
+import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
+
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+final class ChargingSimulationSupport {
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SS");
+
+ private ChargingSimulationSupport() {
+ }
+
+ static PreparedSimulation prepare(String orderCode,
+ List<AccountingStrategyDetailOrder> tariffs,
+ BigDecimal electricityFee,
+ BigDecimal serviceFee,
+ LocalDateTime endTime) {
+ validate(orderCode, tariffs, electricityFee, serviceFee);
+ if (tariffs.size() > 12) {
+ throw new IllegalArgumentException("模拟计费最多支持12个计费时段");
+ }
+ int index = findTariffIndex(tariffs, endTime.toLocalTime());
+ AccountingStrategyDetailOrder tariff = tariffs.get(index);
+ tariff.setElectrovalence(electricityFee);
+ tariff.setServiceCharge(serviceFee);
+ tariff.setCostServiceCharge(serviceFee);
+
+ TransactionRecordMessageVO record = new TransactionRecordMessageVO();
+ record.setTransaction_serial_number(orderCode);
+ record.setStart_time(endTime.minusSeconds(1).format(DATE_TIME_FORMATTER));
+ record.setEnd_time(endTime.format(DATE_TIME_FORMATTER));
+ record.setTrade_date(endTime.format(DATE_TIME_FORMATTER));
+ record.setTotal_electricity(BigDecimal.ONE);
+ record.setLoss_total_electricity(BigDecimal.ONE);
+ record.setPayment_amount(electricityFee.add(serviceFee));
+ record.setMethod(1);
+ record.setStop_reason(1);
+ setPeriodCharge(record, tariff.getType(), index + 1);
+ return new PreparedSimulation(tariff, record);
+ }
+
+ private static void validate(String orderCode,
+ List<AccountingStrategyDetailOrder> tariffs,
+ BigDecimal electricityFee,
+ BigDecimal serviceFee) {
+ if (orderCode == null || orderCode.trim().isEmpty()) {
+ throw new IllegalArgumentException("模拟订单号不能为空");
+ }
+ if (tariffs == null || tariffs.isEmpty()) {
+ throw new IllegalArgumentException("订单计费策略为空");
+ }
+ if (electricityFee == null || electricityFee.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("电费不能小于0");
+ }
+ if (serviceFee == null || serviceFee.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("服务费不能小于0");
+ }
+ if (electricityFee.add(serviceFee).compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("模拟消费金额必须大于0");
+ }
+ }
+
+ private static int findTariffIndex(List<AccountingStrategyDetailOrder> tariffs, LocalTime time) {
+ for (int i = 0; i < tariffs.size(); i++) {
+ AccountingStrategyDetailOrder tariff = tariffs.get(i);
+ LocalTime start = parseTime(tariff.getStartTime(), false);
+ LocalTime end = parseTime(tariff.getEndTime(), true);
+ if (!time.isBefore(start) && !time.isAfter(end)) {
+ return i;
+ }
+ }
+ throw new IllegalArgumentException("当前时间未匹配到订单计费时段");
+ }
+
+ private static LocalTime parseTime(String value, boolean end) {
+ if ("24:00".equals(value)) {
+ return LocalTime.MAX;
+ }
+ LocalTime time = LocalTime.parse(value, DateTimeFormatter.ofPattern("HH:mm"));
+ return end ? time.withSecond(59).withNano(999999999) : time;
+ }
+
+ private static void setPeriodCharge(TransactionRecordMessageVO record, Integer type, int index) {
+ String prefix;
+ if (Integer.valueOf(1).equals(type)) {
+ prefix = "Spike";
+ } else if (Integer.valueOf(2).equals(type)) {
+ prefix = "Peak";
+ } else if (Integer.valueOf(3).equals(type)) {
+ prefix = "Flat";
+ } else if (Integer.valueOf(4).equals(type)) {
+ prefix = "Valley";
+ } else {
+ throw new IllegalArgumentException("不支持的计费时段类型:" + type);
+ }
+ try {
+ Method setter = TransactionRecordMessageVO.class.getMethod("set" + prefix + "_charge" + index, BigDecimal.class);
+ setter.invoke(record, BigDecimal.ONE);
+ } catch (Exception e) {
+ throw new IllegalStateException("构造模拟计费时段失败", e);
+ }
+ }
+
+ static final class PreparedSimulation {
+ private final AccountingStrategyDetailOrder tariff;
+ private final TransactionRecordMessageVO record;
+
+ PreparedSimulation(AccountingStrategyDetailOrder tariff, TransactionRecordMessageVO record) {
+ this.tariff = tariff;
+ this.record = record;
+ }
+
+ AccountingStrategyDetailOrder getTariff() {
+ return tariff;
+ }
+
+ TransactionRecordMessageVO getRecord() {
+ return record;
+ }
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java
new file mode 100644
index 0000000..c6d8f5b
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupport.java
@@ -0,0 +1,148 @@
+package com.ruoyi.order.service.impl;
+
+import com.ruoyi.order.api.model.TChargingOrder;
+import com.ruoyi.order.api.model.TChargingOrderShareRecord;
+import com.ruoyi.payment.api.vo.IcbcRefundReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
+
+import java.math.BigDecimal;
+
+final class IcbcAdminFundOperationSupport {
+ private IcbcAdminFundOperationSupport() {
+ }
+
+ static void validateOrder(TChargingOrder order) {
+ if (order == null) {
+ throw new IllegalArgumentException("充电订单不存在");
+ }
+ if (!"ICBC".equals(order.getSourceName())) {
+ throw new IllegalArgumentException("仅支持工商银行支付订单");
+ }
+ if (!Integer.valueOf(2).equals(order.getRechargePaymentStatus())) {
+ throw new IllegalArgumentException("订单尚未支付成功");
+ }
+ }
+
+ static BigDecimal requireAmount(BigDecimal amount) {
+ if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("操作金额必须大于0");
+ }
+ if (amount.stripTrailingZeros().scale() > 2) {
+ throw new IllegalArgumentException("操作金额最多保留两位小数");
+ }
+ BigDecimal normalized = amount.setScale(2, BigDecimal.ROUND_HALF_UP);
+ if (normalized.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("操作金额必须大于0");
+ }
+ return normalized;
+ }
+
+ static String buildPendingRefundValue(String operationToken, BigDecimal amount) {
+ if (isBlank(operationToken)) {
+ throw new IllegalArgumentException("退款流水号不能为空");
+ }
+ return operationToken + "|" + requireAmount(amount).toPlainString();
+ }
+
+ static String resolveRefundNo(String pendingValue, BigDecimal amount, String newOperationToken) {
+ BigDecimal normalized = requireAmount(amount);
+ if (isBlank(pendingValue)) {
+ return newOperationToken;
+ }
+ int separator = pendingValue.lastIndexOf('|');
+ if (separator <= 0 || separator == pendingValue.length() - 1) {
+ throw new IllegalArgumentException("退款幂等记录格式错误");
+ }
+ BigDecimal pendingAmount;
+ try {
+ pendingAmount = new BigDecimal(pendingValue.substring(separator + 1));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("退款幂等记录格式错误");
+ }
+ if (pendingAmount.compareTo(normalized) != 0) {
+ throw new IllegalArgumentException("上一笔退款结果未知,请按原金额重试");
+ }
+ return pendingValue.substring(0, separator);
+ }
+
+ static boolean shouldClearPendingRefund(boolean reusedPending, String status, boolean definitiveResponse) {
+ return "SUCCESS".equals(status) || (!reusedPending && definitiveResponse);
+ }
+
+ static IcbcRefundReq buildRefundRequest(TChargingOrder order, BigDecimal amount, String outRefundNo) {
+ validateOrder(order);
+ IcbcRefundReq request = new IcbcRefundReq();
+ request.setOutTradeNo(order.getCode());
+ request.setOrderId(order.getRechargeSerialNumber());
+ request.setOutRefundNo(outRefundNo);
+ request.setRefundAmount(requireAmount(amount));
+ request.setTotalAmount(order.getThirdPayAmount());
+ request.setReason("管理后台工行退款");
+ return request;
+ }
+
+ static IcbcSubOrderQueryReq buildSubOrderQueryRequest(TChargingOrder order, TChargingOrderShareRecord shareRecord) {
+ validateOrder(order);
+ validateShareRecord(order, shareRecord);
+ IcbcSubOrderQueryReq request = new IcbcSubOrderQueryReq();
+ request.setOrderNoType("2");
+ request.setMerOrderId(order.getCode());
+ request.setPrtiId(shareRecord.getOriMerId());
+ request.setQueryType("1");
+ request.setMerTrnscSerno(shareRecord.getSubOrderId());
+ request.setSubPrtiId(shareRecord.getSubMerId());
+ return request;
+ }
+
+ static IcbcSubOrderRefundReq buildSubOrderRefundRequest(TChargingOrder order,
+ TChargingOrderShareRecord shareRecord,
+ BigDecimal amount,
+ String retSubOrderId,
+ String outtrxSerialNo) {
+ validateOrder(order);
+ validateShareRecord(order, shareRecord);
+ if (Boolean.TRUE.equals(shareRecord.getDelFlag())) {
+ throw new IllegalArgumentException("分账记录已删除");
+ }
+ if (!"SUCCESS".equals(shareRecord.getOnlineShareStatus())) {
+ throw new IllegalArgumentException("仅支持已成功的工行分账记录退款");
+ }
+ if (isBlank(shareRecord.getSeqNo()) || isBlank(shareRecord.getBusiType())
+ || isBlank(shareRecord.getOriMerId()) || shareRecord.getOriTrxDate() == null
+ || isBlank(shareRecord.getSubMerId()) || isBlank(shareRecord.getSubMerPrtclNo())) {
+ throw new IllegalArgumentException("分账记录缺少工行退款参数");
+ }
+ IcbcSubOrderRefundReq request = new IcbcSubOrderRefundReq();
+ request.setChargingOrderCode(order.getCode());
+ request.setSeqNo(shareRecord.getSeqNo());
+ request.setBusiType(shareRecord.getBusiType());
+ request.setOriMerId(shareRecord.getOriMerId());
+ request.setOriTrxDate(shareRecord.getOriTrxDate() == null ? null : shareRecord.getOriTrxDate().toString());
+ request.setSubOrderId(shareRecord.getSubOrderId());
+ request.setRetSubOrderId(retSubOrderId);
+ request.setOuttrxSerialNo(outtrxSerialNo);
+ request.setSubMerId(shareRecord.getSubMerId());
+ request.setSubMerPrtclNo(shareRecord.getSubMerPrtclNo());
+ request.setRefundAmount(requireAmount(amount));
+ request.setMerAcct(shareRecord.getMerAcct());
+ request.setMerSpInfor("管理后台工行分账退款");
+ return request;
+ }
+
+ private static void validateShareRecord(TChargingOrder order, TChargingOrderShareRecord shareRecord) {
+ if (shareRecord == null || !order.getId().equals(shareRecord.getChargingOrderId())) {
+ throw new IllegalArgumentException("分账记录不属于当前订单");
+ }
+ if (!"ICBC".equals(shareRecord.getPayChannel())) {
+ throw new IllegalArgumentException("仅支持工商银行分账记录");
+ }
+ if (shareRecord.getSubOrderId() == null || shareRecord.getSubOrderId().trim().isEmpty()) {
+ throw new IllegalArgumentException("分账记录缺少工行子订单号");
+ }
+ }
+
+ private static boolean isBlank(String value) {
+ return value == null || value.trim().isEmpty();
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java
index e5b8809..29e25b1 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java
@@ -10,9 +10,11 @@
import com.ruoyi.chargingPile.api.feignClient.ChargingPileClient;
import com.ruoyi.chargingPile.api.feignClient.PartnerClient;
import com.ruoyi.chargingPile.api.feignClient.SiteClient;
+import com.ruoyi.chargingPile.api.model.Partner;
import com.ruoyi.chargingPile.api.model.Site;
import com.ruoyi.chargingPile.api.model.TChargingGun;
import com.ruoyi.chargingPile.api.model.TChargingPile;
+import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.security.service.TokenService;
@@ -31,10 +33,13 @@
import javax.annotation.Resource;
import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -151,9 +156,12 @@
BigDecimal serviceChargeTotal = new BigDecimal("0");
BigDecimal commissionAmountTotal = new BigDecimal("0");
BigDecimal sharingAmountTotal = new BigDecimal("0");
+ BigDecimal platformIncomeTotal = new BigDecimal("0");
+ BigDecimal partnerIncomeTotal = new BigDecimal("0");
BigDecimal chargingCapacityTotal = new BigDecimal("0");
BigDecimal discountTotal = new BigDecimal("0");
List<TChargingOrderAccountingStrategy> list3 = chargingOrderAccountingStrategyService.list();
+ Map<Integer, BigDecimal> siteServiceShareRatioMap = loadSiteServiceShareRatioMap();
for (ChargingBillListVO chargingBillListVO : list) {
@@ -209,6 +217,8 @@
tChargingOrders = total.stream().filter(e -> e.getPayTime().isBefore(lastDayOfMonth.withHour(23).withMinute(59).withSecond(59))
&& e.getPayTime().isAfter(firstDayOfMonth.withHour(0).withMinute(0).withSecond(0))).collect(Collectors.toList());
}
+ tChargingOrders = filterAccountSettlementOrders(tChargingOrders, chargingBillListVO.getPayType());
+ chargingBillListVO.setPayTypeName(resolveAccountPayTypeName(chargingBillListVO.getPayType(), tChargingOrders));
@@ -235,16 +245,6 @@
if (data1!=null && data2!=null){
tChargingOrder.setTerminalName(data1.getName()+data2.getName());
tChargingOrder.setTerminalCode(data2.getCode());
- }
- if (tChargingOrder.getRechargePaymentType()!=null && tChargingOrder.getRechargePaymentType()==1){
- chargingBillListVO.setPayTypeName("微信商户");
- }
- if (tChargingOrder.getRechargePaymentType()!=null && tChargingOrder.getRechargePaymentType()==2){
- if (StringUtils.hasLength(chargingBillListVO.getPayTypeName())){
- chargingBillListVO.setPayTypeName("微信商户+支付宝商户");
- }else{
- chargingBillListVO.setPayTypeName("支付宝商户");
- }
}
tChargingOrder.setOrderClassification1("线上订单");
tChargingOrder.setRechargePaymentType1("预付");
@@ -393,6 +393,10 @@
chargingBillListVO.setChargingCapacity(chargingCapacity == null?BigDecimal.ZERO:chargingCapacity);
chargingBillListVO.setBillMoney(paymentAmount
.subtract(commissionAmount.setScale(2,BigDecimal.ROUND_DOWN)).subtract(sharingAmount).setScale(2, BigDecimal.ROUND_DOWN));
+ Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(chargingBillListVO.getType(), chargingBillListVO.getSiteId(),
+ chargingBillListVO.getBillWeek(), tChargingOrders, list3, siteServiceShareRatioMap);
+ chargingBillListVO.setPlatformIncome(incomeMap.get("platformIncome"));
+ chargingBillListVO.setPartnerIncome(incomeMap.get("partnerIncome"));
switch (chargingBillListVO.getType()){
case 1:
chargingBillListVO.setSiteName("全站");
@@ -447,6 +451,7 @@
tChargingOrders = total.stream().filter(e -> e.getPayTime().isBefore(lastDayOfMonth.withHour(23).withMinute(59).withSecond(59))
&& e.getPayTime().isAfter(firstDayOfMonth.withHour(0).withMinute(0).withSecond(0))).collect(Collectors.toList());
}
+ tChargingOrders = filterAccountSettlementOrders(tChargingOrders, chargingBillListVO.getPayType());
for (TChargingOrder tChargingOrder : tChargingOrders) {
List<TChargingOrderAccountingStrategy> list4 = list3.stream().filter(e -> e.getChargingOrderId().equals(tChargingOrder.getId())).collect(Collectors.toList());
BigDecimal electronic_reduce = list4.stream().map(TChargingOrderAccountingStrategy::getPeriodElectricPrice).reduce(BigDecimal.ZERO, BigDecimal::add);
@@ -498,6 +503,10 @@
discountTotal = discountTotal.add(tChargingOrder.getVipDiscountAmount());
}
}
+ Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(chargingBillListVO.getType(), chargingBillListVO.getSiteId(),
+ chargingBillListVO.getBillWeek(), tChargingOrders, list3, siteServiceShareRatioMap);
+ platformIncomeTotal = platformIncomeTotal.add(incomeMap.get("platformIncome"));
+ partnerIncomeTotal = partnerIncomeTotal.add(incomeMap.get("partnerIncome"));
tChargingOrders1.addAll(tChargingOrders);
chargingBillVO.setExportList(tChargingOrders);
}
@@ -509,6 +518,8 @@
chargingBillVO.setServiceCharge(serviceChargeTotal);
chargingBillVO.setCommissionAmount(commissionAmountTotal);
chargingBillVO.setSharingAmount(sharingAmountTotal);
+ chargingBillVO.setPlatformIncome(platformIncomeTotal.setScale(2, RoundingMode.HALF_UP));
+ chargingBillVO.setPartnerIncome(partnerIncomeTotal.setScale(2, RoundingMode.HALF_UP));
chargingBillVO.setDiscount(discountTotal);
chargingBillVO.setRefundAmount(refundAmountTotal);
List<TChargingOrder> collect = tChargingOrders1.stream().distinct().collect(Collectors.toList());
@@ -547,6 +558,7 @@
List<TAppUser> userList = appUserClient.getAllUser().getData();
List<TAppUserCar> carList = appUserCarClient.getAllCar().getData();
List<TChargingOrderAccountingStrategy> list3 = chargingOrderAccountingStrategyService.list();
+ Map<Integer, BigDecimal> siteServiceShareRatioMap = loadSiteServiceShareRatioMap();
Long userId = tokenService.getLoginUser().getUserid();
// 查询当前登陆人按钮权限
@@ -598,6 +610,8 @@
BigDecimal serviceChargeTotal = new BigDecimal("0");
BigDecimal commissionAmountTotal = new BigDecimal("0");
BigDecimal sharingAmountTotal = new BigDecimal("0");
+ BigDecimal platformIncomeTotal = new BigDecimal("0");
+ BigDecimal partnerIncomeTotal = new BigDecimal("0");
BigDecimal chargingCapacityTotal = new BigDecimal("0");
BigDecimal discountTotal = new BigDecimal("0");
BigDecimal refundAmountTotal = new BigDecimal("0");
@@ -858,6 +872,10 @@
chargingBillListVO.setOrderCount(tChargingOrders.size());
// 充电时间秒
chargingBillListVO.setChargingSecond(chargingSecond);
+ Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(chargingBillListVO.getType(), chargingBillListVO.getSiteId(),
+ chargingBillListVO.getBillWeek(), tChargingOrders, list3, siteServiceShareRatioMap);
+ chargingBillListVO.setPlatformIncome(incomeMap.get("platformIncome"));
+ chargingBillListVO.setPartnerIncome(incomeMap.get("partnerIncome"));
}
for (ChargingBillListVO chargingBillListVO : list1) {
if (dto.getType() == 1){
@@ -965,6 +983,10 @@
discountTotal = discountTotal.add(tChargingOrder.getVipDiscountAmount());
}
}
+ Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(chargingBillListVO.getType(), chargingBillListVO.getSiteId(),
+ chargingBillListVO.getBillWeek(), tChargingOrders, list3, siteServiceShareRatioMap);
+ platformIncomeTotal = platformIncomeTotal.add(incomeMap.get("platformIncome"));
+ partnerIncomeTotal = partnerIncomeTotal.add(incomeMap.get("partnerIncome"));
}
chargingBillVO.setChargingCapacity(chargingCapacityTotal);
chargingBillVO.setPaymentAmount(paymentAmountTotal.subtract(commissionAmountTotal).subtract(sharingAmountTotal).setScale(2, BigDecimal.ROUND_DOWN));
@@ -974,6 +996,8 @@
chargingBillVO.setOrderCount(orderCount);
chargingBillVO.setCommissionAmount(commissionAmountTotal.setScale(2, BigDecimal.ROUND_DOWN));
chargingBillVO.setSharingAmount(sharingAmountTotal.setScale(2, BigDecimal.ROUND_DOWN));
+ chargingBillVO.setPlatformIncome(platformIncomeTotal.setScale(2, RoundingMode.HALF_UP));
+ chargingBillVO.setPartnerIncome(partnerIncomeTotal.setScale(2, RoundingMode.HALF_UP));
chargingBillVO.setDiscount(discountTotal.setScale(2, BigDecimal.ROUND_DOWN));
pageInfo.setRecords(chargingBillListVOS);
chargingBillVO.setList(pageInfo);
@@ -1033,6 +1057,10 @@
List<Map<String, Object>> list = chargingSettlementBillService.queryList(pageInfo, dto.getType(), dto.getState(), dto.getDateRange(), statisticalDateType, dto.getSiteIds());
ChargingBillWapper chargingBillVO = new ChargingBillWapper();
List<Map<String, Object>> pageList = new ArrayList<>();
+ List<TChargingOrder> total = chargingOrderList.selectList(new LambdaQueryWrapper<>(TChargingOrder.class)
+ .isNotNull(TChargingOrder::getPayTime));
+ List<TChargingOrderAccountingStrategy> strategyList = chargingOrderAccountingStrategyService.list();
+ Map<Integer, BigDecimal> siteServiceShareRatioMap = loadSiteServiceShareRatioMap();
if(null != list){
for (Map<String, Object> map : list) {
Integer site_id = Integer.valueOf(map.get("site_id").toString());
@@ -1049,6 +1077,7 @@
String chargingCapacity = map.get("total_electricity").toString();
String electrovalence = map.get("total_electricity_charge").toString();
String serviceCharge = map.get("total_service_fee").toString();
+ Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(dto.getType(), site_id, billingCycle, total, strategyList, siteServiceShareRatioMap);
Map<String, Object> data1 = new HashMap<>();
data1.put("code", code);
data1.put("orderState", 2);
@@ -1062,6 +1091,8 @@
data1.put("commissionAmount", commissionAmount);
data1.put("sharingAmount", sharingAmount);
data1.put("paymentAmount", paymentAmount);
+ data1.put("platformIncome", incomeMap.get("platformIncome"));
+ data1.put("partnerIncome", incomeMap.get("partnerIncome"));
data1.put("chargingCapacity", chargingCapacity);
data1.put("electrovalence", electrovalence);
data1.put("serviceCharge", serviceCharge);
@@ -1085,6 +1116,19 @@
BigDecimal commissionAmount = (null == map || null == map.get("platformFee")) ? BigDecimal.ZERO : new BigDecimal(map.get("platformFee").toString());
BigDecimal sharingAmount = (null == map || null == map.get("tripartiteCommission")) ? BigDecimal.ZERO : new BigDecimal(map.get("tripartiteCommission").toString());
BigDecimal discount = (null == map || null == map.get("couponDeduction")) ? BigDecimal.ZERO : new BigDecimal(map.get("couponDeduction").toString());
+ BigDecimal platformIncome = BigDecimal.ZERO;
+ BigDecimal partnerIncome = BigDecimal.ZERO;
+ PageInfo<Map<String, Object>> sumPageInfo = new PageInfo<>(1, 100000);
+ List<Map<String, Object>> sumList = chargingSettlementBillService.queryList(sumPageInfo, dto.getType(), dto.getState(), dto.getDateRange(), statisticalDateType, dto.getSiteIds());
+ if (sumList != null) {
+ for (Map<String, Object> row : sumList) {
+ Integer siteId = Integer.valueOf(row.get("site_id").toString());
+ String billingCycle = row.get("billingCycle").toString();
+ Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(dto.getType(), siteId, billingCycle, total, strategyList, siteServiceShareRatioMap);
+ platformIncome = platformIncome.add(incomeMap.get("platformIncome"));
+ partnerIncome = partnerIncome.add(incomeMap.get("partnerIncome"));
+ }
+ }
chargingBillVO.setChargingCapacity(chargingCapacity);
chargingBillVO.setPaymentAmount(paymentAmount.setScale(2, BigDecimal.ROUND_DOWN));
chargingBillVO.setOrderAmount(orderAmount.setScale(2, BigDecimal.ROUND_DOWN));
@@ -1093,6 +1137,8 @@
chargingBillVO.setOrderCount(orderCount);
chargingBillVO.setCommissionAmount(commissionAmount.setScale(2, BigDecimal.ROUND_DOWN));
chargingBillVO.setSharingAmount(sharingAmount.setScale(2, BigDecimal.ROUND_DOWN));
+ chargingBillVO.setPlatformIncome(platformIncome.setScale(2, RoundingMode.HALF_UP));
+ chargingBillVO.setPartnerIncome(partnerIncome.setScale(2, RoundingMode.HALF_UP));
chargingBillVO.setDiscount(discount.setScale(2, BigDecimal.ROUND_DOWN));
pageInfo.setRecords(pageList);
chargingBillVO.setList(pageInfo);
@@ -1125,6 +1171,7 @@
List<TAppUser> userList = appUserClient.getAllUser().getData();
List<TAppUserCar> carList = appUserCarClient.getAllCar().getData();
List<TChargingOrderAccountingStrategy> list3 = chargingOrderAccountingStrategyService.list();
+ Map<Integer, BigDecimal> siteServiceShareRatioMap = loadSiteServiceShareRatioMap();
Long userId = tokenService.getLoginUser().getUserid();
// 查询当前登陆人按钮权限
@@ -1176,6 +1223,8 @@
BigDecimal serviceChargeTotal = new BigDecimal("0");
BigDecimal commissionAmountTotal = new BigDecimal("0");
BigDecimal sharingAmountTotal = new BigDecimal("0");
+ BigDecimal platformIncomeTotal = new BigDecimal("0");
+ BigDecimal partnerIncomeTotal = new BigDecimal("0");
BigDecimal chargingCapacityTotal = new BigDecimal("0");
BigDecimal discountTotal = new BigDecimal("0");
BigDecimal refundAmountTotal = new BigDecimal("0");
@@ -1436,6 +1485,10 @@
chargingBillListVO.setOrderCount(tChargingOrders.size());
// 充电时间秒
chargingBillListVO.setChargingSecond(chargingSecond);
+ Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(chargingBillListVO.getType(), chargingBillListVO.getSiteId(),
+ chargingBillListVO.getBillWeek(), tChargingOrders, list3, siteServiceShareRatioMap);
+ chargingBillListVO.setPlatformIncome(incomeMap.get("platformIncome"));
+ chargingBillListVO.setPartnerIncome(incomeMap.get("partnerIncome"));
}
for (ChargingBillListVO chargingBillListVO : list1) {
if (dto.getType() == 1){
@@ -1543,6 +1596,10 @@
discountTotal = discountTotal.add(tChargingOrder.getVipDiscountAmount());
}
}
+ Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(chargingBillListVO.getType(), chargingBillListVO.getSiteId(),
+ chargingBillListVO.getBillWeek(), tChargingOrders, list3, siteServiceShareRatioMap);
+ platformIncomeTotal = platformIncomeTotal.add(incomeMap.get("platformIncome"));
+ partnerIncomeTotal = partnerIncomeTotal.add(incomeMap.get("partnerIncome"));
}
chargingBillVO.setChargingCapacity(chargingCapacityTotal);
chargingBillVO.setPaymentAmount(paymentAmountTotal.subtract(commissionAmountTotal).subtract(sharingAmountTotal).setScale(2, BigDecimal.ROUND_DOWN));
@@ -1552,12 +1609,160 @@
chargingBillVO.setOrderCount(orderCount);
chargingBillVO.setCommissionAmount(commissionAmountTotal.setScale(2, BigDecimal.ROUND_DOWN));
chargingBillVO.setSharingAmount(sharingAmountTotal.setScale(2, BigDecimal.ROUND_DOWN));
+ chargingBillVO.setPlatformIncome(platformIncomeTotal.setScale(2, RoundingMode.HALF_UP));
+ chargingBillVO.setPartnerIncome(partnerIncomeTotal.setScale(2, RoundingMode.HALF_UP));
chargingBillVO.setDiscount(discountTotal.setScale(2, BigDecimal.ROUND_DOWN));
pageInfo.setRecords(chargingBillListVOS);
chargingBillVO.setList(pageInfo);
return chargingBillVO;
}
+ private Map<String, BigDecimal> calculateChargingBillIncome(Integer billType, Integer billSiteId, String billingCycle,
+ List<TChargingOrder> allOrders,
+ List<TChargingOrderAccountingStrategy> allStrategies,
+ Map<Integer, BigDecimal> siteServiceShareRatioMap) {
+ Map<String, BigDecimal> incomeMap = new HashMap<>();
+ BigDecimal platformIncome = BigDecimal.ZERO;
+ BigDecimal partnerIncome = BigDecimal.ZERO;
+ if (!StringUtils.hasLength(billingCycle) || allOrders == null || allOrders.isEmpty()) {
+ incomeMap.put("platformIncome", platformIncome);
+ incomeMap.put("partnerIncome", partnerIncome);
+ return incomeMap;
+ }
+ LocalDate billMonth = LocalDate.parse(billingCycle + "-01");
+ LocalDateTime firstDayOfMonth = billMonth.withDayOfMonth(1).atStartOfDay();
+ LocalDateTime lastDayOfMonth = billMonth.with(TemporalAdjusters.lastDayOfMonth()).atTime(23, 59, 59);
+ List<TChargingOrderAccountingStrategy> strategies = allStrategies == null ? Collections.emptyList() : allStrategies;
+ for (TChargingOrder order : allOrders) {
+ if (order == null || order.getPayTime() == null) {
+ continue;
+ }
+ if (billType != null && billType == 2 && !billSiteId.equals(order.getSiteId())) {
+ continue;
+ }
+ if (order.getPayTime().isBefore(firstDayOfMonth) || order.getPayTime().isAfter(lastDayOfMonth)) {
+ continue;
+ }
+ List<TChargingOrderAccountingStrategy> orderStrategies = strategies.stream()
+ .filter(strategy -> strategy.getChargingOrderId().equals(order.getId()))
+ .collect(Collectors.toList());
+ BigDecimal electronicReduce = orderStrategies.stream()
+ .map(TChargingOrderAccountingStrategy::getPeriodElectricPrice)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ BigDecimal serviceReduce = orderStrategies.stream()
+ .map(TChargingOrderAccountingStrategy::getPeriodServicePrice)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ BigDecimal paymentAmount;
+ if (Integer.valueOf(2).equals(order.getOrderSource())) {
+ paymentAmount = electronicReduce.add(serviceReduce.multiply(new BigDecimal("0.8")).setScale(2, RoundingMode.DOWN));
+ } else {
+ paymentAmount = electronicReduce.add(serviceReduce);
+ if (Integer.valueOf(1).equals(order.getOrderSource())) {
+ BigDecimal serviceShareRatio = siteServiceShareRatioMap.get(order.getSiteId());
+ if (serviceShareRatio != null && serviceShareRatio.compareTo(BigDecimal.ZERO) > 0) {
+ partnerIncome = partnerIncome.add(serviceReduce.multiply(serviceShareRatio).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP));
+ }
+ }
+ }
+ platformIncome = platformIncome.add(paymentAmount);
+ }
+ platformIncome = platformIncome.subtract(partnerIncome).max(BigDecimal.ZERO);
+ incomeMap.put("platformIncome", platformIncome.setScale(2, RoundingMode.HALF_UP));
+ incomeMap.put("partnerIncome", partnerIncome.setScale(2, RoundingMode.HALF_UP));
+ return incomeMap;
+ }
+ private List<TChargingOrder> filterAccountSettlementOrders(List<TChargingOrder> orders, Integer payType) {
+ if (orders == null || orders.isEmpty() || payType == null || payType == 0) {
+ return orders == null ? Collections.emptyList() : orders;
+ }
+ return orders.stream()
+ .filter(order -> accountSettlementOrderMatches(order, payType))
+ .collect(Collectors.toList());
+ }
+
+ private boolean accountSettlementOrderMatches(TChargingOrder order, Integer payType) {
+ if (order == null || payType == null) {
+ return true;
+ }
+ if (payType == 1) {
+ return !isIcbcOrder(order) && Integer.valueOf(1).equals(order.getRechargePaymentType());
+ }
+ if (payType == 2) {
+ return !isIcbcOrder(order) && (Integer.valueOf(2).equals(order.getRechargePaymentType()) || Integer.valueOf(3).equals(order.getRechargePaymentType()));
+ }
+ if (payType == 3) {
+ return isIcbcOrder(order) && Integer.valueOf(1).equals(order.getOrderSource());
+ }
+ return true;
+ }
+
+ private String resolveAccountPayTypeName(Integer payType, List<TChargingOrder> orders) {
+ if (payType != null && payType != 0) {
+ switch (payType) {
+ case 1:
+ return "微信商户";
+ case 2:
+ return "支付宝商户";
+ case 3:
+ return "工商银行商户";
+ default:
+ break;
+ }
+ }
+ boolean hasWx = false;
+ boolean hasAli = false;
+ boolean hasIcbc = false;
+ if (orders != null) {
+ for (TChargingOrder order : orders) {
+ if (isIcbcOrder(order)) {
+ hasIcbc = true;
+ } else if (Integer.valueOf(1).equals(order.getRechargePaymentType())) {
+ hasWx = true;
+ } else if (Integer.valueOf(2).equals(order.getRechargePaymentType()) || Integer.valueOf(3).equals(order.getRechargePaymentType())) {
+ hasAli = true;
+ }
+ }
+ }
+ List<String> names = new ArrayList<>();
+ if (hasWx) {
+ names.add("微信商户");
+ }
+ if (hasAli) {
+ names.add("支付宝商户");
+ }
+ if (hasIcbc) {
+ names.add("工商银行商户");
+ }
+ return names.isEmpty() ? "" : String.join("+", names);
+ }
+
+ private boolean isIcbcOrder(TChargingOrder order) {
+ return order != null && "ICBC".equals(order.getSourceName());
+ }
+
+ private Map<Integer, BigDecimal> loadSiteServiceShareRatioMap() {
+ Map<Integer, BigDecimal> ratioMap = new HashMap<>();
+ R<List<Site>> siteResult = siteClient.getSiteAll();
+ R<List<Partner>> partnerResult = siteClient.getAllPartner();
+ List<Site> sites = siteResult == null ? Collections.emptyList() : siteResult.getData();
+ List<Partner> partners = partnerResult == null ? Collections.emptyList() : partnerResult.getData();
+ if (sites == null || sites.isEmpty() || partners == null || partners.isEmpty()) {
+ return ratioMap;
+ }
+ Map<Integer, Partner> partnerMap = partners.stream()
+ .filter(partner -> partner != null && partner.getId() != null)
+ .collect(Collectors.toMap(Partner::getId, partner -> partner, (left, right) -> left));
+ for (Site site : sites) {
+ if (site == null || site.getId() == null || site.getPartnerId() == null) {
+ continue;
+ }
+ Partner partner = partnerMap.get(site.getPartnerId());
+ if (partner != null && partner.getServiceShareRatio() != null) {
+ ratioMap.put(site.getId(), partner.getServiceShareRatio());
+ }
+ }
+ return ratioMap;
+ }
}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
index 593aff1..4d6455b 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -5,6 +5,7 @@
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.api.dto.SendMessageDTO;
@@ -51,6 +52,7 @@
import com.ruoyi.order.api.vo.PlatformStopChargingReplyVO;
import com.ruoyi.order.dto.*;
import com.ruoyi.order.mapper.TChargingOrderMapper;
+import com.ruoyi.order.mapper.TChargingOrderShareRecordMapper;
import com.ruoyi.order.mapper.TSettlementConfirmMapper;
import com.ruoyi.order.service.TChargingOrderAccountingStrategyService;
import com.ruoyi.order.service.TChargingOrderRefundService;
@@ -61,6 +63,8 @@
import com.ruoyi.order.util.mongodb.service.TransactionRecordService;
import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
import com.ruoyi.order.vo.EndOfChargePageInfo;
+import com.ruoyi.order.vo.ChargingSimulationVO;
+import com.ruoyi.order.vo.IcbcAdminSubOrderVO;
import com.ruoyi.order.vo.OrderCountByDate;
import com.ruoyi.order.vo.StatisticsOfBatteryVO;
import com.ruoyi.other.api.domain.*;
@@ -71,11 +75,13 @@
import com.ruoyi.other.api.vo.ExchangeCodeConsumeResultVO;
import com.ruoyi.payment.api.feignClient.AliPaymentClient;
import com.ruoyi.payment.api.feignClient.H5AliPaymentClient;
+import com.ruoyi.payment.api.feignClient.IcbcPaymentClient;
import com.ruoyi.payment.api.feignClient.WxPaymentClient;
import com.ruoyi.payment.api.model.*;
import com.ruoyi.payment.api.vo.*;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.feignClient.SysUserClient;
+import org.redisson.api.RBucket;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
@@ -150,6 +156,12 @@
private AliPaymentClient aliPaymentClient;
@Resource
private H5AliPaymentClient h5AliPaymentClient;
+ @Resource
+ private IcbcPaymentClient icbcPaymentClient;
+ @Resource
+ private TChargingOrderShareRecordMapper chargingOrderShareRecordMapper;
+ @Resource
+ private ChargingOrderSettlementExecutor chargingOrderSettlementExecutor;
@Resource
private AppCouponClient appCouponClient;
@@ -593,7 +605,38 @@
changeUserBalance(chargingOrder.getAppUserId(), balanceRefundAmount);
recordBalanceRefund(chargingOrder, balanceRefundAmount, refund.getRefundCode());
}
+ triggerChargingOrderShare(chargingOrder);
}
+
+ private boolean refundByIcbcAndFinish(TChargingOrder chargingOrder, TChargingOrderRefund refund, BigDecimal thirdRefundAmount,
+ String reason, boolean finishManual, BigDecimal refundAmount, BigDecimal balanceRefundAmount) {
+ IcbcRefundReq req = new IcbcRefundReq();
+ req.setOutTradeNo(chargingOrder.getCode());
+ req.setOrderId(chargingOrder.getRechargeSerialNumber());
+ req.setOutRefundNo(refund.getRefundCode());
+ req.setRefundAmount(thirdRefundAmount);
+ req.setTotalAmount(getRefundableThirdPayAmount(chargingOrder));
+ req.setReason(reason);
+ R<IcbcRefundResp> result = icbcPaymentClient.refund(req);
+ IcbcRefundResp resp = result == null ? null : result.getData();
+ if (result == null || result.getCode() != 200 || resp == null || !"SUCCESS".equals(resp.getStatus())) {
+ log.error("工行退款失败,订单号:{},退款单号:{},返回:{}", chargingOrder.getCode(), refund.getRefundCode(), result == null ? null : result.getMsg());
+ return false;
+ }
+ if (finishManual) {
+ refund.setRefundSerialNumber(firstNotBlank(resp.getRefundId(), resp.getOutRefundNo()));
+ refund.setRefundTime(LocalDateTime.now());
+ finishManualChargingRefund(chargingOrder, refund, refundAmount, balanceRefundAmount, thirdRefundAmount);
+ } else {
+ chargingOrderRefundService.save(refund);
+ chargingOrderStartupFailureWxRefund(refund.getRefundCode(), firstNotBlank(resp.getRefundId(), resp.getOutRefundNo()), "SUCCESS", resp.getRefundTime());
+ }
+ return true;
+ }
+
+ private boolean isIcbcOrder(TChargingOrder chargingOrder) {
+ return chargingOrder != null && "ICBC".equals(chargingOrder.getSourceName());
+ }
/**
* 判断订单是否为白名单订单
@@ -696,7 +739,13 @@
.eq(TChargingOrder::getRechargePaymentStatus, 1).eq(TChargingOrder::getDelFlag, 0));
for (TChargingOrder tChargingOrder : list) {
Integer rechargePaymentType = tChargingOrder.getRechargePaymentType();
- if (1 == rechargePaymentType) {
+ if ("ICBC".equals(tChargingOrder.getSourceName())) {
+ IcbcPaymentResp data = icbcPaymentClient.query(tChargingOrder.getCode()).getData();
+ if (null != data && isIcbcRemovableStatus(data.getStatus(), tChargingOrder)) {
+ this.removeById(tChargingOrder.getId());
+ }
+ }
+ if (!"ICBC".equals(tChargingOrder.getSourceName()) && 1 == rechargePaymentType) {
NotifyV3PayDecodeRespBody data = wxPaymentClient.queryOrderInfo(tChargingOrder.getCode()).getData();
if (null != data) {
String trade_state = data.getTrade_state();
@@ -706,7 +755,7 @@
}
}
}
- if (2 == rechargePaymentType) {
+ if (!"ICBC".equals(tChargingOrder.getSourceName()) && 2 == rechargePaymentType) {
AliQueryOrder data = aliPaymentClient.query(tChargingOrder.getCode()).getData();
if (null != data) {
//支付失败,删除无效的订单
@@ -716,7 +765,7 @@
}
}
}
- if (3 == rechargePaymentType) {
+ if (!"ICBC".equals(tChargingOrder.getSourceName()) && 3 == rechargePaymentType) {
AliQueryOrder data = h5AliPaymentClient.query(tChargingOrder.getCode()).getData();
if (null != data) {
//支付失败,删除无效的订单
@@ -739,7 +788,15 @@
.eq(TChargingOrder::getAppUserCarId, userId).eq(TChargingOrder::getRechargePaymentStatus, 1).eq(TChargingOrder::getDelFlag, 0));
if (null != one) {
//查询三方支付数据,支付中直接结束三方订单
- if (1 == one.getRechargePaymentType()) {
+ if ("ICBC".equals(one.getSourceName())) {
+ IcbcPaymentResp data = icbcPaymentClient.query(one.getCode()).getData();
+ if (null != data && isIcbcRemovableStatus(data.getStatus(), one)) {
+ this.removeById(one.getId());
+ } else {
+ return AjaxResult.error("存在支付中订单,请稍后再试");
+ }
+ }
+ if (!"ICBC".equals(one.getSourceName()) && 1 == one.getRechargePaymentType()) {
NotifyV3PayDecodeRespBody data = wxPaymentClient.queryOrderInfo(one.getCode()).getData();
if (null != data) {
String trade_state = data.getTrade_state();
@@ -753,7 +810,7 @@
}
}
}
- if (2 == one.getRechargePaymentType()) {
+ if (!"ICBC".equals(one.getSourceName()) && 2 == one.getRechargePaymentType()) {
AliQueryOrder data = aliPaymentClient.query(one.getCode()).getData();
if (null != data) {
String trade_state = data.getTradeStatus();
@@ -768,7 +825,7 @@
}
}
}
- if (3 == one.getRechargePaymentType()) {
+ if (!"ICBC".equals(one.getSourceName()) && 3 == one.getRechargePaymentType()) {
AliQueryOrder data = h5AliPaymentClient.query(one.getCode()).getData();
if (null != data) {
String trade_state = data.getTradeStatus();
@@ -820,6 +877,9 @@
chargingOrder.setWhitelistServiceRefundAmount(BigDecimal.ZERO);
chargingOrder.setVipDiscountAmount(BigDecimal.ZERO);
chargingOrder.setOrderSource(1);
+ if (thirdPayAmount.compareTo(BigDecimal.ZERO) > 0 && Integer.valueOf(1).equals(addChargingOrder.getPaymentType())) {
+ chargingOrder.setSourceName("ICBC");
+ }
chargingOrder.setTitle("【充电桩充电】" + chargingPile.getNumber() + "号桩/" + tChargingGun.getCode() + "号枪");
Site site = siteClient.getSiteByIds(Arrays.asList(tChargingGun.getSiteId())).getData().get(0);
@@ -884,17 +944,28 @@
}
//会员优惠折扣将其计入增加充电时长(增加总充电金额)
//如果充电总金额未使用完,则需要退回费用=(原金额/总金额)*(总金额-实际充电金额)
- if (1 == addChargingOrder.getPaymentType()) {
- PaymentOrder paymentOrder = new PaymentOrder();
- paymentOrder.setCode(chargingOrder.getCode());
- paymentOrder.setAmount(thirdPayAmount);
- paymentOrder.setOpenId(appUser.getWxOpenid());
- paymentOrder.setDescription("充电充值");
- paymentOrder.setNotifyUrl("/payment/wx/pay/notify");
- Map<String, Object> data = wxPaymentClient.orderPay(paymentOrder).getData();
- return AjaxResult.success(data);
+ if (Integer.valueOf(1).equals(addChargingOrder.getPaymentType())) {
+ IcbcPaymentReq req = new IcbcPaymentReq();
+ req.setOutTradeNo(chargingOrder.getCode());
+ req.setAmount(thirdPayAmount);
+ req.setSubject("充电充值");
+ req.setBody("充电充值");
+ req.setPaymentType(1);
+ req.setNotifyUrl("/payment/icbc/callBack");
+ req.setReturnUrl(addChargingOrder.getReturnUrl());
+ req.setOpenId(appUser.getWxOpenid());
+ req.setProfitSharing(buildIcbcProfitSharing(site));
+ IcbcPaymentResp data = icbcPaymentClient.payment(req).getData();
+ if (isIcbcPaymentCreated(data)) {
+ if (StringUtils.hasLength(data.getTradeNo())) {
+ chargingOrder.setRechargeSerialNumber(data.getTradeNo());
+ this.updateById(chargingOrder);
+ }
+ return AjaxResult.success(data);
+ }
+ throw new RuntimeException("调起工行支付失败");
}
- if (2 == addChargingOrder.getPaymentType()) {
+ if (Integer.valueOf(2).equals(addChargingOrder.getPaymentType())) {
AliPaymentReq req = new AliPaymentReq();
req.setOutTradeNo(chargingOrder.getCode());
req.setTotalAmount(thirdPayAmount.toString());
@@ -903,11 +974,12 @@
req.setBody("充电充值");
req.setNotifyUrl("/payment/ali/callBack");
AliPaymentResp data = aliPaymentClient.payment(req).getData();
- if (null != data) {
+ if (data != null) {
return AjaxResult.success(data);
}
+ throw new RuntimeException("调起支付宝支付失败");
}
- if (3 == addChargingOrder.getPaymentType()) {
+ if (Integer.valueOf(3).equals(addChargingOrder.getPaymentType())) {
AliPaymentReq req = new AliPaymentReq();
req.setOutTradeNo(chargingOrder.getCode());
req.setTotalAmount(thirdPayAmount.toString());
@@ -922,6 +994,321 @@
}
}
throw new RuntimeException("无效的支付方式");
+ }
+
+ @Override
+ public ChargingSimulationVO getCurrentSimulationOrder(Integer gunId) {
+ if (gunId == null) {
+ throw new ServiceException("充电枪不能为空");
+ }
+ Long userId = tokenService.getLoginUserApplet().getUserId();
+ return buildSimulationVO(findSimulationOrder(userId, gunId));
+ }
+
+ @Override
+ public ChargingSimulationVO completeSimulation(ChargingSimulationReq request) {
+ if (request == null || request.getGunId() == null) {
+ throw new ServiceException("充电枪不能为空");
+ }
+ Long userId = tokenService.getLoginUserApplet().getUserId();
+ TChargingOrder order = findSimulationOrder(userId, request.getGunId());
+ List<UploadRealTimeMonitoringData> monitoringData = uploadRealTimeMonitoringDataService.getDataByOrderCode(order.getCode());
+ if (monitoringData != null && !monitoringData.isEmpty()) {
+ throw new ServiceException("订单已存在真实充电监控数据,禁止模拟结算");
+ }
+ BigDecimal electricityFee = money(request.getElectricityFee());
+ BigDecimal serviceFee = money(request.getServiceFee());
+ BigDecimal simulationTotal = electricityFee.add(serviceFee).setScale(2, RoundingMode.HALF_UP);
+ BigDecimal availableAmount = money(order.getChargeAmount()).compareTo(BigDecimal.ZERO) > 0
+ ? money(order.getChargeAmount()) : money(order.getRechargeAmount());
+ if (simulationTotal.compareTo(availableAmount) > 0) {
+ throw new ServiceException("模拟消费金额不能超过订单可消费金额" + availableAmount.toPlainString() + "元");
+ }
+
+ List<AccountingStrategyDetailOrder> tariffs = accountingStrategyDetailOrderService.list(
+ new LambdaQueryWrapper<AccountingStrategyDetailOrder>()
+ .eq(AccountingStrategyDetailOrder::getChargingOrderId, order.getId())
+ .orderByAsc(AccountingStrategyDetailOrder::getStartTime));
+ ChargingSimulationSupport.PreparedSimulation prepared;
+ try {
+ prepared = ChargingSimulationSupport.prepare(order.getCode(), tariffs, electricityFee, serviceFee, LocalDateTime.now());
+ } catch (IllegalArgumentException e) {
+ throw new ServiceException(e.getMessage());
+ }
+ accountingStrategyDetailOrderService.updateById(prepared.getTariff());
+
+ TChargingGun gun = chargingGunClient.getChargingGunById(order.getChargingGunId()).getData();
+ TChargingPile pile = chargingPileClient.getChargingPileById(order.getChargingPileId()).getData();
+ TransactionRecordMessageVO record = prepared.getRecord();
+ record.setCharging_gun_code(gun == null ? null : gun.getCode());
+ record.setCharging_pile_code(pile == null ? null : pile.getCode());
+ if (Integer.valueOf(2).equals(order.getStatus())) {
+ TChargingOrder charging = new TChargingOrder();
+ charging.setId(order.getId());
+ charging.setStatus(3);
+ charging.setStartTime(LocalDateTime.now().minusSeconds(1));
+ this.updateById(charging);
+ }
+ R settlement = chargingOrderSettlementExecutor.settle(record);
+ if (R.isError(settlement)) {
+ throw new ServiceException(StringUtils.hasLength(settlement.getMsg()) ? settlement.getMsg() : "模拟结算失败");
+ }
+ return buildSimulationVO(this.getById(order.getId()));
+ }
+
+ private TChargingOrder findSimulationOrder(Long userId, Integer gunId) {
+ TChargingOrder order = this.getOne(new LambdaQueryWrapper<TChargingOrder>()
+ .eq(TChargingOrder::getAppUserId, userId)
+ .eq(TChargingOrder::getChargingGunId, gunId)
+ .eq(TChargingOrder::getSourceName, "ICBC")
+ .eq(TChargingOrder::getRechargePaymentStatus, 2)
+ .in(TChargingOrder::getStatus, Arrays.asList(2, 3, 4))
+ .eq(TChargingOrder::getDelFlag, 0)
+ .isNull(TChargingOrder::getRefundStatus)
+ .orderByDesc(TChargingOrder::getId)
+ .last("limit 1"));
+ if (order == null) {
+ throw new ServiceException("当前枪没有属于你的可模拟ICBC已支付订单");
+ }
+ return order;
+ }
+
+ private ChargingSimulationVO buildSimulationVO(TChargingOrder order) {
+ ChargingSimulationVO result = new ChargingSimulationVO();
+ result.setOrderId(order.getId());
+ result.setOrderCode(order.getCode());
+ result.setOrderStatus(order.getStatus());
+ result.setRechargeAmount(order.getRechargeAmount());
+ result.setChargeAmount(order.getChargeAmount());
+ result.setElectricityFee(order.getElectrovalence());
+ result.setServiceFee(order.getServiceCharge());
+ result.setPaymentAmount(order.getPaymentAmount());
+ result.setRefundAmount(order.getRefundAmount());
+ result.setRefundStatus(order.getRefundStatus());
+ result.setIcbcOrderId(order.getRechargeSerialNumber());
+ TChargingOrderShareRecord shareRecord = chargingOrderShareRecordMapper.selectOne(
+ new LambdaQueryWrapper<TChargingOrderShareRecord>()
+ .eq(TChargingOrderShareRecord::getChargingOrderId, order.getId())
+ .eq(TChargingOrderShareRecord::getPayChannel, "ICBC")
+ .eq(TChargingOrderShareRecord::getDelFlag, false)
+ .orderByDesc(TChargingOrderShareRecord::getId)
+ .last("limit 1"));
+ if (shareRecord != null) {
+ result.setShareAmount(shareRecord.getShareAmount());
+ result.setShareStatus(shareRecord.getOnlineShareStatus());
+ result.setSubOrderId(shareRecord.getSubOrderId());
+ result.setSubOrderSeq(shareRecord.getSubOrderSeq());
+ result.setSubOrderTrxid(shareRecord.getSubOrderTrxid());
+ }
+ return result;
+ }
+
+ @Override
+ public IcbcPaymentResp queryIcbcAdminPayment(String orderCode) {
+ TChargingOrder order = loadIcbcAdminOrder(orderCode);
+ return requireFeignData(icbcPaymentClient.query(order.getCode()), "工行支付查询失败");
+ }
+
+ @Override
+ public IcbcRefundResp refundIcbcAdminPayment(IcbcAdminFundOperationReq request) {
+ if (request == null) {
+ throw new ServiceException("请求参数不能为空");
+ }
+ TChargingOrder order = loadIcbcAdminOrder(request.getOrderCode());
+ BigDecimal amount;
+ try {
+ amount = IcbcAdminFundOperationSupport.requireAmount(request.getAmount());
+ } catch (IllegalArgumentException e) {
+ throw new ServiceException(e.getMessage());
+ }
+ BigDecimal paidAmount = money(order.getThirdPayAmount()).compareTo(BigDecimal.ZERO) > 0
+ ? money(order.getThirdPayAmount()) : money(order.getRechargeAmount());
+ if (amount.compareTo(paidAmount) > 0) {
+ throw new ServiceException("退款金额不能超过订单第三方支付金额" + paidAmount.toPlainString() + "元");
+ }
+ RLock lock = redissonClient.getLock("icbcAdminFund:order:" + order.getCode());
+ if (!lock.tryLock()) {
+ throw new ServiceException("该订单正在执行工行资金操作,请勿重复提交");
+ }
+ try {
+ IcbcPaymentResp payment = requireFeignData(icbcPaymentClient.query(order.getCode()), "工行支付查询失败");
+ String orderId = extractIcbcOrderId(payment);
+ order.setRechargeSerialNumber(orderId);
+ RBucket<String> pendingRefund = redissonClient.getBucket("icbcAdminFund:pending:main:" + order.getCode());
+ String generatedRefundNo = "ICBCADMINREFUND" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
+ String pendingValue = pendingRefund.get();
+ boolean reusingPending = StringUtils.hasLength(pendingValue);
+ String refundNo;
+ try {
+ refundNo = IcbcAdminFundOperationSupport.resolveRefundNo(pendingValue, amount, generatedRefundNo);
+ } catch (IllegalArgumentException e) {
+ throw new ServiceException(e.getMessage());
+ }
+ IcbcRefundReq refundRequest = IcbcAdminFundOperationSupport.buildRefundRequest(order, amount, refundNo);
+ if (!reusingPending) {
+ pendingRefund.set(IcbcAdminFundOperationSupport.buildPendingRefundValue(refundNo, amount));
+ }
+ IcbcRefundResp response = requireFeignData(icbcPaymentClient.refund(refundRequest), "工行退款调用失败");
+ if (IcbcAdminFundOperationSupport.shouldClearPendingRefund(
+ reusingPending, response.getStatus(), isDefinitiveIcbcResponse(response.getRawResponse()))) {
+ pendingRefund.delete();
+ }
+ return response;
+ } finally {
+ if (lock.isHeldByCurrentThread()) {
+ lock.unlock();
+ }
+ }
+ }
+
+ @Override
+ public List<IcbcAdminSubOrderVO> queryIcbcAdminSubOrders(String orderCode) {
+ TChargingOrder order = loadIcbcAdminOrder(orderCode);
+ List<TChargingOrderShareRecord> records = chargingOrderShareRecordMapper.selectList(
+ new LambdaQueryWrapper<TChargingOrderShareRecord>()
+ .eq(TChargingOrderShareRecord::getChargingOrderId, order.getId())
+ .eq(TChargingOrderShareRecord::getPayChannel, "ICBC")
+ .eq(TChargingOrderShareRecord::getDelFlag, false)
+ .orderByAsc(TChargingOrderShareRecord::getId));
+ List<IcbcAdminSubOrderVO> result = new ArrayList<>();
+ for (TChargingOrderShareRecord record : records) {
+ IcbcAdminSubOrderVO item = buildIcbcAdminSubOrderVO(record);
+ try {
+ IcbcSubOrderQueryReq query = IcbcAdminFundOperationSupport.buildSubOrderQueryRequest(order, record);
+ item.applyQuery(requireFeignData(icbcPaymentClient.querySubOrder(query), "工行子订单查询失败"));
+ } catch (Exception e) {
+ item.setQueryStatus("FAIL");
+ item.setErrorMessage(e.getMessage());
+ }
+ result.add(item);
+ }
+ return result;
+ }
+
+ @Override
+ public IcbcSubOrderRefundResp refundIcbcAdminSubOrder(IcbcAdminFundOperationReq request) {
+ if (request == null || request.getShareRecordId() == null) {
+ throw new ServiceException("请选择需要退款的分账子订单");
+ }
+ TChargingOrder order = loadIcbcAdminOrder(request.getOrderCode());
+ TChargingOrderShareRecord shareRecord = chargingOrderShareRecordMapper.selectById(request.getShareRecordId());
+ if (shareRecord == null || !order.getId().equals(shareRecord.getChargingOrderId())) {
+ throw new ServiceException("分账记录不属于当前订单");
+ }
+ BigDecimal amount;
+ try {
+ amount = IcbcAdminFundOperationSupport.requireAmount(request.getAmount());
+ } catch (IllegalArgumentException e) {
+ throw new ServiceException(e.getMessage());
+ }
+ if (shareRecord.getShareAmount() != null && amount.compareTo(shareRecord.getShareAmount()) > 0) {
+ throw new ServiceException("分账退款金额不能超过分账金额" + shareRecord.getShareAmount().toPlainString() + "元");
+ }
+ RLock lock = redissonClient.getLock("icbcAdminFund:order:" + order.getCode());
+ if (!lock.tryLock()) {
+ throw new ServiceException("该分账子订单正在退款,请勿重复提交");
+ }
+ try {
+ String suffix = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
+ RBucket<String> pendingRefund = redissonClient.getBucket("icbcAdminFund:pending:sub:" + shareRecord.getId());
+ String generatedToken = "ICBCSUBREFUND" + suffix + ":ICBCSUBREFUNDNO" + suffix;
+ String pendingValue = pendingRefund.get();
+ boolean reusingPending = StringUtils.hasLength(pendingValue);
+ String operationToken;
+ try {
+ operationToken = IcbcAdminFundOperationSupport.resolveRefundNo(pendingValue, amount, generatedToken);
+ } catch (IllegalArgumentException e) {
+ throw new ServiceException(e.getMessage());
+ }
+ String[] refundNumbers = operationToken.split(":", 2);
+ if (refundNumbers.length != 2) {
+ throw new ServiceException("工行分账退款幂等记录格式错误");
+ }
+ IcbcSubOrderRefundReq refundRequest;
+ try {
+ refundRequest = IcbcAdminFundOperationSupport.buildSubOrderRefundRequest(
+ order, shareRecord, amount, refundNumbers[0], refundNumbers[1]);
+ } catch (IllegalArgumentException e) {
+ throw new ServiceException(e.getMessage());
+ }
+ if (!reusingPending) {
+ pendingRefund.set(IcbcAdminFundOperationSupport.buildPendingRefundValue(operationToken, amount));
+ }
+ IcbcSubOrderRefundResp response = requireFeignData(
+ icbcPaymentClient.refundSubOrder(refundRequest), "工行分账退款调用失败");
+ if (IcbcAdminFundOperationSupport.shouldClearPendingRefund(
+ reusingPending, response.getStatus(), isDefinitiveIcbcResponse(response.getRawResponse()))) {
+ pendingRefund.delete();
+ }
+ return response;
+ } finally {
+ if (lock.isHeldByCurrentThread()) {
+ lock.unlock();
+ }
+ }
+ }
+
+ private TChargingOrder loadIcbcAdminOrder(String orderCode) {
+ if (!StringUtils.hasLength(orderCode)) {
+ throw new ServiceException("订单号不能为空");
+ }
+ TChargingOrder order = this.getOne(new LambdaQueryWrapper<TChargingOrder>()
+ .eq(TChargingOrder::getCode, orderCode)
+ .eq(TChargingOrder::getDelFlag, 0)
+ .last("limit 1"));
+ try {
+ IcbcAdminFundOperationSupport.validateOrder(order);
+ } catch (IllegalArgumentException e) {
+ throw new ServiceException(e.getMessage());
+ }
+ return order;
+ }
+
+ private String extractIcbcOrderId(IcbcPaymentResp payment) {
+ try {
+ JSONObject response = JSON.parseObject(payment.getRawResponse());
+ String orderId = response == null ? null : response.getString("order_id");
+ if (StringUtils.hasLength(orderId)) {
+ return orderId;
+ }
+ } catch (Exception ignored) {
+ }
+ throw new ServiceException("工行支付查询未返回真实order_id,禁止退款");
+ }
+
+ private boolean isDefinitiveIcbcResponse(String rawResponse) {
+ if (!StringUtils.hasLength(rawResponse)) {
+ return false;
+ }
+ try {
+ JSONObject response = JSON.parseObject(rawResponse);
+ return response != null && (response.containsKey("returnCode") || response.containsKey("return_code"));
+ } catch (Exception ignored) {
+ return false;
+ }
+ }
+
+ private IcbcAdminSubOrderVO buildIcbcAdminSubOrderVO(TChargingOrderShareRecord record) {
+ IcbcAdminSubOrderVO item = new IcbcAdminSubOrderVO();
+ item.setShareRecordId(record.getId());
+ item.setOrderCode(record.getChargingOrderCode());
+ item.setShareAmount(record.getShareAmount());
+ item.setOnlineShareStatus(record.getOnlineShareStatus());
+ item.setSubOrderId(record.getSubOrderId());
+ item.setSubMerId(record.getSubMerId());
+ item.setSubMerPrtclNo(record.getSubMerPrtclNo());
+ item.setOriTrxDate(record.getOriTrxDate());
+ item.setBankOrderId(record.getIcbcOrderId());
+ item.setSubOrderSeq(record.getSubOrderSeq());
+ item.setSubOrderTrxid(record.getSubOrderTrxid());
+ return item;
+ }
+
+ private <T> T requireFeignData(R<T> result, String message) {
+ if (result == null || R.isError(result) || result.getData() == null) {
+ throw new ServiceException(result != null && StringUtils.hasLength(result.getMsg()) ? result.getMsg() : message);
+ }
+ return result.getData();
}
@@ -938,7 +1325,26 @@
@Override
public AjaxResult chargingOrderCallback(Integer paymentType, String out_trade_no, String transaction_id, String attach) {
TChargingOrder chargingOrder = this.getOne(new LambdaQueryWrapper<TChargingOrder>().eq(TChargingOrder::getCode, out_trade_no));
+ if (chargingOrder == null) {
+ return AjaxResult.error("支付回调订单不存在");
+ }
if (chargingOrder.getRechargePaymentStatus() == 2) {
+ if (isIcbcOrder(chargingOrder) && StringUtils.hasLength(transaction_id)
+ && !transaction_id.equals(chargingOrder.getRechargeSerialNumber())) {
+ chargingOrder.setRechargeSerialNumber(transaction_id);
+ this.updateById(chargingOrder);
+ }
+ return AjaxResult.success();
+ }
+ LocalDateTime callbackPayTime = LocalDateTime.now();
+ int claimed = this.baseMapper.update(null, new LambdaUpdateWrapper<TChargingOrder>()
+ .eq(TChargingOrder::getId, chargingOrder.getId())
+ .eq(TChargingOrder::getRechargePaymentStatus, 1)
+ .set(TChargingOrder::getRechargePaymentStatus, 2)
+ .set(TChargingOrder::getRechargeSerialNumber, transaction_id)
+ .set(TChargingOrder::getStatus, 2)
+ .set(TChargingOrder::getPayTime, callbackPayTime));
+ if (claimed == 0) {
return AjaxResult.success();
}
if (isBalancePayMode(chargingOrder.getPayMethod())
@@ -954,7 +1360,7 @@
chargingOrder.setRechargePaymentStatus(2);
chargingOrder.setRechargeSerialNumber(transaction_id);
chargingOrder.setStatus(2);
- chargingOrder.setPayTime(LocalDateTime.now());
+ chargingOrder.setPayTime(callbackPayTime);
//添加安全检测数据到缓存中,每步安全检测完成后需要更新缓存数据
PreChargeCheck preChargeCheck = new PreChargeCheck();
@@ -1509,6 +1915,11 @@
return;
}
this.updateById(chargingOrder);
+ if (isIcbcOrder(chargingOrder)) {
+ refundByIcbcAndFinish(chargingOrder, chargingOrderRefund, thirdRefundAmount, "充电失败,取消充电订单", false,
+ recordedRefundAmount, balanceRefundAmount);
+ return;
+ }
if (thirdRefundAmount.compareTo(BigDecimal.ZERO) > 0 && 1 == rechargePaymentType) {
WxPaymentRefundModel model = new WxPaymentRefundModel();
model.setOut_trade_no(chargingOrder.getCode());
@@ -1660,6 +2071,11 @@
finishManualChargingRefund(chargingOrder, chargingOrderRefund, money, balanceRefundAmount, thirdRefundAmount);
return;
}
+ if (isIcbcOrder(chargingOrder)) {
+ refundByIcbcAndFinish(chargingOrder, chargingOrderRefund, thirdRefundAmount, "充电完成退款", true,
+ money, balanceRefundAmount);
+ return;
+ }
if (1 == rechargePaymentType) {
WxPaymentRefundModel model = new WxPaymentRefundModel();
model.setOut_trade_no(chargingOrder.getCode());
@@ -1764,6 +2180,7 @@
chargingOrder.setRefundSerialNumber(refund_id);
chargingOrder.setRefundTime(LocalDateTime.now());
this.updateById(chargingOrder);
+ triggerChargingOrderShare(this.getById(order.getId()));
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
@@ -2089,6 +2506,7 @@
List<Integer> siteIds = dto.getSiteIds();
List<ChargingOrderVO> list = this.baseMapper.chargingOrder(pageInfo, dto, startTime1, startTime2, endTime1, endTime2);
fillChargingOrderBalanceIncome(list);
+ fillChargingOrderShareIncome(list);
List<TChargingOrderAccountingStrategy> list3 = chargingOrderAccountingStrategyService.list();
for (ChargingOrderVO chargingOrderVO : list) {
if (roleType == 2) {
@@ -2164,13 +2582,14 @@
}
Map<String, Object> fundMap = this.baseMapper.chargingOrderFundSummary(dto, startTime1, startTime2, endTime1, endTime2);
if (null != fundMap) {
- tCharingOrderVO.setPlatformIncomeTotal(getSummaryDecimal(fundMap, "platformIncomeTotal"));
- tCharingOrderVO.setPartnerIncomeTotal(getSummaryDecimal(fundMap, "partnerIncomeTotal"));
tCharingOrderVO.setWechatIncome(getSummaryDecimal(fundMap, "wechatIncome"));
tCharingOrderVO.setAlipayIncome(getSummaryDecimal(fundMap, "alipayIncome"));
tCharingOrderVO.setWhitelistChargingIncome(getSummaryDecimal(fundMap, "whitelistChargingIncome"));
tCharingOrderVO.setWhitelistRefund(getSummaryDecimal(fundMap, "whitelistRefund"));
}
+ Map<String, Object> shareIncomeMap = getChargingOrderShareIncomeSummary(dto, startTime1, startTime2, endTime1, endTime2);
+ tCharingOrderVO.setPlatformIncomeTotal(getSummaryDecimal(shareIncomeMap, "platformIncomeTotal"));
+ tCharingOrderVO.setPartnerIncomeTotal(getSummaryDecimal(shareIncomeMap, "partnerIncomeTotal"));
Map<String, Object> balanceMap = getExchangeCodeBalanceSummary(dto, startTime1, startTime2, endTime1, endTime2);
tCharingOrderVO.setBalanceIncome(getSummaryDecimal(balanceMap, "balanceIncome"));
tCharingOrderVO.setBalanceGiftIncome(getSummaryDecimal(balanceMap, "balanceGiftIncome"));
@@ -2351,6 +2770,30 @@
return totalMap;
}
+ private Map<String, Object> getChargingOrderShareIncomeSummary(ChargingOrderQuery dto, String startTime1, String startTime2, String endTime1, String endTime2) {
+ Map<String, Object> totalMap = new HashMap<>();
+ totalMap.put("platformIncomeTotal", BigDecimal.ZERO);
+ totalMap.put("partnerIncomeTotal", BigDecimal.ZERO);
+ List<Long> orderIds = this.baseMapper.chargingOrderSummaryOrderIds(dto, startTime1, startTime2, endTime1, endTime2);
+ if (orderIds == null || orderIds.isEmpty()) {
+ return totalMap;
+ }
+ BigDecimal platformIncomeTotal = BigDecimal.ZERO;
+ BigDecimal partnerIncomeTotal = BigDecimal.ZERO;
+ int batchSize = 500;
+ for (int i = 0; i < orderIds.size(); i += batchSize) {
+ List<Long> batchIds = orderIds.subList(i, Math.min(i + batchSize, orderIds.size()));
+ Map<String, Object> batchMap = this.baseMapper.chargingOrderShareIncomeSummaryByOrderIds(batchIds);
+ if (batchMap != null) {
+ platformIncomeTotal = platformIncomeTotal.add(getSummaryDecimal(batchMap, "platformIncomeTotal"));
+ partnerIncomeTotal = partnerIncomeTotal.add(getSummaryDecimal(batchMap, "partnerIncomeTotal"));
+ }
+ }
+ totalMap.put("platformIncomeTotal", platformIncomeTotal);
+ totalMap.put("partnerIncomeTotal", partnerIncomeTotal);
+ return totalMap;
+ }
+
private BigDecimal getSummaryDecimal(Map<String, Object> map, String key) {
if (map == null) {
return BigDecimal.ZERO;
@@ -2471,27 +2914,30 @@
}
@Override
- public List<SixChargingDto> charge(LocalDate sixBefore, List<Integer> siteIds) {
- return this.baseMapper.charge(sixBefore, siteIds);
+ public List<SixChargingDto> charge(LocalDate sixBefore, List<Integer> siteIds, Boolean partnerView) {
+ return this.baseMapper.charge(sixBefore, siteIds, partnerView);
}
@Override
- public List<SixCircleDto> circle(List<Integer> siteIds, LocalDate sixBefore) {
- return this.baseMapper.circle(siteIds, sixBefore);
+ public List<SixCircleDto> circle(List<Integer> siteIds, LocalDate sixBefore, Boolean partnerView) {
+ return this.baseMapper.circle(siteIds, sixBefore, partnerView);
}
@Override
- public Map<String, Object> countAll(List<Integer> siteIds) {
- return this.baseMapper.countAll(siteIds);
+ public Map<String, Object> countAll(List<Integer> siteIds, Boolean partnerView) {
+ return this.baseMapper.countAll(siteIds, partnerView);
}
/**
* 统计近六月订单收入支付方式占比;服务层聚合避免复杂CASE SQL在当前数据库驱动中转换异常。
*/
@Override
- public List<SixIncomeCircleDto> payTypeCircle(LocalDate sixBefore, List<Integer> siteIds) {
+ public List<SixIncomeCircleDto> payTypeCircle(LocalDate sixBefore, List<Integer> siteIds, Boolean partnerView) {
if (siteIds == null || siteIds.isEmpty()) {
return buildPayTypeCircleData(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
+ }
+ if (Boolean.TRUE.equals(partnerView)) {
+ return buildPartnerShareCircleData(this.baseMapper.chargingOrderShareIncomeByPayChannel(sixBefore, siteIds));
}
List<TChargingOrder> orders = this.lambdaQuery()
.select(TChargingOrder::getRechargePaymentType, TChargingOrder::getPayMethod,
@@ -2521,6 +2967,28 @@
Integer payMethod = order.getPayMethod();
if (Integer.valueOf(1).equals(payMethod) || Integer.valueOf(2).equals(payMethod)) {
balanceAmount = balanceAmount.add(balancePayAmount);
+ }
+ }
+ return buildPayTypeCircleData(wechatAmount, alipayAmount, balanceAmount);
+ }
+
+ private List<SixIncomeCircleDto> buildPartnerShareCircleData(List<Map<String, Object>> rows) {
+ BigDecimal wechatAmount = BigDecimal.ZERO;
+ BigDecimal alipayAmount = BigDecimal.ZERO;
+ BigDecimal balanceAmount = BigDecimal.ZERO;
+ if (rows != null) {
+ for (Map<String, Object> row : rows) {
+ String channel = row.get("payChannel") == null ? "" : row.get("payChannel").toString();
+ BigDecimal shareAmount = getSummaryDecimal(row, "shareAmount");
+ if ("WECHAT".equalsIgnoreCase(channel) || "WX".equalsIgnoreCase(channel)) {
+ wechatAmount = wechatAmount.add(shareAmount);
+ } else if ("ALIPAY".equalsIgnoreCase(channel) || "ALI".equalsIgnoreCase(channel)) {
+ alipayAmount = alipayAmount.add(shareAmount);
+ } else if ("BALANCE".equalsIgnoreCase(channel) || "WHITELIST".equalsIgnoreCase(channel)) {
+ balanceAmount = balanceAmount.add(shareAmount);
+ } else {
+ wechatAmount = wechatAmount.add(shareAmount);
+ }
}
}
return buildPayTypeCircleData(wechatAmount, alipayAmount, balanceAmount);
@@ -2717,6 +3185,7 @@
PageInfo<ChargingOrderListVO> pageInfo = new PageInfo<>(dto.getPageCurr(), dto.getPageSize());
List<ChargingOrderListVO> list = new ArrayList<>();
+ Map<Integer, BigDecimal> siteServiceShareRatioMap = loadSiteServiceShareRatioMap();
List<Map<String, Object>> maps = this.baseMapper.chargingList1(pageInfo, dto, startTime1, startTime2, endTime1, endTime2);
for (Map<String, Object> map : maps) {
if(null == map){
@@ -2730,34 +3199,36 @@
}
chargingOrderListVO.setUid(map.get("id").toString());
chargingOrderListVO.setCode(map.get("code").toString());
+ chargingOrderListVO.setId(Long.valueOf(map.get("id").toString()));
+ chargingOrderListVO.setSiteId(Integer.valueOf(map.get("siteId").toString()));
+ chargingOrderListVO.setOrderSource(Integer.valueOf(map.get("orderSource").toString()));
chargingOrderListVO.setSiteName(map.get("siteName").toString());
chargingOrderListVO.setOrderType(map.get("orderType").toString());
chargingOrderListVO.setTerminalName(map.get("terminalName").toString());
chargingOrderListVO.setElectricity(null == map.get("electricity") ? BigDecimal.ZERO : new BigDecimal(map.get("electricity").toString()));
chargingOrderListVO.setChargingCapacity(chargingOrderListVO.getElectricity());
- Object periodElectricPrice = map.get("periodElectricPrice");
- Object periodServicePrice = map.get("periodServicePrice");
- if(null != periodServicePrice){
- Integer orderSource = Integer.valueOf(map.get("orderSource").toString());
+ BigDecimal periodElectricPrice = getSummaryDecimal(map, "periodElectricPrice");
+ BigDecimal periodServicePrice = getSummaryDecimal(map, "periodServicePrice");
+ if(periodServicePrice.compareTo(BigDecimal.ZERO) != 0){
+ Integer orderSource = chargingOrderListVO.getOrderSource();
if(2 == orderSource){
- BigDecimal bigDecimal = new BigDecimal(periodServicePrice.toString()).multiply(new BigDecimal(0.8)).setScale(2, RoundingMode.HALF_EVEN);
- chargingOrderListVO.setPaymentAmount(new BigDecimal(periodElectricPrice.toString()).add(bigDecimal));
- BigDecimal sharingAmount = new BigDecimal(periodServicePrice.toString()).multiply(new BigDecimal(0.2)).setScale(2, RoundingMode.HALF_EVEN);
+ BigDecimal bigDecimal = periodServicePrice.multiply(new BigDecimal("0.8")).setScale(2, RoundingMode.HALF_EVEN);
+ chargingOrderListVO.setServiceCharge(bigDecimal);
+ chargingOrderListVO.setPaymentAmount(periodElectricPrice.add(bigDecimal).setScale(2, RoundingMode.HALF_EVEN));
+ BigDecimal sharingAmount = periodServicePrice.multiply(new BigDecimal("0.2")).setScale(2, RoundingMode.HALF_EVEN);
chargingOrderListVO.setSharingAmount(sharingAmount);
}else{
- chargingOrderListVO.setPaymentAmount(new BigDecimal(periodElectricPrice.toString()).add(new BigDecimal(periodServicePrice.toString())).setScale(2, RoundingMode.HALF_EVEN));
+ chargingOrderListVO.setServiceCharge(periodServicePrice);
+ chargingOrderListVO.setPaymentAmount(periodElectricPrice.add(periodServicePrice).setScale(2, RoundingMode.HALF_EVEN));
chargingOrderListVO.setSharingAmount(BigDecimal.ZERO);
}
}else{
+ chargingOrderListVO.setServiceCharge(BigDecimal.ZERO);
chargingOrderListVO.setPaymentAmount(BigDecimal.ZERO);
chargingOrderListVO.setSharingAmount(BigDecimal.ZERO);
}
chargingOrderListVO.setElectrovalence(null == map.get("electrovalence") ? BigDecimal.ZERO : new BigDecimal(map.get("electrovalence").toString()));
- chargingOrderListVO.setServiceCharge(null == map.get("serviceCharge") ? BigDecimal.ZERO : new BigDecimal(map.get("serviceCharge").toString()));
- BigDecimal bigDecimal = new BigDecimal("0.006");
- if (!"0".equals(map.get("rechargePaymentType").toString())) {
- chargingOrderListVO.setCommissionAmount(new BigDecimal(map.get("rechargeAmount").toString()).multiply(bigDecimal));
- }
+ fillChargingPeriodIncome(chargingOrderListVO, siteServiceShareRatioMap);
if(null != map.get("tripartitePlatformName")){
chargingOrderListVO.setTripartitePlatformName(map.get("tripartitePlatformName").toString());
}
@@ -2854,21 +3325,41 @@
Map<String, Object> map = this.baseMapper.chargingListSum1(dto, startTime1, startTime2, endTime1, endTime2);
System.err.println("充电时段统计-查询参数:" + JSON.toJSONString(dto));
System.err.println("充电时段统计-汇总数据:" + JSON.toJSONString(map));
+ if (map == null) {
+ map = new HashMap<>();
+ }
//订单数量
- int orderCount = Integer.parseInt(map.get("orderCount").toString());
+ int orderCount = Integer.parseInt(String.valueOf(map.getOrDefault("orderCount", "0")));
// 计算充电总度数
- BigDecimal electronic = new BigDecimal(map.get("electronic").toString());
+ BigDecimal electronic = new BigDecimal(String.valueOf(map.getOrDefault("electronic", "0")));
// 支付金额
- BigDecimal paymentAmount = new BigDecimal(map.get("paymentAmount").toString());
+ BigDecimal paymentAmount = new BigDecimal(String.valueOf(map.getOrDefault("paymentAmount", "0")));
// 电费
- BigDecimal electrovalence = new BigDecimal(map.get("electrovalence").toString());
+ BigDecimal electrovalence = new BigDecimal(String.valueOf(map.getOrDefault("electrovalence", "0")));
// 服务费
- BigDecimal serviceCharge = new BigDecimal(map.get("serviceCharge").toString());
- chargingOrderTimeVO.setOrderCount(orderCount);
+ BigDecimal serviceCharge = new BigDecimal(String.valueOf(map.getOrDefault("serviceCharge", "0")));
+ chargingOrderTimeVO.setOrderCount(orderCount);
chargingOrderTimeVO.setChargingCapacity(electronic.setScale(2, BigDecimal.ROUND_HALF_EVEN));
chargingOrderTimeVO.setPaymentAmount(paymentAmount.setScale(2, BigDecimal.ROUND_HALF_EVEN));
chargingOrderTimeVO.setElectrovalence(electrovalence.setScale(2, BigDecimal.ROUND_HALF_EVEN));
chargingOrderTimeVO.setServiceCharge(serviceCharge.setScale(2, BigDecimal.ROUND_HALF_EVEN));
+ BigDecimal platformIncome = BigDecimal.ZERO;
+ BigDecimal partnerIncome = BigDecimal.ZERO;
+ for (Map<String, Object> sumMap : list1) {
+ if (sumMap == null) {
+ continue;
+ }
+ ChargingOrderListVO item = new ChargingOrderListVO();
+ item.setSiteId(getSummaryLong(sumMap, "siteId").intValue());
+ item.setOrderSource(getSummaryLong(sumMap, "orderSource").intValue());
+ item.setPaymentAmount(getSummaryDecimal(sumMap, "paymentAmount"));
+ item.setServiceCharge(getSummaryDecimal(sumMap, "serviceCharge"));
+ fillChargingPeriodIncome(item, siteServiceShareRatioMap);
+ platformIncome = platformIncome.add(nullToZero(item.getPlatformIncome()));
+ partnerIncome = partnerIncome.add(nullToZero(item.getPartnerIncome()));
+ }
+ chargingOrderTimeVO.setPlatformIncome(platformIncome.setScale(2, RoundingMode.HALF_UP));
+ chargingOrderTimeVO.setPartnerIncome(partnerIncome.setScale(2, RoundingMode.HALF_UP));
// 计算尖峰平谷充电到账占比
BigDecimal add = jianElectronic.add(fengElectronic).add(pingElectronic).add(guElectronic);
// 计算尖峰平谷服务费占比
@@ -2998,7 +3489,60 @@
dto.setSiteIds(siteIds);
List<ChargingOrderListVO> list1 = this.baseMapper.chargingList(dto, startTime1, startTime2, endTime1, endTime2);
fillChargingPeriodExportBalanceIncome(list1);
+ fillChargingPeriodIncome(list1);
return list1;
+ }
+
+ private void fillChargingPeriodIncome(List<ChargingOrderListVO> list) {
+ if (list == null || list.isEmpty()) {
+ return;
+ }
+ Map<Integer, BigDecimal> siteServiceShareRatioMap = loadSiteServiceShareRatioMap();
+ for (ChargingOrderListVO item : list) {
+ fillChargingPeriodIncome(item, siteServiceShareRatioMap);
+ }
+ }
+
+ private void fillChargingPeriodIncome(ChargingOrderListVO item, Map<Integer, BigDecimal> siteServiceShareRatioMap) {
+ if (item == null) {
+ return;
+ }
+ BigDecimal paymentAmount = nullToZero(item.getPaymentAmount());
+ BigDecimal partnerIncome = BigDecimal.ZERO;
+ if (!Integer.valueOf(2).equals(item.getOrderSource())) {
+ BigDecimal serviceShareRatio = siteServiceShareRatioMap.get(item.getSiteId());
+ if (serviceShareRatio != null && serviceShareRatio.compareTo(BigDecimal.ZERO) > 0) {
+ partnerIncome = nullToZero(item.getServiceCharge())
+ .multiply(serviceShareRatio)
+ .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
+ }
+ }
+ item.setPartnerIncome(partnerIncome);
+ item.setPlatformIncome(paymentAmount.subtract(partnerIncome).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP));
+ }
+
+ private Map<Integer, BigDecimal> loadSiteServiceShareRatioMap() {
+ Map<Integer, BigDecimal> ratioMap = new HashMap<>();
+ R<List<Site>> siteResult = siteClient.getSiteAll();
+ R<List<Partner>> partnerResult = siteClient.getAllPartner();
+ List<Site> sites = siteResult == null ? Collections.emptyList() : siteResult.getData();
+ List<Partner> partners = partnerResult == null ? Collections.emptyList() : partnerResult.getData();
+ if (sites == null || sites.isEmpty() || partners == null || partners.isEmpty()) {
+ return ratioMap;
+ }
+ Map<Integer, Partner> partnerMap = partners.stream()
+ .filter(partner -> partner != null && partner.getId() != null)
+ .collect(Collectors.toMap(Partner::getId, partner -> partner, (left, right) -> left));
+ for (Site site : sites) {
+ if (site == null || site.getId() == null || site.getPartnerId() == null) {
+ continue;
+ }
+ Partner partner = partnerMap.get(site.getPartnerId());
+ if (partner != null && partner.getServiceShareRatio() != null) {
+ ratioMap.put(site.getId(), partner.getServiceShareRatio());
+ }
+ }
+ return ratioMap;
}
/**
@@ -3026,6 +3570,31 @@
Map<String, Object> row = incomeMap.get(item.getId());
item.setBalanceIncome(getSummaryDecimal(row, "balanceIncome"));
item.setBalanceGiftIncome(getSummaryDecimal(row, "balanceGiftIncome"));
+ }
+ }
+
+ private void fillChargingOrderShareIncome(List<ChargingOrderVO> list) {
+ if (list == null || list.isEmpty()) {
+ return;
+ }
+ List<Long> orderIds = list.stream().map(ChargingOrderVO::getId).filter(Objects::nonNull).collect(Collectors.toList());
+ if (orderIds.isEmpty()) {
+ return;
+ }
+ Map<Long, Map<String, Object>> incomeMap = new HashMap<>();
+ List<Map<String, Object>> rows = this.baseMapper.chargingOrderShareIncomeByOrderIds(orderIds);
+ if (rows != null) {
+ for (Map<String, Object> row : rows) {
+ Long orderId = getSummaryLong(row, "orderId");
+ if (orderId != null) {
+ incomeMap.put(orderId, row);
+ }
+ }
+ }
+ for (ChargingOrderVO item : list) {
+ Map<String, Object> row = incomeMap.get(item.getId());
+ item.setPlatformIncome(getSummaryDecimal(row, "platformIncome"));
+ item.setPartnerIncome(getSummaryDecimal(row, "partnerIncome"));
}
}
@@ -3220,15 +3789,19 @@
@Transactional
public R endChargeBillingCharge(TransactionRecordMessageVO vo) {
RLock lock = redissonClient.getLock("endOrder:" + vo.getTransaction_serial_number());
+ boolean locked = false;
try {
- boolean tryLock = lock.tryLock(60, 60, TimeUnit.SECONDS);
+ locked = lock.tryLock(60, 60, TimeUnit.SECONDS);
+ if (!locked) {
+ return R.fail("订单正在结算,请勿重复操作");
+ }
//添加redis缓存数据,过期时间,用于防止处理缓慢且硬件重复发送报文导致的重复退款
Boolean hasKey = redisService.hasKey("END_ORDER:" + vo.getTransaction_serial_number());
if(hasKey){
return R.fail("不能重复退款");
}
redisService.setCacheObject("END_ORDER:" + vo.getTransaction_serial_number(), System.currentTimeMillis() + "", 10L, TimeUnit.MINUTES);
- if(tryLock){
+ if(locked){
TChargingOrder chargingOrder = this.getOne(new LambdaQueryWrapper<TChargingOrder>().eq(TChargingOrder::getCode, vo.getTransaction_serial_number()));
if (null == chargingOrder) {
log.error("订单不存在,订单号:{}", vo.getTransaction_serial_number());
@@ -3724,6 +4297,17 @@
chargingOrder.setRefundStatus(2);
chargingOrder.setRefundTime(LocalDateTime.now());
this.updateById(chargingOrder);
+ triggerChargingOrderShare(chargingOrder);
+ } else if (isIcbcOrder(chargingOrder)) {
+ boolean refundSuccess = refundByIcbcAndFinish(chargingOrder, chargingOrderRefund, thirdRefundAmount,
+ "充电完成退款", false, refundAmount, balanceRefundAmount);
+ if (!refundSuccess) {
+ chargingOrderRefund.setRefundStatus(1);
+ chargingOrderRefundService.save(chargingOrderRefund);
+ chargingOrder.setRefundStatus(1);
+ this.updateById(chargingOrder);
+ throw new ServiceException("工行退款失败,结算未完成,请检查支付流水后重试退款");
+ }
} else if (1 == rechargePaymentType) {
WxPaymentRefundModel model = new WxPaymentRefundModel();
model.setOut_trade_no(chargingOrder.getCode());
@@ -3793,6 +4377,7 @@
chargingOrder.setRefundStatus(2);
chargingOrder.setRefundTime(LocalDateTime.now());
this.updateById(chargingOrder);
+ triggerChargingOrderShare(chargingOrder);
} else {
//构建退款明细
TChargingOrderRefund chargingOrderRefund = new TChargingOrderRefund();
@@ -3821,6 +4406,7 @@
chargingOrder.setRefundStatus(2);
chargingOrder.setRefundTime(LocalDateTime.now());
this.updateById(chargingOrder);
+ triggerChargingOrderShare(chargingOrder);
}
}
@@ -3853,6 +4439,9 @@
summaryData.setElectricPrice(electronic_reduce.setScale(2, BigDecimal.ROUND_HALF_EVEN));
summaryData.setServicePrice(serviceMoney.setScale(2, BigDecimal.ROUND_HALF_EVEN));
chargingOrderSummaryDataService.save(summaryData);
+ if (channelRefundAmount.compareTo(BigDecimal.ZERO) == 0 && Integer.valueOf(2).equals(chargingOrder.getRefundStatus())) {
+ triggerChargingOrderShare(chargingOrder, site);
+ }
//推送监管平台订单状态和订单详情
ChargingOrderVo chargingOrderVo = new ChargingOrderVo();
@@ -3861,9 +4450,12 @@
chargingMessageClient.pushOrderStatus(chargingOrderVo);
}
} catch (Exception e) {
- e.printStackTrace();
+ log.error("充电订单结算失败,订单号:{}", vo == null ? null : vo.getTransaction_serial_number(), e);
+ return R.fail("充电订单结算失败:" + firstNotBlank(e.getMessage(), "未知异常"));
} finally {
- lock.unlock();
+ if (locked && lock.isHeldByCurrentThread()) {
+ lock.unlock();
+ }
}
return R.ok();
}
@@ -4004,6 +4596,10 @@
if (thirdRefundAmount.compareTo(BigDecimal.ZERO) == 0) {
chargingOrderRefund.setRefundSerialNumber("BALANCE");
finishManualChargingRefund(tChargingOrder, chargingOrderRefund, manualRefundAmount, balanceRefundAmount, thirdRefundAmount);
+ }
+ else if (isIcbcOrder(tChargingOrder)) {
+ refundByIcbcAndFinish(tChargingOrder, chargingOrderRefund, thirdRefundAmount, "取消订单", true,
+ manualRefundAmount, balanceRefundAmount);
}
else if (1 == tChargingOrder.getRechargePaymentType()) {
WxPaymentRefundModel model = new WxPaymentRefundModel();
@@ -6143,4 +6739,414 @@
e.printStackTrace();
}
}
+
+ private boolean isIcbcRemovableStatus(String status, TChargingOrder order) {
+ if (!StringUtils.hasLength(status)) {
+ return false;
+ }
+ if (Arrays.asList("FAIL", "FAILED", "CLOSED", "TRADE_CLOSED", "REVOKED", "PAYERROR").contains(status)) {
+ return true;
+ }
+ // 工行B2C不支持主动关单;NOTPAY超时仅释放本地待支付订单,不代表远端订单已关闭。
+ return "NOTPAY".equals(status) && isLocalPendingOrderExpired(order);
+ }
+
+ private boolean isLocalPendingOrderExpired(TChargingOrder order) {
+ return order != null && order.getCreateTime() != null && order.getCreateTime().plusMinutes(30).isBefore(LocalDateTime.now());
+ }
+
+ private boolean isIcbcPaymentCreated(IcbcPaymentResp resp) {
+ if (resp == null || !StringUtils.hasLength(resp.getStatus())) {
+ return false;
+ }
+ if (!"SUCCESS".equals(resp.getStatus()) || !StringUtils.hasLength(resp.getPayData())) {
+ return false;
+ }
+ try {
+ JSONObject payData = JSON.parseObject(resp.getPayData());
+ return StringUtils.hasLength(payData.getString("wxDataPackage")) || StringUtils.hasLength(payData.getString("package"));
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private void triggerChargingOrderShare(TChargingOrder chargingOrder, Site site) {
+ try {
+ if (!isNeedChargingOrderShare(chargingOrder)) {
+ return;
+ }
+ Partner partner = loadPartner(site);
+ for (TChargingOrderShareRecord shareRecord : buildChargingOrderShareRecords(chargingOrder, site, partner)) {
+ TChargingOrderShareRecord exist = chargingOrderShareRecordMapper.selectOne(new LambdaQueryWrapper<TChargingOrderShareRecord>()
+ .eq(TChargingOrderShareRecord::getChargingOrderCode, shareRecord.getChargingOrderCode())
+ .eq(TChargingOrderShareRecord::getPayChannel, shareRecord.getPayChannel())
+ .eq(TChargingOrderShareRecord::getDelFlag, false)
+ .last("limit 1"));
+ if (exist != null && ("SUCCESS".equals(exist.getOnlineShareStatus()) || "PROCESSING".equals(exist.getOnlineShareStatus()))) {
+ continue;
+ }
+ shareRecord = mergeChargingOrderShareRecord(exist, shareRecord);
+ saveOrUpdateChargingOrderShareRecord(shareRecord);
+ if (Boolean.TRUE.equals(shareRecord.getNeedOnlineShare())) {
+ processIcbcOnlineShare(shareRecord, partner);
+ }
+ }
+ } catch (Exception e) {
+ log.error("充电订单分账记录触发失败,订单号:{}", chargingOrder == null ? null : chargingOrder.getCode(), e);
+ recordChargingOrderShareFailure(chargingOrder, site, e);
+ }
+ }
+
+ private void triggerChargingOrderShare(TChargingOrder chargingOrder) {
+ try {
+ if (chargingOrder == null || chargingOrder.getSiteId() == null) {
+ return;
+ }
+ R<List<Site>> siteResult = siteClient.getSiteByIds(Arrays.asList(chargingOrder.getSiteId()));
+ List<Site> sites = siteResult == null ? null : siteResult.getData();
+ Site site = sites == null || sites.isEmpty() ? null : sites.get(0);
+ triggerChargingOrderShare(chargingOrder, site);
+ } catch (Exception e) {
+ log.error("充电订单分账站点加载失败,订单号:{}", chargingOrder == null ? null : chargingOrder.getCode(), e);
+ }
+ }
+
+ private boolean isNeedChargingOrderShare(TChargingOrder chargingOrder) {
+ return chargingOrder != null
+ && Integer.valueOf(5).equals(chargingOrder.getStatus())
+ && Integer.valueOf(2).equals(chargingOrder.getRechargePaymentStatus())
+ && Integer.valueOf(2).equals(chargingOrder.getRefundStatus())
+ && getChargingOrderShareBaseAmount(chargingOrder).compareTo(BigDecimal.ZERO) > 0;
+ }
+
+ private Partner loadPartner(Site site) {
+ if (site == null || site.getPartnerId() == null) {
+ return null;
+ }
+ R<Partner> partnerResult = partnerClient.getPartnerById(site.getPartnerId());
+ return partnerResult == null ? null : partnerResult.getData();
+ }
+
+ private List<TChargingOrderShareRecord> buildChargingOrderShareRecords(TChargingOrder chargingOrder, Site site, Partner partner) {
+ BigDecimal actualThirdPay = getActualThirdPayAmount(chargingOrder);
+ BalanceShareAmount balanceShareAmount = getBalanceShareAmount(chargingOrder);
+ BigDecimal actualBalancePay = balanceShareAmount.getPrincipalAmount();
+ BigDecimal actualTotalPay = actualThirdPay.add(actualBalancePay).setScale(2, RoundingMode.HALF_UP);
+ BigDecimal shareElectricityAmount = getChargingOrderShareElectricityAmount(chargingOrder);
+ BigDecimal shareServiceAmount = getChargingOrderShareServiceAmount(chargingOrder);
+ BigDecimal shareBaseAmount = shareElectricityAmount.add(shareServiceAmount).setScale(2, RoundingMode.HALF_UP);
+ if (actualTotalPay.compareTo(BigDecimal.ZERO) <= 0 && isWhitelistOrder(chargingOrder)) {
+ actualTotalPay = shareBaseAmount;
+ }
+ if (actualTotalPay.compareTo(BigDecimal.ZERO) <= 0) {
+ return Collections.emptyList();
+ }
+ if (!isWhitelistOrder(chargingOrder) && shareBaseAmount.compareTo(BigDecimal.ZERO) > 0 && actualTotalPay.compareTo(shareBaseAmount) < 0) {
+ BigDecimal effectiveRatio = actualTotalPay.divide(shareBaseAmount, 6, RoundingMode.HALF_UP);
+ shareElectricityAmount = shareElectricityAmount.multiply(effectiveRatio).setScale(2, RoundingMode.HALF_UP);
+ shareServiceAmount = shareServiceAmount.multiply(effectiveRatio).setScale(2, RoundingMode.HALF_UP);
+ }
+ List<TChargingOrderShareRecord> records = new ArrayList<>();
+ if (actualBalancePay.compareTo(BigDecimal.ZERO) > 0) {
+ records.add(buildChargingOrderShareRecord(chargingOrder, site, partner, "BALANCE", actualBalancePay,
+ balanceShareAmount.getGiftAmount(), actualTotalPay, shareElectricityAmount, shareServiceAmount));
+ }
+ if (actualThirdPay.compareTo(BigDecimal.ZERO) > 0) {
+ records.add(buildChargingOrderShareRecord(chargingOrder, site, partner, resolveThirdPayChannel(chargingOrder), actualThirdPay,
+ BigDecimal.ZERO, actualTotalPay, shareElectricityAmount, shareServiceAmount));
+ }
+ if (records.isEmpty() && isWhitelistOrder(chargingOrder)) {
+ records.add(buildChargingOrderShareRecord(chargingOrder, site, partner, "WHITELIST", actualTotalPay,
+ BigDecimal.ZERO, actualTotalPay, shareElectricityAmount, shareServiceAmount));
+ }
+ return records;
+ }
+
+ private TChargingOrderShareRecord buildChargingOrderShareRecord(TChargingOrder chargingOrder, Site site, Partner partner,
+ String payChannel, BigDecimal channelPayAmount, BigDecimal channelGiftAmount,
+ BigDecimal actualTotalPay, BigDecimal shareElectricityAmount,
+ BigDecimal shareServiceAmount) {
+ TChargingOrderShareRecord record = new TChargingOrderShareRecord();
+ record.setChargingOrderId(chargingOrder.getId());
+ record.setChargingOrderCode(chargingOrder.getCode());
+ record.setSiteId(chargingOrder.getSiteId());
+ record.setPartnerId(site == null ? null : site.getPartnerId());
+ record.setPayChannel(payChannel);
+ record.setChannelPayAmount(money(channelPayAmount));
+ record.setChannelGiftAmount(money(channelGiftAmount));
+ BigDecimal channelRatio = channelPayAmount.divide(actualTotalPay, 6, RoundingMode.HALF_UP);
+ record.setChannelPayRatio(channelRatio);
+ BigDecimal channelElectricityAmount = shareElectricityAmount.multiply(channelRatio).setScale(2, RoundingMode.HALF_UP);
+ BigDecimal channelServiceAmount = shareServiceAmount.multiply(channelRatio).setScale(2, RoundingMode.HALF_UP);
+ record.setChannelElectricityAmount(channelElectricityAmount);
+ record.setChannelServiceAmount(channelServiceAmount);
+ record.setElectricityShareRatio(partner == null ? null : partner.getElectricityShareRatio());
+ record.setServiceShareRatio(partner == null ? null : partner.getServiceShareRatio());
+ BigDecimal electricityShareAmount = calculateShareAmount(channelElectricityAmount, record.getElectricityShareRatio());
+ BigDecimal serviceShareAmount = calculateShareAmount(channelServiceAmount, record.getServiceShareRatio());
+ record.setElectricityShareAmount(electricityShareAmount);
+ record.setServiceShareAmount(serviceShareAmount);
+ record.setShareAmount(electricityShareAmount.add(serviceShareAmount).setScale(2, RoundingMode.HALF_UP));
+ record.setNeedOnlineShare("ICBC".equals(payChannel) && record.getShareAmount().compareTo(BigDecimal.ZERO) > 0);
+ record.setOnlineShareStatus(Boolean.TRUE.equals(record.getNeedOnlineShare()) ? "INIT" : "RECORDED");
+ if ("ICBC".equals(payChannel)) {
+ record.setSeqNo(chargingOrder.getCode());
+ record.setBusiType("1");
+ record.setOriTrxDate(chargingOrder.getPayTime() == null ? LocalDate.now() : chargingOrder.getPayTime().toLocalDate());
+ record.setSubOrderId(chargingOrder.getCode());
+ }
+ if ("ICBC".equals(payChannel) && partner != null) {
+ record.setSubMerId(partner.getIcbcSubMerId());
+ record.setSubMerPrtclNo(partner.getIcbcSubMerPrtclNo());
+ record.setMerAcct(partner.getIcbcMerAcct());
+ }
+ record.setRetryCount(0);
+ record.setDelFlag(false);
+ return record;
+ }
+
+ private TChargingOrderShareRecord mergeChargingOrderShareRecord(TChargingOrderShareRecord exist, TChargingOrderShareRecord fresh) {
+ if (exist == null) {
+ return fresh;
+ }
+ fresh.setId(exist.getId());
+ fresh.setRetryCount(exist.getRetryCount() == null ? 0 : exist.getRetryCount());
+ fresh.setRequestJson(exist.getRequestJson());
+ fresh.setResponseJson(exist.getResponseJson());
+ fresh.setIcbcOrderId(exist.getIcbcOrderId());
+ fresh.setSubOrderSeq(exist.getSubOrderSeq());
+ fresh.setSubOrderTrxid(exist.getSubOrderTrxid());
+ fresh.setRecResult(exist.getRecResult());
+ fresh.setLastRetryTime(exist.getLastRetryTime());
+ return fresh;
+ }
+
+ private BigDecimal getActualThirdPayAmount(TChargingOrder chargingOrder) {
+ return getRefundableThirdPayAmount(chargingOrder).subtract(money(chargingOrder.getThirdRefundAmount())).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal getActualBalancePayAmount(TChargingOrder chargingOrder) {
+ return money(chargingOrder.getBalancePayAmount()).subtract(money(chargingOrder.getBalanceRefundAmount())).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private BalanceShareAmount getBalanceShareAmount(TChargingOrder chargingOrder) {
+ if (chargingOrder == null || chargingOrder.getId() == null) {
+ return new BalanceShareAmount(BigDecimal.ZERO, BigDecimal.ZERO);
+ }
+ BigDecimal fallbackBalancePay = getActualBalancePayAmount(chargingOrder);
+ if (fallbackBalancePay.compareTo(BigDecimal.ZERO) <= 0) {
+ return new BalanceShareAmount(BigDecimal.ZERO, BigDecimal.ZERO);
+ }
+ List<Map<String, Object>> rows = this.baseMapper.exchangeCodeBalanceIncomeByOrderIds(Collections.singletonList(chargingOrder.getId()));
+ if (rows == null || rows.isEmpty()) {
+ return new BalanceShareAmount(fallbackBalancePay, BigDecimal.ZERO);
+ }
+ Map<String, Object> row = rows.get(0);
+ BigDecimal principalAmount = getSummaryDecimal(row, "balanceIncome").max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
+ BigDecimal giftAmount = getSummaryDecimal(row, "balanceGiftIncome").max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
+ return new BalanceShareAmount(principalAmount, giftAmount);
+ }
+
+ private BigDecimal getChargingOrderShareElectricityAmount(TChargingOrder chargingOrder) {
+ if (chargingOrder == null) {
+ return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ }
+ return money(chargingOrder.getElectrovalence()).add(money(chargingOrder.getWhitelistElectricRefundAmount())).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal getChargingOrderShareServiceAmount(TChargingOrder chargingOrder) {
+ if (chargingOrder == null) {
+ return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ }
+ return money(chargingOrder.getServiceCharge()).add(money(chargingOrder.getWhitelistServiceRefundAmount())).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal getChargingOrderShareBaseAmount(TChargingOrder chargingOrder) {
+ return getChargingOrderShareElectricityAmount(chargingOrder).add(getChargingOrderShareServiceAmount(chargingOrder)).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private String resolveThirdPayChannel(TChargingOrder chargingOrder) {
+ if ("ICBC".equals(chargingOrder.getSourceName())) {
+ return "ICBC";
+ }
+ if (Integer.valueOf(1).equals(chargingOrder.getRechargePaymentType())) {
+ return "WECHAT";
+ }
+ if (Integer.valueOf(2).equals(chargingOrder.getRechargePaymentType()) || Integer.valueOf(3).equals(chargingOrder.getRechargePaymentType())) {
+ return "ALIPAY";
+ }
+ return "OTHER";
+ }
+
+ private void processIcbcOnlineShare(TChargingOrderShareRecord record, Partner partner) {
+ try {
+ String invalidReason = validateIcbcShareRecord(record, partner);
+ if (StringUtils.hasLength(invalidReason)) {
+ markChargingOrderShare(record, "FAIL", invalidReason);
+ return;
+ }
+ IcbcSubOrderSplitReq req = buildIcbcSubOrderSplitReq(record);
+ record.setRequestJson(JSON.toJSONString(req));
+ record.setOnlineShareStatus("PROCESSING");
+ record.setRetryCount(record.getRetryCount() == null ? 1 : record.getRetryCount() + 1);
+ record.setLastRetryTime(LocalDateTime.now());
+ saveOrUpdateChargingOrderShareRecord(record);
+
+ R<IcbcSubOrderSplitResp> result = icbcPaymentClient.splitSubOrder(req);
+ IcbcSubOrderSplitResp resp = result == null ? null : result.getData();
+ record.setResponseJson(resp == null ? (result == null ? null : JSON.toJSONString(result)) : resp.getRawResponse());
+ if (result == null || result.getCode() != 200 || resp == null) {
+ markChargingOrderShare(record, "FAIL", result == null ? "工行拆分接口无响应" : result.getMsg());
+ return;
+ }
+ record.setIcbcOrderId(resp.getOrderId());
+ record.setSubOrderSeq(resp.getSubOrderSeq());
+ record.setSubOrderTrxid(resp.getSubOrderTrxid());
+ record.setRecResult(resp.getRecResult());
+ if ("SUCCESS".equals(resp.getStatus()) && ("0".equals(resp.getRecResult()) || !StringUtils.hasLength(resp.getRecResult()))) {
+ markChargingOrderShare(record, "SUCCESS", null);
+ } else {
+ markChargingOrderShare(record, "FAIL", "工行拆分未成功:" + firstNotBlank(resp.getRecResult(), resp.getStatus()));
+ }
+ } catch (Exception e) {
+ log.error("工行线上分账处理失败,订单号:{},渠道:{}", record == null ? null : record.getChargingOrderCode(), record == null ? null : record.getPayChannel(), e);
+ if (record != null) {
+ markChargingOrderShare(record, "FAIL", firstNotBlank(e.getMessage(), "工行线上分账处理异常"));
+ }
+ }
+ }
+
+ private void recordChargingOrderShareFailure(TChargingOrder chargingOrder, Site site, Exception e) {
+ try {
+ if (chargingOrder == null || !"ICBC".equals(chargingOrder.getSourceName()) || getActualThirdPayAmount(chargingOrder).compareTo(BigDecimal.ZERO) <= 0) {
+ return;
+ }
+ TChargingOrderShareRecord record = chargingOrderShareRecordMapper.selectOne(new LambdaQueryWrapper<TChargingOrderShareRecord>()
+ .eq(TChargingOrderShareRecord::getChargingOrderCode, chargingOrder.getCode())
+ .eq(TChargingOrderShareRecord::getPayChannel, "ICBC")
+ .eq(TChargingOrderShareRecord::getDelFlag, false)
+ .last("limit 1"));
+ if (record == null) {
+ BigDecimal actualThirdPay = getActualThirdPayAmount(chargingOrder);
+ BigDecimal actualTotalPay = actualThirdPay.add(getBalanceShareAmount(chargingOrder).getPrincipalAmount()).setScale(2, RoundingMode.HALF_UP);
+ BigDecimal shareElectricityAmount = getChargingOrderShareElectricityAmount(chargingOrder);
+ BigDecimal shareServiceAmount = getChargingOrderShareServiceAmount(chargingOrder);
+ record = buildChargingOrderShareRecord(chargingOrder, site, null, "ICBC", actualThirdPay, BigDecimal.ZERO,
+ actualTotalPay.compareTo(BigDecimal.ZERO) > 0 ? actualTotalPay : actualThirdPay, shareElectricityAmount, shareServiceAmount);
+ }
+ record.setNeedOnlineShare(true);
+ markChargingOrderShare(record, "FAIL", firstNotBlank(e.getMessage(), "充电订单分账记录触发异常"));
+ } catch (Exception ex) {
+ log.error("充电订单分账失败补偿记录落库失败,订单号:{}", chargingOrder == null ? null : chargingOrder.getCode(), ex);
+ }
+ }
+
+ private String validateIcbcShareRecord(TChargingOrderShareRecord record, Partner partner) {
+ if (partner == null) {
+ return "站点未配置合作商,无法工行拆分";
+ }
+ if (partner.getElectricityShareRatio() == null && partner.getServiceShareRatio() == null) {
+ return "合作商未配置分成比例,跳过工行拆分";
+ }
+ if (!StringUtils.hasLength(partner.getIcbcSubMerId())) {
+ return "合作商未配置工行二级商户编号";
+ }
+ if (!StringUtils.hasLength(partner.getIcbcSubMerPrtclNo())) {
+ return "合作商未配置工行二级商户协议编号";
+ }
+ if (money(record.getShareAmount()).compareTo(BigDecimal.ZERO) <= 0) {
+ return "工行拆分金额为0";
+ }
+ return null;
+ }
+
+ private BigDecimal calculateShareAmount(BigDecimal amount, BigDecimal ratio) {
+ if (amount == null || ratio == null) {
+ return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ }
+ return amount.multiply(ratio).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
+ }
+
+ private IcbcSubOrderSplitReq buildIcbcSubOrderSplitReq(TChargingOrderShareRecord record) {
+ IcbcSubOrderSplitReq req = new IcbcSubOrderSplitReq();
+ req.setChargingOrderCode(record.getChargingOrderCode());
+ req.setSeqNo(record.getSeqNo());
+ req.setBusiType(record.getBusiType());
+ req.setOriMerId(record.getOriMerId());
+ req.setOriTrxDate(record.getOriTrxDate() == null ? null : record.getOriTrxDate().toString());
+ req.setSubOrderId(record.getSubOrderId());
+ req.setSubMerId(record.getSubMerId());
+ req.setSubMerPrtclNo(record.getSubMerPrtclNo());
+ req.setClassifyAmt(record.getShareAmount());
+ req.setMerAcct(record.getMerAcct());
+ req.setMerSpInfor("充电订单分账:" + record.getChargingOrderCode());
+ return req;
+ }
+
+ private void markChargingOrderShare(TChargingOrderShareRecord record, String status, String reason) {
+ record.setOnlineShareStatus(status);
+ record.setFailReason(reason);
+ saveOrUpdateChargingOrderShareRecord(record);
+ }
+
+ private void saveOrUpdateChargingOrderShareRecord(TChargingOrderShareRecord record) {
+ if (record.getId() == null) {
+ chargingOrderShareRecordMapper.insert(record);
+ } else {
+ chargingOrderShareRecordMapper.updateById(record);
+ }
+ }
+
+ private String buildIcbcProfitSharing(Site site) {
+ if (site == null || site.getPartnerId() == null) {
+ return null;
+ }
+ R<Partner> partnerResult = partnerClient.getPartnerById(site.getPartnerId());
+ Partner partner = partnerResult == null ? null : partnerResult.getData();
+ if (partner == null || !StringUtils.hasLength(partner.getIcbcSubMerId()) || !StringUtils.hasLength(partner.getIcbcSubMerPrtclNo())) {
+ return null;
+ }
+ if (partner.getElectricityShareRatio() == null && partner.getServiceShareRatio() == null) {
+ return null;
+ }
+ JSONObject data = new JSONObject();
+ data.put("icbcPartnerId", partner.getIcbcPartnerId());
+ data.put("icbcSubMerId", partner.getIcbcSubMerId());
+ data.put("icbcSubMerPrtclNo", partner.getIcbcSubMerPrtclNo());
+ data.put("icbcMerAcct", partner.getIcbcMerAcct());
+ if (partner.getElectricityShareRatio() != null) {
+ data.put("electricityShareRatio", partner.getElectricityShareRatio());
+ }
+ if (partner.getServiceShareRatio() != null) {
+ data.put("serviceShareRatio", partner.getServiceShareRatio());
+ }
+ return data.toJSONString();
+ }
+
+ private static class BalanceShareAmount {
+ private final BigDecimal principalAmount;
+ private final BigDecimal giftAmount;
+
+ private BalanceShareAmount(BigDecimal principalAmount, BigDecimal giftAmount) {
+ this.principalAmount = principalAmount == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : principalAmount.setScale(2, RoundingMode.HALF_UP);
+ this.giftAmount = giftAmount == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : giftAmount.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal getPrincipalAmount() {
+ return principalAmount;
+ }
+
+ private BigDecimal getGiftAmount() {
+ return giftAmount;
+ }
+ }
+
+ private String firstNotBlank(String... values) {
+ for (String value : values) {
+ if (StringUtils.hasLength(value)) {
+ return value;
+ }
+ }
+ return "";
+ }
}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/ChargingSimulationVO.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/ChargingSimulationVO.java
new file mode 100644
index 0000000..922ca1b
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/ChargingSimulationVO.java
@@ -0,0 +1,25 @@
+package com.ruoyi.order.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class ChargingSimulationVO {
+ private Long orderId;
+ private String orderCode;
+ private Integer orderStatus;
+ private BigDecimal rechargeAmount;
+ private BigDecimal chargeAmount;
+ private BigDecimal electricityFee;
+ private BigDecimal serviceFee;
+ private BigDecimal paymentAmount;
+ private BigDecimal refundAmount;
+ private Integer refundStatus;
+ private BigDecimal shareAmount;
+ private String shareStatus;
+ private String icbcOrderId;
+ private String subOrderId;
+ private String subOrderSeq;
+ private String subOrderTrxid;
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/IcbcAdminSubOrderVO.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/IcbcAdminSubOrderVO.java
new file mode 100644
index 0000000..83c1dda
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/vo/IcbcAdminSubOrderVO.java
@@ -0,0 +1,45 @@
+package com.ruoyi.order.vo;
+
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Data
+public class IcbcAdminSubOrderVO {
+ private Long shareRecordId;
+ private String orderCode;
+ private BigDecimal shareAmount;
+ private String onlineShareStatus;
+ private String subOrderId;
+ private String subMerId;
+ private String subMerPrtclNo;
+ private LocalDate oriTrxDate;
+ private String queryStatus;
+ private String bankOrderStatus;
+ private String bankOrderId;
+ private String subOrderSeq;
+ private String subOrderTrxid;
+ private String bankOrderAmount;
+ private String bankRefundAmount;
+ private String rawResponse;
+ private String errorMessage;
+
+ public void applyQuery(IcbcSubOrderQueryResp response) {
+ if (response == null) {
+ queryStatus = "FAIL";
+ errorMessage = "工行子订单查询无响应";
+ return;
+ }
+ queryStatus = response.getStatus();
+ bankOrderStatus = response.getOrderStatus();
+ bankOrderId = response.getOrderId();
+ bankOrderAmount = response.getOrderAmnt();
+ bankRefundAmount = response.getOrderRefundAmnt();
+ rawResponse = response.getRawResponse();
+ if (!"SUCCESS".equals(response.getStatus())) {
+ errorMessage = response.getRawResponse();
+ }
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/main/resources/bootstrap.yml b/ruoyi-service/ruoyi-order/src/main/resources/bootstrap.yml
index 60d6be1..f157478 100644
--- a/ruoyi-service/ruoyi-order/src/main/resources/bootstrap.yml
+++ b/ruoyi-service/ruoyi-order/src/main/resources/bootstrap.yml
@@ -12,8 +12,8 @@
allow-circular-references: true
profiles:
# 环境配置
+# active: dev
active: dev
-# active: prod
---
spring:
config:
diff --git a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingBillMapper.xml b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingBillMapper.xml
index 344a768..da29af7 100644
--- a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingBillMapper.xml
+++ b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingBillMapper.xml
@@ -43,6 +43,9 @@
<if test="req.uid != null ">
and t1.id = #{req.uid}
</if>
+ <if test="req.payType != null and req.payType != 0">
+ and t1.payType = #{req.payType}
+ </if>
<if test="null != req.siteIds and req.siteIds.size()>0" >
and t1.site_id in
<foreach collection="req.siteIds" close=")" open="(" item="item" separator=",">
@@ -93,6 +96,9 @@
<if test="req.uid != null ">
and t1.id = #{req.uid}
</if>
+ <if test="req.payType != null and req.payType != 0">
+ and t1.payType = #{req.payType}
+ </if>
<if test="null != req.siteIds and req.siteIds.size()>0" >
and t1.site_id in
<foreach collection="req.siteIds" close=")" open="(" item="item" separator=",">
diff --git a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
index cd8128d..578d252 100644
--- a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
+++ b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TChargingOrderMapper.xml
@@ -387,6 +387,36 @@
</select>
<select id="charge" resultType="com.ruoyi.order.dto.SixChargingDto">
+ <choose>
+ <when test="partnerView != null and partnerView">
+ SELECT
+ DATE_FORMAT(a.create_time, '%m') AS MONTH,
+ ROUND(SUM(IFNULL(s.electricity_share_amount, 0)), 2) AS electrovalence,
+ ROUND(SUM(IFNULL(s.service_share_amount, 0)), 2) AS service_charge
+ FROM t_charging_order a
+ LEFT JOIN (
+ SELECT charging_order_id,
+ SUM(IFNULL(electricity_share_amount, 0)) AS electricity_share_amount,
+ SUM(IFNULL(service_share_amount, 0)) AS service_share_amount
+ FROM t_charging_order_share_record
+ WHERE del_flag = 0 AND charging_order_id IS NOT NULL
+ GROUP BY charging_order_id
+ ) s ON a.id = s.charging_order_id
+ <where>
+ a.del_flag = 0 and a.recharge_payment_status = 2 and a.status = 5
+ <if test="sixBefore != null">
+ AND a.create_time >= #{sixBefore}
+ </if>
+ <if test="siteIds != null and siteIds.size() > 0">
+ AND a.site_id IN
+ <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+ #{siteId}
+ </foreach>
+ </if>
+ </where>
+ GROUP BY DATE_FORMAT(a.create_time, '%m')
+ </when>
+ <otherwise>
SELECT
DATE_FORMAT(a.create_time, '%m') AS MONTH,
SUM(IFNULL(b.period_electric_price, 0)) AS electrovalence,
@@ -414,10 +444,39 @@
</where>
GROUP BY
DATE_FORMAT(a.create_time, '%m')
+ </otherwise>
+ </choose>
</select>
<select id="circle" resultType="com.ruoyi.order.dto.SixCircleDto">
+ <choose>
+ <when test="partnerView != null and partnerView">
+ SELECT
+ a.site_id as siteId,
+ ROUND(SUM(IFNULL(s.share_amount, 0)), 2) AS paymentAmount
+ FROM t_charging_order a
+ LEFT JOIN (
+ SELECT charging_order_id, SUM(IFNULL(share_amount, 0)) AS share_amount
+ FROM t_charging_order_share_record
+ WHERE del_flag = 0 AND charging_order_id IS NOT NULL
+ GROUP BY charging_order_id
+ ) s ON a.id = s.charging_order_id
+ <where>
+ a.del_flag = 0 and a.recharge_payment_status = 2 and a.status = 5
+ <if test="siteIds != null and siteIds.size() > 0">
+ AND a.site_id IN
+ <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+ #{siteId}
+ </foreach>
+ </if>
+ <if test="sixBefore != null">
+ AND a.create_time >= #{sixBefore}
+ </if>
+ </where>
+ GROUP BY a.site_id
+ </when>
+ <otherwise>
SELECT
a.site_id as siteId,
ROUND(SUM(CASE
@@ -441,26 +500,44 @@
</if>
</where>
GROUP BY a.site_id
+ </otherwise>
+ </choose>
</select>
<select id="countAll" resultType="java.util.Map">
SELECT
+ <choose>
+ <when test="partnerView != null and partnerView">
+ ROUND(SUM(IFNULL(s.electricity_share_amount, 0)), 2) AS electrovalence,
+ ROUND(SUM(IFNULL(s.service_share_amount, 0)), 2) AS service_charge,
+ ROUND(SUM(IFNULL(s.share_amount, 0)), 2) AS commission_amount,
+ 0 AS sharing_amount
+ </when>
+ <otherwise>
ROUND(SUM(IFNULL(b.period_electric_price, 0)), 2) AS electrovalence,
- ROUND(SUM(CASE
- WHEN a.order_source = 2 THEN IFNULL(b.period_service_price, 0) * 0.8
- ELSE IFNULL(b.period_service_price, 0)
- END), 2) AS service_charge,
- ROUND(SUM(CASE
- WHEN a.order_source = 2 THEN 0
- ELSE IFNULL(b.period_electric_price, 0) + IFNULL(b.period_service_price, 0)
- END) * 0.006, 2) AS commission_amount,
- ROUND(SUM(CASE
- WHEN a.order_source = 2 THEN IFNULL(b.period_service_price, 0) * 0.2
- ELSE 0
- END), 2) AS sharing_amount
+ ROUND(SUM(CASE WHEN a.order_source = 2 THEN IFNULL(b.period_service_price, 0) * 0.8 ELSE IFNULL(b.period_service_price, 0) END), 2) AS service_charge,
+ ROUND(SUM(IFNULL(s.share_amount, 0)), 2) AS commission_amount,
+ ROUND(SUM(CASE WHEN a.order_source = 2 THEN IFNULL(b.period_service_price, 0) * 0.2 ELSE 0 END), 2) AS sharing_amount
+ </otherwise>
+ </choose>
FROM t_charging_order a
- LEFT JOIN t_charging_order_accounting_strategy b ON a.id = b.charging_order_id
+ LEFT JOIN (
+ SELECT charging_order_id,
+ SUM(IFNULL(period_electric_price, 0)) AS period_electric_price,
+ SUM(IFNULL(period_service_price, 0)) AS period_service_price
+ FROM t_charging_order_accounting_strategy
+ GROUP BY charging_order_id
+ ) b ON a.id = b.charging_order_id
+ LEFT JOIN (
+ SELECT charging_order_id,
+ SUM(IFNULL(electricity_share_amount, 0)) AS electricity_share_amount,
+ SUM(IFNULL(service_share_amount, 0)) AS service_share_amount,
+ SUM(IFNULL(share_amount, 0)) AS share_amount
+ FROM t_charging_order_share_record
+ WHERE del_flag = 0 AND charging_order_id IS NOT NULL
+ GROUP BY charging_order_id
+ ) s ON a.id = s.charging_order_id
WHERE a.del_flag = 0 AND a.recharge_payment_status = 2 AND a.status = 5
<if test="siteIds != null and siteIds.size() > 0">
AND a.site_id IN
@@ -1029,6 +1106,8 @@
a.start_charge_seq as startChargeSeq,
a.charging_gun_id as chargingGunId,
a.order_source as orderSource,
+ a.source_name as sourceName,
+ a.recharge_serial_number as rechargeSerialNumber,
a.`code`,
a.order_type as orderType,
d.`name` as terminalName,
@@ -1257,6 +1336,8 @@
<select id="chargingListSum" resultType="map">
select
a.id,
+ a.siteId,
+ a.order_source as orderSource,
sum(a.charging_capacity) as electronic,
sum(a.period_electric_price) as electrovalence,
sum(CASE WHEN a.order_source = 2 THEN a.period_service_price * 0.8 ELSE a.period_service_price END) as serviceCharge,
@@ -1274,7 +1355,7 @@
sum(CASE WHEN a.type = 3 THEN a.charging_capacity ELSE 0 END) as ping,
sum(CASE WHEN a.type = 4 THEN a.charging_capacity ELSE 0 END) as gu
from (
- select co.id,coas.type, coas.charging_capacity, coas.period_electric_price, coas.period_service_price, co.order_source
+ select co.id, co.site_id as siteId, coas.type, coas.charging_capacity, coas.period_electric_price, coas.period_service_price, co.order_source
from t_charging_order_accounting_strategy coas
left join t_charging_order co on (coas.charging_order_id = co.id)
where co.del_flag = 0 and co.recharge_payment_status = 2
@@ -1321,7 +1402,7 @@
and co.end_time between #{endTime1} and #{endTime2}
</if>
and co.start_time IS NOT NULL and co.end_time is not null
- ) as a group by a.id
+ ) as a group by a.id, a.siteId, a.order_source
</select>
@@ -2231,6 +2312,59 @@
) as aa
</select>
+ <select id="chargingOrderShareIncomeByOrderIds" resultType="map">
+ select
+ charging_order_id as orderId,
+ sum(IFNULL(channel_pay_amount, 0) - IFNULL(share_amount, 0)) as platformIncome,
+ sum(IFNULL(share_amount, 0)) as partnerIncome
+ from `charging_pile_order`.t_charging_order_share_record
+ where del_flag = 0 and charging_order_id is not null
+ <if test="orderIds != null and orderIds.size()>0">
+ and charging_order_id in
+ <foreach collection="orderIds" item="item" index="index" separator="," open="(" close=")">
+ #{item}
+ </foreach>
+ </if>
+ group by charging_order_id
+ </select>
+
+ <select id="chargingOrderShareIncomeByPayChannel" resultType="map">
+ select
+ s.pay_channel as payChannel,
+ sum(IFNULL(s.share_amount, 0)) as shareAmount
+ from t_charging_order_share_record s
+ inner join t_charging_order o on s.charging_order_id = o.id
+ where s.del_flag = 0
+ and s.charging_order_id is not null
+ and o.del_flag = 0
+ and o.recharge_payment_status = 2
+ and o.status = 5
+ <if test="sixBefore != null">
+ and o.create_time >= #{sixBefore}
+ </if>
+ <if test="siteIds != null and siteIds.size() > 0">
+ and o.site_id in
+ <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+ #{siteId}
+ </foreach>
+ </if>
+ group by s.pay_channel
+ </select>
+
+ <select id="chargingOrderShareIncomeSummaryByOrderIds" resultType="map">
+ select
+ sum(IFNULL(channel_pay_amount, 0) - IFNULL(share_amount, 0)) as platformIncomeTotal,
+ sum(IFNULL(share_amount, 0)) as partnerIncomeTotal
+ from `charging_pile_order`.t_charging_order_share_record
+ where del_flag = 0 and charging_order_id is not null
+ <if test="orderIds != null and orderIds.size()>0">
+ and charging_order_id in
+ <foreach collection="orderIds" item="item" index="index" separator="," open="(" close=")">
+ #{item}
+ </foreach>
+ </if>
+ </select>
+
<select id="chargingOrderSummaryOrderIds" resultType="java.lang.Long">
select b.id
from `charging_pile_order`.t_charging_order b
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingSimulationSupportTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingSimulationSupportTest.java
new file mode 100644
index 0000000..13d2754
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/ChargingSimulationSupportTest.java
@@ -0,0 +1,49 @@
+package com.ruoyi.order.service.impl;
+
+import com.ruoyi.order.api.model.AccountingStrategyDetailOrder;
+import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ChargingSimulationSupportTest {
+
+ @Test
+ void buildsOneKwhRecordForSelectedTariffPeriod() {
+ AccountingStrategyDetailOrder first = detail(1, 1, "00:00", "12:00");
+ AccountingStrategyDetailOrder second = detail(2, 3, "12:00", "23:59");
+ LocalDateTime endTime = LocalDateTime.of(2026, 7, 11, 15, 30, 0);
+
+ ChargingSimulationSupport.PreparedSimulation prepared = ChargingSimulationSupport.prepare(
+ "CD202607110001",
+ Arrays.asList(first, second),
+ new BigDecimal("0.03"),
+ new BigDecimal("0.02"),
+ endTime);
+
+ assertEquals(second, prepared.getTariff());
+ assertEquals(new BigDecimal("0.03"), second.getElectrovalence());
+ assertEquals(new BigDecimal("0.02"), second.getServiceCharge());
+ assertEquals(new BigDecimal("0.02"), second.getCostServiceCharge());
+
+ TransactionRecordMessageVO record = prepared.getRecord();
+ assertEquals("CD202607110001", record.getTransaction_serial_number());
+ assertEquals(BigDecimal.ONE, record.getTotal_electricity());
+ assertEquals(BigDecimal.ONE, record.getLoss_total_electricity());
+ assertEquals(BigDecimal.ONE, record.getFlat_charge2());
+ assertEquals(new BigDecimal("0.05"), record.getPayment_amount());
+ }
+
+ private AccountingStrategyDetailOrder detail(int id, int type, String start, String end) {
+ AccountingStrategyDetailOrder detail = new AccountingStrategyDetailOrder();
+ detail.setId(id);
+ detail.setType(type);
+ detail.setStartTime(start);
+ detail.setEndTime(end);
+ return detail;
+ }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java
new file mode 100644
index 0000000..41d9cb3
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/IcbcAdminFundOperationSupportTest.java
@@ -0,0 +1,159 @@
+package com.ruoyi.order.service.impl;
+
+import com.ruoyi.order.api.model.TChargingOrder;
+import com.ruoyi.order.api.model.TChargingOrderShareRecord;
+import com.ruoyi.payment.api.vo.IcbcRefundReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class IcbcAdminFundOperationSupportTest {
+
+ @Test
+ void rejectsNonIcbcOrder() {
+ TChargingOrder order = paidOrder();
+ order.setSourceName("ALIPAY");
+
+ IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
+ () -> IcbcAdminFundOperationSupport.validateOrder(order));
+
+ assertEquals("仅支持工商银行支付订单", error.getMessage());
+ }
+
+ @Test
+ void buildsMainOrderRefundFromBusinessOrder() {
+ IcbcRefundReq request = IcbcAdminFundOperationSupport.buildRefundRequest(
+ paidOrder(), new BigDecimal("0.12"), "ICBCADMINREFUND001");
+
+ assertEquals("CD202607110001", request.getOutTradeNo());
+ assertEquals("231011990056000012345", request.getOrderId());
+ assertEquals("ICBCADMINREFUND001", request.getOutRefundNo());
+ assertEquals(new BigDecimal("0.12"), request.getRefundAmount());
+ assertEquals("管理后台工行退款", request.getReason());
+ }
+
+ @Test
+ void buildsConsumptionSubOrderQueryFromShareRecord() {
+ IcbcSubOrderQueryReq request = IcbcAdminFundOperationSupport.buildSubOrderQueryRequest(
+ paidOrder(), shareRecord());
+
+ assertEquals("2", request.getOrderNoType());
+ assertEquals("CD202607110001", request.getMerOrderId());
+ assertEquals("231011990056", request.getPrtiId());
+ assertEquals("1", request.getQueryType());
+ assertEquals("ICBCSUB202607110001", request.getMerTrnscSerno());
+ assertEquals("231011990055", request.getSubPrtiId());
+ }
+
+ @Test
+ void buildsSubOrderRefundFromSelectedShareRecord() {
+ IcbcSubOrderRefundReq request = IcbcAdminFundOperationSupport.buildSubOrderRefundRequest(
+ paidOrder(), shareRecord(), new BigDecimal("0.03"),
+ "ICBCSUBREFUND001", "ICBCSUBREFUNDSERIAL001");
+
+ assertEquals("CD202607110001", request.getChargingOrderCode());
+ assertEquals("CD202607110001", request.getSeqNo());
+ assertEquals("1", request.getBusiType());
+ assertEquals("231011990056", request.getOriMerId());
+ assertEquals("2026-07-11", request.getOriTrxDate());
+ assertEquals("ICBCSUB202607110001", request.getSubOrderId());
+ assertEquals("231011990055", request.getSubMerId());
+ assertEquals("2310119900550201", request.getSubMerPrtclNo());
+ assertEquals(new BigDecimal("0.03"), request.getRefundAmount());
+ }
+
+ @Test
+ void rejectsAmountWithMoreThanTwoDecimalPlaces() {
+ IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
+ () -> IcbcAdminFundOperationSupport.requireAmount(new BigDecimal("0.004")));
+
+ assertEquals("操作金额最多保留两位小数", error.getMessage());
+ }
+
+ @Test
+ void rejectsDeletedOrUnsuccessfulShareRecord() {
+ TChargingOrderShareRecord deleted = shareRecord();
+ deleted.setDelFlag(true);
+ IllegalArgumentException deletedError = assertThrows(IllegalArgumentException.class,
+ () -> IcbcAdminFundOperationSupport.buildSubOrderRefundRequest(
+ paidOrder(), deleted, new BigDecimal("0.01"), "REFUND1", "SERIAL1"));
+ assertEquals("分账记录已删除", deletedError.getMessage());
+
+ TChargingOrderShareRecord failed = shareRecord();
+ failed.setOnlineShareStatus("FAIL");
+ IllegalArgumentException failedError = assertThrows(IllegalArgumentException.class,
+ () -> IcbcAdminFundOperationSupport.buildSubOrderRefundRequest(
+ paidOrder(), failed, new BigDecimal("0.01"), "REFUND2", "SERIAL2"));
+ assertEquals("仅支持已成功的工行分账记录退款", failedError.getMessage());
+ }
+
+ @Test
+ void rejectsIncompleteSuccessfulShareRecord() {
+ TChargingOrderShareRecord record = shareRecord();
+ record.setSubMerPrtclNo(null);
+
+ IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
+ () -> IcbcAdminFundOperationSupport.buildSubOrderRefundRequest(
+ paidOrder(), record, new BigDecimal("0.01"), "REFUND3", "SERIAL3"));
+
+ assertEquals("分账记录缺少工行退款参数", error.getMessage());
+ }
+
+ @Test
+ void reusesUnknownRefundSerialOnlyForTheSameAmount() {
+ String pending = IcbcAdminFundOperationSupport.buildPendingRefundValue(
+ "ICBCADMINREFUND001", new BigDecimal("0.04"));
+
+ assertEquals("ICBCADMINREFUND001", IcbcAdminFundOperationSupport.resolveRefundNo(
+ pending, new BigDecimal("0.04"), "ICBCADMINREFUND002"));
+
+ IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
+ () -> IcbcAdminFundOperationSupport.resolveRefundNo(
+ pending, new BigDecimal("0.03"), "ICBCADMINREFUND003"));
+ assertEquals("上一笔退款结果未知,请按原金额重试", error.getMessage());
+ }
+
+ @Test
+ void keepsUnknownRetrySerialUntilAConfirmedSuccess() {
+ assertFalse(IcbcAdminFundOperationSupport.shouldClearPendingRefund(true, "FAIL", true));
+ assertTrue(IcbcAdminFundOperationSupport.shouldClearPendingRefund(false, "FAIL", true));
+ assertTrue(IcbcAdminFundOperationSupport.shouldClearPendingRefund(true, "SUCCESS", true));
+ assertFalse(IcbcAdminFundOperationSupport.shouldClearPendingRefund(false, "FAIL", false));
+ }
+
+ private TChargingOrder paidOrder() {
+ TChargingOrder order = new TChargingOrder();
+ order.setId(100L);
+ order.setCode("CD202607110001");
+ order.setSourceName("ICBC");
+ order.setRechargePaymentStatus(2);
+ order.setRechargeSerialNumber("231011990056000012345");
+ return order;
+ }
+
+ private TChargingOrderShareRecord shareRecord() {
+ TChargingOrderShareRecord record = new TChargingOrderShareRecord();
+ record.setId(200L);
+ record.setChargingOrderId(100L);
+ record.setChargingOrderCode("CD202607110001");
+ record.setPayChannel("ICBC");
+ record.setDelFlag(false);
+ record.setOnlineShareStatus("SUCCESS");
+ record.setSeqNo("CD202607110001");
+ record.setBusiType("1");
+ record.setOriMerId("231011990056");
+ record.setOriTrxDate(LocalDate.of(2026, 7, 11));
+ record.setSubOrderId("ICBCSUB202607110001");
+ record.setSubMerId("231011990055");
+ record.setSubMerPrtclNo("2310119900550201");
+ return record;
+ }
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/resources/bootstrap.yml b/ruoyi-service/ruoyi-other/src/main/resources/bootstrap.yml
index 8f7006c..3535253 100644
--- a/ruoyi-service/ruoyi-other/src/main/resources/bootstrap.yml
+++ b/ruoyi-service/ruoyi-other/src/main/resources/bootstrap.yml
@@ -13,7 +13,7 @@
profiles:
# 环境配置
# active: dev
- active: prod
+ active: dev
---
spring:
config:
@@ -23,7 +23,7 @@
nacos:
discovery:
# 服务注册地址
- server-addr: 192.168.110.169:8848
+ server-addr: 192.168.110.80:8848
service: ${spring.application.name}
group: DEFAULT_GROUP
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
@@ -31,7 +31,7 @@
password: nacos
config:
# 配置中心地址
- server-addr: 192.168.110.169:8848
+ server-addr: 192.168.110.80:8848
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
group: DEFAULT_GROUP
name: ${spring.application.name}
diff --git a/ruoyi-service/ruoyi-payment/lib/InfosecCrypto_Java1_02_JDK14+.jar b/ruoyi-service/ruoyi-payment/lib/InfosecCrypto_Java1_02_JDK14+.jar
new file mode 100644
index 0000000..353497a
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/lib/InfosecCrypto_Java1_02_JDK14+.jar
Binary files differ
diff --git a/ruoyi-service/ruoyi-payment/lib/commons-logging-1.2.jar b/ruoyi-service/ruoyi-payment/lib/commons-logging-1.2.jar
new file mode 100644
index 0000000..2cd945a
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/lib/commons-logging-1.2.jar
Binary files differ
diff --git a/ruoyi-service/ruoyi-payment/lib/hsm-software-share-1.0.5.jar b/ruoyi-service/ruoyi-payment/lib/hsm-software-share-1.0.5.jar
new file mode 100644
index 0000000..486426b
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/lib/hsm-software-share-1.0.5.jar
Binary files differ
diff --git a/ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop-io.jar b/ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop-io.jar
new file mode 100644
index 0000000..ca9738e
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop-io.jar
Binary files differ
diff --git a/ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop.jar b/ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop.jar
new file mode 100644
index 0000000..3c8a72e
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/lib/icbc-api-sdk-cop.jar
Binary files differ
diff --git a/ruoyi-service/ruoyi-payment/lib/icbc-ca.jar b/ruoyi-service/ruoyi-payment/lib/icbc-ca.jar
new file mode 100644
index 0000000..870b73b
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/lib/icbc-ca.jar
Binary files differ
diff --git a/ruoyi-service/ruoyi-payment/lib/readme.md b/ruoyi-service/ruoyi-payment/lib/readme.md
new file mode 100644
index 0000000..a86d9eb
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/lib/readme.md
@@ -0,0 +1,14 @@
+### jar包说明
+commons-logging-1.2.jar----------------与日志记录相关
+icbc-api-sdk-cop.jar-------------------sdk基础jar包
+icbc-api-sdk-cop-io.jar----------------sdk各接口输入输出
+icbc-ca.jar----------------------------SHA1&SHA256签名工具包
+InfosecCrypto_Java1_02_JDK14+.jar------SHA1&SHA256签名工具包依赖
+
+### jar包使用说明
+1.支持java版本1.5及以上
+2.icbc-api-sdk-cop.jar、icbc-api-sdk-cop-io.jar为必须jar包
+3.icbc-ca.jar、InfosecCrypto_Java1_02_JDK14+.jar工具包升级后兼容SHA256和SHA1算法。
+
+### 版本升级
+版本升级说明请参考update-info.md
\ No newline at end of file
diff --git a/ruoyi-service/ruoyi-payment/lib/update-info.md b/ruoyi-service/ruoyi-payment/lib/update-info.md
new file mode 100644
index 0000000..5674d9e
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/lib/update-info.md
@@ -0,0 +1,2 @@
+## 20201113
+1. 升级SHA256签名工具包icbc-ca.jar、InfosecCrypto_Java1_02_JDK14+.jar
\ No newline at end of file
diff --git a/ruoyi-service/ruoyi-payment/pom.xml b/ruoyi-service/ruoyi-payment/pom.xml
index eda3a8e..2b57139 100644
--- a/ruoyi-service/ruoyi-payment/pom.xml
+++ b/ruoyi-service/ruoyi-payment/pom.xml
@@ -133,6 +133,48 @@
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-api-order</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.icbc.sdk</groupId>
+ <artifactId>commons-logging</artifactId>
+ <version>1.2</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/lib/commons-logging-1.2.jar</systemPath>
+ </dependency>
+ <dependency>
+ <groupId>com.icbc.sdk</groupId>
+ <artifactId>hsm-software-share</artifactId>
+ <version>1.0.5</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/lib/hsm-software-share-1.0.5.jar</systemPath>
+ </dependency>
+ <dependency>
+ <groupId>com.icbc.sdk</groupId>
+ <artifactId>icbc-api-sdk-cop-io</artifactId>
+ <version>1.0.0</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/lib/icbc-api-sdk-cop-io.jar</systemPath>
+ </dependency>
+ <dependency>
+ <groupId>com.icbc.sdk</groupId>
+ <artifactId>icbc-api-sdk-cop</artifactId>
+ <version>1.0.0</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/lib/icbc-api-sdk-cop.jar</systemPath>
+ </dependency>
+ <dependency>
+ <groupId>com.icbc.sdk</groupId>
+ <artifactId>icbc-ca</artifactId>
+ <version>1.0.0</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/lib/icbc-ca.jar</systemPath>
+ </dependency>
+ <dependency>
+ <groupId>com.icbc.sdk</groupId>
+ <artifactId>infosec-crypto</artifactId>
+ <version>1.0.2</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/lib/InfosecCrypto_Java1_02_JDK14+.jar</systemPath>
+ </dependency>
</dependencies>
<build>
@@ -141,6 +183,9 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
+ <configuration>
+ <includeSystemScope>true</includeSystemScope>
+ </configuration>
<executions>
<execution>
<goals>
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java
new file mode 100644
index 0000000..52bdbb1
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java
@@ -0,0 +1,327 @@
+package com.ruoyi.payment.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.order.api.feignClient.ChargingOrderClient;
+import com.ruoyi.payment.api.vo.IcbcPaymentReq;
+import com.ruoyi.payment.api.vo.IcbcPaymentResp;
+import com.ruoyi.payment.api.vo.IcbcRefundReq;
+import com.ruoyi.payment.api.vo.IcbcRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
+import com.ruoyi.payment.icbc.config.IcbcPayProperties;
+import com.ruoyi.payment.icbc.service.IcbcPayService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedReader;
+import java.math.BigDecimal;
+import java.io.PrintWriter;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 工行统一支付入口。
+ */
+@Slf4j
+@RestController
+@RequestMapping("/icbc")
+public class IcbcPayController {
+ private static final String TEST_ORDER_PREFIX = "ICBCTEST";
+ private static final String TEST_SUB_MER_ID = "231011990055";
+ private static final String TEST_SUB_MER_PRTCL_NO = "2310119900550201";
+
+ @Resource
+ private IcbcPayService icbcPayService;
+
+ @Resource
+ private IcbcPayProperties icbcPayProperties;
+
+ @Resource
+ private ChargingOrderClient chargingOrderClient;
+
+ @PostMapping("/payment")
+ public R<IcbcPaymentResp> payment(@RequestBody IcbcPaymentReq req) {
+ log.info("工行支付请求入参:{}", JSON.toJSONString(req));
+ IcbcPaymentResp resp = icbcPayService.payment(req);
+ log.info("工行支付返回前端:{}", JSON.toJSONString(resp));
+ return R.ok(resp);
+ }
+
+ @PostMapping("/test/payment")
+ public R<IcbcPaymentResp> testPayment(@RequestBody(required = false) IcbcPaymentReq req) {
+ log.info("工行测试支付请求入参:{}", JSON.toJSONString(req));
+ IcbcPaymentReq paymentReq = req == null ? new IcbcPaymentReq() : req;
+ paymentReq.setOutTradeNo(TEST_ORDER_PREFIX + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
+ if (paymentReq.getAmount() == null || paymentReq.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
+ paymentReq.setAmount(new BigDecimal("0.01"));
+ }
+ if (paymentReq.getSubject() == null || paymentReq.getSubject().trim().isEmpty()) {
+ paymentReq.setSubject("工行测试支付");
+ }
+ if (paymentReq.getBody() == null || paymentReq.getBody().trim().isEmpty()) {
+ paymentReq.setBody("工行测试支付");
+ }
+ if (paymentReq.getPaymentType() == null) {
+ paymentReq.setPaymentType(1);
+ }
+ log.info("工行测试支付实际下单参数:{}", JSON.toJSONString(paymentReq));
+ IcbcPaymentResp resp = icbcPayService.payment(paymentReq);
+ log.info("工行测试支付返回前端:{}", JSON.toJSONString(resp));
+ return R.ok(resp);
+ }
+
+ @PostMapping("/test/query")
+ public R<IcbcPaymentResp> testQuery(@RequestBody Map<String, Object> req) {
+ String outTradeNo = getString(req, "outTradeNo");
+ log.info("工行测试查询请求入参:{}", JSON.toJSONString(req));
+ IcbcPaymentResp resp = icbcPayService.query(outTradeNo);
+ log.info("工行测试查询返回前端:{}", JSON.toJSONString(resp));
+ return R.ok(resp);
+ }
+
+ @PostMapping("/test/refund")
+ public R<IcbcRefundResp> testRefund(@RequestBody(required = false) IcbcRefundReq req) {
+ req = req == null ? new IcbcRefundReq() : req;
+ log.info("工行测试退款请求入参:{}", JSON.toJSONString(req));
+ if (req.getRefundAmount() == null || req.getRefundAmount().compareTo(BigDecimal.ZERO) <= 0) {
+ req.setRefundAmount(new BigDecimal("0.01"));
+ }
+ if (req.getTotalAmount() == null || req.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) {
+ req.setTotalAmount(req.getRefundAmount());
+ }
+ if (req.getOutRefundNo() == null || req.getOutRefundNo().trim().isEmpty()) {
+ req.setOutRefundNo("ICBCREFUND" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
+ }
+ if (req.getReason() == null || req.getReason().trim().isEmpty()) {
+ req.setReason("工行测试退款");
+ }
+ log.info("工行测试退款实际参数:{}", JSON.toJSONString(req));
+ IcbcRefundResp resp = icbcPayService.refund(req);
+ log.info("工行测试退款返回前端:{}", JSON.toJSONString(resp));
+ return R.ok(resp);
+ }
+
+ @PostMapping("/test/sub-order/split")
+ public R<IcbcSubOrderSplitResp> testSplitSubOrder(@RequestBody(required = false) IcbcSubOrderSplitReq req) {
+ req = req == null ? new IcbcSubOrderSplitReq() : req;
+ log.info("工行测试分账请求入参:{}", JSON.toJSONString(req));
+ if (req.getSubOrderId() == null || req.getSubOrderId().trim().isEmpty()) {
+ req.setSubOrderId("ICBCSUB" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
+ }
+ if (req.getSeqNo() == null || req.getSeqNo().trim().isEmpty()) {
+ req.setSeqNo(req.getChargingOrderCode());
+ }
+ if (req.getBusiType() == null || req.getBusiType().trim().isEmpty()) {
+ req.setBusiType("1");
+ }
+ if (req.getOriTrxDate() == null || req.getOriTrxDate().trim().isEmpty()) {
+ req.setOriTrxDate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
+ }
+ if (req.getClassifyAmt() == null || req.getClassifyAmt().compareTo(BigDecimal.ZERO) <= 0) {
+ req.setClassifyAmt(new BigDecimal("0.01"));
+ }
+ if (req.getSubMerId() == null || req.getSubMerId().trim().isEmpty()) {
+ req.setSubMerId(TEST_SUB_MER_ID);
+ }
+ if (req.getSubMerPrtclNo() == null || req.getSubMerPrtclNo().trim().isEmpty()) {
+ req.setSubMerPrtclNo(TEST_SUB_MER_PRTCL_NO);
+ }
+ if (req.getOriMerId() == null || req.getOriMerId().trim().isEmpty()) {
+ req.setOriMerId(icbcPayProperties.getMerId());
+ }
+ log.info("工行测试分账实际参数:{}", JSON.toJSONString(req));
+ IcbcSubOrderSplitResp resp = icbcPayService.splitSubOrder(req);
+ log.info("工行测试分账返回前端:{}", JSON.toJSONString(resp));
+ return R.ok(resp);
+ }
+
+ @PostMapping("/test/sub-order/refund")
+ public R<IcbcSubOrderRefundResp> testRefundSubOrder(@RequestBody(required = false) IcbcSubOrderRefundReq req) {
+ req = req == null ? new IcbcSubOrderRefundReq() : req;
+ log.info("工行测试分账退款请求入参:{}", JSON.toJSONString(req));
+ if (req.getSeqNo() == null || req.getSeqNo().trim().isEmpty()) {
+ req.setSeqNo(req.getChargingOrderCode());
+ }
+ if (req.getBusiType() == null || req.getBusiType().trim().isEmpty()) {
+ req.setBusiType("1");
+ }
+ if (req.getOriTrxDate() == null || req.getOriTrxDate().trim().isEmpty()) {
+ req.setOriTrxDate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
+ }
+ if (req.getRefundAmount() == null || req.getRefundAmount().compareTo(BigDecimal.ZERO) <= 0) {
+ req.setRefundAmount(new BigDecimal("0.01"));
+ }
+ if (req.getRetSubOrderId() == null || req.getRetSubOrderId().trim().isEmpty()) {
+ req.setRetSubOrderId("ICBCSUBREFUND" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
+ }
+ if (req.getOuttrxSerialNo() == null || req.getOuttrxSerialNo().trim().isEmpty()) {
+ req.setOuttrxSerialNo("ICBCSUBREFUNDNO" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
+ }
+ if (req.getSubMerId() == null || req.getSubMerId().trim().isEmpty()) {
+ req.setSubMerId(TEST_SUB_MER_ID);
+ }
+ if (req.getSubMerPrtclNo() == null || req.getSubMerPrtclNo().trim().isEmpty()) {
+ req.setSubMerPrtclNo(TEST_SUB_MER_PRTCL_NO);
+ }
+ if (req.getOriMerId() == null || req.getOriMerId().trim().isEmpty()) {
+ req.setOriMerId(icbcPayProperties.getMerId());
+ }
+ log.info("工行测试分账退款实际参数:{}", JSON.toJSONString(req));
+ IcbcSubOrderRefundResp resp = icbcPayService.refundSubOrder(req);
+ log.info("工行测试分账退款返回前端:{}", JSON.toJSONString(resp));
+ return R.ok(resp);
+ }
+
+ @PostMapping("/query")
+ public R<IcbcPaymentResp> query(@RequestParam("outTradeNo") String outTradeNo) {
+ return R.ok(icbcPayService.query(outTradeNo));
+ }
+
+ @PostMapping("/close")
+ public R<IcbcPaymentResp> close(@RequestParam("outTradeNo") String outTradeNo) {
+ return R.ok(icbcPayService.close(outTradeNo));
+ }
+
+ @PostMapping("/refund")
+ public R<IcbcRefundResp> refund(@RequestBody IcbcRefundReq req) {
+ return R.ok(icbcPayService.refund(req));
+ }
+
+ @PostMapping("/sub-order/split")
+ public R<IcbcSubOrderSplitResp> splitSubOrder(@RequestBody IcbcSubOrderSplitReq req) {
+ return R.ok(icbcPayService.splitSubOrder(req));
+ }
+
+ @PostMapping("/sub-order/refund")
+ public R<IcbcSubOrderRefundResp> refundSubOrder(@RequestBody IcbcSubOrderRefundReq req) {
+ return R.ok(icbcPayService.refundSubOrder(req));
+ }
+
+ @PostMapping("/sub-order/query")
+ public R<IcbcSubOrderQueryResp> querySubOrder(@RequestBody IcbcSubOrderQueryReq req) {
+ return R.ok(icbcPayService.querySubOrder(req));
+ }
+
+ @RequestMapping(value = "/callBack", method = {RequestMethod.GET, RequestMethod.POST})
+ public void callBack(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, String[]> requestParams = new LinkedHashMap<>(request.getParameterMap());
+ String rawBody = requestParams.isEmpty() ? readRequestBody(request) : "";
+ mergeJsonBody(requestParams, rawBody);
+ log.info("工行支付回调原始请求,method:{},uri:{},query:{},contentType:{},params:{},body:{},headers:{}",
+ request.getMethod(),
+ request.getRequestURI(),
+ request.getQueryString(),
+ request.getContentType(),
+ JSON.toJSONString(requestParams),
+ rawBody,
+ summarizeHeaders(request));
+ try {
+ Map<String, String> callback = icbcPayService.parseNotify(requestParams);
+ log.info("工行支付回调解析结果:{}", JSON.toJSONString(callback));
+ dispatchChargingOrderCallback(callback);
+ } catch (Exception e) {
+ log.error("工行支付回调处理失败,已记录原始请求并返回success避免重复通知", e);
+ } finally {
+ writeCallbackResponse(response);
+ }
+ }
+
+ private void dispatchChargingOrderCallback(Map<String, String> callback) {
+ String outTradeNo = callback.get("out_trade_no");
+ String transactionId = callback.get("transaction_id");
+ String attach = callback.get("attach");
+ if (outTradeNo != null && outTradeNo.startsWith(TEST_ORDER_PREFIX)) {
+ log.info("工行测试支付回调成功,订单号:{},流水号:{},支付类型:{}", outTradeNo, transactionId, attach);
+ return;
+ }
+ Integer paymentType = Integer.valueOf(attach);
+ if (Integer.valueOf(1).equals(paymentType)) {
+ chargingOrderClient.chargingOrderWXCallback(outTradeNo, transactionId, attach);
+ return;
+ }
+ if (Integer.valueOf(2).equals(paymentType)) {
+ chargingOrderClient.chargingOrderALICallback(outTradeNo, transactionId, attach);
+ return;
+ }
+ if (Integer.valueOf(3).equals(paymentType)) {
+ chargingOrderClient.chargingOrderALIH5Callback(outTradeNo, transactionId, attach);
+ return;
+ }
+ throw new IllegalArgumentException("工行回调支付类型不支持:" + attach);
+ }
+
+ private String getString(Map<String, Object> req, String key) {
+ if (req == null || req.get(key) == null) {
+ throw new IllegalArgumentException("缺少参数:" + key);
+ }
+ return String.valueOf(req.get(key));
+ }
+
+ private String readRequestBody(HttpServletRequest request) {
+ StringBuilder body = new StringBuilder();
+ try (BufferedReader reader = request.getReader()) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ body.append(line);
+ }
+ } catch (Exception e) {
+ log.warn("读取工行支付回调body失败", e);
+ }
+ return body.toString();
+ }
+
+ private void mergeJsonBody(Map<String, String[]> requestParams, String rawBody) {
+ if (rawBody == null || rawBody.trim().isEmpty()) {
+ return;
+ }
+ try {
+ JSONObject jsonObject = JSON.parseObject(rawBody);
+ if (jsonObject == null) {
+ return;
+ }
+ for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
+ if (entry.getValue() != null && !requestParams.containsKey(entry.getKey())) {
+ requestParams.put(entry.getKey(), new String[]{String.valueOf(entry.getValue())});
+ }
+ }
+ } catch (Exception e) {
+ log.warn("工行支付回调body不是JSON格式,body:{}", rawBody);
+ }
+ }
+
+ private String summarizeHeaders(HttpServletRequest request) {
+ Map<String, String> headers = new LinkedHashMap<>();
+ Enumeration<String> names = request.getHeaderNames();
+ while (names != null && names.hasMoreElements()) {
+ String name = names.nextElement();
+ if ("user-agent".equalsIgnoreCase(name)
+ || "host".equalsIgnoreCase(name)
+ || "x-forwarded-for".equalsIgnoreCase(name)
+ || "x-real-ip".equalsIgnoreCase(name)) {
+ headers.put(name, request.getHeader(name));
+ }
+ }
+ return JSON.toJSONString(headers);
+ }
+
+ private void writeCallbackResponse(HttpServletResponse response) throws Exception {
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("text/plain;charset=UTF-8");
+ PrintWriter writer = response.getWriter();
+ writer.println("success");
+ writer.flush();
+ writer.close();
+ }
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/domain/IcbcPaymentFlowRecord.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/domain/IcbcPaymentFlowRecord.java
new file mode 100644
index 0000000..11e8d2c
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/domain/IcbcPaymentFlowRecord.java
@@ -0,0 +1,31 @@
+package com.ruoyi.payment.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_icbc_payment_flow_record")
+public class IcbcPaymentFlowRecord {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private String outTradeNo;
+ private String businessType;
+ private String eventType;
+ private Integer paymentType;
+ private BigDecimal amount;
+ private String icbcOrderId;
+ private String icbcMsgId;
+ private String thirdTradeNo;
+ private String status;
+ private String requestJson;
+ private String responseJson;
+ private String errorMessage;
+ private LocalDateTime createTime;
+ private LocalDateTime updateTime;
+ private Integer delFlag;
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java
new file mode 100644
index 0000000..5ea37d0
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java
@@ -0,0 +1,39 @@
+package com.ruoyi.payment.icbc.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
+
+/**
+ * 工行支付配置,实际值从 Nacos 的 icbc.pay 前缀读取。
+ */
+@Data
+@Component
+@Configuration
+@ConfigurationProperties(prefix = "icbc.pay")
+public class IcbcPayProperties {
+ private String appId;
+ private String privateKey;
+ private String icbcPublicKey;
+ private String signType = "RSA2";
+ private String charset = "UTF-8";
+ private String format = "json";
+ private String gatewayUrl;
+ private String orderQueryUrl;
+ private String refundUrl;
+ private String subOrderSplitUrl;
+ private String subOrderRefundUrl;
+ private String subOrderQueryUrl;
+ private String notifyUrl;
+ private String merId;
+ private String merPrtclNo;
+ private String serviceMerId;
+ private String serviceMerPrtclNo;
+ private String tpAppId;
+ private String wxTpAppId;
+ private String aliTpAppId;
+ private String spbillCreateIp;
+ private String profitSharing;
+ private Boolean verifyNotifySign = true;
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPayService.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPayService.java
new file mode 100644
index 0000000..a7d3c43
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPayService.java
@@ -0,0 +1,35 @@
+package com.ruoyi.payment.icbc.service;
+
+import com.ruoyi.payment.api.vo.IcbcPaymentReq;
+import com.ruoyi.payment.api.vo.IcbcPaymentResp;
+import com.ruoyi.payment.api.vo.IcbcRefundReq;
+import com.ruoyi.payment.api.vo.IcbcRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
+
+import java.util.Map;
+
+/**
+ * 工行统一支付服务。
+ */
+public interface IcbcPayService {
+ IcbcPaymentResp payment(IcbcPaymentReq req);
+
+ IcbcPaymentResp query(String outTradeNo);
+
+ IcbcPaymentResp close(String outTradeNo);
+
+ IcbcRefundResp refund(IcbcRefundReq req);
+
+ IcbcSubOrderSplitResp splitSubOrder(IcbcSubOrderSplitReq req);
+
+ IcbcSubOrderRefundResp refundSubOrder(IcbcSubOrderRefundReq req);
+
+ IcbcSubOrderQueryResp querySubOrder(IcbcSubOrderQueryReq req);
+
+ Map<String, String> parseNotify(Map<String, String[]> requestParams);
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPaymentFlowRecorder.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPaymentFlowRecorder.java
new file mode 100644
index 0000000..fd9729f
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPaymentFlowRecorder.java
@@ -0,0 +1,92 @@
+package com.ruoyi.payment.icbc.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.ruoyi.payment.domain.IcbcPaymentFlowRecord;
+import com.ruoyi.payment.mapper.IcbcPaymentFlowRecordMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Slf4j
+@Service
+public class IcbcPaymentFlowRecorder {
+ @Resource
+ private IcbcPaymentFlowRecordMapper mapper;
+
+ public void record(String eventType,
+ String outTradeNo,
+ Integer paymentType,
+ BigDecimal amount,
+ Object request,
+ Object response,
+ String status,
+ String errorMessage) {
+ try {
+ String requestJson = toJson(request);
+ String responseJson = toJson(response);
+ JSONObject requestObject = parseObject(requestJson);
+ JSONObject responseObject = parseObject(responseJson);
+ IcbcPaymentFlowRecord record = new IcbcPaymentFlowRecord();
+ record.setOutTradeNo(outTradeNo);
+ record.setBusinessType(outTradeNo != null && outTradeNo.startsWith("ICBCTEST") ? "TEST" : "CHARGING_ORDER");
+ record.setEventType(eventType);
+ record.setPaymentType(paymentType);
+ record.setAmount(amount);
+ record.setIcbcOrderId(firstNotBlank(value(responseObject, "order_id"), value(responseObject, "orderId"),
+ value(responseObject, "transaction_id"), value(requestObject, "order_id"), value(requestObject, "orderId")));
+ record.setIcbcMsgId(firstNotBlank(value(responseObject, "msg_id"), value(responseObject, "msgId"),
+ value(requestObject, "msg_id"), value(requestObject, "msgId")));
+ record.setThirdTradeNo(firstNotBlank(value(responseObject, "third_trade_no"), value(responseObject, "thirdTradeNo"),
+ value(requestObject, "third_trade_no"), value(requestObject, "thirdTradeNo")));
+ record.setStatus(status);
+ record.setRequestJson(requestJson);
+ record.setResponseJson(responseJson);
+ record.setErrorMessage(truncate(errorMessage, 1000));
+ record.setCreateTime(LocalDateTime.now());
+ record.setUpdateTime(LocalDateTime.now());
+ record.setDelFlag(0);
+ mapper.insert(record);
+ } catch (Exception e) {
+ log.warn("工行支付流水审计记录失败,事件:{},订单号:{}", eventType, outTradeNo, e);
+ }
+ }
+
+ private String toJson(Object value) {
+ if (value == null) {
+ return null;
+ }
+ return value instanceof String ? (String) value : JSON.toJSONString(value);
+ }
+
+ private JSONObject parseObject(String value) {
+ if (value == null || value.trim().isEmpty()) {
+ return null;
+ }
+ try {
+ return JSON.parseObject(value);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private String value(JSONObject object, String key) {
+ return object == null ? null : object.getString(key);
+ }
+
+ private String firstNotBlank(String... values) {
+ for (String value : values) {
+ if (value != null && !value.trim().isEmpty()) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ private String truncate(String value, int maxLength) {
+ return value == null || value.length() <= maxLength ? value : value.substring(0, maxLength);
+ }
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcNotifyParser.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcNotifyParser.java
new file mode 100644
index 0000000..cc028f9
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcNotifyParser.java
@@ -0,0 +1,54 @@
+package com.ruoyi.payment.icbc.service.impl;
+
+import com.ruoyi.common.core.utils.StringUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+final class IcbcNotifyParser {
+ private IcbcNotifyParser() {
+ }
+
+ static Map<String, String> parseVerified(Map<String, String> params) {
+ String status = firstNotBlank(params.get("return_code"), params.get("orderStatus"), params.get("pay_status"), params.get("status"));
+ if (StringUtils.isNotEmpty(status) && !isPaidStatus(status)) {
+ throw new IllegalArgumentException("工行回调支付状态非成功:" + status);
+ }
+ String outTradeNo = firstNotBlank(params.get("outTradeNo"), params.get("out_trade_no"));
+ if (StringUtils.isEmpty(outTradeNo)) {
+ throw new IllegalArgumentException("工行回调缺少订单号");
+ }
+ String tradeNo = firstNotBlank(params.get("order_id"), params.get("tradeNo"), params.get("trade_no"), params.get("msg_id"));
+ if (StringUtils.isEmpty(tradeNo)) {
+ throw new IllegalArgumentException("工行回调缺少工行订单号");
+ }
+ String attach = firstNotBlank(params.get("attach"), params.get("paymentType"), params.get("payType"));
+ if (StringUtils.isEmpty(attach)) {
+ throw new IllegalArgumentException("工行回调缺少支付类型");
+ }
+ Map<String, String> result = new HashMap<>();
+ result.put("out_trade_no", outTradeNo);
+ result.put("transaction_id", tradeNo);
+ result.put("attach", attach);
+ return result;
+ }
+
+ private static boolean isPaidStatus(String status) {
+ return "0".equalsIgnoreCase(status)
+ || "SUCCESS".equalsIgnoreCase(status)
+ || "PAID".equalsIgnoreCase(status)
+ || "PAY_SUCCESS".equalsIgnoreCase(status);
+ }
+
+ private static String firstNotBlank(String... values) {
+ if (values == null) {
+ return null;
+ }
+ for (String value : values) {
+ if (StringUtils.isNotEmpty(value)) {
+ return value;
+ }
+ }
+ return null;
+ }
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java
new file mode 100644
index 0000000..20196ab
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java
@@ -0,0 +1,668 @@
+package com.ruoyi.payment.icbc.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.icbc.api.DefaultIcbcClient;
+import com.icbc.api.IcbcRequest;
+import com.icbc.api.IcbcResponse;
+import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2;
+import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2;
+import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1;
+import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineSersuborderRequestV1;
+import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1;
+import com.icbc.api.request.CardbusinessOrderSuborderqueryRequestV1;
+import com.icbc.api.internal.util.internal.util.fastjson.JSONArray;
+import com.icbc.api.internal.util.internal.util.fastjson.JSONObject;
+import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineOrderqryResponseV2;
+import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineConsumepurchaseResponseV2;
+import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineMerrefundResponseV1;
+import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineSersuborderResponseV1;
+import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineSersuborderrefundResponseV1;
+import com.icbc.api.response.CardbusinessOrderSuborderqueryResponseV1;
+import com.icbc.api.utils.IcbcSignature;
+import com.ruoyi.common.core.utils.StringUtils;
+import com.ruoyi.payment.api.vo.IcbcPaymentReq;
+import com.ruoyi.payment.api.vo.IcbcPaymentResp;
+import com.ruoyi.payment.api.vo.IcbcRefundReq;
+import com.ruoyi.payment.api.vo.IcbcRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp;
+import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq;
+import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp;
+import com.ruoyi.payment.icbc.config.IcbcPayProperties;
+import com.ruoyi.payment.icbc.service.IcbcPaymentFlowRecorder;
+import com.ruoyi.payment.icbc.service.IcbcPayService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * 工行聚合支付骨架实现。
+ */
+@Slf4j
+@Service
+public class IcbcPayServiceImpl implements IcbcPayService {
+ @Resource
+ private IcbcPayProperties properties;
+ @Resource
+ private IcbcPaymentFlowRecorder flowRecorder;
+
+ @Override
+ public IcbcPaymentResp payment(IcbcPaymentReq req) {
+ IcbcPaymentResp resp = new IcbcPaymentResp();
+ if (req != null) {
+ resp.setOrderId(req.getOutTradeNo());
+ }
+ try {
+ validateCommonConfig();
+ validateConfig("icbc.pay.gatewayUrl", properties.getGatewayUrl());
+ validatePaymentReq(req);
+ CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2 request = new CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2();
+ request.setServiceUrl(properties.getGatewayUrl());
+ request.setBizContent(buildConsumePurchaseBiz(req));
+ flowRecorder.record("PAY_REQUEST", req.getOutTradeNo(), req.getPaymentType(), req.getAmount(),
+ request.getBizContent(), null, "SENT", null);
+
+ CardbusinessAggregatepayB2cOnlineConsumepurchaseResponseV2 response = executeIcbc("聚合支付下单", request);
+ resp.setOrderId(firstNotBlank(response.getOut_trade_no(), req.getOutTradeNo()));
+ resp.setTradeNo(response.getOrder_id());
+ resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
+ resp.setPayData(buildConsumePurchasePayData(response, req.getPaymentType()));
+ resp.setRawResponse(JSON.toJSONString(response));
+ flowRecorder.record("PAY_RESPONSE", resp.getOrderId(), req.getPaymentType(), req.getAmount(),
+ request.getBizContent(), response, resp.getStatus(), null);
+ } catch (Exception e) {
+ log.error("工行聚合支付调用失败,订单号:{}", req == null ? null : req.getOutTradeNo(), e);
+ resp.setStatus("FAIL");
+ resp.setRawResponse(e.getMessage());
+ flowRecorder.record("PAY_RESPONSE", req == null ? null : req.getOutTradeNo(), req == null ? null : req.getPaymentType(),
+ req == null ? null : req.getAmount(), req, null, "FAIL", e.getMessage());
+ }
+ return resp;
+ }
+
+ @Override
+ public IcbcPaymentResp query(String outTradeNo) {
+ IcbcPaymentResp resp = new IcbcPaymentResp();
+ resp.setOrderId(outTradeNo);
+ try {
+ validateCommonConfig();
+ validateConfig("icbc.pay.orderQueryUrl", properties.getOrderQueryUrl());
+ CardbusinessAggregatepayB2cOnlineOrderqryRequestV2 request = new CardbusinessAggregatepayB2cOnlineOrderqryRequestV2();
+ request.setServiceUrl(properties.getOrderQueryUrl());
+ request.setBizContent(buildOrderQueryBiz(outTradeNo));
+ CardbusinessAggregatepayB2cOnlineOrderqryResponseV2 response = executeIcbc("聚合支付订单查询", request);
+ resp.setOrderId(firstNotBlank(response.getOut_trade_no(), outTradeNo));
+ resp.setTradeNo(firstNotBlank(response.getOrder_id(), response.getThird_trade_no(), response.getMsgId()));
+ resp.setStatus(firstNotBlank(response.getOrderStatus(), response.getPay_status()));
+ resp.setPayData(JSON.toJSONString(response));
+ resp.setRawResponse(JSON.toJSONString(response));
+ flowRecorder.record("QUERY", outTradeNo, null, null, buildOrderQueryBiz(outTradeNo), response,
+ response.isSuccess() ? "SUCCESS" : "FAIL", response.isSuccess() ? null : response.getReturnMsg());
+ } catch (Exception e) {
+ log.error("工行支付订单查询失败,订单号:{}", outTradeNo, e);
+ resp.setStatus("QUERY_FAIL");
+ resp.setRawResponse(e.getMessage());
+ flowRecorder.record("QUERY", outTradeNo, null, null, null, null, "FAIL", e.getMessage());
+ }
+ return resp;
+ }
+
+ @Override
+ public IcbcPaymentResp close(String outTradeNo) {
+ IcbcPaymentResp resp = new IcbcPaymentResp();
+ resp.setOrderId(outTradeNo);
+ resp.setStatus("CLOSE_TODO");
+ resp.setRawResponse("ICBC close skeleton: SDK close request is not mapped yet");
+ log.info("工行关闭订单骨架,订单号:{}", outTradeNo);
+ return resp;
+ }
+
+ @Override
+ public IcbcRefundResp refund(IcbcRefundReq req) {
+ IcbcRefundResp resp = new IcbcRefundResp();
+ if (req != null) {
+ resp.setOutTradeNo(req.getOutTradeNo());
+ resp.setOrderId(req.getOrderId());
+ resp.setOutRefundNo(req.getOutRefundNo());
+ }
+ try {
+ validateCommonConfig();
+ validateConfig("icbc.pay.refundUrl", properties.getRefundUrl());
+ if (req == null) {
+ throw new IllegalArgumentException("工行退款请求不能为空");
+ }
+ CardbusinessAggregatepayB2cOnlineMerrefundRequestV1 request = new CardbusinessAggregatepayB2cOnlineMerrefundRequestV1();
+ request.setServiceUrl(properties.getRefundUrl());
+ request.setBizContent(buildRefundBiz(req));
+ CardbusinessAggregatepayB2cOnlineMerrefundResponseV1 response = executeIcbc("线上POS退款", request);
+ resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
+ resp.setRawResponse(JSON.toJSONString(response));
+ resp.setOutTradeNo(firstNotBlank(response.getOutTradeNo(), req.getOutTradeNo()));
+ resp.setOrderId(firstNotBlank(response.getOrderId(), req.getOrderId()));
+ resp.setOutRefundNo(firstNotBlank(response.getOuttrxSerialNo(), req.getOutRefundNo()));
+ resp.setRefundId(firstNotBlank(response.getIntrx_serial_no(), response.getOuttrxSerialNo()));
+ resp.setRefundTime(response.getRefund_time());
+ resp.setRefundAmount(firstNotBlank(response.getRealRejectAmt(), response.getRejectAmt(), toFen(req.getRefundAmount())));
+ flowRecorder.record("REFUND", req.getOutTradeNo(), null, req.getRefundAmount(), request.getBizContent(), response,
+ resp.getStatus(), null);
+ } catch (Exception e) {
+ log.error("工行退款失败,订单号:{},退款单号:{}",
+ req == null ? null : req.getOutTradeNo(), req == null ? null : req.getOutRefundNo(), e);
+ resp.setStatus("FAIL");
+ resp.setRawResponse(e.getMessage());
+ flowRecorder.record("REFUND", req == null ? null : req.getOutTradeNo(), null, req == null ? null : req.getRefundAmount(),
+ req, null, "FAIL", e.getMessage());
+ }
+ return resp;
+ }
+
+ @Override
+ public IcbcSubOrderSplitResp splitSubOrder(IcbcSubOrderSplitReq req) {
+ IcbcSubOrderSplitResp resp = new IcbcSubOrderSplitResp();
+ if (req != null) {
+ resp.setChargingOrderCode(req.getChargingOrderCode());
+ resp.setSubOrderId(req.getSubOrderId());
+ }
+ try {
+ validateCommonConfig();
+ validateConfig("icbc.pay.merPrtclNo", properties.getMerPrtclNo());
+ validateConfig("icbc.pay.subOrderSplitUrl", properties.getSubOrderSplitUrl());
+ if (req == null) {
+ throw new IllegalArgumentException("工行分账请求不能为空");
+ }
+ CardbusinessAggregatepayB2cOnlineSersuborderRequestV1 request = new CardbusinessAggregatepayB2cOnlineSersuborderRequestV1();
+ request.setServiceUrl(properties.getSubOrderSplitUrl());
+ request.setBizContent(buildSubOrderSplitBiz(req));
+ CardbusinessAggregatepayB2cOnlineSersuborderResponseV1 response = executeIcbc("B2C小订单拆分", request);
+ resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
+ resp.setRawResponse(JSON.toJSONString(response));
+ fillSplitOutput(resp, response.getOrderSubmitActOutput());
+ flowRecorder.record("SPLIT", req.getChargingOrderCode(), null, req.getClassifyAmt(), request.getBizContent(), response,
+ resp.getStatus(), null);
+ } catch (Exception e) {
+ log.error("工行小订单拆分失败,订单号:{}", req == null ? null : req.getChargingOrderCode(), e);
+ resp.setStatus("FAIL");
+ resp.setRawResponse(e.getMessage());
+ flowRecorder.record("SPLIT", req == null ? null : req.getChargingOrderCode(), null, req == null ? null : req.getClassifyAmt(),
+ req, null, "FAIL", e.getMessage());
+ }
+ return resp;
+ }
+
+ @Override
+ public IcbcSubOrderRefundResp refundSubOrder(IcbcSubOrderRefundReq req) {
+ IcbcSubOrderRefundResp resp = new IcbcSubOrderRefundResp();
+ if (req != null) {
+ resp.setChargingOrderCode(req.getChargingOrderCode());
+ resp.setSubOrderId(req.getSubOrderId());
+ resp.setRetSubOrderId(req.getRetSubOrderId());
+ resp.setOuttrxSerialNo(req.getOuttrxSerialNo());
+ }
+ try {
+ validateCommonConfig();
+ validateConfig("icbc.pay.subOrderRefundUrl", properties.getSubOrderRefundUrl());
+ if (req == null) {
+ throw new IllegalArgumentException("工行分账退款请求不能为空");
+ }
+ CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1 request = new CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1();
+ request.setServiceUrl(properties.getSubOrderRefundUrl());
+ request.setBizContent(buildSubOrderRefundBiz(req));
+ CardbusinessAggregatepayB2cOnlineSersuborderrefundResponseV1 response = executeIcbc("B2C小订单拆分退款", request);
+ resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
+ resp.setRawResponse(JSON.toJSONString(response));
+ resp.setOrderId(response.getOrder_id());
+ resp.setSubOrderSeq(response.getSub_order_seq());
+ resp.setSubOrderTrxid(response.getSub_order_trxid());
+ resp.setRefundAmount(toFen(req.getRefundAmount()));
+ flowRecorder.record("SPLIT_REFUND", req.getChargingOrderCode(), null, req.getRefundAmount(), request.getBizContent(), response,
+ resp.getStatus(), null);
+ } catch (Exception e) {
+ log.error("工行小订单拆分退款失败,订单号:{},子订单号:{},退款流水:{}",
+ req == null ? null : req.getChargingOrderCode(), req == null ? null : req.getSubOrderId(),
+ req == null ? null : req.getOuttrxSerialNo(), e);
+ resp.setStatus("FAIL");
+ resp.setRawResponse(e.getMessage());
+ flowRecorder.record("SPLIT_REFUND", req == null ? null : req.getChargingOrderCode(), null,
+ req == null ? null : req.getRefundAmount(), req, null, "FAIL", e.getMessage());
+ }
+ return resp;
+ }
+
+ @Override
+ public IcbcSubOrderQueryResp querySubOrder(IcbcSubOrderQueryReq req) {
+ IcbcSubOrderQueryResp resp = new IcbcSubOrderQueryResp();
+ try {
+ validateCommonConfig();
+ validateConfig("icbc.pay.subOrderQueryUrl", properties.getSubOrderQueryUrl());
+ if (req == null) {
+ throw new IllegalArgumentException("工行子订单查询请求不能为空");
+ }
+ CardbusinessOrderSuborderqueryRequestV1 request = new CardbusinessOrderSuborderqueryRequestV1();
+ request.setServiceUrl(properties.getSubOrderQueryUrl());
+ request.setBizContent(buildSubOrderQueryBiz(req));
+ CardbusinessOrderSuborderqueryResponseV1 response = executeIcbc("子订单查询", request);
+ resp.setOrderStatus(response.getOrderStatus());
+ resp.setSubOutMerNo(response.getSubOutMerNo());
+ resp.setSubOutAgreeNo(response.getSubOutAgreeNo());
+ resp.setOutTradeNo(response.getOutTradeNo());
+ resp.setOrderId(response.getOrderId());
+ resp.setSubOrderNo(response.getSubOrderNo());
+ resp.setSubMerOrderId(response.getSubMerOrderId());
+ resp.setOrderAmnt(response.getOrderAmnt());
+ resp.setOrderRefundAmnt(response.getOrderRefundAmnt());
+ resp.setPayDate(response.getPayDate());
+ resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL");
+ resp.setRawResponse(JSON.toJSONString(response));
+ flowRecorder.record("SPLIT_QUERY", firstNotBlank(req.getMerOrderId(), req.getOrderId(), req.getSubOrderNo()), null, null, request.getBizContent(), response,
+ resp.getStatus(), null);
+ } catch (Exception e) {
+ log.error("工行子订单查询失败,子订单号:{}", req == null ? null : req.getSubOrderNo(), e);
+ resp.setStatus("FAIL");
+ resp.setRawResponse(e.getMessage());
+ flowRecorder.record("SPLIT_QUERY", req == null ? null : firstNotBlank(req.getMerOrderId(), req.getOrderId(), req.getSubOrderNo()), null, null,
+ req, null, "FAIL", e.getMessage());
+ }
+ return resp;
+ }
+
+ @Override
+ public Map<String, String> parseNotify(Map<String, String[]> requestParams) {
+ Map<String, String> params = new HashMap<>();
+ for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
+ String[] values = entry.getValue();
+ params.put(entry.getKey(), values == null || values.length == 0 ? "" : String.join(",", values));
+ }
+ try {
+ if (!verifyNotify(params)) {
+ throw new IllegalArgumentException("工行回调验签失败");
+ }
+ mergeBizContent(params);
+ Map<String, String> result = IcbcNotifyParser.parseVerified(params);
+ flowRecorder.record("PAY_NOTIFY", result.get("out_trade_no"), parseInteger(result.get("attach")), null,
+ params, result, "SUCCESS", null);
+ return result;
+ } catch (RuntimeException e) {
+ flowRecorder.record("PAY_NOTIFY", firstNotBlank(params.get("out_trade_no"), params.get("outTradeNo")),
+ parseInteger(firstNotBlank(params.get("attach"), params.get("paymentType"))), null,
+ params, null, "FAIL", e.getMessage());
+ throw e;
+ }
+ }
+
+ private Integer parseInteger(String value) {
+ try {
+ return StringUtils.isEmpty(value) ? null : Integer.valueOf(value);
+ } catch (NumberFormatException ignored) {
+ return null;
+ }
+ }
+
+ private DefaultIcbcClient buildClient() {
+ return new DefaultIcbcClient(
+ properties.getAppId(),
+ properties.getSignType(),
+ properties.getPrivateKey(),
+ properties.getIcbcPublicKey());
+ }
+
+ private <T extends IcbcResponse> T executeIcbc(String scene, IcbcRequest<T> request) throws Exception {
+ log.info("工行{}请求,url:{},bizContent:{}", scene, request.getServiceUrl(), JSON.toJSONString(request.getBizContent()));
+ T response = buildClient().execute(request, System.currentTimeMillis() + "");
+ log.info("工行{}响应:{}", scene, JSON.toJSONString(response));
+ return response;
+ }
+
+ private void validateCommonConfig() {
+ validateConfig("icbc.pay.appId", properties.getAppId());
+ validateConfig("icbc.pay.privateKey", properties.getPrivateKey());
+ validateConfig("icbc.pay.icbcPublicKey", properties.getIcbcPublicKey());
+ validateConfig("icbc.pay.merId", properties.getMerId());
+ }
+
+ private void validateConfig(String name, String value) {
+ if (StringUtils.isEmpty(value)) {
+ throw new IllegalStateException("工行配置缺失:" + name);
+ }
+ }
+
+ private void validatePaymentReq(IcbcPaymentReq req) {
+ if (req == null) {
+ throw new IllegalArgumentException("工行支付请求不能为空");
+ }
+ validateConfig("outTradeNo", req.getOutTradeNo());
+ if (req.getAmount() == null || req.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("工行支付金额必须大于0");
+ }
+ if (Integer.valueOf(1).equals(req.getPaymentType()) || Integer.valueOf(2).equals(req.getPaymentType())) {
+ validateConfig("icbc.pay." + (Integer.valueOf(1).equals(req.getPaymentType()) ? "wxTpAppId" : "aliTpAppId"), getTpAppId(req.getPaymentType()));
+ }
+ if (Integer.valueOf(1).equals(req.getPaymentType()) || Integer.valueOf(2).equals(req.getPaymentType())) {
+ validateConfig("openId", req.getOpenId());
+ }
+ }
+
+ private CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz buildConsumePurchaseBiz(IcbcPaymentReq req) {
+ CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz biz =
+ new CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz();
+ biz.setMer_id(properties.getMerId());
+ setMerPrtclNoIfPresent(biz::setMer_prtcl_no);
+ biz.setOut_trade_no(req.getOutTradeNo());
+ biz.setIcbc_appid(properties.getAppId());
+ biz.setShop_appid(getTpAppId(req.getPaymentType()));
+ if (Integer.valueOf(1).equals(req.getPaymentType()) || Integer.valueOf(2).equals(req.getPaymentType())) {
+ biz.setOpen_id(req.getOpenId());
+ }
+ setAlipayUnionIdIfNeeded(biz, req);
+ biz.setPay_mode(toIcbcPayMode(req.getPaymentType()));
+ biz.setAccess_type(toIcbcAccessType(req.getPaymentType()));
+ biz.setOrig_date_time(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(new Date()));
+ biz.setDecive_info(buildDeviceInfo(req));
+ biz.setBody(firstNotBlank(req.getBody(), req.getSubject(), "充电充值"));
+ biz.setFee_type("001");
+ biz.setTotal_fee(toFen(req.getAmount()));
+ biz.setSpbill_create_ip(properties.getSpbillCreateIp());
+ biz.setMer_url(StringUtils.isNotEmpty(properties.getNotifyUrl()) ? properties.getNotifyUrl() : req.getNotifyUrl());
+ biz.setReturn_url(req.getReturnUrl());
+ biz.setAttach(String.valueOf(req.getPaymentType()));
+ biz.setNotify_type("HS");
+ biz.setResult_type("0");
+ biz.setOrder_apd_inf(req.getSubject());
+ return biz;
+ }
+
+ private void setAlipayUnionIdIfNeeded(CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz biz,
+ IcbcPaymentReq req) {
+ if (!Integer.valueOf(2).equals(req.getPaymentType()) && !Integer.valueOf(3).equals(req.getPaymentType())) {
+ return;
+ }
+ if (StringUtils.isNotEmpty(req.getUnionId())) {
+ biz.setUnion_id(truncate(req.getUnionId(), 16));
+ }
+ }
+
+ private String buildConsumePurchasePayData(CardbusinessAggregatepayB2cOnlineConsumepurchaseResponseV2 response, Integer paymentType) {
+ Map<String, Object> payData = new HashMap<>();
+ payData.put("orderId", response.getOrder_id());
+ payData.put("outTradeNo", response.getOut_trade_no());
+ payData.put("totalAmt", response.getTotal_amt());
+ payData.put("payMode", response.getPay_mode());
+ payData.put("accessType", response.getAccess_type());
+ payData.put("tradeType", response.getTrade_type());
+ payData.put("wxDataPackage", response.getWx_data_package());
+ payData.put("zfbDataPackage", response.getZfb_data_package());
+ payData.put("unionDataPackage", response.getUnion_data_package());
+ if (Integer.valueOf(1).equals(paymentType) && StringUtils.isNotEmpty(response.getWx_data_package())) {
+ payData.put("package", response.getWx_data_package());
+ }
+ if ((Integer.valueOf(2).equals(paymentType) || Integer.valueOf(3).equals(paymentType)) && StringUtils.isNotEmpty(response.getZfb_data_package())) {
+ payData.put("package", response.getZfb_data_package());
+ }
+ return JSON.toJSONString(payData);
+ }
+
+ private CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz buildOrderQueryBiz(String outTradeNo) {
+ CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz biz =
+ new CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz();
+ biz.setOut_trade_no(outTradeNo);
+ biz.setDeal_flag("0");
+ biz.setIcbc_appid(properties.getAppId());
+ biz.setMer_id(properties.getMerId());
+ setMerPrtclNoIfPresent(biz::setMer_prtcl_no);
+ return biz;
+ }
+
+ private CardbusinessAggregatepayB2cOnlineMerrefundRequestV1.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1Biz buildRefundBiz(IcbcRefundReq req) {
+ CardbusinessAggregatepayB2cOnlineMerrefundRequestV1.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1Biz biz =
+ new CardbusinessAggregatepayB2cOnlineMerrefundRequestV1.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1Biz();
+ biz.setOrder_id(req.getOrderId());
+ biz.setOut_trade_no(req.getOutTradeNo());
+ biz.setOuttrx_serial_no(req.getOutRefundNo());
+ biz.setRet_total_amt(toFen(req.getRefundAmount()));
+ biz.setTrnsc_ccy("001");
+ biz.setMer_id(properties.getMerId());
+ setMerPrtclNoIfPresent(biz::setMer_prtcl_no);
+ biz.setIcbc_appid(properties.getAppId());
+ biz.setMer_acct(req.getMerAcct());
+ biz.setOrder_apd_inf(req.getReason());
+ return biz;
+ }
+
+ private CardbusinessAggregatepayB2cOnlineSersuborderRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderRequestV1Biz buildSubOrderSplitBiz(IcbcSubOrderSplitReq req) {
+ CardbusinessAggregatepayB2cOnlineSersuborderRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderRequestV1Biz biz =
+ new CardbusinessAggregatepayB2cOnlineSersuborderRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderRequestV1Biz();
+ JSONArray orderSubmitActInput = new JSONArray();
+ JSONObject item = new JSONObject();
+ item.put("rec_num", "1");
+ item.put("oper_flag", "0");
+ item.put("sub_order_id", req.getSubOrderId());
+ item.put("busi_type", firstNotBlank(req.getBusiType(), "1"));
+ item.put("seq_no", req.getSeqNo());
+ item.put("ori_mer_id", firstNotBlank(req.getOriMerId(), properties.getMerId()));
+ item.put("ori_trx_date", req.getOriTrxDate());
+ item.put("sub_mer_id", req.getSubMerId());
+ item.put("sub_mer_prtcl_no", req.getSubMerPrtclNo());
+ item.put("classify_amt", toFen(req.getClassifyAmt()));
+ item.put("mer_acct", req.getMerAcct());
+ item.put("mer_sp_infor", req.getMerSpInfor());
+ item.put("term_id", req.getTermId());
+ orderSubmitActInput.add(item);
+ biz.setMerId(firstNotBlank(properties.getServiceMerId(), properties.getMerId()));
+ biz.setMerPrtclNo(firstNotBlank(properties.getServiceMerPrtclNo(), properties.getMerPrtclNo()));
+ biz.setIcbcAppid(properties.getAppId());
+ biz.setOrderNum("1");
+ biz.setOrderSubmitActInput(orderSubmitActInput);
+ return biz;
+ }
+
+ private CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1Biz buildSubOrderRefundBiz(IcbcSubOrderRefundReq req) {
+ CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1Biz biz =
+ new CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1Biz();
+ biz.setMerId(firstNotBlank(properties.getServiceMerId(), properties.getMerId()));
+ biz.setMerPrtclNo(firstNotBlank(properties.getServiceMerPrtclNo(), properties.getMerPrtclNo()));
+ biz.setIcbcAppid(properties.getAppId());
+ biz.setOperFlag("0");
+ biz.setSubOrderId(req.getSubOrderId());
+ biz.setSeqNo(req.getSeqNo());
+ biz.setBusiType(firstNotBlank(req.getBusiType(), "1"));
+ biz.setOriTrxDate(req.getOriTrxDate());
+ biz.setOriMerId(firstNotBlank(req.getOriMerId(), properties.getMerId()));
+ biz.setSubMerId(req.getSubMerId());
+ biz.setSubMerPrtclNo(req.getSubMerPrtclNo());
+ biz.setClassifyAmt(toFen(req.getRefundAmount()));
+ biz.setRetSubOrderId(req.getRetSubOrderId());
+ biz.setOuttrxSerialNo(req.getOuttrxSerialNo());
+ biz.setRefundSource(req.getRefundSource());
+ biz.setMerAcct(req.getMerAcct());
+ biz.setMerSpInfor(req.getMerSpInfor());
+ biz.setTermId(req.getTermId());
+ return biz;
+ }
+
+ private CardbusinessOrderSuborderqueryRequestV1.CardbusinessOrderSuborderqueryRequestV1Biz buildSubOrderQueryBiz(IcbcSubOrderQueryReq req) {
+ CardbusinessOrderSuborderqueryRequestV1.CardbusinessOrderSuborderqueryRequestV1Biz biz =
+ new CardbusinessOrderSuborderqueryRequestV1.CardbusinessOrderSuborderqueryRequestV1Biz();
+ biz.setIcbcAppid(properties.getAppId());
+ biz.setSerNo(firstNotBlank(req.getSerNo(), properties.getServiceMerId(), properties.getMerId()));
+ biz.setOrderNoType(req.getOrderNoType());
+ biz.setOrderId(req.getOrderId());
+ biz.setMerOrderId(req.getMerOrderId());
+ biz.setPrtiId(firstNotBlank(req.getPrtiId(), properties.getMerId()));
+ biz.setQueryType(req.getQueryType());
+ biz.setSubOrderNo(req.getSubOrderNo());
+ biz.setMerTrnscSerno(req.getMerTrnscSerno());
+ biz.setSubPrtiId(req.getSubPrtiId());
+ return biz;
+ }
+
+ private void fillSplitOutput(IcbcSubOrderSplitResp resp, JSONArray output) {
+ if (output == null || output.isEmpty()) {
+ return;
+ }
+ JSONObject item = output.getJSONObject(0);
+ resp.setRecResult(item.getString("rec_result"));
+ resp.setClassifyAmt(item.getString("classify_amt"));
+ resp.setSubMerId(item.getString("sub_mer_id"));
+ resp.setOrderId(item.getString("order_id"));
+ resp.setSubOrderSeq(item.getString("sub_order_seq"));
+ resp.setSubOrderTrxid(item.getString("sub_order_trxid"));
+ resp.setSubOrderId(firstNotBlank(item.getString("sub_order_id"), resp.getSubOrderId()));
+ if (!"0".equals(resp.getRecResult())) {
+ resp.setStatus("FAIL");
+ }
+ }
+
+ private String toFen(BigDecimal amount) {
+ return amount == null ? "0" : amount.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toPlainString();
+ }
+
+ private String getTpAppId(Integer paymentType) {
+ if (Integer.valueOf(1).equals(paymentType)) {
+ return firstNotBlank(properties.getWxTpAppId(), properties.getTpAppId());
+ }
+ if (isAlipay(paymentType)) {
+ return firstNotBlank(properties.getAliTpAppId(), properties.getTpAppId());
+ }
+ return properties.getTpAppId();
+ }
+
+ private boolean isAlipay(Integer paymentType) {
+ return Integer.valueOf(2).equals(paymentType) || Integer.valueOf(3).equals(paymentType);
+ }
+
+ private void setMerPrtclNoIfPresent(java.util.function.Consumer<String> setter) {
+ if (StringUtils.isNotEmpty(properties.getMerPrtclNo())) {
+ setter.accept(properties.getMerPrtclNo());
+ }
+ }
+
+ private String buildDeviceInfo(IcbcPaymentReq req) {
+ String value = firstNotBlank(req.getOpenId(), req.getUnionId(), "USER") + "_" + firstNotBlank(req.getDeviceInfo(), properties.getSpbillCreateIp(), "MINIPROGRAM");
+ return value.length() > 32 ? value.substring(0, 32) : value;
+ }
+
+ private String toIcbcPayMode(Integer paymentType) {
+ if (Integer.valueOf(1).equals(paymentType)) {
+ return "9";
+ }
+ if (Integer.valueOf(2).equals(paymentType) || Integer.valueOf(3).equals(paymentType)) {
+ return "10";
+ }
+ return "9";
+ }
+
+ private String toIcbcAccessType(Integer paymentType) {
+ if (Integer.valueOf(3).equals(paymentType)) {
+ return "7";
+ }
+ return "9";
+ }
+
+ private boolean verifyNotify(Map<String, String> params) {
+ if (Boolean.FALSE.equals(properties.getVerifyNotifySign())) {
+ return true;
+ }
+ String sign = params.get("sign");
+ if (StringUtils.isEmpty(sign)) {
+ return false;
+ }
+ try {
+ String content = buildSignContent(params);
+ String signType = firstNotBlank(params.get("sign_type"), properties.getSignType());
+ String publicKey = normalizePublicKey(properties.getIcbcPublicKey());
+ log.info("工行回调验签参数,signType:{},publicKeyLength:{},content:{}", signType, publicKey == null ? 0 : publicKey.length(), content);
+ return IcbcSignature.verify(content, signType, sign, properties.getCharset(), publicKey);
+ } catch (Exception e) {
+ log.error("工行回调验签异常", e);
+ return false;
+ }
+ }
+
+ private void mergeBizContent(Map<String, String> params) {
+ String bizContent = params.get("biz_content");
+ if (StringUtils.isEmpty(bizContent)) {
+ return;
+ }
+ try {
+ JSONObject biz = JSONObject.parseObject(bizContent);
+ if (biz == null) {
+ return;
+ }
+ for (Map.Entry<String, Object> entry : biz.entrySet()) {
+ if (entry.getValue() != null && !params.containsKey(entry.getKey())) {
+ params.put(entry.getKey(), String.valueOf(entry.getValue()));
+ }
+ }
+ } catch (Exception e) {
+ log.warn("工行回调biz_content解析失败,bizContent:{}", bizContent, e);
+ }
+ }
+
+ private String normalizePublicKey(String publicKey) {
+ if (publicKey == null) {
+ return null;
+ }
+ String normalized = publicKey;
+ String lower = normalized.toLowerCase();
+ int start = lower.indexOf("start public key");
+ int end = lower.indexOf("end public key");
+ if (start >= 0 && end > start) {
+ normalized = normalized.substring(start + "start public key".length(), end);
+ }
+ return normalized
+ .replace("-----BEGIN PUBLIC KEY-----", "")
+ .replace("-----END PUBLIC KEY-----", "")
+ .replace("#", "")
+ .replaceAll("\\s+", "");
+ }
+
+ private String buildSignContent(Map<String, String> params) {
+ Map<String, String> sorted = new TreeMap<>(params);
+ String api = sorted.remove("api");
+ sorted.remove("sign");
+ StringBuilder content = new StringBuilder(firstNotBlank(api, ""));
+ content.append('?');
+ boolean hasParam = false;
+ for (Map.Entry<String, String> entry : sorted.entrySet()) {
+ if (StringUtils.isEmpty(entry.getValue())) {
+ continue;
+ }
+ if (hasParam) {
+ content.append('&');
+ }
+ hasParam = true;
+ content.append(entry.getKey()).append('=').append(entry.getValue());
+ }
+ return content.toString();
+ }
+
+ private boolean isPaidStatus(String status) {
+ return Arrays.asList("1", "2", "SUCCESS", "PAID", "PAY_SUCCESS", "TRADE_SUCCESS").contains(status);
+ }
+
+ private String firstNotBlank(String... values) {
+ for (String value : values) {
+ if (StringUtils.isNotEmpty(value)) {
+ return value;
+ }
+ }
+ return "";
+ }
+
+ private String truncate(String value, int maxLength) {
+ if (StringUtils.isEmpty(value) || value.length() <= maxLength) {
+ return value;
+ }
+ return value.substring(0, maxLength);
+ }
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/mapper/IcbcPaymentFlowRecordMapper.java b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/mapper/IcbcPaymentFlowRecordMapper.java
new file mode 100644
index 0000000..438a9ee
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/mapper/IcbcPaymentFlowRecordMapper.java
@@ -0,0 +1,7 @@
+package com.ruoyi.payment.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.payment.domain.IcbcPaymentFlowRecord;
+
+public interface IcbcPaymentFlowRecordMapper extends BaseMapper<IcbcPaymentFlowRecord> {
+}
diff --git a/ruoyi-service/ruoyi-payment/src/main/resources/bootstrap.yml b/ruoyi-service/ruoyi-payment/src/main/resources/bootstrap.yml
index 291aca3..165a3ef 100644
--- a/ruoyi-service/ruoyi-payment/src/main/resources/bootstrap.yml
+++ b/ruoyi-service/ruoyi-payment/src/main/resources/bootstrap.yml
@@ -9,7 +9,7 @@
profiles:
# 环境配置
# active: dev
- active: prod
+ active: dev
---
spring:
config:
@@ -19,7 +19,7 @@
nacos:
discovery:
# 服务注册地址
- server-addr: 127.0.0.1:8848
+ server-addr: 192.168.110.80:8848
service: ${spring.application.name}
group: DEFAULT_GROUP
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
@@ -27,7 +27,7 @@
password: nacos
config:
# 配置中心地址
- server-addr: 127.0.0.1:8848
+ server-addr: 192.168.110.80:8848
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
group: DEFAULT_GROUP
name: ${spring.application.name}
diff --git a/ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/icbc/service/impl/IcbcNotifyParserTest.java b/ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/icbc/service/impl/IcbcNotifyParserTest.java
new file mode 100644
index 0000000..6de3884
--- /dev/null
+++ b/ruoyi-service/ruoyi-payment/src/test/java/com/ruoyi/payment/icbc/service/impl/IcbcNotifyParserTest.java
@@ -0,0 +1,27 @@
+package com.ruoyi.payment.icbc.service.impl;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class IcbcNotifyParserTest {
+
+ @Test
+ void usesIcbcOrderIdAsRefundTransactionId() {
+ Map<String, String> params = new HashMap<>();
+ params.put("out_trade_no", "CD202607110001");
+ params.put("order_id", "231011990056000522607080001058");
+ params.put("msg_id", "050200126189883020726302842");
+ params.put("attach", "1");
+ params.put("return_code", "0");
+
+ Map<String, String> result = IcbcNotifyParser.parseVerified(params);
+
+ assertEquals("CD202607110001", result.get("out_trade_no"));
+ assertEquals("231011990056000522607080001058", result.get("transaction_id"));
+ assertEquals("1", result.get("attach"));
+ }
+}
diff --git a/ruoyi-visual/ruoyi-monitor/src/main/resources/bootstrap.yml b/ruoyi-visual/ruoyi-monitor/src/main/resources/bootstrap.yml
index e2b56b8..81f8192 100644
--- a/ruoyi-visual/ruoyi-monitor/src/main/resources/bootstrap.yml
+++ b/ruoyi-visual/ruoyi-monitor/src/main/resources/bootstrap.yml
@@ -8,7 +8,7 @@
profiles:
# 环境配置
# active: dev
- active: prod
+ active: dev
---
spring:
config:
@@ -18,7 +18,7 @@
nacos:
discovery:
# 服务注册地址
- server-addr: 127.0.0.1:8848
+ server-addr: 192.168.110.80:8848
service: ${spring.application.name}
group: DEFAULT_GROUP
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
@@ -26,7 +26,7 @@
password: nacos
config:
# 配置中心地址
- server-addr: 127.0.0.1:8848
+ server-addr: 192.168.110.80:8848
namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb
group: DEFAULT_GROUP
name: ${spring.application.name}
diff --git a/sql/20260529_icbc_partner_profit_sharing.sql b/sql/20260529_icbc_partner_profit_sharing.sql
new file mode 100644
index 0000000..9c806f0
--- /dev/null
+++ b/sql/20260529_icbc_partner_profit_sharing.sql
@@ -0,0 +1,7 @@
+ALTER TABLE `t_partner`
+ ADD COLUMN `icbc_partner_id` varchar(64) NULL DEFAULT NULL COMMENT '工行合作商ID' AFTER `rate`,
+ ADD COLUMN `icbc_sub_mer_id` varchar(20) NULL DEFAULT NULL COMMENT '工行二级商户编号' AFTER `icbc_partner_id`,
+ ADD COLUMN `icbc_sub_mer_prtcl_no` varchar(20) NULL DEFAULT NULL COMMENT '工行二级商户协议编号' AFTER `icbc_sub_mer_id`,
+ ADD COLUMN `icbc_mer_acct` varchar(34) NULL DEFAULT NULL COMMENT '工行直接分账商户清算账号' AFTER `icbc_sub_mer_prtcl_no`,
+ ADD COLUMN `electricity_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '电费分成比例' AFTER `icbc_mer_acct`,
+ ADD COLUMN `service_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '服务费分成比例' AFTER `electricity_share_ratio`;
diff --git a/sql/20260530_charging_order_share_record.sql b/sql/20260530_charging_order_share_record.sql
new file mode 100644
index 0000000..3ddd606
--- /dev/null
+++ b/sql/20260530_charging_order_share_record.sql
@@ -0,0 +1,44 @@
+CREATE TABLE IF NOT EXISTS `t_charging_order_share_record` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+ `charging_order_id` bigint NULL DEFAULT NULL COMMENT '充电订单ID',
+ `charging_order_code` varchar(64) NOT NULL COMMENT '充电订单号',
+ `site_id` int NULL DEFAULT NULL COMMENT '站点ID',
+ `partner_id` int NULL DEFAULT NULL COMMENT '合作商ID',
+ `pay_channel` varchar(16) NOT NULL COMMENT '支付渠道:BALANCE/ICBC/WECHAT/ALIPAY/WHITELIST/OTHER',
+ `channel_pay_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '该渠道最终实收本金金额,元,不含赠送',
+ `channel_gift_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '该渠道赠送抵扣金额,元,不参与分账',
+ `channel_pay_ratio` decimal(10, 6) NOT NULL DEFAULT 0.000000 COMMENT '该渠道实收占订单最终实收比例',
+ `channel_electricity_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '该渠道分摊电费金额,元',
+ `channel_service_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '该渠道分摊服务费金额,元',
+ `electricity_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '电费分成比例',
+ `service_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '服务费分成比例',
+ `electricity_share_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '电费分账金额,元',
+ `service_share_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '服务费分账金额,元',
+ `share_amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '分账总金额,元',
+ `need_online_share` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否需要线上分账',
+ `online_share_status` varchar(16) NOT NULL DEFAULT 'RECORDED' COMMENT '线上分账状态:RECORDED/PROCESSING/SUCCESS/FAIL/SKIP',
+ `seq_no` varchar(50) NULL DEFAULT NULL COMMENT '工行原交易消费订单号',
+ `busi_type` varchar(1) NULL DEFAULT NULL COMMENT '工行原交易消费订单类型:1外部订单号,2工行订单号',
+ `ori_mer_id` varchar(20) NULL DEFAULT NULL COMMENT '工行原交易商户编号',
+ `ori_trx_date` date NULL DEFAULT NULL COMMENT '工行原交易日期',
+ `sub_order_id` varchar(50) NULL DEFAULT NULL COMMENT '工行商户自定义子订单号',
+ `sub_mer_id` varchar(20) NULL DEFAULT NULL COMMENT '工行二级商户编号',
+ `sub_mer_prtcl_no` varchar(20) NULL DEFAULT NULL COMMENT '工行二级商户协议编号',
+ `mer_acct` varchar(34) NULL DEFAULT NULL COMMENT '工行直接分账商户清算账号',
+ `icbc_order_id` varchar(64) NULL DEFAULT NULL COMMENT '工行返回订单号',
+ `sub_order_seq` varchar(32) NULL DEFAULT NULL COMMENT '工行子订单序号',
+ `sub_order_trxid` varchar(64) NULL DEFAULT NULL COMMENT '工行子订单交易流水',
+ `rec_result` varchar(8) NULL DEFAULT NULL COMMENT '工行记录处理结果',
+ `request_json` text NULL COMMENT '线上分账请求报文',
+ `response_json` text NULL COMMENT '线上分账响应报文',
+ `fail_reason` varchar(512) NULL DEFAULT NULL COMMENT '失败或跳过原因',
+ `retry_count` int NOT NULL DEFAULT 0 COMMENT '重试次数',
+ `last_retry_time` datetime NULL DEFAULT NULL COMMENT '最近重试时间',
+ `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+ `del_flag` int NULL DEFAULT 0 COMMENT '删除状态(0=否,1=是)',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_order_channel` (`charging_order_code`, `pay_channel`),
+ KEY `idx_share_status` (`online_share_status`),
+ KEY `idx_share_sub_order_id` (`sub_order_id`),
+ KEY `idx_share_partner` (`partner_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='充电订单全渠道分账记录';
diff --git a/sql/20260711_icbc_payment_flow_record.sql b/sql/20260711_icbc_payment_flow_record.sql
new file mode 100644
index 0000000..86c1cba
--- /dev/null
+++ b/sql/20260711_icbc_payment_flow_record.sql
@@ -0,0 +1,22 @@
+CREATE TABLE IF NOT EXISTS `t_icbc_payment_flow_record` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+ `out_trade_no` varchar(64) DEFAULT NULL COMMENT '业务订单号',
+ `business_type` varchar(32) DEFAULT NULL COMMENT '业务类型',
+ `event_type` varchar(32) NOT NULL COMMENT '事件类型',
+ `payment_type` int DEFAULT NULL COMMENT '支付类型',
+ `amount` decimal(12,2) DEFAULT NULL COMMENT '金额(元)',
+ `icbc_order_id` varchar(64) DEFAULT NULL COMMENT '工行订单号',
+ `icbc_msg_id` varchar(64) DEFAULT NULL COMMENT '工行消息号',
+ `third_trade_no` varchar(64) DEFAULT NULL COMMENT '第三方支付流水号',
+ `status` varchar(32) DEFAULT NULL COMMENT '处理状态',
+ `request_json` longtext COMMENT '请求报文',
+ `response_json` longtext COMMENT '响应报文',
+ `error_message` varchar(1000) DEFAULT NULL COMMENT '异常信息',
+ `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `del_flag` tinyint NOT NULL DEFAULT 0 COMMENT '删除标记',
+ PRIMARY KEY (`id`),
+ KEY `idx_icbc_flow_out_trade_no` (`out_trade_no`),
+ KEY `idx_icbc_flow_order_id` (`icbc_order_id`),
+ KEY `idx_icbc_flow_event_time` (`event_type`,`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工行支付流程审计记录';
diff --git a/sql/charging_pile_service.sql b/sql/charging_pile_service.sql
index 6381502..fae42cf 100644
--- a/sql/charging_pile_service.sql
+++ b/sql/charging_pile_service.sql
@@ -1897,6 +1897,12 @@
`role_id` int NULL DEFAULT NULL COMMENT '角色id',
`logo_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT 'logo地址',
`rate` double(10, 2) NULL DEFAULT NULL COMMENT '费率',
+ `icbc_partner_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '工行合作商ID',
+ `icbc_sub_mer_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '工行二级商户编号',
+ `icbc_sub_mer_prtcl_no` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '工行二级商户协议编号',
+ `icbc_mer_acct` varchar(34) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '工行直接分账商户清算账号',
+ `electricity_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '电费分成比例',
+ `service_share_ratio` decimal(5, 2) NULL DEFAULT NULL COMMENT '服务费分成比例',
`address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '注册地址',
`phone_one` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '合作商电话1',
`phone_two` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '合作商电话2',
--
Gitblit v1.7.1