wip: tiered deliver price
This commit is contained in:
1 parent
8423007350
commit
a0c13b91da
23 files changed
+426
-39
No files matched your search
+1
@@ -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, "当前收货地址不在配送范围内");
|
||||
|
||||
|
||||
|
||||
|
||||
+22
-10
@@ -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());
|
||||
|
||||
+5
-1
@@ -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;
|
||||
|
||||
+9
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -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;
|
||||
|
||||
|
||||
+25
@@ -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;
|
||||
|
||||
}
|
||||
+13
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+37
@@ -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;
|
||||
|
||||
}
|
||||
+8
-1
@@ -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;
|
||||
/**
|
||||
* 公告
|
||||
*/
|
||||
|
||||
+33
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+10
@@ -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);
|
||||
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
}
|
||||
+86
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user