wip: tiered deliver price

This commit is contained in:
Kroytz committed 2026-07-03 19:17:26 +08:00
1 parent 8423007350
commit a0c13b91da
23 files changed
+426 -39

No files matched your search

@@ -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, "当前收货地址不在配送范围内");
@@ -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<StoreOrderMapper,Store
BigDecimal sumPrice = BigDecimal.ZERO;
BigDecimal couponPrice = BigDecimal.ZERO;
BigDecimal postagePrice = storeShopDO.getDeliveryPrice();
BigDecimal deductionPrice = BigDecimal.ZERO;
// 计算配送费:外卖按收货地址距离匹配配送规则,自取不收配送费
BigDecimal postagePrice = BigDecimal.ZERO;
UserAddressDO userAddress = null;
if (OrderLogEnum.ORDER_TAKE_OUT.getValue().equals(param.getOrderType())) {
if (StrUtil.isEmpty(param.getAddressId())) {
throw exception(SELECT_ADDRESS);
}
userAddress = appUserAddressService.getById(param.getAddressId());
if (ObjectUtil.isNull(userAddress)) {
throw exception(USER_ADDRESS_NOT_EXISTS);
}
double distanceKm = DeliveryCalculator.calcDistanceKm(
storeShopDO.getLng(), storeShopDO.getLat(),
userAddress.getLongitude(), userAddress.getLatitude());
postagePrice = DeliveryCalculator.calcDeliveryPrice(storeShopDO, distanceKm);
if (postagePrice == null) {
throw exception(STORE_ORDER_OUT_OF_DELIVERY_RANGE);
}
}
//对库存检查加锁
RLock lock = redissonClient.getLock(LOCK_KEY);
@@ -264,13 +283,6 @@ public class AppStoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper,Store
storeOrder.setOrderId(orderSn);
//处理如果是外卖 地址
if(OrderLogEnum.ORDER_TAKE_OUT.getValue().equals(param.getOrderType())){
if (StrUtil.isEmpty(param.getAddressId())) {
throw exception(SELECT_ADDRESS);
}
UserAddressDO userAddress = appUserAddressService.getById(param.getAddressId());
if (ObjectUtil.isNull(userAddress)) {
throw exception(USER_ADDRESS_NOT_EXISTS);
}
storeOrder.setRealName(userAddress.getRealName());
storeOrder.setUserPhone(userAddress.getPhone());
storeOrder.setUserAddress(userAddress.getAddress() + " " + userAddress.getDetail());
@@ -278,12 +290,12 @@ public class AppStoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper,Store
storeOrder.setCartId(StrUtil.join(",", cartIds));
storeOrder.setTotalNum(totalNum);
storeOrder.setTotalPrice(sumPrice);
storeOrder.setTotalPostage(storeShopDO.getDeliveryPrice());
storeOrder.setTotalPostage(postagePrice);
storeOrder.setCouponId(StrUtil.isBlank(param.getCouponId()) ? 0 : Integer.valueOf(param.getCouponId()));
storeOrder.setCouponPrice(couponPrice);
storeOrder.setPayPrice(payPrice);
storeOrder.setPayPostage(storeShopDO.getDeliveryPrice());
storeOrder.setPayPostage(postagePrice);
storeOrder.setDeductionPrice(deductionPrice);
storeOrder.setPaid(OrderInfoEnum.PAY_STATUS_0.getValue());
storeOrder.setPayType(param.getPayType());
@@ -1,5 +1,6 @@
package co.yixiang.yshop.module.store.controller.admin.storeshop.vo;
import co.yixiang.yshop.module.store.dal.dataobject.storeshop.DeliveryRule;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
@@ -60,9 +61,12 @@ public class StoreShopBaseVO {
@Schema(description = "起送价钱", required = true, example = "15157")
private BigDecimal minPrice;
@Schema(description = "配送价格", required = true, example = "11771")
@Schema(description = "默认配送费(未配置规则或无匹配时兜底)", example = "8")
private BigDecimal deliveryPrice;
@Schema(description = "配送规则(按距离区间阶梯计费)", example = "[{\"minKm\":1,\"maxKm\":3,\"price\":8}]")
private List<DeliveryRule> deliveryRules;
@Schema(description = "公告", required = true)
@NotNull(message = "公告不能为空")
private String notice;
@@ -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<DeliveryFeeRespVO> getDeliveryFee(@RequestParam("shopId") Long shopId,
@RequestParam("lng") double lng,
@RequestParam("lat") double lat) {
return success(appStoreShopService.getDeliveryFee(shopId, lng, lat));
}
}
@@ -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;
@@ -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;
}
@@ -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<StoreShopRespVO> convertList(List<StoreShopDO> list);
@@ -34,4 +39,12 @@ public interface StoreShopConvert {
List<StoreShopExcelVO> convertList02(List<StoreShopDO> list);
/**
* 将配送规则 List 序列化为 JSON 文本,供小程序端解析展示
*/
@Named("rulesToJson")
default String rulesToJson(List<DeliveryRule> rules) {
return rules == null ? null : JsonUtils.toJsonString(rules);
}
}
@@ -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
* <p>
* 描述一段距离区间对应的配送费,区间为左闭右开 [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;
}
@@ -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<DeliveryRule> deliveryRules;
/**
* 公告
*/
@@ -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<DeliveryRule> 的类型转换器实现类,对应数据库的 text 类型
* <p>
* 参考 {@link com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} 实现,
* 使用 TypeReference 保证反序列化为 List<DeliveryRule> 时泛型不丢失。
* 放置于 store 模块以避免框架层反向依赖业务模块。
*
* @author yshop
*/
public class DeliveryRuleListTypeHandler extends AbstractJsonTypeHandler<Object> {
private static final TypeReference<List<DeliveryRule>> TYPE_REFERENCE = new TypeReference<List<DeliveryRule>>(){};
@Override
protected Object parse(String json) {
return JsonUtils.parseObject(json, TYPE_REFERENCE);
}
@Override
protected String toJson(Object obj) {
return JsonUtils.toJsonString(obj);
}
}
@@ -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<StoreShopDO> {
*/
List<AppStoreShopVO> 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);
}
@@ -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<StoreShopMapper, StoreS
public List<AppStoreShopVO> 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;
}
}
@@ -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;
/**
* 配送费计算工具
* <p>
* 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<DeliveryRule> 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();
}
}