Эх сурвалжийг харах

取消商家骑手订单抽成及骑手运费税费

qmj 4 өдөр өмнө
parent
commit
e80f5d9478

+ 14 - 31
ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderService.java

@@ -5,8 +5,6 @@ import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.ruoyi.app.service.WalletService;
-import com.ruoyi.common.core.domain.entity.SysDictData;
-import com.ruoyi.common.utils.DictUtils;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.system.domain.*;
@@ -20,10 +18,9 @@ import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.text.DecimalFormat;
-import java.util.List;
 
 /**
- * 订单账单生成服务(商家分成、骑手分成)
+ * 订单账单生成服务(商家商品收入、骑手配送收入,不扣平台抽成)
  */
 @Service
 public class OrderService {
@@ -40,7 +37,7 @@ public class OrderService {
     private IVipUserQuanyiService userQuanyiService;
 
     /**
-     * 执行商户分成
+     * 商家商品收入全额入账,沿用原有优惠及配送费划分
      */
     public void setSanghuBilling(PosOrder posOrder) {
         long count = userBillingService.count(new LambdaQueryWrapper<UserBilling>()
@@ -61,13 +58,6 @@ public class OrderService {
             PosStore store = posStoreService.getById(posOrder.getMdId());
             boolean isStall = store != null && store.getIsStall() != null && store.getIsStall() == 1 && store.getNightMarketId() != null;
 
-            InfoUser user;
-            if (isStall) {
-                user = infoUserService.getById(store.getNightMarketId());
-            } else {
-                user = infoUserService.getById(posOrder.getShId());
-            }
-            Double fcbili = user.getCommission() == null ? 0.00 : user.getCommission();
             int fenc;
             String remark;
             if (Integer.valueOf(2).equals(posOrder.getPromotionPricingVersion())) {
@@ -113,17 +103,16 @@ public class OrderService {
                 remark = legacyRemark.toString();
             }
             billing.setIllustrate(remark);
-            Double chouc = fenc * fcbili;
-            Double shfc = fenc - chouc;
+            // 2026-11-04:平台不再抽成,历史配置不参与新账单;优惠后的商品收入全额入账。
             billing.setUserId(posOrder.getShId());
             billing.setDdId(String.valueOf(posOrder.getDdId()));
             billing.setType("0");
-            billing.setAmount(Double.valueOf(df.format(shfc)));
-            billing.setDivvy(Double.valueOf(df.format(chouc)));
+            billing.setAmount(Double.valueOf(df.format(fenc)));
+            billing.setDivvy(0.00);
             billing.setState("0");
             billing.setMdId(posOrder.getMdId());
             billing.setUserType("1");
-            billing.setDivvyRate(fcbili);
+            billing.setDivvyRate(0.00);
             if (isStall) {
                 billing.setStoreId(Long.valueOf(store.getId()));
                 walletService.addStallBalance(Long.valueOf(store.getId()), BigDecimal.valueOf(billing.getAmount()), billing);
@@ -134,7 +123,7 @@ public class OrderService {
     }
 
     /**
-     * 执行骑手分成
+     * 骑手配送费全额入账,不扣平台抽成和骑手运费税费
      */
     public void setQishouBilling(PosOrder posOrder) {
         long count = userBillingService.count(new LambdaQueryWrapper<UserBilling>()
@@ -148,26 +137,20 @@ public class OrderService {
             if (user == null) {
                 System.out.println("订单无骑手");
             } else {
-                Double fcbili = user.getCommission() == null ? 0.00 : user.getCommission();
+                // 2026-11-04:运费全额归骑手,旧commission和sys_qs_freight_bl均不再参与扣款。
+                // 显式记录零值,保留账单字段及历史账单,不改变到付代收和防重复结算逻辑。
                 Double fenc = posOrder.getFreight();
-                Double chouc = fenc * fcbili;
-                Double shfc = fenc - chouc;
-                List<SysDictData> taxBl = DictUtils.getDictCache("sys_qs_freight_bl");
-                if (taxBl != null && taxBl.get(0) != null) {
-                    Double tax = shfc * Double.parseDouble(taxBl.get(0).getDictValue());
-                    billing.setTax(BigDecimal.valueOf(tax));
-                    billing.setTaxRate(taxBl.get(0).getDictValue());
-                    shfc = shfc - tax;
-                }
+                billing.setTax(BigDecimal.ZERO);
+                billing.setTaxRate("0");
                 billing.setUserId(posOrder.getQsId());
                 billing.setDdId(String.valueOf(posOrder.getDdId()));
                 billing.setType("0");
-                billing.setAmount(Double.valueOf(df.format(shfc)));
-                billing.setDivvy(Double.valueOf(df.format(chouc)));
+                billing.setAmount(Double.valueOf(df.format(fenc)));
+                billing.setDivvy(0.00);
                 billing.setState("0");
                 billing.setMdId(posOrder.getMdId());
                 billing.setUserType("2");
-                billing.setDivvyRate(user.getCommission());
+                billing.setDivvyRate(0.00);
                 if ("1".equals(posOrder.getCollectPayment())) {
                     billing.setBehalfAmount(Double.valueOf(posOrder.getAmount()));
                 }

+ 4 - 14
ruoyi-admin/src/main/java/com/ruoyi/app/pay/UserBillingController.java

@@ -9,8 +9,6 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.app.order.ZaloPayCommonService;
 import com.ruoyi.app.pay.dto.QsBillingDto;
-import com.ruoyi.common.core.domain.entity.SysDictData;
-import com.ruoyi.common.utils.DictUtils;
 import com.ruoyi.system.domain.vo.ShBillingDto;
 import com.ruoyi.app.utils.DateUtil;
 import com.ruoyi.app.utils.zaloPay.ZaloPay;
@@ -69,8 +67,6 @@ public class UserBillingController extends BaseController {
     @Autowired
     private IPosOrderRatingService posOrderRatingService;
     @Autowired
-    private ICommissionRatesService commissionRatesService;
-    @Autowired
     private IPosStoreService posStoreService;
     @Autowired
     private FlashDeliveryOrderMapper flashDeliveryOrderMapper;
@@ -377,9 +373,8 @@ public class UserBillingController extends BaseController {
                 result.add(dto);
             });
         }
-        double bl = user.getCommission() == null ? 0.00 : user.getCommission();
-        bl = commissionRatesService.getShCommissionRate(bl, user.getUserId());
-        org.put("commissionRate", bl);
+        // 当前平台抽成已取消;历史明细和汇总仍按保存的账单金额展示。
+        org.put("commissionRate", 0.00);
         org.put("list", result);
     }
 
@@ -430,13 +425,8 @@ public class UserBillingController extends BaseController {
                 result.add(dto);
             });
         }
-        String taxRate="0";
-        List<SysDictData> taxBlData = DictUtils.getDictCache("sys_qs_freight_bl");
-
-        if (taxBlData != null && taxBlData.get(0) != null) {
-            taxRate = taxBlData.get(0).getDictValue();
-        }
-        org.put("taxRate", taxRate);
+        // 当前骑手运费不再扣税费;不能继续展示旧字典费率,历史税费记录保留。
+        org.put("taxRate", "0");
         org.put("list", result);
     }
 

+ 87 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderBillingNoDeductionTest.java

@@ -0,0 +1,87 @@
+package com.ruoyi.app.order;
+
+import com.ruoyi.app.service.WalletService;
+import com.ruoyi.common.core.domain.entity.SysDictData;
+import com.ruoyi.common.utils.DictUtils;
+import com.ruoyi.system.domain.*;
+import com.ruoyi.system.service.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+import java.math.BigDecimal;
+import java.util.List;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/** 已配置非零旧抽成/税率也必须全额入账,并保留摊位钱包和重复结算保护。 */
+class OrderBillingNoDeductionTest {
+    private final IUserBillingService bills = mock(IUserBillingService.class);
+    private final IPosStoreService stores = mock(IPosStoreService.class);
+    private final IInfoUserService users = mock(IInfoUserService.class);
+    private final WalletService wallet = mock(WalletService.class);
+    private final OrderService service = new OrderService();
+
+    OrderBillingNoDeductionTest() {
+        ReflectionTestUtils.setField(service, "userBillingService", bills);
+        ReflectionTestUtils.setField(service, "posStoreService", stores);
+        ReflectionTestUtils.setField(service, "infoUserService", users);
+        ReflectionTestUtils.setField(service, "walletService", wallet);
+    }
+
+    private PosOrder order() {
+        PosOrder o = new PosOrder(); o.setDdId("NO-DEDUCTION"); o.setMdId(9L);
+        o.setShId(10L); o.setQsId(20L); o.setAmount(120); o.setFreight(30.0);
+        return o;
+    }
+
+    @ParameterizedTest
+    @CsvSource({"false,false", "false,true", "true,false", "true,true"})
+    void merchantReceivesFullGoodsAmountForLegacyAndVersionTwo(boolean stall, boolean versionTwo) {
+        PosStore store = new PosStore(); store.setId(9); store.setIsStall(stall ? 1 : 0);
+        store.setNightMarketId(stall ? 10L : null);
+        when(stores.getById(9L)).thenReturn(store);
+        InfoUser merchant = new InfoUser(); merchant.setCommission(0.25);
+        when(users.getById(10L)).thenReturn(merchant);
+        PosOrder o = order();
+        o.setFood("[{\"price\":45,\"otherPrice\":5,\"number\":2}]");
+        if (versionTwo) {
+            o.setPromotionPricingVersion(2); o.setCustomerDeliveryFee(BigDecimal.valueOf(20));
+            o.setMerchantDeliverySubsidy(BigDecimal.TEN);
+        }
+        when(bills.count(any())).thenReturn(0L, 1L);
+        service.setSanghuBilling(o);
+        service.setSanghuBilling(o);
+        var capture = ArgumentCaptor.forClass(UserBilling.class);
+        if (stall) verify(wallet).addStallBalance(eq(9L), eq(BigDecimal.valueOf(100.0)), capture.capture());
+        else verify(wallet).addBalance(eq(10L), eq(BigDecimal.valueOf(100.0)), capture.capture());
+        assertEquals(100.0, capture.getValue().getAmount());
+        assertEquals(0.0, capture.getValue().getDivvy());
+        assertEquals(0.0, capture.getValue().getDivvyRate());
+        verifyNoMoreInteractions(wallet);
+    }
+
+    @Test
+    void riderReceivesAllFreightDespiteOldTaxAndCommissionAndKeepsCashCollection() {
+        InfoUser rider = new InfoUser(); rider.setCommission(0.25);
+        when(users.getById(20L)).thenReturn(rider);
+        SysDictData tax = new SysDictData(); tax.setDictValue("0.10");
+        try (var dict = mockStatic(DictUtils.class)) {
+            dict.when(() -> DictUtils.getDictCache("sys_qs_freight_bl")).thenReturn(List.of(tax));
+            PosOrder o = order(); o.setCollectPayment("1");
+            when(bills.count(any())).thenReturn(0L, 1L);
+            service.setQishouBilling(o);
+            service.setQishouBilling(o);
+            var capture = ArgumentCaptor.forClass(UserBilling.class);
+            verify(wallet).addBalance(eq(20L), eq(BigDecimal.valueOf(30.0)), capture.capture());
+            UserBilling bill = capture.getValue();
+            assertEquals(30.0, bill.getAmount());
+            assertEquals(0.0, bill.getDivvy()); assertEquals(0.0, bill.getDivvyRate());
+            assertEquals(0, BigDecimal.ZERO.compareTo(bill.getTax())); assertEquals("0", bill.getTaxRate());
+            assertEquals(120.0, bill.getBehalfAmount());
+            verifyNoMoreInteractions(wallet);
+        }
+    }
+}

+ 6 - 6
ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderFreeDeliverySettlementTest.java

@@ -93,7 +93,7 @@ class OrderFreeDeliverySettlementTest
         InfoUser merchant = new InfoUser();
         merchant.setUserId(5L);
         merchant.setCommission(0.10);
-        when(infoUserService.getById(5L)).thenReturn(merchant);
+        lenient().when(infoUserService.getById(5L)).thenReturn(merchant);
 
         PosOrder order = versionTwoOrder(100, 30.0, BigDecimal.ZERO, BigDecimal.valueOf(30));
         order.setMdSalesReduction(10);
@@ -105,8 +105,8 @@ class OrderFreeDeliverySettlementTest
         // 基数 = A−D = 100(商品优惠已含在A内,免运F不重复扣;旧券ID不查 VipUserQuanyi)
         ArgumentCaptor<UserBilling> captor = ArgumentCaptor.forClass(UserBilling.class);
         verify(walletService).addBalance(eq(5L), any(BigDecimal.class), captor.capture());
-        assertEquals(90.0, captor.getValue().getAmount(), 0.001);
-        assertEquals(10.0, captor.getValue().getDivvy(), 0.001);
+        assertEquals(100.0, captor.getValue().getAmount(), 0.001);
+        assertEquals(0.0, captor.getValue().getDivvy(), 0.001);
         verify(userQuanyiService, never()).getById(anyLong());
     }
 
@@ -137,9 +137,9 @@ class OrderFreeDeliverySettlementTest
         orderService.setQishouBilling(order);
         ArgumentCaptor<UserBilling> captor = ArgumentCaptor.forClass(UserBilling.class);
         verify(walletService).addBalance(eq(66L), any(BigDecimal.class), captor.capture());
-        // 骑手毛配送费仍为 R=30:佣金10%后 27,线上单无代收 behalfAmount
-        assertEquals(27.0, captor.getValue().getAmount(), 0.001);
-        assertEquals(3.0, captor.getValue().getDivvy(), 0.001);
+        // 骑手毛配送费仍为 R=30:不扣佣金及税费,全额30,线上单无代收 behalfAmount
+        assertEquals(30.0, captor.getValue().getAmount(), 0.001);
+        assertEquals(0.0, captor.getValue().getDivvy(), 0.001);
         assertEquals(null, captor.getValue().getBehalfAmount());
 
         // 重复送达不新增账单(幂等守卫命中)

+ 9 - 1
ruoyi-admin/src/test/java/com/ruoyi/app/pay/UserBillingFlashDeliveryTest.java

@@ -39,7 +39,8 @@ class UserBillingFlashDeliveryTest {
     @BeforeEach void setup() {
         mocks = MockitoAnnotations.openMocks(this);
         dictionaries = mockStatic(DictUtils.class);
-        dictionaries.when(() -> DictUtils.getDictCache("sys_qs_freight_bl")).thenReturn(null);
+        var oldTax = new com.ruoyi.common.core.domain.entity.SysDictData(); oldTax.setDictValue("0.10");
+        dictionaries.when(() -> DictUtils.getDictCache("sys_qs_freight_bl")).thenReturn(List.of(oldTax));
         messages = mockStatic(MessageUtils.class);
         tokens = mockConstruction(JwtUtil.class, (jwt, context) -> when(jwt.getusid("token")).thenReturn("8"));
         var rider = new InfoUser(); rider.setUserId(8L); rider.setUserType("2");
@@ -134,7 +135,14 @@ class UserBillingFlashDeliveryTest {
         walletController.getUserBills("token", 1, 10, "0");
         verifyNoInteractions(foodOrders);
     }
+    @Test void merchantCurrentRateIsZeroEvenWhenUserHasOldCommission() {
+        var merchant = new InfoUser(); merchant.setCommission(0.25);
+        var data = new com.alibaba.fastjson.JSONObject();
+        org.springframework.test.util.ReflectionTestUtils.invokeMethod(controller, "setShList", List.of(), data, merchant);
+        assertEquals(0.0, data.getDoubleValue("commissionRate"));
+    }
     private void assertFlash(com.alibaba.fastjson.JSONObject data) {
+        assertEquals("0", data.getString("taxRate"));
         List<QsBillingDto> list = (List<QsBillingDto>) data.get("list");
         assertEquals(1, list.size());
         var bill = list.get(0);

+ 6 - 0
specs/003-merchant-billing/plan.md

@@ -210,6 +210,12 @@ foodie-store/                                     # 商家前端
 git commit -m "feat(billing): 商家端账单功能 — 列表查询、汇总、mdId 过滤"
 ```
 
+## 2026-11-04 无平台抽成调整计划
+
+1. 为普通商家、摊位、旧金额口径、版本2免运、骑手非零旧抽成与税率配置补充回归用例,验证全额入账和重复结算保护。
+2. 在共用 `OrderService` 移除抽成及骑手税费扣减,零值写入原账单字段;在 `UserBillingController` 返回零当前费率,保留历史账单事实。
+3. 定向运行账单、免运、订单完成、自配送和闪送回归并打包;同步规格和任务记录。不修改数据库与历史记录。
+
 ## 2026-11-04 客户端字典接口增量
 
 1. 独立 `AppDictController` 接收显式请求头及查询参数,复用 `ISysDictTypeService.selectDictDataByType`;响应投影为客户端选项。

+ 8 - 0
specs/003-merchant-billing/spec.md

@@ -147,6 +147,14 @@
 
 mdId 过滤采用 `(#{mdId} IS NULL OR md_id = #{mdId})` 模式,mdId 不传时查询全部数据,传入时按门店过滤。涉及 join 查询的方法使用表别名前缀 `b.md_id`。
 
+## 2026-11-04 取消订单账单抽成及骑手税费
+
+- 平台不再从新生成的商家和骑手订单收入账单扣除抽成;骑手也不再按 `sys_qs_freight_bl` 扣税费。旧的用户抽成比例、商家分成配置和骑手税率配置不参与本次结算。
+- 商家商品收入保留现有商品优惠和运费划分:版本2按 `amount - customerDeliveryFee`;旧订单按原商品金额及商家优惠口径。该金额全额入账,摊位仍入摊位钱包。
+- 骑手按订单 `freight` 全额入账,到付代收金额和账单防重复规则保持。闪送含小费全额收入、自配送运费全额收入沿用现有实现。
+- 新账单 `divvy=0`、`divvyRate=0`;骑手另记 `tax=0`、`taxRate="0"`。商家账单响应当前 `commissionRate=0`,骑手响应当前 `taxRate="0"`。历史明细及汇总仍读取原账单,不抹除历史扣款事实。
+- 仅改变新账单生成及当前费率展示,不修改历史账单/钱包,不新增SQL,不调整支付渠道、真实收款和运费交接,不改App或平台页面。
+
 ## 2026-11-04 客户端共用字典查询
 
 新增 `GET /app/dict/data?dictType=sys_billing_type`,商家及用户端共用 `token` 请求头,使用现有客户端会话认证。用户明确确认允许已登录客户端查询任意字典类型,不增加类型白名单。只返回启用项,复用现有缓存及排序;返回 `dictLabel`、`dictValue`、`dictSort`、`isDefault`,不返回后台审计字段。缺失、空白或超过100字符的类型返回国际化业务错误;不存在或无启用项返回空数组。标签使用后台维护原文,不自动翻译或改写类型5的业务含义。本增量仅提供共用接口,不改商家页面或字典数据。

+ 8 - 0
specs/003-merchant-billing/tasks.md

@@ -114,6 +114,14 @@ description: "Task list for 商家前端账单功能 feature implementation"
 - mdId 过滤采用 `(#{mdId} IS NULL OR md_id = #{mdId})` 模式,不传时查全部
 - 前端日/周/月账单明细页(getzddaylist / getzdmeeklist / getzdmatlist)尚未在前端实现,仅后端接口已就绪
 
+## 2026-11-04 取消平台抽成及骑手税费
+
+- [x] T013 商家及摊位商品收入不扣抽成,保留原有优惠、免运基数及钱包归属。
+- [x] T014 骑手运费不扣抽成和税费;账单明细保存零费率,当前费率接口归零,历史汇总不改写。
+- [x] T015 完成定向回归、后端打包及关联规格同步,无数据库迁移和历史回写。
+
+验证记录(2026-11-04):旧实现定向测试 9 项中 7 项按预期失败;修复后 JDK 21 执行上述 6 个测试类共 55 项全部通过,Maven ruoyi-admin 及依赖模块 package 成功,git diff --check 通过。未执行真实数据库入账、支付渠道或 App 联调,未部署。无数据库变更,不回写历史账单。
+
 ## 2026-11-04 客户端共用字典接口
 
 - [x] T016 新增按dictType查询的共用接口,支持商家/用户token与任意类型,返回启用项精简字段。

+ 4 - 0
specs/008-promotion-coupon/contracts/free-delivery.md

@@ -139,3 +139,7 @@ PromotionDetail的originalAmount/deliveryFee/customerDeliveryFee/threshold为typ
 新增本地化key至少包含`promotion.freeDelivery.invalidThreshold`、`invalidRules`、`forbiddenStore`、`overlap`、`invalidAddress`、`invalidProduct`、`invalidSpec`、`unsupportedOrder`、`freightUnavailable`、`alreadyApplied`、`quoteChanged`、`invalidSnapshot`(后三段同前缀)。沿用AjaxResult业务错误机制,不硬编码中文。
 
 本轮验收覆盖商家PC及后端新接口;App用户端、商家App、骑手App的渲染与新报价接入未实施,不得在交付中标记已完成。将来App只需按本契约对接,不重新讨论门槛与预算规则。
+
+## 2026-11-04 账单不再扣平台抽成
+
+免运及商品优惠金额公式保持:版本2商家商品收入取 G=A−D,免运补贴不从商品收入重复扣减;骑手收入取订单配送费 R。新生成账单不再扣平台佣金及骑手运费税费,商品收入 G 和配送费 R 分别全额入账,旧比例配置不再参与。到付/线上交款方向、客户实际应付、优惠及历史账单保持原口径,详见 `specs/003-merchant-billing/spec.md` 的 2026-11-04 调整。

+ 4 - 0
specs/029-merchant-self-delivery/spec.md

@@ -185,3 +185,7 @@
 - "合并订单/同一配送人一趟多单"的批量合并操作机制不在本功能范围,由独立需求实现;本功能中夜市主对旗下订单逐单操作
 - 客户 App(uni-app)与骑手端的界面改造由对应客户端配合交付;本仓库范围包含后端与商家端 PC(含门店设置、订单标识、配送操作、多语言)
 - 推送渠道沿用现有推送体系(iOS 云函数 + 消息入库),不新增渠道
+
+## 2026-11-04 账单抽成规则更新
+
+自配送运费仍全额入配送方,商品收入改为按现有优惠后基数全额入商家/摊位钱包,不再扣平台抽成。普通骑手配送费也不再扣抽成和运费税费。既有配送收入 type=5、归属、防重复及历史账单不变,详见 `specs/003-merchant-billing/spec.md`。