From a0c13b91da27aed87992a1c0ca4c079d58d222a0 Mon Sep 17 00:00:00 2001 From: Kroytz Date: Fri, 3 Jul 2026 19:17:26 +0800 Subject: [PATCH] wip: tiered deliver price --- .../sql/upgrade_delivery_rules.sql | 14 +++ yshop-drink-boot3/sql/yixiang-drink-open.sql | 5 +- .../order/enums/ErrorCodeConstants.java | 1 + .../storeorder/AppStoreOrderServiceImpl.java | 32 ++++--- .../admin/storeshop/vo/StoreShopBaseVO.java | 6 +- .../app/storeshop/AppStoreController.java | 9 ++ .../app/storeshop/vo/AppStoreShopVO.java | 3 + .../app/storeshop/vo/DeliveryFeeRespVO.java | 25 ++++++ .../convert/storeshop/StoreShopConvert.java | 13 +++ .../dataobject/storeshop/DeliveryRule.java | 37 ++++++++ .../dal/dataobject/storeshop/StoreShopDO.java | 9 +- .../dal/type/DeliveryRuleListTypeHandler.java | 33 +++++++ .../storeshop/AppStoreShopService.java | 10 +++ .../storeshop/AppStoreShopServiceImpl.java | 22 +++++ .../service/storeshop/DeliveryCalculator.java | 86 +++++++++++++++++++ yshop-drink-uniapp-vue3/api/goods.js | 7 ++ yshop-drink-uniapp-vue3/config/index.js | 6 +- yshop-drink-uniapp-vue3/manifest.json | 24 +++--- .../pages/components/pages/pay/pay.vue | 42 ++++++++- .../pages/components/pages/shop/shop.vue | 16 +++- yshop-drink-uniapp-vue3/pages/menu/menu.vue | 8 +- .../src/api/mall/store/shop/index.ts | 7 ++ .../src/views/mall/store/shop/ShopForm.vue | 50 ++++++++++- 23 files changed, 426 insertions(+), 39 deletions(-) create mode 100644 yshop-drink-boot3/sql/upgrade_delivery_rules.sql create mode 100644 yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/vo/DeliveryFeeRespVO.java create mode 100644 yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/dataobject/storeshop/DeliveryRule.java create mode 100644 yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/type/DeliveryRuleListTypeHandler.java create mode 100644 yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/DeliveryCalculator.java diff --git a/yshop-drink-boot3/sql/upgrade_delivery_rules.sql b/yshop-drink-boot3/sql/upgrade_delivery_rules.sql new file mode 100644 index 0000000..99dbd8e --- /dev/null +++ b/yshop-drink-boot3/sql/upgrade_delivery_rules.sql @@ -0,0 +1,14 @@ +-- ============================================================ +-- 门店配送规则(阶梯配送费)升级脚本 +-- 将固定的"配送价格"扩展为基于距离区间的"配送规则" +-- 配送规则存为 JSON 数组: [{minKm,maxKm,price}],区间左闭右开 +-- 无规则匹配时回退 delivery_price(默认配送费) +-- ============================================================ + +ALTER TABLE `yshop_store_shop` + ADD COLUMN `delivery_rules` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '配送规则JSON:[{minKm,maxKm,price}]' AFTER `delivery_price`; + +-- 可选:为已有支持外卖的门店初始化一条示例规则(请按需调整) +-- UPDATE `yshop_store_shop` +-- SET `delivery_rules` = '[{"minKm":0,"maxKm":3,"price":8},{"minKm":3,"maxKm":5,"price":12}]' +-- WHERE `distance` > 0 AND `delivery_rules` IS NULL; diff --git a/yshop-drink-boot3/sql/yixiang-drink-open.sql b/yshop-drink-boot3/sql/yixiang-drink-open.sql index f3b8379..b9f7b4f 100644 --- a/yshop-drink-boot3/sql/yixiang-drink-open.sql +++ b/yshop-drink-boot3/sql/yixiang-drink-open.sql @@ -13698,6 +13698,7 @@ CREATE TABLE `yshop_store_shop` ( `distance` int NOT NULL DEFAULT '0' COMMENT '外卖配送距离,单位为千米。0表示不送外卖', `min_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '起送价钱', `delivery_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '配送价格', + `delivery_rules` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '配送规则JSON:[{minKm,maxKm,price}]', `notice` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '公告', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否营业:0=否,1=是', `admin_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '管理员id', @@ -13714,8 +13715,8 @@ CREATE TABLE `yshop_store_shop` ( -- Records of yshop_store_shop -- ---------------------------- BEGIN; -INSERT INTO `yshop_store_shop` VALUES (2, '意向餐饮店', '18000000000', 'https://apidc.yixiang.co/admin-api/infra/file/4/get/44723283500f656cdd5906e13f2997cb292d17f516065ff4b3b38f9810bce4cb.jpg', 'https://apidc.yixiang.co/admin-api/infra/file/4/get/44723283500f656cdd5906e13f2997cb292d17f516065ff4b3b38f9810bce4cb.jpg', '源汇区人民东路', '河南省漯河市源汇区团结路50号-20', '2023-08-19 09:00:00', '2023-08-19 23:00:00', '114.04877877439665', '33.57093831826829', 10, 10.00, 20.00, '新店开张', 1, '126', '1', '2023-08-06 11:48:43', '2023-09-14 17:30:59', '1', '1', b'0'); -INSERT INTO `yshop_store_shop` VALUES (3, 'yshop', '18000000000', 'https://apidc.yixiang.co/admin-api/infra/file/4/get/44723283500f656cdd5906e13f2997cb292d17f516065ff4b3b38f9810bce4cb.jpg', 'https://apidc.yixiang.co/admin-api/infra/file/4/get/44723283500f656cdd5906e13f2997cb292d17f516065ff4b3b38f9810bce4cb.jpg', '67号', '北京市东城区', '2023-08-06 13:35:02', '2023-08-06 15:35:02', '116.40403593222032', '39.91643892263258', 0, 1.00, 2.00, '2222', 1, '100', '2', '2023-08-06 13:14:53', '2024-03-26 15:42:20', '1', '1', b'0'); +INSERT INTO `yshop_store_shop` VALUES (2, '意向餐饮店', '18000000000', 'https://apidc.yixiang.co/admin-api/infra/file/4/get/44723283500f656cdd5906e13f2997cb292d17f516065ff4b3b38f9810bce4cb.jpg', 'https://apidc.yixiang.co/admin-api/infra/file/4/get/44723283500f656cdd5906e13f2997cb292d17f516065ff4b3b38f9810bce4cb.jpg', '源汇区人民东路', '河南省漯河市源汇区团结路50号-20', '2023-08-19 09:00:00', '2023-08-19 23:00:00', '114.04877877439665', '33.57093831826829', 10, 10.00, 20.00, '[{"minKm":0,"maxKm":3,"price":8},{"minKm":3,"maxKm":5,"price":12},{"minKm":5,"maxKm":10,"price":18}]', '新店开张', 1, '126', '1', '2023-08-06 11:48:43', '2023-09-14 17:30:59', '1', '1', b'0'); +INSERT INTO `yshop_store_shop` VALUES (3, 'yshop', '18000000000', 'https://apidc.yixiang.co/admin-api/infra/file/4/get/44723283500f656cdd5906e13f2997cb292d17f516065ff4b3b38f9810bce4cb.jpg', 'https://apidc.yixiang.co/admin-api/infra/file/4/get/44723283500f656cdd5906e13f2997cb292d17f516065ff4b3b38f9810bce4cb.jpg', '67号', '北京市东城区', '2023-08-06 13:35:02', '2023-08-06 15:35:02', '116.40403593222032', '39.91643892263258', 0, 1.00, 2.00, NULL, '2222', 1, '100', '2', '2023-08-06 13:14:53', '2024-03-26 15:42:20', '1', '1', b'0'); COMMIT; -- ---------------------------- diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/ErrorCodeConstants.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/ErrorCodeConstants.java index 30a3333..6433368 100644 --- a/yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/ErrorCodeConstants.java +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-order-api/src/main/java/co/yixiang/yshop/module/order/enums/ErrorCodeConstants.java @@ -36,6 +36,7 @@ public interface ErrorCodeConstants { ErrorCode STORE_ORDER_DESK_NOT = new ErrorCode(1008007023, "当前桌号不存在"); + ErrorCode STORE_ORDER_OUT_OF_DELIVERY_RANGE = new ErrorCode(1008007024, "当前收货地址不在配送范围内"); diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/AppStoreOrderServiceImpl.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/AppStoreOrderServiceImpl.java index 23a3363..0c02af3 100644 --- a/yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/AppStoreOrderServiceImpl.java +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-order-biz/src/main/java/co/yixiang/yshop/module/order/service/storeorder/AppStoreOrderServiceImpl.java @@ -49,6 +49,7 @@ import co.yixiang.yshop.module.product.service.storeproductattrvalue.StoreProduc import co.yixiang.yshop.module.store.convert.storeshop.StoreShopConvert; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.service.storeshop.AppStoreShopService; +import co.yixiang.yshop.module.store.service.storeshop.DeliveryCalculator; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -180,8 +181,26 @@ public class AppStoreOrderServiceImpl extends ServiceImpl deliveryRules; + @Schema(description = "公告", required = true) @NotNull(message = "公告不能为空") private String notice; diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/AppStoreController.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/AppStoreController.java index 4ca3400..ba6d7c2 100644 --- a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/AppStoreController.java +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/AppStoreController.java @@ -10,6 +10,7 @@ package co.yixiang.yshop.module.store.controller.app.storeshop; import co.yixiang.yshop.framework.common.pojo.CommonResult; import co.yixiang.yshop.module.store.controller.app.storeshop.vo.AppStoreShopVO; +import co.yixiang.yshop.module.store.controller.app.storeshop.vo.DeliveryFeeRespVO; import co.yixiang.yshop.module.store.convert.storeshop.StoreShopConvert; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.service.storeshop.AppStoreShopService; @@ -75,6 +76,14 @@ public class AppStoreController { return success(StoreShopConvert.INSTANCE.convert02(storeShopDO)); } + @GetMapping("/delivery-fee") + @Operation(summary = "预估配送费") + public CommonResult getDeliveryFee(@RequestParam("shopId") Long shopId, + @RequestParam("lng") double lng, + @RequestParam("lat") double lat) { + return success(appStoreShopService.getDeliveryFee(shopId, lng, lat)); + } + } diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/vo/AppStoreShopVO.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/vo/AppStoreShopVO.java index eb973d7..eeaefd3 100644 --- a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/vo/AppStoreShopVO.java +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/vo/AppStoreShopVO.java @@ -54,6 +54,9 @@ public class AppStoreShopVO { @Schema(description = "配送价格", required = true, example = "11771") private BigDecimal deliveryPrice; + @Schema(description = "配送规则JSON文本", example = "[{\"minKm\":1,\"maxKm\":3,\"price\":8}]") + private String deliveryRules; + @Schema(description = "公告", required = true) private String notice; diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/vo/DeliveryFeeRespVO.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/vo/DeliveryFeeRespVO.java new file mode 100644 index 0000000..4e2eb06 --- /dev/null +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/controller/app/storeshop/vo/DeliveryFeeRespVO.java @@ -0,0 +1,25 @@ +package co.yixiang.yshop.module.store.controller.app.storeshop.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 预估配送费 Response VO + * + * @author yshop + */ +@Data +public class DeliveryFeeRespVO { + + @Schema(description = "收货地址到门店的距离(千米)") + private Double distanceKm; + + @Schema(description = "配送费(元),超出配送范围时为 0") + private BigDecimal deliveryPrice; + + @Schema(description = "是否超出配送范围") + private Boolean outOfRange; + +} diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/convert/storeshop/StoreShopConvert.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/convert/storeshop/StoreShopConvert.java index 0ea4c13..d87b9f5 100644 --- a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/convert/storeshop/StoreShopConvert.java +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/convert/storeshop/StoreShopConvert.java @@ -3,11 +3,15 @@ package co.yixiang.yshop.module.store.convert.storeshop; import java.util.*; import co.yixiang.yshop.framework.common.pojo.PageResult; +import co.yixiang.yshop.framework.common.util.json.JsonUtils; import co.yixiang.yshop.module.store.controller.app.storeshop.vo.AppStoreShopVO; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; import org.mapstruct.factory.Mappers; import co.yixiang.yshop.module.store.controller.admin.storeshop.vo.*; +import co.yixiang.yshop.module.store.dal.dataobject.storeshop.DeliveryRule; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; /** @@ -26,6 +30,7 @@ public interface StoreShopConvert { StoreShopRespVO convert(StoreShopDO bean); + @Mapping(source = "deliveryRules", target = "deliveryRules", qualifiedByName = "rulesToJson") AppStoreShopVO convert02(StoreShopDO bean); List convertList(List list); @@ -34,4 +39,12 @@ public interface StoreShopConvert { List convertList02(List list); + /** + * 将配送规则 List 序列化为 JSON 文本,供小程序端解析展示 + */ + @Named("rulesToJson") + default String rulesToJson(List rules) { + return rules == null ? null : JsonUtils.toJsonString(rules); + } + } diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/dataobject/storeshop/DeliveryRule.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/dataobject/storeshop/DeliveryRule.java new file mode 100644 index 0000000..8c51170 --- /dev/null +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/dataobject/storeshop/DeliveryRule.java @@ -0,0 +1,37 @@ +package co.yixiang.yshop.module.store.dal.dataobject.storeshop; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * 配送规则 DO + *

+ * 描述一段距离区间对应的配送费,区间为左闭右开 [minKm, maxKm)。 + * 例如:minKm=1, maxKm=3, price=8 表示 1km(含) ~ 3km(不含) 配送费 8 元。 + * + * @author yshop + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeliveryRule { + + /** + * 最小距离(单位:千米,含) + */ + private BigDecimal minKm; + + /** + * 最大距离(单位:千米,不含) + */ + private BigDecimal maxKm; + + /** + * 配送费(单位:元) + */ + private BigDecimal price; + +} diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/dataobject/storeshop/StoreShopDO.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/dataobject/storeshop/StoreShopDO.java index 869d7ab..6427d19 100644 --- a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/dataobject/storeshop/StoreShopDO.java +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/dataobject/storeshop/StoreShopDO.java @@ -1,6 +1,7 @@ package co.yixiang.yshop.module.store.dal.dataobject.storeshop; import co.yixiang.yshop.framework.mybatis.core.type.StringListTypeHandler; +import co.yixiang.yshop.module.store.dal.type.DeliveryRuleListTypeHandler; import lombok.*; import java.util.*; import java.math.BigDecimal; @@ -74,9 +75,15 @@ public class StoreShopDO extends BaseDO { */ private BigDecimal minPrice; /** - * 配送价格 + * 配送价格(默认/兜底配送费) */ private BigDecimal deliveryPrice; + /** + * 配送规则(按距离区间阶梯计费),JSON 数组:[{minKm,maxKm,price}] + * 区间左闭右开,无匹配时回退 deliveryPrice + */ + @TableField(typeHandler = DeliveryRuleListTypeHandler.class) + private List deliveryRules; /** * 公告 */ diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/type/DeliveryRuleListTypeHandler.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/type/DeliveryRuleListTypeHandler.java new file mode 100644 index 0000000..41fc60a --- /dev/null +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/dal/type/DeliveryRuleListTypeHandler.java @@ -0,0 +1,33 @@ +package co.yixiang.yshop.module.store.dal.type; + +import co.yixiang.yshop.framework.common.util.json.JsonUtils; +import co.yixiang.yshop.module.store.dal.dataobject.storeshop.DeliveryRule; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import com.fasterxml.jackson.core.type.TypeReference; + +import java.util.List; + +/** + * List 的类型转换器实现类,对应数据库的 text 类型 + *

+ * 参考 {@link com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} 实现, + * 使用 TypeReference 保证反序列化为 List 时泛型不丢失。 + * 放置于 store 模块以避免框架层反向依赖业务模块。 + * + * @author yshop + */ +public class DeliveryRuleListTypeHandler extends AbstractJsonTypeHandler { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference>(){}; + + @Override + protected Object parse(String json) { + return JsonUtils.parseObject(json, TYPE_REFERENCE); + } + + @Override + protected String toJson(Object obj) { + return JsonUtils.toJsonString(obj); + } + +} diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/AppStoreShopService.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/AppStoreShopService.java index 3069d41..90783fc 100644 --- a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/AppStoreShopService.java +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/AppStoreShopService.java @@ -1,6 +1,7 @@ package co.yixiang.yshop.module.store.service.storeshop; import co.yixiang.yshop.module.store.controller.app.storeshop.vo.AppStoreShopVO; +import co.yixiang.yshop.module.store.controller.app.storeshop.vo.DeliveryFeeRespVO; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import com.baomidou.mybatisplus.extension.service.IService; @@ -22,4 +23,13 @@ public interface AppStoreShopService extends IService { */ List getStoreList(double lon,double lat,String name,Integer shopId); + /** + * 预估配送费 + * @param shopId 门店id + * @param lng 收货地址经度 + * @param lat 收货地址纬度 + * @return 配送费及是否超出配送范围 + */ + DeliveryFeeRespVO getDeliveryFee(Long shopId, double lng, double lat); + } diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/AppStoreShopServiceImpl.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/AppStoreShopServiceImpl.java index cd7b9e9..69002f9 100644 --- a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/AppStoreShopServiceImpl.java +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/AppStoreShopServiceImpl.java @@ -1,6 +1,7 @@ package co.yixiang.yshop.module.store.service.storeshop; import co.yixiang.yshop.module.store.controller.app.storeshop.vo.AppStoreShopVO; +import co.yixiang.yshop.module.store.controller.app.storeshop.vo.DeliveryFeeRespVO; import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; @@ -8,6 +9,7 @@ import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; +import java.math.BigDecimal; import java.util.List; /** @@ -34,4 +36,24 @@ public class AppStoreShopServiceImpl extends ServiceImpl getStoreList(double lon, double lat,String name,Integer shopId) { return shopMapper.getStoreList(lon,lat,name,shopId); } + + @Override + public DeliveryFeeRespVO getDeliveryFee(Long shopId, double lng, double lat) { + StoreShopDO shop = getById(shopId); + DeliveryFeeRespVO vo = new DeliveryFeeRespVO(); + vo.setOutOfRange(true); + vo.setDeliveryPrice(BigDecimal.ZERO); + if (shop == null) { + return vo; + } + double distanceKm = DeliveryCalculator.calcDistanceKm( + shop.getLng(), shop.getLat(), String.valueOf(lng), String.valueOf(lat)); + BigDecimal price = DeliveryCalculator.calcDeliveryPrice(shop, distanceKm); + vo.setDistanceKm(distanceKm); + if (price != null) { + vo.setOutOfRange(false); + vo.setDeliveryPrice(price); + } + return vo; + } } diff --git a/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/DeliveryCalculator.java b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/DeliveryCalculator.java new file mode 100644 index 0000000..cbd5f0a --- /dev/null +++ b/yshop-drink-boot3/yshop-module-mall/yshop-module-store-biz/src/main/java/co/yixiang/yshop/module/store/service/storeshop/DeliveryCalculator.java @@ -0,0 +1,86 @@ +package co.yixiang.yshop.module.store.service.storeshop; + +import co.yixiang.yshop.module.store.dal.dataobject.storeshop.DeliveryRule; +import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 配送费计算工具 + *

+ * 1. {@link #calcDistanceKm} 使用 Haversine 公式计算两个经纬度之间的球面距离(千米), + * 与 StoreShopMapper.getStoreList 中的 SQL 计算口径一致(地球半径 6378.138km)。 + * 2. {@link #calcDeliveryPrice} 根据距离匹配门店的配送规则(左闭右开区间), + * 无匹配时回退 deliveryPrice(兜底);超出配送范围返回 null。 + * + * @author yshop + */ +public class DeliveryCalculator { + + /** 地球半径(千米),与 Mapper SQL 保持一致 */ + private static final double EARTH_RADIUS_KM = 6378.138; + + private DeliveryCalculator() { + } + + /** + * 计算两个经纬度之间的球面距离(千米) + * + * @param lng1 起点-经度 + * @param lat1 起点-纬度 + * @param lng2 终点-经度 + * @param lat2 终点-纬度 + * @return 距离(千米) + */ + public static double calcDistanceKm(String lng1, String lat1, String lng2, String lat2) { + double lon1 = Double.parseDouble(lng1); + double latt1 = Double.parseDouble(lat1); + double lon2 = Double.parseDouble(lng2); + double latt2 = Double.parseDouble(lat2); + double radLat1 = Math.toRadians(latt1); + double radLat2 = Math.toRadians(latt2); + double a = radLat1 - radLat2; + double b = Math.toRadians(lon1) - Math.toRadians(lon2); + double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); + distance = distance * EARTH_RADIUS_KM; + return Math.round(distance * 1000.0) / 1000.0; + } + + /** + * 根据距离匹配配送规则计算配送费 + * + * @param shop 门店 + * @param distanceKm 收货地址到门店的距离(千米) + * @return 配送费;返回 null 表示不在配送范围内(不可配送) + */ + public static BigDecimal calcDeliveryPrice(StoreShopDO shop, double distanceKm) { + // distance<=0 表示不送外卖 + Integer maxDistance = shop.getDistance(); + if (maxDistance == null || maxDistance <= 0) { + return null; + } + // 超出配送范围 + if (distanceKm > maxDistance) { + return null; + } + // 匹配阶梯规则(左闭右开) + List rules = shop.getDeliveryRules(); + if (rules != null && !rules.isEmpty()) { + for (DeliveryRule rule : rules) { + if (rule.getMinKm() == null || rule.getMaxKm() == null || rule.getPrice() == null) { + continue; + } + double min = rule.getMinKm().doubleValue(); + double max = rule.getMaxKm().doubleValue(); + if (distanceKm >= min && distanceKm < max) { + return rule.getPrice(); + } + } + } + // 无规则匹配,回退默认配送费 + return shop.getDeliveryPrice(); + } + +} diff --git a/yshop-drink-uniapp-vue3/api/goods.js b/yshop-drink-uniapp-vue3/api/goods.js index 79863f2..fba231c 100644 --- a/yshop-drink-uniapp-vue3/api/goods.js +++ b/yshop-drink-uniapp-vue3/api/goods.js @@ -13,3 +13,10 @@ export function menuGoods(data) { return api.get('/product/products', data, { login: false }) } +/** + * 预估配送费(根据收货地址距离匹配配送规则) + */ +export function getDeliveryFee(data) { + return api.get('/store/delivery-fee', data, { login: false }) +} + diff --git a/yshop-drink-uniapp-vue3/config/index.js b/yshop-drink-uniapp-vue3/config/index.js index 8e2a3c3..ce80b56 100644 --- a/yshop-drink-uniapp-vue3/config/index.js +++ b/yshop-drink-uniapp-vue3/config/index.js @@ -1,5 +1,5 @@ -const DEV_API_URL = 'http://localhost:48081/app-api' -const PROD_API_URL = 'https://apidc.yixiang.co/app-api' +const DEV_API_URL = 'http://maitre.imomoe.cn/api/app-api' +const PROD_API_URL = 'https://maitre.imomoe.cn/api/app-api' // #ifdef APP-PLUS || H5 || MP-WEIXIN export const VUE_APP_API_URL = @@ -8,7 +8,7 @@ export const VUE_APP_API_URL = export const VUE_APP_RESOURCES_URL = 'https://h5.yixiang.co/static' export const VUE_APP_UPLOAD_URL = VUE_APP_API_URL + '/infra/file/upload' -export const APP_ID = 'wxdbdbc123c8c30b45' +export const APP_ID = 'wx3548436813df3f60' const orderListStatus = {} diff --git a/yshop-drink-uniapp-vue3/manifest.json b/yshop-drink-uniapp-vue3/manifest.json index 3f6567d..6f68056 100644 --- a/yshop-drink-uniapp-vue3/manifest.json +++ b/yshop-drink-uniapp-vue3/manifest.json @@ -431,18 +431,18 @@ "router" : { "base" : "/h5/" }, - "sdkConfigs" : { - // 使用地图或位置相关功能必须填写其一 - "maps" : { - "bmap" : { - // 百度地图秘钥(HBuilderX 3.99+)http://lbsyun.baidu.com/apiconsole/key#/home - "key" : "" - }, - "qqmap" : { - "key" : "OGABZ-Y5OCF-5UWJ5-N7DHH-VFIG7-DHFEB" - } - } - } + "sdkConfigs" : { + // 使用地图或位置相关功能必须填写其一 + "maps" : { + "bmap" : { + // 百度地图秘钥(HBuilderX 3.99+)http://lbsyun.baidu.com/apiconsole/key#/home + "key" : "" + }, + "qqmap" : { + "key" : "OGABZ-Y5OCF-5UWJ5-N7DHH-VFIG7-DHFEB" + } + } + } } } /* 模块配置 *//* 应用发布信息 *//* android打包配置 */ diff --git a/yshop-drink-uniapp-vue3/pages/components/pages/pay/pay.vue b/yshop-drink-uniapp-vue3/pages/components/pages/pay/pay.vue index 3f76505..13756c2 100644 --- a/yshop-drink-uniapp-vue3/pages/components/pages/pay/pay.vue +++ b/yshop-drink-uniapp-vue3/pages/components/pages/pay/pay.vue @@ -125,7 +125,7 @@ 总计¥{{ total }} - ,配送费¥{{ store.deliveryPrice }} + ,配送费¥{{ deliveryFee }} ,¥-{{ coupon.value }} ,实付 @@ -219,6 +219,7 @@ import { ref, computed, + watch, nextTick } from 'vue' import { useMainStore } from '@/store/store' @@ -235,12 +236,15 @@ import { import { couponCount } from '@/api/coupon' +import { getDeliveryFee } from '@/api/goods' // #ifdef H5 import * as jweixin from 'weixin-js-sdk' // #endif const main = useMainStore() const { orderType,address, store,location,isLogin,member,mycoupon } = storeToRefs(main) const active = ref(false) +const deliveryFee = ref(0) +const outOfRange = ref(false) const title = ref('支付') const jsStr = ref('') const cart = ref([]) @@ -302,8 +306,8 @@ const total = computed(() =>{ const amount = computed(() =>{ let amount = cart.value.reduce((acc, cur) => acc + cur.number * cur.price, 0); // 加配送费 - if (store.value.distance > 0 && orderType.value == 'takeout') { - amount += parseFloat(store.value.deliveryPrice); + if (orderType.value == 'takeout') { + amount += deliveryFee.value; } @@ -317,6 +321,33 @@ const amount = computed(() =>{ return amount.toFixed(2); }) +// 选地址/切换外卖后预估配送费(基于收货地址距离匹配配送规则) +watch([address, store, orderType], async () => { + if (orderType.value !== 'takeout' || !store.value.id) { + deliveryFee.value = 0 + outOfRange.value = false + return + } + // 未选地址时显示默认配送费 + if (!address.value || !address.value.longitude) { + deliveryFee.value = parseFloat(store.value.deliveryPrice) || 0 + outOfRange.value = false + return + } + try { + const res = await getDeliveryFee({ + shopId: store.value.id, + lng: address.value.longitude, + lat: address.value.latitude + }) + outOfRange.value = !!res.outOfRange + deliveryFee.value = parseFloat(res.deliveryPrice) || 0 + } catch (e) { + deliveryFee.value = parseFloat(store.value.deliveryPrice) || 0 + outOfRange.value = false + } +}, { immediate: true }) + onShow(() => { coupon.value = main.mycoupon let date = new Date(new Date().getTime() + 3600000); // 一个小时后 @@ -494,6 +525,11 @@ const submit = () => { } } const pay = async() => { + // 外卖时校验收货地址是否在配送范围 + if (orderType.value == 'takeout' && outOfRange.value) { + uToast.value.show({message:'当前收货地址不在配送范围内',type: 'error'}); + return; + } let that = this; // // #ifdef MP-WEIXIN // await new Promise(function(revolve) { diff --git a/yshop-drink-uniapp-vue3/pages/components/pages/shop/shop.vue b/yshop-drink-uniapp-vue3/pages/components/pages/shop/shop.vue index 9f0fd61..7c31cad 100644 --- a/yshop-drink-uniapp-vue3/pages/components/pages/shop/shop.vue +++ b/yshop-drink-uniapp-vue3/pages/components/pages/shop/shop.vue @@ -14,7 +14,7 @@ 距离您 {{kmUnit(item.dis)}} - 配送距离:{{item.distance + 'km '}} & 配送费:{{item.delivery_price}} + 配送距离:{{item.distance + 'km '}} & 配送费:{{deliveryRuleText(item)}} 外卖不配送 {{item.addressMap + ' ' + item.address}} 营业时间 {{formatDateTime(item.startTime,'hh:mm')}} - {{formatDateTime(item.endTime,'hh:mm')}} @@ -74,6 +74,20 @@ const getShop = async(keywork = '') => { } } } +// 配送规则摘要文本 +const deliveryRuleText = (item) => { + let rules = [] + try { + rules = typeof item.deliveryRules === 'string' && item.deliveryRules + ? JSON.parse(item.deliveryRules) : (item.deliveryRules || []) + } catch (e) { + rules = [] + } + if (rules && rules.length) { + return rules.map(r => r.minKm + '-' + r.maxKm + 'km ¥' + r.price).join(' / ') + } + return '¥' + (item.deliveryPrice || 0) +} //打开定位 const openLocation = (shop) => { //console.log(shop); diff --git a/yshop-drink-uniapp-vue3/pages/menu/menu.vue b/yshop-drink-uniapp-vue3/pages/menu/menu.vue index aba4dc1..f8311e8 100644 --- a/yshop-drink-uniapp-vue3/pages/menu/menu.vue +++ b/yshop-drink-uniapp-vue3/pages/menu/menu.vue @@ -366,12 +366,12 @@ const getCartGoodsPrice = computed(() =>{ //计算购物车总价 return parseFloat(price).toFixed(2); }) const disabledPay = computed(() => { //是否达到起送价 - return orderType.value == 'takeout' && (getCartGoodsPrice < parseFloat(store.value.min_price)) ? true : + return orderType.value == 'takeout' && (getCartGoodsPrice < parseFloat(store.value.minPrice)) ? true : false }) const spread = computed(() => { //差多少元起送 if (orderType.value != 'takeout') return - return parseFloat((store.value.min_price - getCartGoodsPrice).toFixed(2)) + return parseFloat((store.value.minPrice - getCartGoodsPrice).toFixed(2)) }) // 监听自定义事件 @@ -750,8 +750,8 @@ const toPay = () => { uToast.value.show({message:'本店不支持外卖',type: 'error'}); return; } - // 判断当前是否在配送范围内 - if (orderType.value == 'takeout' && store.value.distance < store.value.far) { + // 判断当前是否在配送范围内(基于用户定位距离粗略判断,最终以下单时收货地址校验为准) + if (orderType.value == 'takeout' && store.value.dis && store.value.distance < (store.value.dis / 1000)) { uToast.value.show({message:'选中的地址不在配送范围',type: 'error'}); return; } diff --git a/yshop-drink-vue3/src/api/mall/store/shop/index.ts b/yshop-drink-vue3/src/api/mall/store/shop/index.ts index 933c796..6219acc 100644 --- a/yshop-drink-vue3/src/api/mall/store/shop/index.ts +++ b/yshop-drink-vue3/src/api/mall/store/shop/index.ts @@ -1,5 +1,11 @@ import request from '@/config/axios' +export interface DeliveryRule { + minKm: number + maxKm: number + price: number +} + export interface ShopVO { id: number name: string @@ -13,6 +19,7 @@ export interface ShopVO { distance: number minPrice: number deliveryPrice: number + deliveryRules: DeliveryRule[] notice: string status: boolean adminId: string diff --git a/yshop-drink-vue3/src/views/mall/store/shop/ShopForm.vue b/yshop-drink-vue3/src/views/mall/store/shop/ShopForm.vue index c7f7b66..4ad0370 100644 --- a/yshop-drink-vue3/src/views/mall/store/shop/ShopForm.vue +++ b/yshop-drink-vue3/src/views/mall/store/shop/ShopForm.vue @@ -46,8 +46,23 @@ - - + + + + +

+
+ + ~ + + km + + 元 + 删除 +
+ + 添加规则 +
区间左闭右开,如 1~3km 含 1 不含 3;不配置规则时使用默认配送费
+
@@ -111,6 +126,7 @@ const formData = ref({ distance: undefined, minPrice: undefined, deliveryPrice: undefined, + deliveryRules: [], notice: undefined, status: undefined, adminId: undefined, @@ -159,6 +175,7 @@ const open = async (type: string, id?: number) => { try { formData.value = await ShopApi.getShop(id) formData.value.adminId = formData.value.adminId.map(Number) + formData.value.deliveryRules = formData.value.deliveryRules || [] } finally { formLoading.value = false } @@ -197,6 +214,19 @@ const openForm = () => { formRef1.value.open() } +/** 新增一条配送规则 */ +const addRule = () => { + if (!formData.value.deliveryRules) { + formData.value.deliveryRules = [] + } + formData.value.deliveryRules.push({ minKm: undefined, maxKm: undefined, price: undefined }) +} + +/** 删除一条配送规则 */ +const removeRule = (index: number) => { + formData.value.deliveryRules.splice(index, 1) +} + const mapConfirm = (mapResult) => { console.log('mapResult:',mapResult) formData.value.addressMap = mapResult.address @@ -233,6 +263,7 @@ const resetForm = () => { distance: undefined, minPrice: undefined, deliveryPrice: undefined, + deliveryRules: [], notice: undefined, status: 1, adminId: undefined, @@ -248,4 +279,19 @@ const resetForm = () => { width: 100%; height: 300px; } +.delivery-rule-row { + display: flex; + align-items: center; + margin-bottom: 8px; + gap: 6px; +} +.delivery-rule-sep, +.delivery-rule-unit { + color: #606266; +} +.delivery-rule-tip { + color: #999; + font-size: 12px; + margin-top: 4px; +}