Просмотр исходного кода

校验订单整数运费、修正账单汇总精度并删除旧addorder

qmj 2 дней назад
Родитель
Сommit
948825fe5f
27 измененных файлов с 229 добавлено и 371 удалено
  1. 1 1
      .specify/feature.json
  2. 1 1
      AGENTS.md
  3. 1 1
      CLAUDE.md
  4. 4 2
      docs/order-api-usage.md
  5. 29 0
      ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderMoneyValidator.java
  6. 2 343
      ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java
  7. 2 11
      ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java
  8. 1 0
      ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java
  9. 1 1
      ruoyi-admin/src/main/java/com/ruoyi/app/order/dto/OrderCreatItem.java
  10. 2 2
      ruoyi-admin/src/main/java/com/ruoyi/app/order/dto/OrderCreateInput.java
  11. 11 3
      ruoyi-admin/src/main/java/com/ruoyi/app/pay/UserBillingController.java
  12. 3 0
      ruoyi-admin/src/main/resources/i18n/messages.properties
  13. 3 0
      ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
  14. 3 0
      ruoyi-admin/src/main/resources/i18n/messages_th_TH.properties
  15. 3 0
      ruoyi-admin/src/main/resources/i18n/messages_vi.properties
  16. 3 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
  17. 3 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_TW.properties
  18. 74 0
      ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderWholeYuanFreightTest.java
  19. 29 0
      ruoyi-admin/src/test/java/com/ruoyi/app/pay/StoreBillingSubaccountAccessTest.java
  20. 1 1
      ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSessionAuthAnnotationTest.java
  21. 10 0
      specs/003-merchant-billing/plan.md
  22. 15 0
      specs/003-merchant-billing/spec.md
  23. 23 0
      specs/003-merchant-billing/tasks.md
  24. 1 1
      specs/012-im-user-integration/code-status.md
  25. 1 2
      specs/031-pay-method-config/contracts/api.md
  26. 1 1
      specs/032-self-hosted-map-integration/spec.md
  27. 1 1
      specs/034-stall-qualification/quickstart.md

+ 1 - 1
.specify/feature.json

@@ -1 +1 @@
-{"feature_directory": "specs/038-self-delivery-completion"}
+{"feature_directory":"specs/003-merchant-billing"}

+ 1 - 1
AGENTS.md

@@ -67,7 +67,7 @@
 - `PosOrderShOprateController`:商家出餐和取消。
 - `PosOrderQsOprateController`:骑手接单、取餐、送达,使用 `deliveryStatus` 1/2/3。
 - `UserOrderController`:用户下单和取消。
-- `PosOrderController`:通用管理接口 `POST /system/order` 的 `add(...)`(新增订单)和 `PUT /system/order` 的 `edit(...)`(修改订单信息)已废弃;代码仅保留备查,不要作为订单创建或修改入口,也不要在其上做增量修改。`/addorder`、`/setorderuzt` 和 list 查询仍在使用,主要订单状态流转已经迁出。
+- `PosOrderController`:通用管理接口 `POST /system/order` 的 `add(...)`(新增订单)和 `PUT /system/order` 的 `edit(...)`(修改订单信息)已废弃;代码仅保留备查,不要作为订单创建或修改入口,也不要在其上做增量修改。`/addorder` 已按用户要求于2026-11-04移除,商家下单使用 `/system/orderShOprate/createOrder`;`/setorderuzt` 和 list 查询仍在使用,主要订单状态流转已经迁出。
 
 推送以当前真实通道为准:`PayPush` 没有 Android FCM;本项目仅有 iOS uni 云函数 `msduser`、`msdrider`、`msdstore`,以及 `PushEvent -> push_message` 入库。`cidType` 当前没有实际区分作用。相关状态见 `specs/012-im-user-integration/code-status.md`。
 

+ 1 - 1
CLAUDE.md

@@ -36,7 +36,7 @@ Auto-generated from all feature plans. Last updated: 2026-08-18
 - `PosOrderShOprateController` — 商家操作(出餐 chuCan、商家取消)
 - `PosOrderQsOprateController` — 骑手操作(接单/取餐/送达,按 `deliveryStatus` 1/2/3)
 - `UserOrderController` — 用户操作(下单、用户取消)
-- `PosOrderController` — 通用管理接口 `POST /system/order` 的 `add(...)`、`PUT /system/order` 的 `edit(...)` 及 `/addorder`(旧代客外送下单,货到付款)均已废弃(2026-09-29 确认),代码仅保留备查,不要作为业务入口或在其上增量修改;商家端下单走 `PosOrderShOprateController` 的 `/createOrder`;`/setorderuzt`(平台改状态)及 list 查询仍在用;订单状态流转已迁出。详见 docs/order-api-usage.md
+- `PosOrderController` — 通用管理接口 `POST /system/order` 的 `add(...)`、`PUT /system/order` 的 `edit(...)` 均已废弃(2026-09-29 确认),代码仅保留备查,不要作为业务入口或在其上增量修改;`/addorder` 已于2026-11-04移除;商家端下单走 `PosOrderShOprateController` 的 `/createOrder`;`/setorderuzt`(平台改状态)及 list 查询仍在用;订单状态流转已迁出。详见 docs/order-api-usage.md
 
 **推送通道:** `PayPush` 无 Android FCM(cte_server 有,本项目砍掉),仅 iOS uni 云函数(`msduser/msdrider/msdstore`)+ 入库(`PushEvent`→`push_message`);`cidType` 形同虚设(到处传 `""`)。详见 `specs/012-im-user-integration/code-status.md`。
 

+ 4 - 2
docs/order-api-usage.md

@@ -26,15 +26,17 @@
 
 | 接口 | 说明 |
 |---|---|
-| `/addorder` | 旧代客外送下单流程(货到付款 + sendHdfkMessage 推送);2026-09-29 确认废弃,商家端下单走 `PosOrderShOprateController /createOrder`。内含 031 支付闸门 `assertUsable` 调用点,随入口一并废弃,勿再以此为基准增量开发 |
+
 | `POST /system/order`(`add`) | RuoYi CRUD 新增订单;用户下单走 `UserOrderController.createOrder` |
 | `PUT /system/order`(`edit`) | 修改订单信息入口不再使用;状态流转已迁出 |
 | `/testYjsdTime` | 随 `setYjsdTime` 停用(依赖已停用的 Mapbox 接口,见 map-api-usage.md) |
 
+`POST /system/order/addorder` 已于2026-11-04删除方法及路由,不再接收旧请求;商家代客下单使用 `POST /system/orderShOprate/createOrder`。
+
 ## 新改动规则
 
 1. 状态流转类新功能(改 state / deliveryStatus / payStatus / afterSaleStatus 的操作)一律实现到对应角色控制器,不进 `PosOrderController`。
-2. `PosOrderController` 仅维护查询接口、平台管理接口与既有的 `/addorder`、`/setorderuzt`。
+2. `PosOrderController` 仅维护查询接口、平台管理接口与既有的 `/setorderuzt`。
 3. 废弃方法不做增量修改;排查问题时也不得以其为基准(同 CLAUDE.md 已废弃代码清单约定)。
 
 ## 关联

+ 29 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderMoneyValidator.java

@@ -0,0 +1,29 @@
+package com.ruoyi.app.order;
+
+import com.ruoyi.app.order.dto.OrderCreateInput;
+import com.ruoyi.app.order.dto.OrderCreatItem;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
+
+/** 新订单金额以新台币元为单位;兼容 Double 运费字段,但不接收分角金额。 */
+final class OrderMoneyValidator {
+    private OrderMoneyValidator() {
+    }
+
+    static void validateFreight(OrderCreateInput input) {
+        if (input != null && input.getItems() != null) {
+            for (OrderCreatItem item : input.getItems()) {
+                if (item != null) {
+                    requireWholeFreight(item.getFreight());
+                }
+            }
+        }
+    }
+
+    static void requireWholeFreight(Double freight) {
+        // 保留原有未传运费场景;不对客户端金额静默四舍五入,避免与订单应付不一致。
+        if (freight != null && (!Double.isFinite(freight) || freight < 0 || freight != Math.rint(freight))) {
+            throw new ServiceException(MessageUtils.message("order.freight.wholeYuan.required"));
+        }
+    }
+}

+ 2 - 343
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java

@@ -8,8 +8,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
 import com.ruoyi.app.order.dto.OrderPositionInfo;
 import com.ruoyi.app.order.dto.OrderPushBodyDto;
 import com.ruoyi.app.order.dto.PositionDto;
@@ -25,7 +23,6 @@ import com.ruoyi.app.service.UserService;
 import com.ruoyi.app.service.WalletService;
 import com.ruoyi.app.user.dto.StoreOutput;
 import com.ruoyi.app.utils.DateUtil;
-import com.ruoyi.app.utils.OperatingUtil;
 import com.ruoyi.app.utils.PayPush;
 import com.ruoyi.app.utils.event.PushEventService;
 import com.ruoyi.common.annotation.Anonymous;
@@ -41,7 +38,6 @@ import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.framework.manager.AsyncManager;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.system.domain.*;
-import com.ruoyi.system.domain.vo.OrderDTO;
 import com.ruoyi.system.domain.vo.PosOrderInvoiceVo;
 import com.ruoyi.system.mapper.PosFoodMapper;
 import com.ruoyi.system.mapper.PosOrderMapper;
@@ -58,14 +54,12 @@ import com.ruoyi.system.utils.OrderLogHelper;
 import com.ruoyi.app.push.UserNotificationScene;
 import com.ruoyi.common.utils.SecurityUtils;
 import com.ruoyi.common.core.domain.entity.SysUser;
-import lombok.SneakyThrows;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.i18n.LocaleContextHolder;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.transaction.annotation.Isolation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.support.TransactionSynchronization;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
@@ -91,7 +85,7 @@ import java.util.stream.Stream;
  * <p>本类仍在用的:列表/详情查询(/getorderlist、/getstoreorderlist、/getqsorderlist、/getorderxq 等)、
  * 平台管理操作(/{id}/status、/{id}/line-refund、/{id}/offline-payment/confirm、/{id}/offline-refund/confirm 等
  * Spring Security 权限接口)、平台改状态 /setorderuzt;
- * add/edit 及 /addorder 均已废弃(商家端下单走 PosOrderShOprateController /createOrder),仅保留备查,勿增量修改。
+ * add/edit 已废弃并仅保留备查;/addorder 已移除,商家端下单走 PosOrderShOprateController /createOrder。
  *
  * @author ruoyi
  * @date 2023-05-23
@@ -108,11 +102,6 @@ public class PosOrderController extends BaseController {
     @Autowired
     private IPosStoreService posStoreService;
     @Autowired
-    private StallQualificationGateService stallQualificationGateService;
-    /** 支付方式闸门(031):商家建单与用户下单共用同一判定入口。 */
-    @Autowired
-    private com.ruoyi.system.service.PaymentMethodGateService paymentMethodGateService;
-    @Autowired
     private IInfoAddressService infoAddressService;
     @Autowired
     private IUserFootprintService userFootprintService;
@@ -130,15 +119,9 @@ public class PosOrderController extends BaseController {
     private ISalesPromotionService salesPromotionService;
     @Autowired
     private IPosAppealService posAppealService;
-    @Autowired  //商品
-    private IPosFoodService posFoodService;
-    @Autowired  //经营时间
-    private IOperatingHoursService operatingHoursService;
     @Autowired
     private IVipUserQuanyiService userQuanyiService;
     @Autowired
-    private ISysPointControlService pointControlService;
-    @Autowired
     private IUserWalletService userWalletService;
     @Autowired
     private IPointsTransactionService pointsTransactionService;
@@ -154,8 +137,6 @@ public class PosOrderController extends BaseController {
     @Autowired
     private IPosOrderRatingService posOrderRatingService;
     @Autowired
-    private IFoodStatisticsService foodStatisticsService;
-    @Autowired
     private IOrderParentService orderParentService;
     @Autowired
     private WalletService walletService;
@@ -1100,278 +1081,7 @@ public class PosOrderController extends BaseController {
 //        Boolean sbsj = dateUtil.isLegalTime(aa,bb);
 //        return success(sbsj);
 //    }
-    //已废弃(2026-09-29 标注):商家端下单走 PosOrderShOprateController /createOrder;
-    //本入口为旧代客外送下单流程(货到付款),保留备查勿增量修改(内含 031 支付闸门调用点,随入口一并废弃)
-    @Deprecated
-    @SneakyThrows
-    @Anonymous
-    @Auth
-    @RepeatSubmit(interval = 1000, message = "请求过于频繁")
-    @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
-    @PostMapping("/addorder")
-    public AjaxResult addorder(@RequestHeader String token, @RequestBody OrderDTO orderDTO) {
-        long methodStart = System.currentTimeMillis(); // 记录方法开始时间
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.enable(SerializationFeature.INDENT_OUTPUT);
-        Integer xiadanjiaoyan = orderDTO.getXiadanjiaoyan();
-        if (xiadanjiaoyan == null || !xiadanjiaoyan.equals(2)) {
-            return error(MessageUtils.message("no.xiadanyz.message"));
-        }
-
-        System.out.println("========== 接收到的orderDTO ==========");
-        System.out.println(mapper.writeValueAsString(orderDTO));
-        JwtUtil jwtUtil = new JwtUtil();
-        DateUtil dateUtil = new DateUtil();
-        String id = jwtUtil.getusid(token);
-        QueryWrapper<OperatingHours> wrapper = new QueryWrapper<>();
-        wrapper.eq("md_id", orderDTO.getMdId());
-        List<OperatingHours> hourslist = operatingHoursService.list(wrapper);
-        PosStore store = posStoreService.getById(orderDTO.getMdId());
-        // 旧路由仍可调用,最小门禁防止从这里绕过摊位材料审核。
-        stallQualificationGateService.requireApproved(store);
-
-        JSONObject usdizhi = new JSONObject();
-        if (orderDTO.getType() != null && orderDTO.getType().equals(0L)) {
-            InfoAddress usadd = infoAddressService.getById(orderDTO.getShdzId());
-            if (usadd == null) {
-                return error(MessageUtils.message("no.address.not.exist"));
-            } else {
-                usdizhi.put("name", usadd.getName());
-                usdizhi.put("phone", usadd.getPhone());
-                usdizhi.put("address", usadd.getAddress());
-                usdizhi.put("country", usadd.getCountry());
-                usdizhi.put("province", usadd.getProvince());
-                usdizhi.put("city", usadd.getCity());
-                usdizhi.put("area", usadd.getArea());
-                usdizhi.put("longitude", String.valueOf(usadd.getLongitude()));
-                usdizhi.put("latitude", String.valueOf(usadd.getLatitude()));
-            }
-        }
-        if (hourslist.size() == 0) {
-            return error(MessageUtils.message("no.mendian.not.set.business.hours"));
-        }
-        if (store.getState() == 1) {
-            return error(MessageUtils.message("no.mendian.is.closed"));
-        }
-        //是否在上班时间内(按星期几和是否营业过滤)
-        boolean isOperating = OperatingUtil.storeIsOperatingOfDay(hourslist);
-        if (!isOperating) {
-            return error(MessageUtils.message("no.mendian.is.closed"));
-        }
-        //订单金额限制100万改为200万
-        if (orderDTO.getCollectPayment().equals("1") && orderDTO.getAmount() > 2000000) {
-            return error(MessageUtils.message("no.cash_on_delivery_amount.exceed.limit_amount"));
-        }
-        QueryWrapper<PosOrder> query = new QueryWrapper<>();
-        query.eq("user_id", id);
-        query.eq("collect_payment", "1");
-        query.notIn("state", 5, 10, 11);
-        List<PosOrder> posOrders = posOrderService.list(query);
-        System.out.println("到付信息:user_id=" + id + " ,到付数量:" + posOrders.size());
-        //到付能存在2单未完成,第3单弹出提示
-        if (posOrders.size() >= 2 && orderDTO.getCollectPayment().equals("1")) {
-            return error(MessageUtils.message("no.exist.cash_on_delivery_order.incomplete"));
-        }
-        JSONArray jsonArray = orderDTO.getFoodlist();
-        Map<Long, Long> map = new HashMap<>();
-        for (int i = 0; i < jsonArray.size(); i++) {
-            Long foodid = jsonArray.getJSONObject(i).getLong("id");
-            PosFood food = posFoodService.getById(foodid);
-            if (food == null) {
-                return error(jsonArray.getJSONObject(i).getString("name") + MessageUtils.message("no.goods.not.exist"));
-            } else {
-                if (food.getStackingUp().equals("1")) {
-                    return error(food.getName() + MessageUtils.message("no.goods.is.off.shelf"));
-                }
-            }
-            Long num = jsonArray.getJSONObject(i).getLong("number");
-            map.put(foodid, num);
-        }
-        VipUserQuanyi yh = null;
-        VipUserQuanyi mdYh = null;
-        if (ObjectUtil.isNotNull(orderDTO.getYhId()) && ObjectUtil.notEqual(orderDTO.getYhId(), 0L)) {
-            LambdaQueryWrapper<VipUserQuanyi> yhQuery = new LambdaQueryWrapper<>();
-            yhQuery.eq(VipUserQuanyi::getId, orderDTO.getYhId());
-            yh = userQuanyiService.getOne(yhQuery);
-            if (yh == null) {
-                throw new ServiceException("no.userquanyi.not.exist");
-            }
-            if (yh.getState().equals("1")) {
-                throw new ServiceException("userquanyi.isUsed");
-            }
-        }
-        if (ObjectUtil.isNotNull(orderDTO.getMdYhId()) && ObjectUtil.notEqual(orderDTO.getMdYhId(), 0L)) {
-            LambdaQueryWrapper<VipUserQuanyi> mdYhquery = new LambdaQueryWrapper<>();
-            mdYhquery.eq(VipUserQuanyi::getId, orderDTO.getMdYhId());
-            mdYh = userQuanyiService.getOne(mdYhquery);
-            if (mdYh == null) {
-                throw new ServiceException("no.userquanyi.not.exist");
-            }
-            if (mdYh.getState().equals("1")) {
-                throw new ServiceException("userquanyi.isUsed");
-            }
-        }
-        SalesPromotion activity = null;
-        SalesPromotion mdActivity = null;
-        if (ObjectUtil.isNotNull(orderDTO.getActivity()) && ObjectUtil.notEqual(orderDTO.getActivity(), 0L)) {
-            LambdaQueryWrapper<SalesPromotion> saleQuery = new LambdaQueryWrapper<>();
-            saleQuery.eq(SalesPromotion::getId, orderDTO.getActivity());
-            activity = salesPromotionService.getOne(saleQuery);
-            if (activity == null) {
-                throw new ServiceException("no.activity.not.exist");
-            }
-//            if (yh.getState().equals("1")) {
-//                throw new ServiceException("userquanyi.isUsed");
-//            }
-        }
-        if (ObjectUtil.isNotNull(orderDTO.getMdActivity()) && ObjectUtil.notEqual(orderDTO.getMdActivity(), 0L)) {
-            LambdaQueryWrapper<SalesPromotion> saleQuery = new LambdaQueryWrapper<>();
-            saleQuery.eq(SalesPromotion::getId, orderDTO.getMdActivity());
-            mdActivity = salesPromotionService.getOne(saleQuery);
-
-            if (mdActivity == null) {
-                throw new ServiceException("no.activity.not.exist");
-            }
-
-        }
-        PosOrder posOrder = new PosOrder();
-        AjaxResult posOrderResult = redeePoints(orderDTO, Long.valueOf(id));
-        if (!posOrderResult.get("code").equals(200)) {
-            return posOrderResult;
-        } else {
-            posOrder.setPoints(orderDTO.getPoints());
-            posOrder.setPointsReduction(orderDTO.getPointsReduction());
-        }
-        posOrder.setDdId(orderDTO.getDdId());
-        posOrder.setShId(orderDTO.getShId());
-        posOrder.setMdId(orderDTO.getMdId());
-        posOrder.setShdzId(orderDTO.getShdzId());
-        posOrder.setShAddress(JSON.toJSONString(usdizhi));
-        posOrder.setUserId(Long.valueOf(id));
-        posOrder.setAmount(orderDTO.getAmount());
-        posOrder.setRemarks(orderDTO.getRemarks());
-        posOrder.setType(orderDTO.getType());
-        posOrder.setDelryTime(orderDTO.getDelryTime());
-        posOrder.setFood(jsonArray.toString());
-        posOrder.setState(0L);
-
-        posOrder.setJvli(orderDTO.getJvli());
-        posOrder.setFreight(orderDTO.getFreight());
-        posOrder.setLongitude(orderDTO.getLongitude());
-        posOrder.setLatitude(orderDTO.getLatitude());
-        posOrder.setDiningStatus(orderDTO.getDiningStatus());
-        posOrder.setQsId(orderDTO.getQsId());
-        posOrder.setJuli(orderDTO.getJuli());
-        posOrder.setPayUrl(orderDTO.getPayUrl());
-        posOrder.setCollectPayment(orderDTO.getCollectPayment());
-        posOrder.setAfterSaleStatus(0L);
-        posOrder.setDeliveryStatus(Long.valueOf(0L).equals(orderDTO.getType()) ? 0L : null);
-        posOrder.setPayStatus(Long.valueOf(0L).equals(orderDTO.getType())
-                && "1".equals(orderDTO.getCollectPayment()) ? 1L : 0L);
-
-        // posOrder.setCretim(dateUtil.getDatetim(orderDTO.getCretim()));
-        //创建时间改成后端生成
-        posOrder.setCretim(new Date());
-
-        if (yh != null) {
-            posOrder.setYhName(yh.getName());
-            posOrder.setYhId(orderDTO.getYhId());
-            posOrder.setDiscountAmount(orderDTO.getDiscountAmount());
-        }
-
-        if (mdYh != null) {
-            posOrder.setMdYhId(orderDTO.getMdYhId());
-            posOrder.setMdYhName(mdYh.getName());
-            posOrder.setMdDiscountAmount(orderDTO.getMdDiscountAmount());
-        }
-
-        if (mdActivity != null) {
-            posOrder.setMdActivity(orderDTO.getMdActivity());
-            posOrder.setMdSalesName(mdActivity.getSalesName());
-            posOrder.setMdSalesReduction(orderDTO.getMdSalesReduction());
-        }
-        if (activity != null) {
-            posOrder.setActivity(orderDTO.getActivity());
-            posOrder.setSalesName(activity.getSalesName());
-            posOrder.setSalesReduction(orderDTO.getSalesReduction());
-        }
-        //设置支付类型为货到付款
-        if ("1".equals(posOrder.getCollectPayment())) {
-            // 031 闸门对称校验:平台关闭到付时商家建单同样被拒,与用户下单同口径(spec FR-004)。
-            com.ruoyi.system.domain.PosStore payStore = posStoreService.getById(posOrder.getMdId());
-            if (payStore != null) {
-                paymentMethodGateService.assertUsable("1",
-                        com.ruoyi.system.service.PaymentMethodGateService.GateScope.MERCHANT,
-                        payStore.getId().longValue(), payStore.getUserId());
-            }
-            posOrder.setPayType("1");
-        }
-        Boolean org = posOrderService.saveOrUpdate(posOrder);
-        foodStatistics(map);
-        if (org) {
-            QueryWrapper<UserBilling> queryWrapper = new QueryWrapper<>();
-            queryWrapper.eq("user_id", id);
-            queryWrapper.eq("dd_id", posOrder.getDdId());
-            UserBilling billing = userBillingService.getOne(queryWrapper);
-            if (billing == null) {
-                UserBilling usb = new UserBilling();
-                usb.setUserId(Long.valueOf(id));
-                usb.setDdId(String.valueOf(posOrder.getDdId()));
-                usb.setMdId(posOrder.getMdId());
-                usb.setState("3");
-                usb.setType("3");
-                userBillingService.saveOrUpdate(usb);
-            }
-            QueryWrapper<UserFootprint> Wrapper = new QueryWrapper<>();
-            Wrapper.eq("user_id", id);
-            Wrapper.eq("md_id", posOrder.getMdId());
-            UserFootprint foot = userFootprintService.getOne(Wrapper);
-            DateUtil dateString = new DateUtil();
-            if (foot == null) {
-                UserFootprint usf = new UserFootprint();
-                usf.setUserId(Long.valueOf(id));
-                usf.setMdId(posOrder.getMdId());
-                usf.setCretim(dateString.GetDate());
-                userFootprintService.saveOrUpdate(usf);
-            } else {
-                foot.setCretim(dateString.GetDate());
-                userFootprintService.saveOrUpdate(foot);
-            }
-            QueryWrapper<PosOrder> querywra = new QueryWrapper<>();
-            querywra.eq("dd_id", posOrder.getDdId());
-            PosOrder order = posOrderService.getOne(querywra);
-            //货到付款、类型外卖,推送给骑手有新订单了
-            if (order.getCollectPayment().equals("1") && order.getType() == 0) {
-                sendHdfkMessage(order);
-            }
-
-            //存在使用优惠券的话,核销优惠券
-            redeemCoupons(yh, mdYh, order.getDdId());
-            System.out.println("========== 输出的order ==========");
-            System.out.println(mapper.writeValueAsString(order));
-            long methodEnd = System.currentTimeMillis(); // 方法结束时间
-            System.out.println("addorder 方法总执行耗时: " + (methodEnd - methodStart) + " ms");
-            return success(MessageUtils.message("no.order.submit.success"), order);
-        } else {
-            long methodEnd = System.currentTimeMillis(); // 方法结束时间
-            System.out.println("addorder 方法总执行耗时: " + (methodEnd - methodStart) + " ms");
-            return error();
-        }
-    }
-
-
-
-
-    //统计美食销售量
-    private void foodStatistics(Map<Long, Long> map) {
-        for (Map.Entry<Long, Long> entry : map.entrySet()) {
-            Long key = entry.getKey();
-            Long value = entry.getValue();
-            // 使用数据库原子UPSERT,避免并发下的竞态问题
-            foodStatisticsService.increaseNumber(key, value);
-        }
-    }
-
+    // 旧 /addorder 创建入口已移除;商家代客下单统一使用 /system/orderShOprate/createOrder。
     /**
      * 货到付款发送推送
      *
@@ -1381,37 +1091,6 @@ public class PosOrderController extends BaseController {
         deliveryOrderNotificationService.notifyOrderAvailable(order);
     }
 
-    //核销积分
-    private AjaxResult redeePoints(OrderDTO orderDTO, Long userId) {
-        if (ObjectUtil.isNotNull(orderDTO.getPoints()) && ObjectUtil.notEqual(orderDTO.getPoints(), 0) && ObjectUtil.isNotNull(orderDTO.getPointsReduction()) && ObjectUtil.notEqual(orderDTO.getPointsReduction(), 0)) {
-            SysPointControl control = pointControlService.getById(1L);
-            if (!control.getOrderUseEnable().equals(1L)) {
-                throw new ServiceException(MessageUtils.message("no.points.use.enable"));
-            }
-            LambdaQueryWrapper<UserWallet> walletQuery = new LambdaQueryWrapper<>();
-            walletQuery.eq(UserWallet::getUserId, userId);
-            walletQuery.isNull(UserWallet::getStoreId);
-            UserWallet userWallet = userWalletService.getOne(walletQuery);
-            if (ObjectUtil.isNotNull(userWallet)) {
-                if (userWallet.getPointsWallet().compareTo(Long.valueOf(orderDTO.getPoints())) >= 0) {
-                    walletQuery.eq(UserWallet::getVersion, userWallet.getVersion());
-                    userWallet.setPointsWallet(userWallet.getPointsWallet() - orderDTO.getPoints());
-                    userWallet.setVersion(userWallet.getVersion() + 1);
-                    boolean update = userWalletService.update(userWallet, walletQuery);
-                    createPointTransaction(userId, Long.valueOf(orderDTO.getPoints()), userWallet.getPointsWallet(), orderDTO.getDdId().toString(), "1");
-                    if (!update) {
-                        throw new ServiceException(MessageUtils.message("no.points.update.fail"));
-                    }
-                } else {
-                    throw new ServiceException(MessageUtils.message("no.points.insufficient"));
-                }
-            } else {
-                throw new ServiceException(MessageUtils.message("no.points.not.exist"));
-            }
-        }
-        return success();
-    }
-
     /**
      * 创建积分流水
      */
@@ -1454,26 +1133,6 @@ public class PosOrderController extends BaseController {
     }
 
 
-    /**
-     * 核销优惠券
-     *
-     * @param orderId
-     */
-    private void redeemCoupons(VipUserQuanyi yh, VipUserQuanyi mdYh, String orderId) {
-        if (ObjectUtil.isNotNull(yh)) {
-            yh.setState("1");
-            yh.setOrderId(orderId);
-            yh.setUpdateTime(new Date());
-            userQuanyiService.saveOrUpdate(yh);
-        }
-        if (ObjectUtil.isNotNull(mdYh)) {
-            mdYh.setState("1");
-            mdYh.setOrderId(orderId);
-            mdYh.setUpdateTime(new Date());
-            userQuanyiService.saveOrUpdate(mdYh);
-        }
-    }
-
     /**
      * 查询posorder列表
      */

+ 2 - 11
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java

@@ -314,6 +314,7 @@ public class PosOrderShOprateController extends BaseController {
                 || item.getAmount() <= 0) {
             throw new ServiceException(MessageUtils.message("line.pay.offline.order.invalid"));
         }
+        OrderMoneyValidator.requireWholeFreight(item.getFreight());
         if ("5".equals(merchant.getUserType())) {
             MerchantAccessContext context = merchantStoreAccessService.resolve(merchant.getUserId());
             merchantStoreAccessService.requireStoreAccess(merchant.getUserId(), item.getMdId());
@@ -984,16 +985,6 @@ public class PosOrderShOprateController extends BaseController {
         return success(result);
     }
 
-    @Anonymous
-    @Auth
-    @GetMapping("/orderDetail")
-    public AjaxResult orderDetail(@RequestHeader String token, @RequestParam Long id) {
-        requireManagedOrder(id);
-        PosOrder order = posOrderService.getById(id);
-        if (order == null) {
-            throw new ServiceException(MessageUtils.message("merchant.order.not.found"));
-        }
-        return success(order);
-    }
+
 
 }

+ 1 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java

@@ -168,6 +168,7 @@ public class UserOrderController extends BaseController {
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult createOrder(@RequestHeader String token, @RequestBody OrderCreateInput input)
     {
+        OrderMoneyValidator.validateFreight(input);
         JwtUtil jwtUtil = new JwtUtil();
         String id = jwtUtil.getusid(token);
         String shdz=null;

+ 1 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/order/dto/OrderCreatItem.java

@@ -54,7 +54,7 @@ public class OrderCreatItem {
     private Double jvli;
 
     /**
-     * 运费
+     * 运费(非负整数新台币元;保留 Double 类型兼容已有请求)
      */
 
     private Double freight;

+ 2 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/order/dto/OrderCreateInput.java

@@ -18,7 +18,7 @@ public class OrderCreateInput {
      * 订单类型:0外送,1自取,2堂食
      */
     private Long type;
-    /** 订单总金额(分) */
+    /** 订单总金额(整数新台币元) */
 
     private Long totalAmount;
 
@@ -27,7 +27,7 @@ public class OrderCreateInput {
 
     //private Long totalDiscount;
 
-    /** 实际支付金额(分) */
+    /** 实际支付金额(整数新台币元) */
 
     private Long actualPayAmount;
 

+ 11 - 3
ruoyi-admin/src/main/java/com/ruoyi/app/pay/UserBillingController.java

@@ -196,9 +196,11 @@ public class UserBillingController extends BaseController {
         );
 
         UserBilling summary = userBillingService.getOne(wrapper);
-        double totalAmount = summary != null && summary.getAmount() != null ? summary.getAmount() : 0;
-        double totalDivvy = summary != null && summary.getDivvy() != null ? summary.getDivvy() : 0;
-        double netIncome = totalAmount - totalDivvy;
+        // 历史账单仍保留实际分角;仅消除 SUM/Double 的二进制尾差,新整数金额不附带 .0。
+        BigDecimal totalAmount = displayAmount(summary == null ? null : summary.getAmount());
+        BigDecimal totalDivvy = displayAmount(summary == null ? null : summary.getDivvy());
+        // amount 已是实际入账金额;历史抽成只展示,不得重复扣减。
+        BigDecimal netIncome = totalAmount;
 
         JSONObject org = new JSONObject();
         org.put("totalAmount", totalAmount);
@@ -207,6 +209,12 @@ public class UserBillingController extends BaseController {
         return success(MessageUtils.message("no.success"), org);
     }
 
+    private BigDecimal displayAmount(Double value) {
+        BigDecimal amount = (value == null ? BigDecimal.ZERO : BigDecimal.valueOf(value))
+                .setScale(2, java.math.RoundingMode.HALF_UP).stripTrailingZeros();
+        return amount.scale() < 0 ? amount.setScale(0) : amount;
+    }
+
     /** 列表和汇总共用分管范围:账单属于主账号,门店权限必须每次重新解析。 */
     private void scopeSubaccountBilling(QueryWrapper<UserBilling> wrapper, Long loginUserId, Long mdId) {
         MerchantAccessContext context = merchantStoreAccessService.resolve(loginUserId);

+ 3 - 0
ruoyi-admin/src/main/resources/i18n/messages.properties

@@ -529,6 +529,9 @@ invoice.print.preparing=发票打印文件正在准备,请稍后重试
 
 app.dict.type.invalid=字典类型不能为空且不能超过100个字符
 
+# New orders use whole TWD yuan; existing billing snapshots are unchanged.
+order.freight.wholeYuan.required=Delivery fee must be a non-negative whole number in TWD.
+
 # Rider income statements
 rider.income.query.invalid=查詢參數無效,請檢查起止時間(年月日時分秒)、訂單類型及分頁
 rider.income.access.denied=僅啟用的騎手帳號可查看收入帳單

+ 3 - 0
ruoyi-admin/src/main/resources/i18n/messages_en_US.properties

@@ -515,6 +515,9 @@ invoice.print.preparing=Invoice print files are being prepared. Please try again
 
 app.dict.type.invalid=Dictionary type is required and must not exceed 100 characters
 
+# New orders use whole TWD yuan; existing billing snapshots are unchanged.
+order.freight.wholeYuan.required=Delivery fee must be a non-negative whole number in TWD.
+
 # Rider income statements
 rider.income.query.invalid=Invalid query. Check start and end times (yyyy-MM-dd HH:mm:ss), order type and pagination.
 rider.income.access.denied=Only active rider accounts can view income statements.

+ 3 - 0
ruoyi-admin/src/main/resources/i18n/messages_th_TH.properties

@@ -498,6 +498,9 @@ no.oauth.binding.conflict=การเชื่อมโยงบัญชีภ
 
 app.dict.type.invalid=ต้องระบุประเภทพจนานุกรมและต้องไม่เกิน 100 อักขระ
 
+# New orders use whole TWD yuan; existing billing snapshots are unchanged.
+order.freight.wholeYuan.required=ค่าจัดส่งต้องเป็นจำนวนเต็มที่ไม่ติดลบในหน่วยดอลลาร์ไต้หวัน
+
 # Rider income statements
 rider.income.query.invalid=ข้อมูลค้นหาไม่ถูกต้อง โปรดตรวจสอบเวลาเริ่มต้นและสิ้นสุด (yyyy-MM-dd HH:mm:ss) ประเภทคำสั่งซื้อ และการแบ่งหน้า
 rider.income.access.denied=เฉพาะบัญชีไรเดอร์ที่ใช้งานอยู่เท่านั้นที่ดูรายการรายได้ได้

+ 3 - 0
ruoyi-admin/src/main/resources/i18n/messages_vi.properties

@@ -53,6 +53,9 @@ invoice.print.preparing=Tệp in hóa đơn đang được chuẩn bị. Vui lò
 
 app.dict.type.invalid=Loại từ điển là bắt buộc và không được vượt quá 100 ký tự
 
+# New orders use whole TWD yuan; existing billing snapshots are unchanged.
+order.freight.wholeYuan.required=Phí giao hàng phải là số nguyên không âm, tính bằng TWD.
+
 # Rider income statements
 rider.income.query.invalid=Tham số không hợp lệ. Vui lòng kiểm tra thời gian bắt đầu và kết thúc (yyyy-MM-dd HH:mm:ss), loại đơn và phân trang.
 rider.income.access.denied=Chỉ tài khoản tài xế đang hoạt động mới được xem thu nhập.

+ 3 - 0
ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties

@@ -517,6 +517,9 @@ invoice.print.preparing=发票打印文件正在准备,请稍后重试
 
 app.dict.type.invalid=字典类型不能为空且不能超过100个字符
 
+# New orders use whole TWD yuan; existing billing snapshots are unchanged.
+order.freight.wholeYuan.required=运费必须为非负整数新台币金额
+
 # Rider income statements
 rider.income.query.invalid=查询参数无效,请检查起止时间(年月日时分秒)、订单类型及分页
 rider.income.access.denied=仅启用的骑手账号可查看收入账单

+ 3 - 0
ruoyi-admin/src/main/resources/i18n/messages_zh_TW.properties

@@ -516,6 +516,9 @@ invoice.print.preparing=發票列印檔案正在準備,請稍後重試
 
 app.dict.type.invalid=字典類型不能為空且不能超過100個字元
 
+# New orders use whole TWD yuan; existing billing snapshots are unchanged.
+order.freight.wholeYuan.required=運費必須為非負整數新臺幣金額
+
 # Rider income statements
 rider.income.query.invalid=查詢參數無效,請檢查起止時間(年月日時分秒)、訂單類型及分頁
 rider.income.access.denied=僅啟用的騎手帳號可查看收入帳單

+ 74 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderWholeYuanFreightTest.java

@@ -0,0 +1,74 @@
+package com.ruoyi.app.order;
+
+import com.ruoyi.app.order.dto.OrderCreateInput;
+import com.ruoyi.app.order.dto.OrderCreatItem;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.PosStore;
+import com.ruoyi.system.service.IInfoAddressService;
+import com.ruoyi.system.service.IPosStoreService;
+import com.ruoyi.system.utils.JwtUtil;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.*;
+import org.springframework.test.util.ReflectionTestUtils;
+import java.util.List;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/** 新订单不得把带分角的运费写进订单,再流入骑手/商家账单。 */
+class OrderWholeYuanFreightTest {
+    MockedStatic<MessageUtils> messages;
+    MockedConstruction<JwtUtil> tokens;
+    @BeforeEach void setup() {
+        messages=mockStatic(MessageUtils.class);
+        messages.when(() -> MessageUtils.message(anyString())).thenAnswer(i -> i.getArgument(0));
+        tokens=mockConstruction(JwtUtil.class,(jwt, ctx)->when(jwt.getusid("token")).thenReturn("9"));
+    }
+    @AfterEach void cleanup() { tokens.close(); messages.close(); }
+    OrderCreateInput input(double freight) {
+        OrderCreatItem item=new OrderCreatItem(); item.setMdId(101L); item.setShId(9L);
+        item.setAmount(100); item.setFreight(freight); item.setType(0L);
+        OrderCreateInput input=new OrderCreateInput(); input.setType(0L); input.setItems(List.of(item));
+        return input;
+    }
+    InfoUser merchant() { InfoUser user=new InfoUser(); user.setUserId(9L); user.setUserType("1"); return user; }
+    PosOrderShOprateController merchantController() {
+        PosOrderShOprateController controller=new PosOrderShOprateController();
+        IPosStoreService stores=mock(IPosStoreService.class);
+        PosStore store=new PosStore(); store.setUserId(9L);
+        when(stores.getById(101L)).thenReturn(store);
+        ReflectionTestUtils.setField(controller,"posStoreService",stores);
+        return controller;
+    }
+    @ParameterizedTest @ValueSource(doubles={40.5,0.01,-1,Double.NaN,Double.POSITIVE_INFINITY})
+    void merchantCannotCreateFractionalOrInvalidFreight(double freight) {
+        ServiceException error=assertThrows(ServiceException.class,
+                ()->merchantController().validateMerchantOrderInput(merchant(),input(freight)));
+        assertEquals("order.freight.wholeYuan.required",error.getMessage());
+    }
+    @ParameterizedTest @ValueSource(doubles={0,40,100})
+    void wholeYuanFreightKeepsExistingMerchantFlow(double freight) {
+        assertDoesNotThrow(()->merchantController().validateMerchantOrderInput(merchant(),input(freight)));
+    }
+    @Test void userRejectsFractionalFreightBeforeAddressAndOrderWrites() {
+        UserOrderController controller=new UserOrderController();
+        IInfoAddressService addresses=mock(IInfoAddressService.class);
+        ReflectionTestUtils.setField(controller,"infoAddressService",addresses);
+        ServiceException error=assertThrows(ServiceException.class,()->controller.createOrder("token",input(40.5)));
+        assertEquals("order.freight.wholeYuan.required",error.getMessage());
+        verifyNoInteractions(addresses);
+    }
+    @Test void removedLegacyCreateEntryHasNoControllerMethodOrPostMapping() {
+        for (java.lang.reflect.Method method : PosOrderController.class.getDeclaredMethods()) {
+            assertNotEquals("addorder", method.getName());
+            var mapping = method.getAnnotation(org.springframework.web.bind.annotation.PostMapping.class);
+            if (mapping != null) {
+                assertFalse(java.util.Arrays.asList(mapping.value()).contains("/addorder"));
+                assertFalse(java.util.Arrays.asList(mapping.path()).contains("/addorder"));
+            }
+        }
+    }
+}

+ 29 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/pay/StoreBillingSubaccountAccessTest.java

@@ -46,6 +46,35 @@ class StoreBillingSubaccountAccessTest {
     }
     @AfterEach void cleanup() throws Exception { messages.close(); tokens.close(); mocks.close(); }
 
+    @Test void summaryDoesNotExposeBinaryFloatingPointTail() {
+        UserBilling summary = new UserBilling();
+        summary.setAmount(3393.6000000000004); summary.setDivvy(0.0);
+        when(bills.getOne(any())).thenReturn(summary);
+        var data = (com.alibaba.fastjson.JSONObject) request(true, null).get("data");
+        assertEquals(0, new java.math.BigDecimal(data.get("totalAmount").toString())
+                .compareTo(new java.math.BigDecimal("3393.60")));
+        assertEquals(0, new java.math.BigDecimal(data.get("netIncome").toString())
+                .compareTo(new java.math.BigDecimal("3393.60")));
+    }
+
+    @Test void wholeYuanSummaryHasNoFractionAndHistoricalAmountIsNotRoundedToWholeYuan() {
+        UserBilling summary = new UserBilling(); summary.setAmount(100.0); summary.setDivvy(0.0);
+        when(bills.getOne(any())).thenReturn(summary);
+        var data = (com.alibaba.fastjson.JSONObject) request(true, null).get("data");
+        assertEquals("100", data.get("totalAmount").toString());
+        summary.setAmount(80.75);
+        data = (com.alibaba.fastjson.JSONObject) request(true, null).get("data");
+        assertEquals("80.75", data.get("totalAmount").toString());
+    }
+
+    @Test void summaryDoesNotDeductCommissionAgainFromAlreadyCreditedIncome() {
+        UserBilling summary = new UserBilling(); summary.setAmount(80.75); summary.setDivvy(14.25);
+        when(bills.getOne(any())).thenReturn(summary);
+        var data = (com.alibaba.fastjson.JSONObject) request(true, null).get("data");
+        assertEquals("80.75", data.get("netIncome").toString());
+        assertEquals("14.25", data.get("totalDivvy").toString());
+    }
+
     private AjaxResult request(boolean summary, Long store) {
         return summary ? controller.storeBillingSummary("token", store, null, null, null)
                 : controller.storeBillingList("token", 1, 10, store, null, null, null);

+ 1 - 1
ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSessionAuthAnnotationTest.java

@@ -80,7 +80,7 @@ class MerchantSessionAuthAnnotationTest {
                 "setorderuzt", "getorderxq", "getstoreorderlist");
         assertAuth(PosOrderShOprateController.class,
                 "getInvoice", "invalidInvoice", "getOrderPrintInfo", "createOrder", "acceptOrder", "dispatchOrder",
-                "completeOrder", "confirmCashPayment", "cancelOrder", "orderList", "orderDetail");
+                "completeOrder", "confirmCashPayment", "cancelOrder", "orderList");
     }
 
     private void assertAuth(Class<?> controllerType, String... methodNames) {

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

@@ -232,6 +232,16 @@ git commit -m "feat(billing): 商家端账单功能 — 列表查询、汇总、
 
 分管账号新增独立日/周/月查询分支;UserBillingMapper XML 共用主账号和门店集合条件,日期绑定参数、空集合显式 AND 1=0。保留旧响应结构和其他角色路径。验证包括授权多店/单店、越权、空授权、撤权、取消范围和 XML BoundSql。
 
+### 整数运费入口与金额汇总(2026-11-04)
+
+1. 共用 OrderMoneyValidator,在用户/商家创建写入前拒绝小数、负数和非有限运费,保留原请求字段及 null 兼容。新增错误信息提供各现有语言。
+2. storeBillingSummary 将数据库汇总值规范为金额精度后用 BigDecimal 运算,防止 3393.6000000000004 等尾差;totalAmount仍为账单合计,netIncome不再从净入账金额重复扣抽成。
+3. 回归非法/整数运费、历史分角不重写、整数汇总,以及既有三类报价、普通下单和分管账单权限。无数据库迁移、不改 App、不自动提交。
+
+4. 删除旧addorder方法、专用积分/销售统计/优惠核销辅助方法和因此无用的依赖;保留被其他入口引用的推送与返还积分方法,更新入口说明。
+5. 按用户最终要求撤回orderAmount、关联订单查询及全部前端改动;列表维持原响应,前端由用户安排将amount直接显示为收入。
+6. 保留后端汇总不重复扣抽成、浮点精度及旧路由删除回归;运行受影响测试并打包后端,不修改或发布前端。
+
 ## 2026-11-05 骑手收入账单实施计划(按最终范围更新)
 
 1. `ruoyi-system` 的 RiderIncomeMapper、XML、RiderIncomeSummary、RiderIncomeItem 共用本人有效订单收入条件。按明确起止时间过滤;汇总不接收分类,明细按分类分页,详情按本人和 billId 查询。

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

@@ -172,6 +172,21 @@ mdId 过滤采用 `(#{mdId} IS NULL OR md_id = #{mdId})` 模式,mdId 不传时
 
 `getzddaylist/getzdmeeklist/getzdmatlist` 对 userType=5 解析主账号及实时授权门店;不传 mdId 查询全部授权门店,指定未授权门店拒绝。明细、金额/商品汇总、取消订单列表及数量使用相同范围,空授权查询零行。原普通商家/夜市/摊主/骑手路径不变,无数据库迁移。
 
+### 2026-11-04 整数新台币与汇总显示精度
+
+新订单及运费以整数新台币元计价。闪送、多店及新版外卖报价沿用已有整数计算;用户、商家创建入口对传入运费增加非负、有限、整数校验。保留原字段类型和未传运费兼容,不静默取整改变应付。账单汇总使用十进制运算消除二进制尾差,整数以整数数字返回。用户明确历史数据不处理,历史分角账单保留,不改钱包、不补差、不迁移。
+
+接入变化:`POST /system/userOrder/createOrder`和 `POST /system/orderShOprate/createOrder` 保留原入参结构;已有运费字段如传入必须是非负整数(40或40.0均可,40.5拒绝),未传时维持原处理。订单总额字段已有Integer/Long类型。`storeBillingSummary` 三个金额字段仍返回JSON数字,消除浮点尾差、整数不附带小数部分;历史真实分角仍保留。本轮不修改App及商家PC代码,前端由用户安排人员调整。
+
+
+### 2026-11-04 账单收入展示口径(以用户最终确认为准)
+
+- 不返回原订单金额,不新增 `orderAmount`,不关联订单表;列表响应结构保持不变。
+- `amount` 保持实际账单入账金额,`divvy` 保持历史抽成记录。前端由用户安排人员将“订单金额”改为“收入”,直接使用 `amount`,不再减 `divvy`;本轮撤回所有前端代码及语言文件改动。
+- `storeBillingSummary.totalAmount` 保持账单金额合计,`totalDivvy` 保留抽成合计,`netIncome` 等于已入账金额合计,不再次减抽成;使用十进制消除浮点尾差。
+- 只读核对订单991793757396125:原订单135、账单114.75、历史抽成20.25;991793757103637及991793756962244:原订单95、账单80.75、抽成14.25。小数来自账单被错当订单展示。历史数据库、钱包均不回写,无SQL迁移。
+- 旧 `POST /system/order/addorder` 按用户要求彻底删除,不保留转发或兼容创建逻辑;用户及商家现有 createOrder 保持。
+
 ## 2026-11-05 骑手收入账单改版(最终范围:前端日期,后端范围查询)
 
 ### 用户故事与验收

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

@@ -141,6 +141,29 @@ description: "Task list for 商家前端账单功能 feature implementation"
 
 T021验证:022 T015所列JDK21定向回归144项全部通过,模块package成功,差异检查通过。分管旧账单控制器12项、SQL范围测试1项,以及既有商家/骑手账单回归均包含在内。无数据库变更,未部署;提交与推送状态以 Git 记录为准。
 
+## 2026-11-04 新订单整数金额与汇总精度
+
+- [x] T022 补齐用户、商家创建入口整数运费校验,并更新金额单位注释和国际化提示。
+- [x] T023 汇总改为十进制计算,消除浮点尾差,保留历史分角记录。
+- [x] T024 运行定向回归及 JDK21 打包,更新验证记录;不修改历史数据或钱包。
+
+已验证:修复前定向测试30项中9项失败,复现入口接收非法运费和汇总浮点尾差。修复后使用JDK21执行:
+
+`mvn -q -pl ruoyi-admin -am -Dtest=OrderWholeYuanFreightTest,StoreBillingSubaccountAccessTest,LegacyBillingSubaccountScopeTest,OrderBillingNoDeductionTest,UserBillingFlashDeliveryTest,FlashDeliveryPricingCalculatorTest,FoodDeliveryPromotionQuoteServiceTest,MultiStoreCheckoutQuoteServiceTest,OrderPromotionHelperFreeDeliveryTest,LinePayOfflineMerchantAccessTest,PosOrderShOprateControllerTest -Dsurefire.failIfNoSpecifiedTests=false package`
+
+退出码0,11个测试类共120项全部通过,失败/错误/跳过均0,模块打包成功;`git diff --check`通过。日志为`ruoyi-admin/target/whole-yuan-red.log`与`whole-yuan-green.log`。未执行全量回归、客户端联调或部署;未修改历史数据库和钱包,无SQL迁移。尚未提交或推送。
+
+
+- [x] T025 删除addorder旧接口及仅供它使用的代码,同步接口边界文档并验证。
+- [x] T026 按用户最终要求不新增orderAmount,撤回关联订单查询与全部前端改动;只保留后端汇总重复扣抽成修复,不回写历史账单。
+- [x] T027 按最终范围完成汇总金额语义、原权限及旧接口删除回归,打包后端并记录验证结果。
+
+
+前一版范围验证(2026-11-04,orderAmount与前端改动已按用户最终要求撤回,以下123项记录不作为最终范围验证):删除旧接口前,OrderWholeYuanFreightTest 10项中1项按预期失败;账单展示修复前,StoreBillingSubaccountAccessTest 23项中2项失败,复现缺失原订单金额及重复扣抽成。最终执行T024同一组11类JDK21测试,123项全部通过(失败/错误/跳过均0),Maven package成功。商家PC执行 `npm run build:test` 成功。两个仓库 `git diff --check`通过。只读核对测试数据库订单与账单,无数据库或钱包修改,无SQL迁移。未执行浏览器/App联调、线上部署及完整回归;尚未提交或推送。
+
+
+最终范围验证(2026-11-04):撤回orderAmount及所有前端改动后,执行JDK21 `mvn -q -pl ruoyi-admin -am -Dtest=StoreBillingSubaccountAccessTest,OrderWholeYuanFreightTest -Dsurefire.failIfNoSpecifiedTests=false package`,31项全部通过,失败/错误/跳过均0,后端打包成功。日志:`ruoyi-admin/target/billing-final-scope.log`。商家PC工作区干净,UserBilling实体无变更,后端差异检查通过;保留汇总重复扣抽成/精度修复、整数运费校验及旧addorder删除。未部署、未提交、未改历史数据。
+
 ## 2026-11-05 骑手收入账单改版
 
 - [x] T028 写入已确认规格与实现计划,确认仅后端和接入文档,不创建分支。

+ 1 - 1
specs/012-im-user-integration/code-status.md

@@ -20,7 +20,7 @@
 - **`PosOrderShOprateController`** — 商家操作:出餐 `chuCan`(推用户+骑手)、商家取消
 - **`PosOrderQsOprateController`** — 骑手操作:接单/取餐/送达,按 `deliveryStatus` 1/2/3(推用户,接单时推商家)
 - **`UserOrderController`** — 用户操作:下单(推商家"新订单")、用户取消
-- **`PosOrderController`** — 仅 `/addorder`(下单,含货到付款 `sendHdfkMessage`)、`/setorderuzt`(平台改状态)及各类 list 查询在用;订单状态流转操作已迁出至上列三个 Controller
+- **`PosOrderController`** — 旧 `/addorder` 已于2026-11-04删除;`/setorderuzt`(平台改状态)及各类 list 查询在用;订单状态流转操作已迁出至上列三个 Controller
 
 ## 推送通道现状
 

+ 1 - 2
specs/031-pay-method-config/contracts/api.md

@@ -18,12 +18,11 @@ List<PayMethodOption> listAvailable(GateScope scope, Long payeeUserId)
 enum GateScope { MERCHANT, RIDER_FLASH }
 ```
 
-调用点(**在用仅此三处**;`/addorder` 一处随入口废弃留存,清理死代码时须一并移除并更新本表;禁止新增重复实现):
+调用点(**在用仅此三处**;旧 `/addorder` 及其闸门调用已于2026-11-04删除;禁止新增重复实现):
 
 | 调用方 | 位置 | 参数 |
 |---|---|---|
 | 用户餐饮下单 | `UserOrderController.createOrder`(setPayType 前后) | payType=input.paymentMethod, MERCHANT, 商家主账号 userId(US7 起经 `assertUsableForOrder`,见 0.1) |
-| ~~商家建单~~ 旧代客外送下单(**已废弃 2026-09-29**) | `PosOrderController /addorder`(方法已标 `@Deprecated`) | payType 固定 "1"(COD), MERCHANT —— 入口被 `/system/orderShOprate/createOrder` 取代,调用点仅随废弃代码留存,勿作为新改动基准 |
 | 闪送下单 | `FlashDeliveryApplicationService.create`(normalizePayType 处) | payType, RIDER_FLASH, 无收款方(抢单前,只校验平台开关) |
 | 027 bankInfo | `ChantingStoreController.bankInfo` | OFFLINE_TRANSFER, MERCHANT, 商家 userId |
 

+ 1 - 1
specs/032-self-hosted-map-integration/spec.md

@@ -153,7 +153,7 @@
 ## Assumptions
 
 - 本 spec 覆盖 foodie 后端与运营配置;App 前端(uni-app,不在本仓库)的选址页、费用展示改造由前端团队按本 spec 接口对接。
-- `PosOrderController` 的通用管理接口 `POST /system/order`(`add(...)`)和 `PUT /system/order`(`edit(...)`)已废弃,不纳入本特性的订单创建、修改入口;它们与仍在使用的 `/system/order/addorder` 不同。
+- `PosOrderController` 的通用管理接口 `POST /system/order`(`add(...)`)和 `PUT /system/order`(`edit(...)`)已废弃,不纳入本特性的订单创建、修改入口;旧 `/system/order/addorder` 也已于2026-11-04删除。
 - 地图服务(Foodie_HCM_Map)部署在与 foodie 服务器同内网或经私网打通;其多站点路线扩展、读接口防护属地图仓库工作,作为跨仓库依赖跟踪。
 - 地图仅提供 car 档道路距离,不提供机车档与实时交通;距离用于计价可信,时长仅作折算参考(见 FR-008)。
 - 现有运费规则载体(pos_freight、flash_delivery_pricing)与管理端 CRUD 继续沿用,本特性不改动计价规则本身。

+ 1 - 1
specs/034-stall-qualification/quickstart.md

@@ -19,7 +19,7 @@ SQL、真实数据库、服务重启及上述部署操作本轮均未执行。
 - 草稿可不完整;提交时负责人、证件及摊位照片、银行账户四项必须齐全,食品业者登录资料选填。每类图片最多 5 张、每张不超过 5MB,仅 JPEG/PNG;上传未完成时不能提交。
 - 待审核不能编辑/重复提交。平台审核通过或填写原因驳回;驳回后修订形成新申请版本,旧材料和审核历史不覆盖。保存使用认证主体 version,审核使用申请 version;冲突需刷新,不能自动重试覆盖别人。
 - 审核通过只开放营业资格,不自动开店;摊主仍通过原营业设置决定是否营业。已有有效材料重新申请时,原有效版本继续有效,直到新版本通过替换。
-- 无记录、草稿、首次待审核或首次驳回均不得开店、新下单;闭店时可以编辑名称/地址来补资料。用户下单、商家创建订单以及仍可调用的旧 `/addorder` 均校验,不能只信列表或前端。旧订单出餐、配送、支付后续处理不调用此新门禁。
+- 无记录、草稿、首次待审核或首次驳回均不得开店、新下单;闭店时可以编辑名称/地址来补资料。用户下单、商家创建订单均校验(旧 `/addorder` 已于2026-11-04删除),不能只信列表或前端。旧订单出餐、配送、支付后续处理不调用此新门禁。
 - 用户端列表标为关闭;夜市摊位详情返回关闭状态;扫码摊位列表隐藏未通过摊位。普通商家的旧审核流程不替换。
 
 ## 本轮自动化验证