diff --git a/hiver-admin/src/main/resources/application.yml b/hiver-admin/src/main/resources/application.yml index 041e8a53..54148e3c 100644 --- a/hiver-admin/src/main/resources/application.yml +++ b/hiver-admin/src/main/resources/application.yml @@ -81,7 +81,7 @@ spring: multi-statement-allow: true jpa: # 显示sql - show-sql: false + show-sql: true # 自动生成表结构 关闭设为none hibernate: ddl-auto: update @@ -398,6 +398,11 @@ ignored: - /hiver/app/productShare/getShareList - /hiver/app/productAttribute/selectAttributeAndValueByCategoryId - /hiver/app/stock/productCount + - /hiver/mall/order/getOrdersByUserId/** + - /hiver/mall/delivery/page + - /hiver/app/shopArea/getByParentId/** + - /hiver/mall/delivery/countOrderByStatus + - /hiver/mall/order/countByShop/** # 选择物流公司 - /hiver/app/logitics/chooseCompany # 根据客户id分页获取销售历史(按商品) diff --git a/hiver-admin/test-output/test-report.html b/hiver-admin/test-output/test-report.html index 6af080fb..c286d0d1 100644 --- a/hiver-admin/test-output/test-report.html +++ b/hiver-admin/test-output/test-report.html @@ -35,7 +35,7 @@ Hiver
  • - 14, 2026 18:24:58 + 19, 2026 13:55:31
  • @@ -84,7 +84,7 @@

    passTest

    -

    18:24:59 / 0.034 secs

    +

    13:55:31 / 0.022 secs

    @@ -92,9 +92,9 @@
    #test-id=1
    passTest
    -06.14.2026 18:24:59 -06.14.2026 18:24:59 -0.034 secs +06.19.2026 13:55:31 +06.19.2026 13:55:31 +0.022 secs
    @@ -104,7 +104,7 @@ Pass - 18:24:59 + 13:55:31 Test passed @@ -128,13 +128,13 @@

    Started

    -

    14, 2026 18:24:58

    +

    19, 2026 13:55:31

    Ended

    -

    14, 2026 18:24:59

    +

    19, 2026 13:55:31

    diff --git a/hiver-core/src/main/java/cc/hiver/core/common/constant/SettingConstant.java b/hiver-core/src/main/java/cc/hiver/core/common/constant/SettingConstant.java index c12fddb3..052a47f2 100644 --- a/hiver-core/src/main/java/cc/hiver/core/common/constant/SettingConstant.java +++ b/hiver-core/src/main/java/cc/hiver/core/common/constant/SettingConstant.java @@ -107,6 +107,8 @@ public interface SettingConstant { SMS_REJECT_ORDER, // 被指定未处理通知 SMS_TIMEOUT, + // 中转单商家送达后校内取货超时提醒 + SMS_TRANSFER, //订单退款 SMS_ORDERRETURN } diff --git a/hiver-core/src/main/java/cc/hiver/core/config/security/WebSecurityConfig.java b/hiver-core/src/main/java/cc/hiver/core/config/security/WebSecurityConfig.java index e58803a8..2e127291 100644 --- a/hiver-core/src/main/java/cc/hiver/core/config/security/WebSecurityConfig.java +++ b/hiver-core/src/main/java/cc/hiver/core/config/security/WebSecurityConfig.java @@ -13,14 +13,20 @@ import cc.hiver.core.config.security.validate.ImageValidateFilter; import cc.hiver.core.config.security.validate.SmsValidateFilter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.StrictHttpFirewall; /** * Security 核心配置类 @@ -62,6 +68,28 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SecurityUtil securityUtil; + @Autowired + private UserDetailsServiceImpl userDetailsService; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public HttpFirewall allowSemicolonHttpFirewall() { + StrictHttpFirewall firewall = new StrictHttpFirewall(); + // 微信小程序真机环境/代理链路偶发携带 matrix 参数(;xxx),默认防火墙会直接拒绝并返回 Servlet 异常。 + // 仅放开分号,其他可疑 URL 编码规则仍保持 StrictHttpFirewall 默认校验。 + firewall.setAllowSemicolon(true); + return firewall; + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + } + @Override protected void configure(HttpSecurity http) throws Exception { ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http diff --git a/hiver-core/src/main/java/cc/hiver/core/entity/Worker.java b/hiver-core/src/main/java/cc/hiver/core/entity/Worker.java index fd978bdc..6a8178fe 100644 --- a/hiver-core/src/main/java/cc/hiver/core/entity/Worker.java +++ b/hiver-core/src/main/java/cc/hiver/core/entity/Worker.java @@ -210,4 +210,8 @@ public class Worker implements Serializable { @ApiModelProperty(value = "超出3楼额外费用") @Column(name = "high_floor_fee") private BigDecimal highFloorFee; + + @ApiModelProperty(value = "送达地点:0送上楼 1送到宿舍门口") + @Column(name = "delivery_location") + private Integer deliveryLocation; } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSeckillGroupController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSeckillGroupController.java index bb94c05a..a59c9bbe 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSeckillGroupController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSeckillGroupController.java @@ -1,14 +1,17 @@ package cc.hiver.mall.controller; +import cc.hiver.core.common.redis.RedisTemplateHelper; import cc.hiver.core.common.utils.ResultUtil; import cc.hiver.core.common.vo.Result; import cc.hiver.mall.entity.SeckillGroupCategory; import cc.hiver.mall.entity.SeckillGroupProduct; import cc.hiver.mall.pojo.dto.SeckillGroupProductDTO; +import cc.hiver.mall.pojo.dto.ShopCacheDTO; import cc.hiver.mall.pojo.query.SeckillGroupProductQuery; import cc.hiver.mall.pojo.vo.SeckillGroupProductVO; import cc.hiver.mall.service.mybatis.SeckillGroupCategoryService; import cc.hiver.mall.service.mybatis.SeckillGroupProductService; +import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import io.swagger.annotations.Api; @@ -28,12 +31,101 @@ import java.util.List; @Transactional public class AdminSeckillGroupController { + public static final String GROUP_DELIVERY_TIME_KEY_PREFIX = "GROUP_DELIVERY_TIME:"; + public static final int DEFAULT_GROUP_DELIVERY_TIME = 30; + @Autowired private SeckillGroupCategoryService seckillGroupCategoryService; @Autowired private SeckillGroupProductService seckillGroupProductService; + @Autowired + private RedisTemplateHelper redisTemplateHelper; + + @RequestMapping(value = "/deliveryTime/save", method = RequestMethod.POST) + @ApiOperation(value = "保存区域拼团配送时长") + public Result saveDeliveryTime(@RequestBody DeliveryTimeSetting setting) { + if (setting == null || StringUtils.isBlank(setting.getRegionId())) { + return ResultUtil.error("区域ID不能为空"); + } + if (setting.getDeliveryTime() == null || setting.getDeliveryTime() < 0) { + return ResultUtil.error("配送时长必须为非负整数"); + } + redisTemplateHelper.set(buildDeliveryTimeKey(setting.getRegionId()), String.valueOf(setting.getDeliveryTime())); + return ResultUtil.success("保存成功"); + } + + @RequestMapping(value = "/deliveryTime/get", method = RequestMethod.GET) + @ApiOperation(value = "查询区域拼团配送时长") + public Result getDeliveryTime(@RequestParam(value = "regionId") String regionId) { + return new ResultUtil().setData(getDeliveryTimeByRegionId(redisTemplateHelper, regionId)); + } + + public static Integer getDeliveryTimeByRegionId(RedisTemplateHelper redisTemplateHelper, String regionId) { + if (redisTemplateHelper == null || StringUtils.isBlank(regionId)) { + return DEFAULT_GROUP_DELIVERY_TIME; + } + String value = redisTemplateHelper.get(buildDeliveryTimeKey(regionId)); + if (StringUtils.isBlank(value)) { + return DEFAULT_GROUP_DELIVERY_TIME; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return DEFAULT_GROUP_DELIVERY_TIME; + } + } + + public static Integer getTotalDeliveryTimeByRegionIdAndShopId(RedisTemplateHelper redisTemplateHelper, String regionId, String shopId) { + return getShopCookingTimeByRegionIdAndShopId(redisTemplateHelper, regionId, shopId) + + getDeliveryTimeByRegionId(redisTemplateHelper, regionId); + } + + public static Integer getShopCookingTimeByRegionIdAndShopId(RedisTemplateHelper redisTemplateHelper, String regionId, String shopId) { + if (redisTemplateHelper == null || StringUtils.isBlank(regionId) || StringUtils.isBlank(shopId)) { + return 0; + } + Object value = redisTemplateHelper.hGet("SHOP_CACHE:" + regionId, shopId); + if (value == null) { + return 0; + } + try { + ShopCacheDTO dto = JSONUtil.toBean(value.toString(), ShopCacheDTO.class); + if (dto != null && dto.getShopTakeaway() != null && dto.getShopTakeaway().getCookingTime() != null) { + return dto.getShopTakeaway().getCookingTime(); + } + } catch (Exception e) { + log.warn("读取店铺出餐时间缓存失败 regionId={}, shopId={}", regionId, shopId, e); + } + return 0; + } + + public static String buildDeliveryTimeKey(String regionId) { + return GROUP_DELIVERY_TIME_KEY_PREFIX + regionId; + } + + public static class DeliveryTimeSetting { + private String regionId; + private Integer deliveryTime; + + public String getRegionId() { + return regionId; + } + + public void setRegionId(String regionId) { + this.regionId = regionId; + } + + public Integer getDeliveryTime() { + return deliveryTime; + } + + public void setDeliveryTime(Integer deliveryTime) { + this.deliveryTime = deliveryTime; + } + } + @RequestMapping(value = "/category/save", method = RequestMethod.POST) @ApiOperation(value = "新增秒杀团分类") public Result saveCategory(@RequestBody SeckillGroupCategory category) { diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallDeliveryOrderController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallDeliveryOrderController.java index e0c6053a..8462b7c5 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallDeliveryOrderController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallDeliveryOrderController.java @@ -11,6 +11,7 @@ import cc.hiver.core.serviceimpl.JPushServiceImpl; import cc.hiver.mall.entity.MallAdPosition; import cc.hiver.mall.entity.MallDeliveryOrder; import cc.hiver.mall.entity.MallOrder; +import cc.hiver.mall.mq.OrderAsyncProducer; import cc.hiver.mall.pojo.query.MallDeliveryOrderPageQuery; import cc.hiver.mall.pojo.vo.WorkerRedisVo; import cc.hiver.mall.pojo.vo.WorkerRewardVO; @@ -25,6 +26,7 @@ import cc.hiver.mall.utils.WorkerRedisCacheUtil; import cc.hiver.mall.utils.WorkerRewardCacheUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -33,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -80,6 +83,9 @@ public class MallDeliveryOrderController { @Autowired private HomeReminderService homeReminderService; + @Autowired + private OrderAsyncProducer orderAsyncProducer; + /** * 分页查询配送单 * hallOnly=true 时查询抢单大厅(未被接单的单) @@ -147,7 +153,7 @@ public class MallDeliveryOrderController { } } } - cacheResult.put("homeReminders", homeReminderService.homeReminders(currentUserId(), q.getRegionId())); + cacheResult.put("homeReminders", homeReminderService.homeReminders(StrUtil.blankToDefault(q.getUserId(), currentUserId()), q.getRegionId())); return new ResultUtil().setData(cacheResult); } @@ -202,6 +208,92 @@ public class MallDeliveryOrderController { return new ResultUtil().setData(page); } + @PostMapping("/pageShopDelivery") + @ApiOperation(value = "分页查询商家自配送单") + public Result pageShopDelivery(@RequestBody MallDeliveryOrderPageQuery q) { + q.setShopDelivery(1); + q.setDeliveryType(1); + IPage page = mallDeliveryOrderService.pageDeliveryByStatus(q); + return new ResultUtil().setData(page); + } + + @PostMapping("/pageTransferDelivery") + @ApiOperation(value = "分页查询商家中转配送单") + public Result pageTransferDelivery(@RequestBody MallDeliveryOrderPageQuery q) { + q.setTransferDelivery(1); + q.setDeliveryType(1); + q.setStatus(null); + q.setTransferArriveEmpty(null); + q.setOrder("transferDelivery"); + IPage page = mallDeliveryOrderService.pageDelivery(q); + return new ResultUtil().setData(page); + } + + @PostMapping("/transferArrive") + @ApiOperation(value = "商家送达中转点") + public Result transferArrive(@RequestParam String deliveryId, + @RequestParam(required = false) String transferArriveImage) { + MallDeliveryOrder delivery = mallDeliveryOrderService.getById(deliveryId); + if (delivery == null) { + return ResultUtil.error("配送单不存在"); + } + if (!Integer.valueOf(1).equals(delivery.getTransferDelivery())) { + return ResultUtil.error("非中转配送单"); + } + Date transferArriveTime = new Date(); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(MallDeliveryOrder::getId, deliveryId) + .set(MallDeliveryOrder::getTransferArriveTime, transferArriveTime) + .set(MallDeliveryOrder::getTransferArriveImage, transferArriveImage); + mallDeliveryOrderService.update(updateWrapper); + + if (StrUtil.isNotBlank(delivery.getOrderId())) { + orderAsyncProducer.sendDelayMessage(delivery.getOrderId(), "Transfer_Pickup_Timeout", 10 * 60 * 1000L); + } + + MallDeliveryOrder cacheDelivery = workerOrderCacheUtil.get(delivery.getWorkerId(), delivery.getId()); + if (cacheDelivery != null) { + cacheDelivery.setTransferArriveTime(transferArriveTime); + cacheDelivery.setTransferArriveImage(transferArriveImage); + workerOrderCacheUtil.update(delivery.getWorkerId(), cacheDelivery); + } + return ResultUtil.success("操作成功"); + } + + @PostMapping("/checkTransferPickupCode") + @ApiOperation(value = "校验中转取餐码") + public Result checkTransferPickupCode(@RequestParam String deliveryId, + @RequestParam String pickupCode) { + MallDeliveryOrder delivery = mallDeliveryOrderService.getById(deliveryId); + if (delivery == null) { + return ResultUtil.error("配送单不存在"); + } + if (!Integer.valueOf(1).equals(delivery.getTransferDelivery())) { + return ResultUtil.success("无需校验"); + } + if (StrUtil.isBlank(pickupCode) || !pickupCode.equals(delivery.getTransferPickupCode())) { + return ResultUtil.error("取餐码错误"); + } + return ResultUtil.success("校验成功"); + } + + @PostMapping("/transferPickup") + @ApiOperation(value = "中转单校验取餐码后取货") + public Result transferPickup(@RequestParam String deliveryId, + @RequestParam String workerId, + @RequestParam String pickupCode) { + MallDeliveryOrder delivery = mallDeliveryOrderService.getById(deliveryId); + if (delivery == null) { + return ResultUtil.error("配送单不存在"); + } + if (Integer.valueOf(1).equals(delivery.getTransferDelivery()) + && (StrUtil.isBlank(pickupCode) || !pickupCode.equals(delivery.getTransferPickupCode()))) { + return ResultUtil.error("取餐码错误"); + } + mallDeliveryOrderService.workerPickup(deliveryId, workerId); + return ResultUtil.success("取货成功"); + } + @PostMapping("/pagebyworkerHistory") @ApiOperation(value = "分页查询配送员配送单") public Result pagebyworkerHistory(@RequestBody MallDeliveryOrderPageQuery q) { diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java index 33deeb6c..0ccfa304 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java @@ -72,8 +72,8 @@ public class ShopAreaController { if(o2.getSortOrder() == null) return 1; return o1.getSortOrder().compareTo(o2.getSortOrder()); }); - // data permission filter - List depIds = securityUtil.getDeparmentIds(); + // data permission filter:匿名放行接口没有登录上下文时,不做部门过滤。 + List depIds = resolveDepartmentIds(openDataFilter); if (openDataFilter && depIds != null && depIds.size() > 0) { List filteredDatas = new java.util.ArrayList<>(); for (ShopArea allData : list) { @@ -109,6 +109,17 @@ public class ShopAreaController { return new ResultUtil>().setData(list); } + private List resolveDepartmentIds(Boolean openDataFilter) { + if (!Boolean.TRUE.equals(openDataFilter)) { + return null; + } + try { + return securityUtil.getDeparmentIds(); + } catch (Exception ignored) { + return null; + } + } + @RequestMapping(value = "/add", method = RequestMethod.POST) @ApiOperation(value = "添加") public Result add(ShopArea ShopArea) { diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java index a67ee230..0a83077d 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java @@ -26,7 +26,9 @@ import cc.hiver.core.common.utils.StringUtils; import cc.hiver.core.common.vo.PageVo; import cc.hiver.core.common.vo.Result; import cc.hiver.core.entity.User; +import cc.hiver.core.entity.Worker; import cc.hiver.core.service.UserService; +import cc.hiver.core.service.WorkerService; import cc.hiver.mall.entity.Shop; import cc.hiver.mall.entity.ShopArea; import cc.hiver.mall.entity.ShopTakeaway; @@ -92,6 +94,9 @@ public class ShopController { @Autowired private ShopTakeawayService shopTakeawayService; + @Autowired + private WorkerService workerService; + @RequestMapping(value = "/getAll", method = RequestMethod.GET) @ApiOperation("获取全部数据") public Result> getAll() { @@ -154,7 +159,7 @@ public class ShopController { if (d != null) { shop.setShopAreaTitle(d.getTitle()); } - // 获取顶级圈层的id和名称,为后续拉包工等选择做准备 + // 获取商圈的id和名称 final ShopArea topShopAreaById = shopAreaService.findTopShopAreaById(shop.getShopArea()); shop.setRegion(topShopAreaById.getTitle()); shop.setRegionId(topShopAreaById.getId()); @@ -208,6 +213,67 @@ public class ShopController { return ResultUtil.success("编辑成功"); } + @RequestMapping(value = "/updateSelfDelivery", method = RequestMethod.POST) + @ResponseBody + @ApiOperation("更新商家自配送配置") + public Result updateSelfDelivery(@RequestBody Shop shop) { + if (CharSequenceUtil.isBlank(shop.getId())) { + return ResultUtil.error("店铺id不能为空"); + } + Shop oldShop = shopService.get(shop.getId()); + if (oldShop == null) { + return ResultUtil.error("店铺不存在"); + } + oldShop.setSupportShopDelivery(shop.getSupportShopDelivery()); + oldShop.setOrderBkge(shop.getOrderBkge()); + oldShop.setShopDeliveryLocation(shop.getShopDeliveryLocation()); + oldShop.setShopDeliveryDuration(shop.getShopDeliveryDuration()); + oldShop.setWorkerId(shop.getWorkerId()); + oldShop.setWorkerName(shop.getWorkerName()); + oldShop.setWorkerPhone(shop.getWorkerPhone()); + shopService.update(oldShop); + shopService.refreshShopCache(oldShop.getId(), oldShop.getRegionId()); + return ResultUtil.success("编辑成功"); + } + + @RequestMapping(value = "/updateTransferDelivery", method = RequestMethod.POST) + @ResponseBody + @ApiOperation("更新商家中转配送配置") + public Result updateTransferDelivery(@RequestBody Shop shop) { + if (CharSequenceUtil.isBlank(shop.getId())) { + return ResultUtil.error("店铺id不能为空"); + } + Shop oldShop = shopService.get(shop.getId()); + if (oldShop == null) { + return ResultUtil.error("店铺不存在"); + } + oldShop.setSupportTransferDelivery(shop.getSupportTransferDelivery()); + oldShop.setTransferDeliveryDuration(shop.getTransferDeliveryDuration()); + oldShop.setTransferAddressId(shop.getTransferAddressId()); + oldShop.setTransferAddressName(shop.getTransferAddressName()); + shopService.update(oldShop); + shopService.refreshShopCache(oldShop.getId(), oldShop.getRegionId()); + return ResultUtil.success("编辑成功"); + } + + @RequestMapping(value = "/findWorkerByMobile", method = RequestMethod.GET) + @ApiOperation("根据手机号查询配送员") + public Result> findWorkerByMobile(@RequestParam String mobile) { + if (CharSequenceUtil.isBlank(mobile)) { + return ResultUtil.error("手机号不能为空"); + } + Worker worker = workerService.findByMobile(mobile); + if (worker == null) { + return ResultUtil.error("未查询到配送员"); + } + Map result = new java.util.HashMap<>(); + result.put("workerId", worker.getWorkerId()); + result.put("workerName", worker.getWorkerName()); + result.put("workerPhone", worker.getMobile()); + result.put("mobile", worker.getMobile()); + return new ResultUtil>().setData(result); + } + @RequestMapping(value = "/delByIds", method = RequestMethod.POST) @ResponseBody @ApiOperation("通过id删除") @@ -374,6 +440,7 @@ public class ShopController { } } } + fillGroupDeliveryTime(pageList, regionId); Page pageResult = new PageImpl<>(pageList, PageUtil.initPage(pageVo), allShops.size()); return new ResultUtil>().setData(pageResult); @@ -417,10 +484,25 @@ public class ShopController { } } }); + fillGroupDeliveryTime(page.getContent(), shop.getRegionId()); } return new ResultUtil>().setData(page); } + private void fillGroupDeliveryTime(List shops, String regionId) { + if (shops == null || shops.isEmpty()) { + return; + } + Integer deliveryTime = AdminSeckillGroupController.getDeliveryTimeByRegionId(redisTemplateHelper, regionId); + for (Shop shop : shops) { + Integer cookingTime = 0; + if (shop.getShopTakeaway() != null && shop.getShopTakeaway().getCookingTime() != null) { + cookingTime = shop.getShopTakeaway().getCookingTime(); + } + shop.setGroupDeliveryTime(cookingTime + deliveryTime); + } + } + @RequestMapping(value = "/enable", method = RequestMethod.POST) @ApiOperation("启用店铺") public Result enable(@RequestParam String id) { diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerController.java index 809af027..36b6e6e4 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerController.java @@ -416,6 +416,7 @@ public class WorkerController { matchVO.setAvgTime(w.getAvgTime()); matchVO.setRebateAmount(w.getRebateAmount()); matchVO.setHighFloorFee(w.getHighFloorFee()); + matchVO.setDeliveryLocation(w.getDeliveryLocation()); matchVO.setOrderBkge(matchedRule.getOrderBkge()); matchVO.setRemark(matchedRule.getRemark()); matchVO.setOrderWaitCount(orderWaitCount); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerRelaPriceController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerRelaPriceController.java index 7dee9071..b373a67e 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerRelaPriceController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerRelaPriceController.java @@ -68,6 +68,7 @@ public class WorkerRelaPriceController { workerRealPriceVo.setGetPushOrder(worker.getGetPushOrder()); workerRealPriceVo.setCardPicture(worker.getCardPicture()); workerRealPriceVo.setHighFloorFee(worker.getHighFloorFee()); + workerRealPriceVo.setDeliveryLocation(worker.getDeliveryLocation()); final List workerRelaPriceList = workerRelaPriceService.selectByWorkerId(workerId); workerRealPriceVo.setWorkerRelaPriceList(workerRelaPriceList); } @@ -86,6 +87,7 @@ public class WorkerRelaPriceController { worker.setCardPicture(workerRealPriceVo.getCardPicture()); worker.setMobile(workerRealPriceVo.getMobile()); worker.setHighFloorFee(workerRealPriceVo.getHighFloorFee()); + worker.setDeliveryLocation(workerRealPriceVo.getDeliveryLocation()); workerService.update(worker); // 批量新增配送规则 if(workerRealPriceVo.getWorkerRelaPriceList() != null){ diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallOrderMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallOrderMapper.java index 4d3bc7f9..4e9e789c 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallOrderMapper.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallOrderMapper.java @@ -33,6 +33,10 @@ public interface MallOrderMapper extends BaseMapper { Integer selectRefundCount(@Param("shopId") String shopId); + Integer selectShopDeliveryPendingCount(@Param("shopId") String shopId); + + Integer selectTransferDeliveryPendingCount(@Param("shopId") String shopId); + List getWeChatId(); List getPeiSongOrders(@Param("userId") String userId); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallDeliveryOrder.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallDeliveryOrder.java index 98b535c7..bdd794e7 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallDeliveryOrder.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallDeliveryOrder.java @@ -95,6 +95,18 @@ public class MallDeliveryOrder implements Serializable { private String getCodes; @ApiModelProperty(value = "取件码图片 , 分割") private String getPictures; + @ApiModelProperty(value = "是否中转配送 0否 1是") + private Integer transferDelivery; + @ApiModelProperty(value = "中转地点ID") + private String transferAddressId; + @ApiModelProperty(value = "中转地点名称") + private String transferAddressName; + @ApiModelProperty(value = "商家送达中转点时间") + private Date transferArriveTime; + @ApiModelProperty(value = "商家送达中转点图片") + private String transferArriveImage; + @ApiModelProperty(value = "中转取餐码") + private String transferPickupCode; @Transient @TableField(exist = false) @ApiModelProperty(value = "商品信息") diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallOrder.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallOrder.java index 3dcd1074..816acdbf 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallOrder.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallOrder.java @@ -89,6 +89,18 @@ public class MallOrder implements Serializable { @ApiModelProperty("免单金额") private BigDecimal freeAmount; + @ApiModelProperty("是否商家自配送 0否 1是") + private Integer shopDelivery; + + @ApiModelProperty("是否中转配送 0否 1是") + private Integer transferDelivery; + + @ApiModelProperty("中转地点ID") + private String transferAddressId; + + @ApiModelProperty("中转地点名称") + private String transferAddressName; + @ApiModelProperty(value = "面对面配送 退款 参团用户订单id集合") @Transient @TableField(exist = false) diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/Shop.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/Shop.java index 32f00fd6..c439d34c 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/Shop.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/Shop.java @@ -178,11 +178,49 @@ public class Shop extends HiverBaseEntity { @ApiModelProperty(value = "1外卖商家 / 2团购商家") private Integer merchantType; + @ApiModelProperty(value = "是否支持商家自配送 0否 1是") + private Integer supportShopDelivery; + + @ApiModelProperty(value = "商家自配送配送费") + private BigDecimal orderBkge; + + @ApiModelProperty(value = "商家自配送送达位置 1送到宿舍 2送到楼下") + private Integer shopDeliveryLocation; + + @ApiModelProperty(value = "商家自配送配送时长(分钟)") + private Integer shopDeliveryDuration; + + @ApiModelProperty(value = "商家自配送配送员ID") + private String workerId; + + @ApiModelProperty(value = "商家自配送配送员姓名") + private String workerName; + + @ApiModelProperty(value = "商家自配送配送员电话") + private String workerPhone; + + @ApiModelProperty(value = "是否支持中转配送 0否 1是") + private Integer supportTransferDelivery; + + @ApiModelProperty(value = "中转配送时长(分钟)") + private Integer transferDeliveryDuration; + + @ApiModelProperty(value = "中转地点ID") + private String transferAddressId; + + @ApiModelProperty(value = "中转地点名称") + private String transferAddressName; + @Transient @TableField(exist = false) @ApiModelProperty(value = "店铺抽佣等设置") private ShopTakeaway shopTakeaway; + @Transient + @TableField(exist = false) + @ApiModelProperty(value = "拼团预计配送时长") + private Integer groupDeliveryTime; + @Transient @TableField(exist = false) @ApiModelProperty(value = "模糊搜索条件") diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeAdminController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeAdminController.java index a82311f3..c0f8d473 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeAdminController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeAdminController.java @@ -6,6 +6,7 @@ import cc.hiver.core.common.vo.Result; import cc.hiver.core.entity.User; import cc.hiver.mall.ie.entity.IeReport; import cc.hiver.mall.ie.service.IeChatService; +import cc.hiver.mall.ie.service.IeRealNameAuthService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; @@ -25,6 +26,9 @@ public class IeAdminController { @Autowired private SecurityUtil securityUtil; + @Autowired + private IeRealNameAuthService realNameAuthService; + @RequestMapping(value = "/rooms/finishExpired", method = RequestMethod.POST) @ApiOperation("批量结束已过期陪伴房间") public Result finishExpiredRooms(Integer limit) { @@ -58,4 +62,19 @@ public class IeAdminController { chatService.deleteReport(reportId); return ResultUtil.success("删除成功"); } + + @RequestMapping(value = "/audit-setting", method = RequestMethod.GET) + @ApiOperation("查询i/e审核管理配置") + public Result> auditSetting() { + java.util.Map result = new java.util.HashMap<>(); + result.put("realNameAuditEnabled", realNameAuthService.isRealNameAuditEnabled()); + return new ResultUtil>().setData(result); + } + + @RequestMapping(value = "/audit-setting/save", method = RequestMethod.POST) + @ApiOperation("保存i/e审核管理配置") + public Result saveAuditSetting(@RequestParam(required = false) Boolean realNameAuditEnabled) { + realNameAuthService.setRealNameAuditEnabled(realNameAuditEnabled); + return ResultUtil.success("保存成功"); + } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeRealNameAuthController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeRealNameAuthController.java new file mode 100644 index 00000000..86111309 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeRealNameAuthController.java @@ -0,0 +1,59 @@ +package cc.hiver.mall.ie.controller; + +import cc.hiver.core.common.utils.ResultUtil; +import cc.hiver.core.common.utils.SecurityUtil; +import cc.hiver.core.common.vo.Result; +import cc.hiver.core.entity.User; +import cc.hiver.mall.ie.dto.IeRealNameVerifyDTO; +import cc.hiver.mall.ie.dto.IeStudentCardVerifyDTO; +import cc.hiver.mall.ie.service.IeRealNameAuthService; +import cc.hiver.mall.ie.vo.IeRealNameAuthVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Api(tags = "i/e进阶实名认证接口") +@RequestMapping("/hiver/app/ie/real-name") +public class IeRealNameAuthController { + + @Autowired + private SecurityUtil securityUtil; + + @Autowired + private IeRealNameAuthService realNameAuthService; + + @RequestMapping(value = "/verify", method = RequestMethod.POST) + @ApiOperation("使用阿里云身份证三要素核验完成成年实名认证") + public Result verify(@RequestBody IeRealNameVerifyDTO dto) { + return new ResultUtil().setData(realNameAuthService.verifyByAliyunThreeElement( + currentUserId(), + dto == null ? null : dto.getCertName(), + dto == null ? null : dto.getCertNo(), + dto == null ? null : dto.getCertPicture())); + } + + @RequestMapping(value = "/student-card/verify", method = RequestMethod.POST) + @ApiOperation("上传学生证照片并识别是否为大学生学生证") + public Result verifyStudentCard(@RequestBody IeStudentCardVerifyDTO dto) { + String imageUrl = dto == null ? null : dto.getImageUrl(); + return new ResultUtil().setData(realNameAuthService.verifyStudentCard(currentUserId(), imageUrl)); + } + + @RequestMapping(value = "/audit-setting", method = RequestMethod.GET) + @ApiOperation("查询i/e实名认证审核开关") + public Result> auditSetting() { + java.util.Map result = new java.util.HashMap<>(); + result.put("realNameAuditEnabled", realNameAuthService.isRealNameAuditEnabled()); + return new ResultUtil>().setData(result); + } + + private Long currentUserId() { + User user = securityUtil.getCurrUser(); + return Long.valueOf(user.getId()); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeRealNameVerifyDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeRealNameVerifyDTO.java new file mode 100644 index 00000000..7f41a366 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeRealNameVerifyDTO.java @@ -0,0 +1,19 @@ +package cc.hiver.mall.ie.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("i/e 进阶实名认证请求") +public class IeRealNameVerifyDTO { + + @ApiModelProperty(value = "待核验姓名", required = true) + private String certName; + + @ApiModelProperty(value = "待核验身份证号", required = true) + private String certNo; + + @ApiModelProperty(value = "待核验人像照片Base64,需去除data:image前缀,且不得超过50KB", required = true) + private String certPicture; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeStudentCardVerifyDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeStudentCardVerifyDTO.java new file mode 100644 index 00000000..18c392c3 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeStudentCardVerifyDTO.java @@ -0,0 +1,13 @@ +package cc.hiver.mall.ie.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("i/e 学生证认证请求") +public class IeStudentCardVerifyDTO { + + @ApiModelProperty(value = "学生证图片上传后的访问地址", required = true) + private String imageUrl; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserProfile.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserProfile.java index acc00fe2..81f867e9 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserProfile.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserProfile.java @@ -30,5 +30,55 @@ public class IeUserProfile extends IeBaseEntity { private Integer usedQuota; private Date lastActiveTime; private Integer profileCompleted; + /** + * 实名认证状态:0=未认证,1=已通过,2=未成年人拦截,3=认证失败。 + * 放在 i/e 档案表中,避免普通用户表对全站其他业务产生额外影响。 + */ + private Integer realNameAuthStatus; + /** + * AES 加密后的真实姓名,仅用于必要的合规追溯,不直接返回前端。 + */ + private String realNameEncrypted; + /** + * AES 加密后的身份证号,仅用于年龄复核和审计,不直接返回前端。 + */ + private String idCardEncrypted; + /** + * 脱敏姓名,例如:张*。前端只展示脱敏值。 + */ + private String realNameMasked; + /** + * 脱敏身份证号,例如:110***********1234。前端只展示脱敏值。 + */ + private String idCardMasked; + /** + * 从身份证解析出的生日,格式 yyyy-MM-dd,避免每次重复解密身份证。 + */ + private String birthDate; + /** + * 最近一次实名校验时计算出的年龄。 + */ + private Integer age; + /** + * 最近一次实名通过时间。 + */ + private Date realNameAuthTime; + /** + * 学生证认证状态:0=未上传,1=已通过,2=第一次识别未通过。 + * 第一次模型判断不是学生证时要求用户重传,第二次上传直接放行,避免模型误判阻塞流程。 + */ + private Integer studentCardAuthStatus; + /** + * 最近一次上传的学生证图片地址。属于敏感资料,不返回前端。 + */ + private String studentCardImageUrl; + /** + * 学生证识别尝试次数,用于实现"首次失败重传、第二次放行"。 + */ + private Integer studentCardVerifyCount; + /** + * 最近一次学生证认证时间。 + */ + private Date studentCardAuthTime; private Integer isDeleted; } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/IeBusinessException.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/IeBusinessException.java new file mode 100644 index 00000000..a7a2aa3c --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/IeBusinessException.java @@ -0,0 +1,19 @@ +package cc.hiver.mall.ie.exception; + +/** + * i/e 业务异常基类。 + * + * 说明: + * 1. 这里统一继承 RuntimeException,方便现有 Controller/全局异常处理直接识别为业务失败。 + * 2. 后续如果项目全局有统一错误码体系,可以在本类中补充 code 字段,不需要改每个子异常。 + */ +public class IeBusinessException extends RuntimeException { + + public IeBusinessException(String message) { + super(message); + } + + public IeBusinessException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/IeExceptionHandler.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/IeExceptionHandler.java new file mode 100644 index 00000000..4d9e5f3f --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/IeExceptionHandler.java @@ -0,0 +1,20 @@ +package cc.hiver.mall.ie.exception; + +import cc.hiver.core.common.utils.ResultUtil; +import cc.hiver.core.common.vo.Result; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * i/e 模块业务异常处理器。 + * + * 只拦截 i/e Controller 包,避免影响商城其他模块既有异常处理行为。 + */ +@RestControllerAdvice(basePackages = "cc.hiver.mall.ie.controller") +public class IeExceptionHandler { + + @ExceptionHandler(IeBusinessException.class) + public Result handleIeBusinessException(IeBusinessException e) { + return ResultUtil.error(e.getMessage()); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/UnderageUserException.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/UnderageUserException.java new file mode 100644 index 00000000..22f0d4f0 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/UnderageUserException.java @@ -0,0 +1,14 @@ +package cc.hiver.mall.ie.exception; + +/** + * 未成年人拦截异常。 + * + * 用单独异常类型区分普通认证失败和未成年拦截,便于后续在全局异常处理、 + * 风控日志、前端提示文案中做精细化处理。 + */ +public class UnderageUserException extends IeBusinessException { + + public UnderageUserException(String message) { + super(message); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeRealNameAuthService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeRealNameAuthService.java new file mode 100644 index 00000000..72e4cf3d --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeRealNameAuthService.java @@ -0,0 +1,47 @@ +package cc.hiver.mall.ie.service; + +import cc.hiver.mall.ie.vo.IeRealNameAuthVO; + +public interface IeRealNameAuthService { + + /** + * 使用阿里云身份证三要素核验完成进阶实名认证。 + * + * @param userId 当前登录用户 ID + * @param certName 待核验姓名 + * @param certNo 待核验身份证号 + * @param certPicture 待核验人像照片 Base64,不能超过 50KB + * @return 脱敏后的实名结果 + */ + IeRealNameAuthVO verifyByAliyunThreeElement(Long userId, String certName, String certNo, String certPicture); + + /** + * 使用学生证照片完成学生身份认证。 + * + * @param userId 当前登录用户 ID + * @param imageUrl 已上传的学生证图片地址 + * @return 学生证认证结果 + */ + IeRealNameAuthVO verifyStudentCard(Long userId, String imageUrl); + + /** + * 查询后台审核开关:是否开启身份证三要素实名认证。 + * + * @return true=需要身份证三要素实名,false=只需要学生证 + */ + boolean isRealNameAuditEnabled(); + + /** + * 后台保存审核开关。 + * + * @param enabled true=开启身份证三要素实名 + */ + void setRealNameAuditEnabled(Boolean enabled); + + /** + * 匹配/建档前的服务端兜底校验,防止用户绕过前端直接请求匹配接口。 + * + * @param userId 当前登录用户 ID + */ + void assertAdultVerified(Long userId); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeStudentCardOcrService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeStudentCardOcrService.java new file mode 100644 index 00000000..966dbda9 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeStudentCardOcrService.java @@ -0,0 +1,12 @@ +package cc.hiver.mall.ie.service; + +public interface IeStudentCardOcrService { + + /** + * 判断图片是否为大学生学生证。 + * + * @param imageUrl 图片地址 + * @return true=模型判断是学生证,false=模型判断不是学生证或识别失败 + */ + boolean isStudentCard(String imageUrl); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java index d111681a..108c75e7 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java @@ -213,19 +213,19 @@ public class IeChatServiceImpl implements IeChatService { return; } if (!redisService.allowMediaMessage(senderId, roomId)) { - throw new RuntimeException("消息太火爆了,稍后再试"); + throw new RuntimeException("消息太火爆了,请1分钟后再试"); } Long userPending = roomMessageMapper.selectCount(new LambdaQueryWrapper() .eq(IeRoomMessage::getSenderId, senderId) .eq(IeRoomMessage::getMediaAuditStatus, IeConstants.MEDIA_AUDIT_PENDING)); if (userPending != null && userPending >= USER_PENDING_MEDIA_LIMIT) { - throw new RuntimeException("还有媒体正在处理,稍后再试"); + throw new RuntimeException("还有媒体正在处理,请1分钟后再试"); } Long roomPending = roomMessageMapper.selectCount(new LambdaQueryWrapper() .eq(IeRoomMessage::getRoomId, roomId) .eq(IeRoomMessage::getMediaAuditStatus, IeConstants.MEDIA_AUDIT_PENDING)); if (roomPending != null && roomPending >= ROOM_PENDING_MEDIA_LIMIT) { - throw new RuntimeException("当前聊天太火爆了,稍后再试"); + throw new RuntimeException("当前聊天太火爆了,请1分钟后再试"); } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeDailyQuestionServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeDailyQuestionServiceImpl.java index 3948b892..66096b8d 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeDailyQuestionServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeDailyQuestionServiceImpl.java @@ -8,6 +8,7 @@ import cc.hiver.mall.ie.mapper.IeDailyQuestionMapper; import cc.hiver.mall.ie.mapper.IeUserProfileMapper; import cc.hiver.mall.ie.service.IeDailyQuestionService; import cc.hiver.mall.ie.service.IeMatchService; +import cc.hiver.mall.ie.service.IeRealNameAuthService; import cc.hiver.mall.ie.service.IeSecurityAuditService; import cc.hiver.mall.ie.vo.IeAuditResult; import cc.hiver.mall.ie.vo.IeMatchVO; @@ -47,6 +48,9 @@ public class IeDailyQuestionServiceImpl implements IeDailyQuestionService { @Autowired private IeMatchService matchService; + @Autowired + private IeRealNameAuthService realNameAuthService; + @Override public Map todayQuestion(Long userId) { IeDailyQuestion question = resolveTodayQuestion(); @@ -76,6 +80,8 @@ public class IeDailyQuestionServiceImpl implements IeDailyQuestionService { @Override @Transactional public Map submitAnswer(Long userId, String content) { + // 同频一题属于 i/e 互动能力,提交前按后台审核开关校验认证状态。 + realNameAuthService.assertAdultVerified(userId); if (CharSequenceUtil.isBlank(content)) { throw new RuntimeException("先写点什么再提交吧"); } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java index 8ca506d7..325715ab 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java @@ -6,10 +6,7 @@ import cc.hiver.mall.ie.dto.IeProfileDTO; import cc.hiver.mall.ie.dto.IeStatusDTO; import cc.hiver.mall.ie.entity.*; import cc.hiver.mall.ie.mapper.*; -import cc.hiver.mall.ie.service.IeAliyunModerationService; -import cc.hiver.mall.ie.service.IeMatchService; -import cc.hiver.mall.ie.service.IeRedisService; -import cc.hiver.mall.ie.service.IeSecurityAuditService; +import cc.hiver.mall.ie.service.*; import cc.hiver.mall.ie.vo.*; import cc.hiver.mall.service.HomeReminderService; import cn.hutool.core.util.IdUtil; @@ -61,26 +58,34 @@ public class IeMatchServiceImpl implements IeMatchService { @Autowired private HomeReminderService homeReminderService; + @Autowired + private IeRealNameAuthService realNameAuthService; + @Override public IeHomeVO home(Long userId) { - IeUserProfile profile = ensureProfile(userId); - assertIeAvailable(profile); + IeUserProfile profile = findProfileByUserId(userId); + if (profile != null) { + assertIeAvailable(profile); + } IeUserStatus status = findStatusByUserId(userId); redisService.refreshOnline(userId); redisService.recordIeOpen(userId); String currentMood = status == null || status.getMood() == null ? "quiet" : status.getMood(); + String currentMode = profile == null || profile.getCurrentMode() == null ? "i" : profile.getCurrentMode(); IeHomeVO vo = new IeHomeVO(); vo.setOnlineCount(redisService.onlineCount()); - vo.setWaitingCount(redisService.waitingCount(profile.getCurrentMode(), currentMood, profile.getRegionId(), hasText(profile.getRegionId()))); + vo.setWaitingCount(redisService.waitingCount(currentMode, currentMood, + profile == null ? null : profile.getRegionId(), + profile != null && hasText(profile.getRegionId()))); vo.setDailyQuota(effectiveDailyQuota(profile)); vo.setUsedQuota((int) redisService.todayUsedQuota(userId)); vo.setUnreadCount(cachedUnreadCount(userId)); - vo.setCurrentMode(profile.getCurrentMode()); + vo.setCurrentMode(currentMode); vo.setCurrentMood(currentMood); - vo.setTargetModePreference(profile.getTargetModePreference()); - vo.setTargetGenderPreference(profile.getTargetGenderPreference()); - vo.setProfileCompleted(profile.getProfileCompleted() == null ? 0 : profile.getProfileCompleted()); - vo.setProfile(toProfileVO(profile)); + vo.setTargetModePreference(profile == null || profile.getTargetModePreference() == null ? "any" : profile.getTargetModePreference()); + vo.setTargetGenderPreference(profile == null || profile.getTargetGenderPreference() == null ? "any" : profile.getTargetGenderPreference()); + vo.setProfileCompleted(profile == null || profile.getProfileCompleted() == null ? 0 : profile.getProfileCompleted()); + vo.setProfile(profile == null ? emptyProfileVO(userId) : toProfileVO(profile)); vo.setHotStatuses(redisService.hotStatuses(8)); vo.setCompanionIntents(latestCompanionIntents(userId, 50)); return vo; @@ -293,6 +298,8 @@ public class IeMatchServiceImpl implements IeMatchService { @Override @Transactional public IeMatchVO startMatch(Long userId, IeMatchStartDTO dto) { + // 服务端兜底:匹配动作按后台审核开关校验认证状态,防止绕过前端直接调接口。 + realNameAuthService.assertAdultVerified(userId); IeUserProfile profile = ensureProfile(userId); // 服务端兜底校验,防止绕过前端直接调接口 if (profile.getProfileCompleted() == null || profile.getProfileCompleted() != 1) { @@ -452,6 +459,8 @@ public class IeMatchServiceImpl implements IeMatchService { if (targetUserId == null || targetUserId.equals(userId)) { throw new RuntimeException("不能和自己建立陪伴关系"); } + // 定向陪伴同样属于匹配行为,也需要按后台审核开关校验认证状态。 + realNameAuthService.assertAdultVerified(userId); IeUserProfile profile = ensureProfile(userId); if (profile.getProfileCompleted() == null || profile.getProfileCompleted() != 1) { throw new RuntimeException("请先完善资料后再发起陪伴"); @@ -780,6 +789,28 @@ public class IeMatchServiceImpl implements IeMatchService { .last("limit 1")); } + private IeUserProfileVO emptyProfileVO(Long userId) { + IeUserProfileVO vo = new IeUserProfileVO(); + vo.setUserId(userId); + vo.setAnonymousName("半匿名漂流者"); + vo.setAvatarText("我"); + vo.setGender("unknown"); + vo.setInterestTags(new ArrayList<>()); + vo.setPersonaImages(new ArrayList<>()); + vo.setCurrentMode("i"); + vo.setRecentPreference("i"); + vo.setTargetModePreference("any"); + vo.setTargetGenderPreference("any"); + vo.setDefaultRoomMinutes(IeConstants.DEFAULT_ROOM_MINUTES); + vo.setDailyQuota(IeConstants.DEFAULT_DAILY_QUOTA); + vo.setUsedQuota(0); + vo.setProfileCompleted(0); + vo.setRealNameVerified(false); + vo.setStudentCardVerified(false); + vo.setExists(false); + return vo; + } + private IeUserStatus findStatusByUserId(Long userId) { if (userId == null) { return null; @@ -1104,6 +1135,11 @@ public class IeMatchServiceImpl implements IeMatchService { vo.setDailyQuota(effectiveDailyQuota(profile)); vo.setUsedQuota(profile.getUsedQuota()); vo.setProfileCompleted(profile.getProfileCompleted() == null ? 0 : profile.getProfileCompleted()); + vo.setRealNameVerified(Integer.valueOf(1).equals(profile.getRealNameAuthStatus())); + vo.setStudentCardVerified(Integer.valueOf(1).equals(profile.getStudentCardAuthStatus())); + vo.setRealNameMasked(profile.getRealNameMasked()); + vo.setIdCardMasked(profile.getIdCardMasked()); + vo.setAge(profile.getAge()); vo.setExists(true); return vo; } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMomentServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMomentServiceImpl.java index 43879ed6..47f85980 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMomentServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMomentServiceImpl.java @@ -10,6 +10,7 @@ import cc.hiver.mall.ie.mapper.IeUserProfileMapper; import cc.hiver.mall.ie.mq.IeChatEventProducer; import cc.hiver.mall.ie.service.IeAliyunModerationService; import cc.hiver.mall.ie.service.IeMomentService; +import cc.hiver.mall.ie.service.IeRealNameAuthService; import cc.hiver.mall.ie.service.IeSecurityAuditService; import cc.hiver.mall.ie.vo.IeAliyunModerationResult; import cc.hiver.mall.ie.vo.IeAuditResult; @@ -58,9 +59,14 @@ public class IeMomentServiceImpl implements IeMomentService { @Autowired private IeChatEventProducer chatEventProducer; + @Autowired + private IeRealNameAuthService realNameAuthService; + @Override @Transactional public IeMoment publish(Long userId, IeMomentDTO dto) { + // 发布动态会进入个人空间信息流,按后台审核开关校验身份证三要素/学生证认证。 + realNameAuthService.assertAdultVerified(userId); if (dto == null) { throw new RuntimeException("发布内容不能为空"); } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRealNameAuthServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRealNameAuthServiceImpl.java new file mode 100644 index 00000000..d81bd8ea --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRealNameAuthServiceImpl.java @@ -0,0 +1,566 @@ +package cc.hiver.mall.ie.service.impl; + +import cc.hiver.core.common.redis.RedisTemplateHelper; +import cc.hiver.mall.ie.constant.IeConstants; +import cc.hiver.mall.ie.entity.IeUserProfile; +import cc.hiver.mall.ie.exception.IeBusinessException; +import cc.hiver.mall.ie.exception.UnderageUserException; +import cc.hiver.mall.ie.mapper.IeUserProfileMapper; +import cc.hiver.mall.ie.service.IeRealNameAuthService; +import cc.hiver.mall.ie.service.IeStudentCardOcrService; +import cc.hiver.mall.ie.vo.IeRealNameAuthVO; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.time.LocalDate; +import java.time.Period; +import java.time.format.DateTimeFormatter; +import java.util.*; + +@Service +public class IeRealNameAuthServiceImpl implements IeRealNameAuthService { + + /** + * 未认证。 + */ + private static final int AUTH_STATUS_NONE = 0; + + /** + * 已认证且年龄满足 18 岁。 + */ + private static final int AUTH_STATUS_VERIFIED = 1; + + /** + * 已识别为未成年人。 + */ + private static final int AUTH_STATUS_UNDERAGE = 2; + + /** + * 学生证未上传。 + */ + private static final int STUDENT_CARD_STATUS_NONE = 0; + + /** + * 学生证已通过。 + */ + private static final int STUDENT_CARD_STATUS_VERIFIED = 1; + + /** + * 首次模型识别不通过,需要重传。 + */ + private static final int STUDENT_CARD_STATUS_RETRY_REQUIRED = 2; + + private static final MediaType FORM = MediaType.parse("application/x-www-form-urlencoded; charset=utf-8"); + private static final DateTimeFormatter BIRTHDAY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final int MAX_CERT_PICTURE_BASE64_BYTES = 50 * 1024; + private static final String REAL_NAME_AUDIT_ENABLED_KEY = "IE:REVIEW:REAL_NAME_AUDIT_ENABLED"; + + private final OkHttpClient httpClient = new OkHttpClient(); + private final SecureRandom secureRandom = new SecureRandom(); + + @Autowired + private IeUserProfileMapper userProfileMapper; + + @Autowired + private IeStudentCardOcrService studentCardOcrService; + + @Autowired + private RedisTemplateHelper redisTemplateHelper; + + /** + * 阿里云身份证三要素核验 AccessKeyId。 + * 建议生产环境使用 RAM 子账号,并仅授予 dytns:CertNoThreeElementVerification 权限。 + */ + @Value("${ie.real-name.aliyun.access-key-id:${ie.aliyun.green.access-key-id:}}") + private String aliyunAccessKeyId; + + /** + * 阿里云身份证三要素核验 AccessKeySecret。 + */ + @Value("${ie.real-name.aliyun.access-key-secret:${ie.aliyun.green.access-key-secret:}}") + private String aliyunAccessKeySecret; + + /** + * 号码百科控制台申请通过后获得的身份证三要素授权码。 + */ + @Value("${ie.real-name.aliyun.auth-code:0H1D5kWc57}") + private String aliyunAuthCode; + + @Value("${ie.real-name.aliyun.endpoint:https://dytnsapi.aliyuncs.com/}") + private String aliyunEndpoint; + + /** + * AES-GCM 加密密钥,支持 Base64 或 16/24/32 字节明文。 + * 生产环境必须放到配置中心/环境变量,不能提交到代码仓库。 + */ + @Value("${ie.real-name.aes-key:}") + private String aesKey; + + @Override + @Transactional + public IeRealNameAuthVO verifyByAliyunThreeElement(Long userId, String certName, String certNo, String certPicture) { + if (userId == null) { + throw new IeBusinessException("登录状态已失效,请重新登录"); + } + IeUserProfile profile = findProfileByUserId(userId); + if (profile != null && Integer.valueOf(AUTH_STATUS_VERIFIED).equals(profile.getRealNameAuthStatus())) { + return buildAuthVO(profile); + } + + String normalizedName = normalizeRequired(certName, "请填写真实姓名"); + String normalizedCertNo = normalizeRequired(certNo, "请填写身份证号").toUpperCase(); + String normalizedPicture = normalizeCertPicture(certPicture); + LocalDate birthday = parseBirthday(normalizedCertNo); + int age = Period.between(birthday, LocalDate.now()).getYears(); + if (age < 18) { + profile = ensureProfile(userId); + profile.setRealNameAuthStatus(AUTH_STATUS_UNDERAGE); + profile.setRealNameMasked(maskName(normalizedName)); + profile.setIdCardMasked(maskIdCard(normalizedCertNo)); + profile.setBirthDate(birthday.format(BIRTHDAY_FORMATTER)); + profile.setAge(age); + profile.setUpdateTime(new Date()); + userProfileMapper.updateById(profile); + throw new UnderageUserException("根据身份证号判断,你暂未满 18 周岁,不能使用 i/e 互动功能"); + } + + // 阿里云返回 Data.IsConsistent = "1" 才代表姓名、身份证号、人像照片为同一人。 + AliyunThreeElementResult verifyResult = callAliyunThreeElement(normalizedName, normalizedCertNo, normalizedPicture); + if (!"1".equals(verifyResult.getIsConsistent())) { + throw new IeBusinessException(aliyunResultMessage(verifyResult.getIsConsistent())); + } + + profile = ensureProfile(userId); + Date now = new Date(); + profile.setRealNameAuthStatus(AUTH_STATUS_VERIFIED); + profile.setRealNameEncrypted(encrypt(normalizedName)); + profile.setIdCardEncrypted(encrypt(normalizedCertNo)); + profile.setRealNameMasked(maskName(normalizedName)); + profile.setIdCardMasked(maskIdCard(normalizedCertNo)); + profile.setBirthDate(birthday.format(BIRTHDAY_FORMATTER)); + profile.setAge(age); + profile.setRealNameAuthTime(now); + // 实名只负责合规校验和创建 i/e 档案;资料完善仍交给前端原 profileSetup 流程。 + profile.setUpdateTime(now); + userProfileMapper.updateById(profile); + return buildAuthVO(profile); + } + + @Override + @Transactional + public IeRealNameAuthVO verifyStudentCard(Long userId, String imageUrl) { + if (userId == null) { + throw new IeBusinessException("登录状态已失效,请重新登录"); + } + if (imageUrl == null || imageUrl.trim().isEmpty()) { + throw new IeBusinessException("请先上传学生证照片"); + } + IeUserProfile profile = ensureProfile(userId); + if (Integer.valueOf(STUDENT_CARD_STATUS_VERIFIED).equals(profile.getStudentCardAuthStatus())) { + return buildAuthVO(profile); + } + + int currentCount = profile.getStudentCardVerifyCount() == null ? 0 : profile.getStudentCardVerifyCount(); + int nextCount = currentCount + 1; + Date now = new Date(); + profile.setStudentCardImageUrl(imageUrl.trim()); + profile.setStudentCardVerifyCount(nextCount); + profile.setStudentCardAuthTime(now); + profile.setUpdateTime(now); + + // 第一次如果模型判断不是学生证,要求用户重新上传;第二次上传直接放行,避免模型误判阻碍流程。 + if (nextCount >= 2) { + profile.setStudentCardAuthStatus(STUDENT_CARD_STATUS_VERIFIED); + userProfileMapper.updateById(profile); + IeRealNameAuthVO vo = buildAuthVO(profile); + vo.setMessage("学生证认证已通过"); + return vo; + } + + boolean isStudentCard = studentCardOcrService.isStudentCard(imageUrl.trim()); + if (isStudentCard) { + profile.setStudentCardAuthStatus(STUDENT_CARD_STATUS_VERIFIED); + userProfileMapper.updateById(profile); + IeRealNameAuthVO vo = buildAuthVO(profile); + vo.setMessage("学生证认证已通过"); + return vo; + } + + profile.setStudentCardAuthStatus(STUDENT_CARD_STATUS_RETRY_REQUIRED); + userProfileMapper.updateById(profile); + IeRealNameAuthVO vo = buildAuthVO(profile); + vo.setStudentCardNeedRetry(true); + vo.setMessage("未识别为大学生学生证,请重新上传学生证照片"); + return vo; + } + + @Override + public void assertAdultVerified(Long userId) { + IeUserProfile profile = findProfileByUserId(userId); + if (isRealNameAuditEnabled()) { + if (profile == null || profile.getRealNameAuthStatus() == null + || profile.getRealNameAuthStatus() == AUTH_STATUS_NONE) { + throw new IeBusinessException("请先完成进阶实名认证后再继续"); + } + if (profile.getRealNameAuthStatus() == AUTH_STATUS_UNDERAGE) { + throw new UnderageUserException("根据实名信息判断,你暂未满 18 周岁,不能使用 i/e 互动功能"); + } + if (profile.getRealNameAuthStatus() != AUTH_STATUS_VERIFIED) { + throw new IeBusinessException("实名认证未通过,请重新认证后再继续"); + } + } + // 学生证认证流程临时注释保留,当前阶段不作为匹配/同频一题/发布动态的强制门槛。 + // if (profile == null || !Integer.valueOf(STUDENT_CARD_STATUS_VERIFIED).equals(profile.getStudentCardAuthStatus())) { + // throw new IeBusinessException("请先上传学生证照片完成学生身份认证后再继续"); + // } + } + + @Override + public boolean isRealNameAuditEnabled() { + String value = redisTemplateHelper == null ? null : redisTemplateHelper.get(REAL_NAME_AUDIT_ENABLED_KEY); + // 默认开启,满足上架审核;后台关闭后跳过实名门槛。 + return value == null || value.trim().isEmpty() || "1".equals(value) || "true".equalsIgnoreCase(value); + } + + @Override + public void setRealNameAuditEnabled(Boolean enabled) { + redisTemplateHelper.set(REAL_NAME_AUDIT_ENABLED_KEY, Boolean.FALSE.equals(enabled) ? "0" : "1"); + } + + private AliyunThreeElementResult callAliyunThreeElement(String certName, String certNo, String certPicture) { + if (isBlank(aliyunAccessKeyId) || isBlank(aliyunAccessKeySecret) || isBlank(aliyunAuthCode)) { + throw new IeBusinessException("未配置阿里云身份证三要素核验参数"); + } + try { + Map params = new HashMap<>(); + params.put("Action", "CertNoThreeElementVerification"); + params.put("Version", "2020-02-17"); + params.put("Format", "JSON"); + params.put("SignatureMethod", "HMAC-SHA1"); + params.put("SignatureVersion", "1.0"); + params.put("SignatureNonce", UUID.randomUUID().toString()); + params.put("Timestamp", java.time.Instant.now().toString()); + params.put("AccessKeyId", aliyunAccessKeyId); + params.put("AuthCode", aliyunAuthCode); + params.put("CertName", certName); + params.put("CertNo", certNo); + params.put("CertPicture", certPicture); + // 号码百科接口 Mask 是固定枚举值,目前不加密只能传 NORMAL,不能传中文“不加密”。 + params.put("Mask", "NORMAL"); + params.put("Signature", signAliyunRpc(params)); + + String formBody = toFormBody(params); + Request request = new Request.Builder() + .url(aliyunEndpoint) + .post(okhttp3.RequestBody.create(FORM, formBody)) + .addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") + .addHeader("Accept", "application/json") + .build(); + try (Response response = httpClient.newCall(request).execute()) { + String body = response.body() == null ? "" : response.body().string(); + JSONObject json = JSONUtil.parseObj(body); + if (!response.isSuccessful() || !"OK".equalsIgnoreCase(json.getStr("Code"))) { + String code = json.getStr("Code", String.valueOf(response.code())); + String requestId = json.getStr("RequestId", ""); + String message = json.getStr("Message", "请稍后重试"); + throw new IeBusinessException("阿里云实名认证失败:" + message + ",错误码:" + code + ",RequestId:" + requestId); + } + JSONObject data = json.getJSONObject("Data"); + AliyunThreeElementResult result = new AliyunThreeElementResult(); + result.setIsConsistent(data == null ? null : data.getStr("IsConsistent")); + result.setRequestId(json.getStr("RequestId")); + return result; + } + } catch (IeBusinessException e) { + throw e; + } catch (Exception e) { + throw new IeBusinessException("调用阿里云身份证三要素核验接口失败", e); + } + } + + private String signAliyunRpc(Map params) throws Exception { + List keys = new ArrayList<>(params.keySet()); + Collections.sort(keys); + StringBuilder canonicalizedQueryString = new StringBuilder(); + for (String key : keys) { + if ("Signature".equals(key)) { + continue; + } + if (canonicalizedQueryString.length() > 0) { + canonicalizedQueryString.append('&'); + } + canonicalizedQueryString.append(percentEncode(key)).append('=').append(percentEncode(params.get(key))); + } + String stringToSign = "POST&%2F&" + percentEncode(canonicalizedQueryString.toString()); + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(new SecretKeySpec((aliyunAccessKeySecret + "&").getBytes(StandardCharsets.UTF_8), "HmacSHA1")); + return Base64.getEncoder().encodeToString(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8))); + } + + private String toFormBody(Map params) throws Exception { + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : params.entrySet()) { + if (builder.length() > 0) { + builder.append('&'); + } + builder.append(percentEncode(entry.getKey())).append('=').append(percentEncode(entry.getValue())); + } + return builder.toString(); + } + + private String percentEncode(String value) throws Exception { + return URLEncoder.encode(value == null ? "" : value, "UTF-8") + .replace("+", "%20") + .replace("*", "%2A") + .replace("%7E", "~"); + } + + private String normalizeCertPicture(String certPicture) { + String value = normalizeRequired(certPicture, "请上传本人清晰人像照片"); + int commaIndex = value.indexOf(','); + if (value.startsWith("data:image") && commaIndex >= 0) { + value = value.substring(commaIndex + 1); + } + value = value.replaceAll("\\s+", ""); + if (value.getBytes(StandardCharsets.UTF_8).length > MAX_CERT_PICTURE_BASE64_BYTES) { + throw new IeBusinessException("人像照片Base64不得超过50KB,请重新拍摄或压缩后再试"); + } + try { + byte[] decoded = Base64.getDecoder().decode(value); + if (decoded.length > 50 * 1024) { + throw new IeBusinessException("人像照片文件不得超过50KB,请重新拍摄或压缩后再试"); + } + } catch (IllegalArgumentException e) { + throw new IeBusinessException("人像照片Base64格式不正确"); + } + return value; + } + + private String normalizeRequired(String value, String message) { + if (isBlank(value)) { + throw new IeBusinessException(message); + } + return value.trim(); + } + + private String aliyunResultMessage(String isConsistent) { + if ("0".equals(isConsistent)) { + return "姓名与身份证号匹配,但照片识别为非同一人,请重新上传本人清晰照片"; + } + if ("2".equals(isConsistent)) { + return "照片疑似为本人但未达到通过标准,请重新上传清晰正脸照片"; + } + if ("3".equals(isConsistent)) { + return "姓名与身份证号匹配,但库中无人像信息,无法完成三要素核验"; + } + if ("4".equals(isConsistent)) { + return "姓名与身份证号不一致,请核对后重新提交"; + } + if ("5".equals(isConsistent)) { + return "照片质量不合格,请重新上传清晰正脸照片"; + } + return "实名认证未通过,请核对信息后重试"; + } + + private LocalDate parseBirthday(String idCardNo) { + String id = idCardNo == null ? "" : idCardNo.trim().toUpperCase(); + if (!id.matches("^\\d{17}[0-9X]$")) { + throw new IeBusinessException("实名身份证号格式不正确"); + } + if (!validateChineseIdCardChecksum(id)) { + throw new IeBusinessException("实名身份证号校验位不正确"); + } + String birthday = id.substring(6, 14); + try { + return LocalDate.parse(birthday, DateTimeFormatter.ofPattern("yyyyMMdd")); + } catch (Exception e) { + throw new IeBusinessException("实名身份证号出生日期不正确"); + } + } + + private boolean validateChineseIdCardChecksum(String id) { + int[] weights = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}; + char[] checks = {'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'}; + int sum = 0; + for (int i = 0; i < 17; i++) { + sum += (id.charAt(i) - '0') * weights[i]; + } + return checks[sum % 11] == id.charAt(17); + } + + private String encrypt(String plainText) { + try { + byte[] key = resolveAesKey(); + byte[] iv = new byte[12]; + secureRandom.nextBytes(iv); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv)); + byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(iv) + ":" + Base64.getEncoder().encodeToString(encrypted); + } catch (IeBusinessException e) { + throw e; + } catch (Exception e) { + throw new IeBusinessException("实名信息加密失败", e); + } + } + + private byte[] resolveAesKey() { + if (aesKey == null || aesKey.trim().isEmpty()) { + if (isBlank(aliyunAccessKeySecret)) { + throw new IeBusinessException("未配置实名信息加密密钥"); + } + // 兼容线上未单独配置 ie.real-name.aes-key 的情况:使用已有阿里云密钥派生稳定的 32 字节 AES key。 + // 生产环境建议后续补充专用配置 ie.real-name.aes-key,避免不同用途共用同一份密钥材料。 + return sha256(aliyunAccessKeySecret.trim()); + } + byte[] key; + try { + key = Base64.getDecoder().decode(aesKey.trim()); + } catch (IllegalArgumentException ignored) { + key = aesKey.trim().getBytes(StandardCharsets.UTF_8); + } + if (key.length != 16 && key.length != 24 && key.length != 32) { + throw new IeBusinessException("实名信息加密密钥长度必须为 16/24/32 字节"); + } + return key; + } + + private byte[] sha256(String value) { + try { + return MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new IeBusinessException("实名信息加密密钥派生失败", e); + } + } + + private IeUserProfile ensureProfile(Long userId) { + IeUserProfile profile = findProfileByUserId(userId); + if (profile != null) { + return profile; + } + Date now = new Date(); + profile = new IeUserProfile(); + profile.setUserId(userId); + profile.setAnonymousName("半匿名漂流者"); + profile.setAvatarText("夜"); + profile.setCurrentMode("i"); + profile.setRecentPreference("i"); + profile.setTargetModePreference("any"); + profile.setGender("unknown"); + profile.setTargetGenderPreference("any"); + profile.setDefaultRoomMinutes(IeConstants.DEFAULT_ROOM_MINUTES); + profile.setDailyQuota(IeConstants.DEFAULT_DAILY_QUOTA); + profile.setUsedQuota(0); + profile.setProfileCompleted(0); + profile.setRealNameAuthStatus(AUTH_STATUS_NONE); + profile.setStudentCardAuthStatus(STUDENT_CARD_STATUS_NONE); + profile.setStudentCardVerifyCount(0); + profile.setIsDeleted(0); + profile.setCreateTime(now); + profile.setUpdateTime(now); + try { + userProfileMapper.insert(profile); + return profile; + } catch (DuplicateKeyException ignored) { + IeUserProfile exists = findProfileByUserId(userId); + if (exists != null) { + return exists; + } + throw ignored; + } + } + + private IeUserProfile findProfileByUserId(Long userId) { + if (userId == null) { + return null; + } + return userProfileMapper.selectOne(new LambdaQueryWrapper() + .eq(IeUserProfile::getUserId, userId) + .last("limit 1")); + } + + private IeRealNameAuthVO buildAuthVO(IeUserProfile profile) { + IeRealNameAuthVO vo = new IeRealNameAuthVO(); + boolean realNameVerified = profile != null && Integer.valueOf(AUTH_STATUS_VERIFIED).equals(profile.getRealNameAuthStatus()); + boolean studentCardVerified = profile != null && Integer.valueOf(STUDENT_CARD_STATUS_VERIFIED).equals(profile.getStudentCardAuthStatus()); + vo.setRealNameVerified(realNameVerified); + vo.setStudentCardVerified(studentCardVerified); + vo.setStudentCardNeedRetry(profile != null && Integer.valueOf(STUDENT_CARD_STATUS_RETRY_REQUIRED).equals(profile.getStudentCardAuthStatus())); + // 学生证认证流程临时注释保留,认证完成状态当前只看后台实名审核开关和实名状态。 + // vo.setVerified(isRealNameAuditEnabled() ? realNameVerified && studentCardVerified : studentCardVerified); + vo.setVerified(isRealNameAuditEnabled() ? realNameVerified : true); + vo.setRealNameMasked(profile == null ? null : profile.getRealNameMasked()); + vo.setIdCardMasked(profile == null ? null : profile.getIdCardMasked()); + vo.setAge(profile == null ? null : profile.getAge()); + return vo; + } + + private String maskName(String name) { + if (name == null || name.trim().isEmpty()) { + return ""; + } + String value = name.trim(); + if (value.length() == 1) { + return "*"; + } + return value.charAt(0) + repeat("*", value.length() - 1); + } + + private String maskIdCard(String idCardNo) { + if (idCardNo == null || idCardNo.length() < 8) { + return "********"; + } + return idCardNo.substring(0, 3) + "***********" + idCardNo.substring(idCardNo.length() - 4); + } + + private String repeat(String value, int count) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < count; i++) { + builder.append(value); + } + return builder.toString(); + } + + private boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } + + private static class AliyunThreeElementResult { + private String isConsistent; + private String requestId; + + public String getIsConsistent() { + return isConsistent; + } + + public void setIsConsistent(String isConsistent) { + this.isConsistent = isConsistent; + } + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeStudentCardOcrServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeStudentCardOcrServiceImpl.java new file mode 100644 index 00000000..38b1953e --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeStudentCardOcrServiceImpl.java @@ -0,0 +1,58 @@ +package cc.hiver.mall.ie.service.impl; + +import cc.hiver.mall.config.aliocr.AliOcrConfig; +import cc.hiver.mall.ie.service.IeStudentCardOcrService; +import cc.hiver.mall.utils.AliOcrUtil; +import com.alibaba.dashscope.aigc.multimodalconversation.*; +import com.alibaba.dashscope.common.Role; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.Collections; + +@Slf4j +@Service +public class IeStudentCardOcrServiceImpl implements IeStudentCardOcrService { + + private static final String STUDENT_CARD_PROMPT = "这是一张大学生的学生证吗?是返回true,不是返回false,禁止返回任何其他内容!"; + + @Autowired + private AliOcrConfig aliOcrConfig; + + @Override + public boolean isStudentCard(String imageUrl) { + if (imageUrl == null || imageUrl.trim().isEmpty()) { + return false; + } + try { + // 复用 AliOcrUtil 中通义千问图像识别的 API Key 初始化,避免线上未配置 aliyun.openapi.api-key 时无法调用。 + AliOcrUtil.prepareDashScopeApiKey(aliOcrConfig == null ? null : aliOcrConfig.getApiKey()); + MultiModalConversation conv = new MultiModalConversation(); + MultiModalConversationMessage systemMessage = MultiModalConversationMessage.builder() + .role(Role.SYSTEM.getValue()) + .content(Collections.singletonList(new MultiModalMessageItemText("You are a helpful assistant."))) + .build(); + MultiModalConversationMessage userMessage = MultiModalConversationMessage.builder() + .role(Role.USER.getValue()) + .content(Arrays.asList( + new MultiModalMessageItemImage(imageUrl.trim()), + new MultiModalMessageItemText(STUDENT_CARD_PROMPT))) + .build(); + MultiModalConversationParam param = MultiModalConversationParam.builder() + .model("qwen3.6-flash") + .messages(Arrays.asList(systemMessage, userMessage)) + .build(); + MultiModalConversationResult result = conv.call(param); + String text = result.getOutput().getChoices().get(0).getMessage().getContent().get(0).get("text").toString(); + String normalized = text == null ? "" : text.trim().toLowerCase(); + log.info("i/e学生证识别结果 imageUrl={}, result={}", imageUrl, normalized); + return "true".equals(normalized); + } catch (Exception e) { + // 图片识别属于辅助风控,识别异常按 false 处理,让用户按"首次失败重传、第二次放行"流程继续。 + log.warn("i/e学生证识别异常 imageUrl={}", imageUrl, e); + return false; + } + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeRealNameAuthVO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeRealNameAuthVO.java new file mode 100644 index 00000000..cc32f74b --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeRealNameAuthVO.java @@ -0,0 +1,47 @@ +package cc.hiver.mall.ie.vo; + +import lombok.Data; + +@Data +public class IeRealNameAuthVO { + + /** + * 是否已经完成整套进阶认证:微信成年实名 + 学生证认证。 + */ + private Boolean verified; + + /** + * 是否已经通过微信成年实名校验。 + */ + private Boolean realNameVerified; + + /** + * 是否已经通过学生证认证。 + */ + private Boolean studentCardVerified; + + /** + * 学生证第一次识别未通过时返回 true,提示用户重新上传。 + */ + private Boolean studentCardNeedRetry; + + /** + * 面向前端的认证提示文案。 + */ + private String message; + + /** + * 脱敏姓名,例如:张*。 + */ + private String realNameMasked; + + /** + * 脱敏身份证号,例如:110***********1234。 + */ + private String idCardMasked; + + /** + * 根据身份证号计算出的年龄。 + */ + private Integer age; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeUserProfileVO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeUserProfileVO.java index 671bac89..bc610b3a 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeUserProfileVO.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeUserProfileVO.java @@ -25,5 +25,25 @@ public class IeUserProfileVO { private Integer dailyQuota; private Integer usedQuota; private Integer profileCompleted; + /** + * 是否已经通过成年实名校验。 + */ + private Boolean realNameVerified; + /** + * 是否已经通过学生证认证。 + */ + private Boolean studentCardVerified; + /** + * 脱敏姓名,仅用于让用户确认认证状态。 + */ + private String realNameMasked; + /** + * 脱敏身份证号,仅用于让用户确认认证状态。 + */ + private String idCardMasked; + /** + * 已认证用户的年龄,未认证时为空。 + */ + private Integer age; private Boolean exists; } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncProducer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncProducer.java index d5e9f147..6ee09e2b 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncProducer.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncProducer.java @@ -2,15 +2,15 @@ package cc.hiver.mall.mq; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; -import org.springframework.amqp.AmqpException; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * 订单相关的MQ异步消息发送者 @@ -22,6 +22,11 @@ public class OrderAsyncProducer { @Autowired private RabbitTemplate rabbitTemplate; + @Autowired + private AmqpAdmin amqpAdmin; + + private final Set declaredDelayMillis = new HashSet<>(); + /** * 发送微信模板推送请求 (包含参数,接收人标识等) */ @@ -61,12 +66,8 @@ public class OrderAsyncProducer { msgBody.put("timestamp", System.currentTimeMillis()); final long delayMillis = 60 * 60 * 1000L; // 1小时 - rabbitTemplate.convertAndSend(OrderQueueConfig.DELAY_EXCHANGE, OrderQueueConfig.DELAY_ROUTING, - JSON.toJSONString(msgBody), - message -> { - message.getMessageProperties().setExpiration(String.valueOf(delayMillis)); - return message; - }); + String routingKey = ensureDelayRoute(delayMillis); + rabbitTemplate.convertAndSend(OrderQueueConfig.DELAY_EXCHANGE, routingKey, JSON.toJSONString(msgBody)); log.info("【退款MQ】已发送退款超时延时消息,1小时后自动处理,refundId={}, orderId={}", refundId, orderId); } @@ -82,16 +83,32 @@ public class OrderAsyncProducer { msgBody.put("delayType", delayType); msgBody.put("timestamp", System.currentTimeMillis()); - rabbitTemplate.convertAndSend(OrderQueueConfig.DELAY_EXCHANGE, OrderQueueConfig.DELAY_ROUTING, JSON.toJSONString(msgBody), - new MessagePostProcessor() { - @Override - public Message postProcessMessage(Message message) throws AmqpException { - // 设置每条消息的 TTL (存活时间) - message.getMessageProperties().setExpiration(String.valueOf(delayMillis)); - return message; - } - }); + String routingKey = ensureDelayRoute(delayMillis); + rabbitTemplate.convertAndSend(OrderQueueConfig.DELAY_EXCHANGE, routingKey, JSON.toJSONString(msgBody)); log.info("【订单MQ】已发送一条{}延时校验消息,设定在{}ms后触发,orderId={}", delayType, delayMillis, orderId); } + + private synchronized String ensureDelayRoute(long delayMillis) { + String routingKey = OrderQueueConfig.DELAY_ROUTING + "." + delayMillis; + if (declaredDelayMillis.contains(delayMillis)) { + return routingKey; + } + + Map args = new HashMap<>(3); + args.put("x-message-ttl", delayMillis); + args.put("x-dead-letter-exchange", OrderQueueConfig.DEAD_EXCHANGE); + args.put("x-dead-letter-routing-key", OrderQueueConfig.DEAD_ROUTING); + + String queueName = OrderQueueConfig.DELAY_QUEUE + "." + delayMillis; + Queue queue = new Queue(queueName, true, false, false, args); + amqpAdmin.declareQueue(queue); + Binding binding = BindingBuilder.bind(queue) + .to(new DirectExchange(OrderQueueConfig.DELAY_EXCHANGE, true, false)) + .with(routingKey); + amqpAdmin.declareBinding(binding); + declaredDelayMillis.add(delayMillis); + log.info("【订单MQ】已声明独立延时队列 queue={}, routingKey={}, ttl={}ms", queueName, routingKey, delayMillis); + return routingKey; + } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java index 0d63ea99..61a1a084 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java @@ -73,6 +73,12 @@ public class OrderDelayConsumer { case "Shop_Cook_Timeout": handleShopCookTimeout(orderId); break; + case "Transfer_Arrive_Timeout": + handleTransferArriveTimeout(orderId); + break; + case "Transfer_Pickup_Timeout": + handleTransferPickupTimeout(orderId); + break; case "Refund_Auto_Agree_1h": String refundId = msg.getString("refundId"); handleRefundAutoAgree(refundId); @@ -116,6 +122,15 @@ public class OrderDelayConsumer { if (StringUtils.isNotBlank(delivery.getReceiverPhone())) { smsUtil.sendCode(delivery.getReceiverPhone(), null,SettingConstant.SMS_TYPE.SMS_NO_ORDER.name()); } + // 给商家通知 + if (StringUtils.isNotBlank(delivery.getShopPhone()) && delivery.getDeliveryType() == 1) { + log.info("【配送订单无人接单】,向商家发送极光推送 orderId={}", orderId); + Shop shop = shopMapper.selectById(delivery.getShopId()); + if (shop != null && StringUtils.isNotBlank(shop.getClientId())) { + // 发送给商家端的 APP Push + jPushService.sendPushNotification(shop.getClientId(), "您有一笔配送订单无人接单!", orderId); + } + } } private void handleNoWorkerTimeout10m(String orderId) { @@ -127,12 +142,21 @@ public class OrderDelayConsumer { if (StringUtils.isNotBlank(delivery.getReceiverPhone())) { smsUtil.sendCode(delivery.getReceiverPhone(),null, SettingConstant.SMS_TYPE.SMS_NO_ORDER.name()); } + // 给商家通知 + if (StringUtils.isNotBlank(delivery.getShopPhone()) && delivery.getDeliveryType() == 1) { + log.info("【配送订单无人接单】,向商家发送极光推送 orderId={}", orderId); + Shop shop = shopMapper.selectById(delivery.getShopId()); + if (shop != null && StringUtils.isNotBlank(shop.getClientId())) { + // 发送给商家端的 APP Push + jPushService.sendPushNotification(shop.getClientId(), "您有一笔配送订单无人接单!", orderId); + } + } } private void handleShopCookTimeout(String orderId) { MallOrder order = mallOrderMapper.selectById(orderId); - if (order == null || order.getShopMakeTime() != null) { - // 商家已经点击了出餐 (shopMakeTime != null) + if (order == null || order.getShopMakeTime() != null || order.getStatus() == null || order.getStatus() >= 4) { + // 商家已出餐,或订单已取货/配送中/结束,不再提醒出餐超时。 return; } log.info("【订单延时处理】商家超时未点击出餐,向商家发送极光推送 orderId={}", orderId); @@ -143,6 +167,36 @@ public class OrderDelayConsumer { } } + private void handleTransferArriveTimeout(String orderId) { + MallDeliveryOrder delivery = getDeliveryOrder(orderId); + if (delivery == null || !Integer.valueOf(1).equals(delivery.getTransferDelivery()) + || delivery.getTransferArriveTime() != null || delivery.getStatus() == null || delivery.getStatus() != 1) { + return; + } + MallOrder order = mallOrderMapper.selectById(orderId); + if (order == null || order.getStatus() == null || order.getStatus() >= 4) { + return; + } + log.info("【订单延时处理】中转配送超时未送达中转点,向商家发送极光推送 orderId={}", orderId); + Shop shop = shopMapper.selectById(delivery.getShopId()); + if (shop != null && StringUtils.isNotBlank(shop.getClientId())) { + jPushService.sendPushNotification(shop.getClientId(), "您有中转配送订单超时未送达中转点,请尽快处理", orderId); + } + } + + private void handleTransferPickupTimeout(String orderId) { + MallDeliveryOrder delivery = getDeliveryOrder(orderId); + if (delivery == null || !Integer.valueOf(1).equals(delivery.getTransferDelivery()) + || delivery.getTransferArriveTime() == null || delivery.getStatus() == null || delivery.getStatus() != 1) { + return; + } + if (StringUtils.isBlank(delivery.getWorkerPhone())) { + return; + } + log.info("【订单延时处理】中转单商家送达10分钟后校内兼职未取货,发送短信提醒 orderId={}", orderId); + smsUtil.sendCode(delivery.getWorkerPhone(), null, SettingConstant.SMS_TYPE.SMS_TRANSFER.name()); + } + private MallDeliveryOrder getDeliveryOrder(String orderId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(MallDeliveryOrder::getOrderId, orderId); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetDrawServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetDrawServiceImpl.java index a5cb5ded..33b85ab7 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetDrawServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetDrawServiceImpl.java @@ -247,6 +247,7 @@ public class PlanetDrawServiceImpl implements PlanetDrawService { rq.eq(PlanetReward::getPoolId, poolId).orderByAsc(PlanetReward::getSort); final List rewards = rewardMapper.selectList(rq); final BigDecimal poolAmount = sumRewardAmount(rewards); + final int rewardQuotaCount = sumRewardQuota(rewards); // 构建加权候选:投入券先按递减规则折算权重,防止大户无限堆券垄断。 final List poolCandidates = new ArrayList<>(); @@ -273,8 +274,8 @@ public class PlanetDrawServiceImpl implements PlanetDrawService { record.setDrawTime(now); record.setCreateTime(now); - final Set cooldownUsers = recentWinnerUserIds(pool.getRegionId(), now); - boolean useCooldown = hasCandidate(poolCandidates, cooldownUsers); + final boolean useCooldown = joins.size() > rewardQuotaCount; + final Set cooldownUsers = useCooldown ? recentWinnerUserIds(pool.getRegionId(), now) : Collections.emptySet(); int winnerCount = 0; final List missedRewards = new ArrayList<>(); for (PlanetReward reward : rewards) { @@ -295,19 +296,21 @@ public class PlanetDrawServiceImpl implements PlanetDrawService { winnerCount++; } } - // 第一轮遵循原有冷却规则;若仍有奖项未开出,则从未中奖参与者中补抽,直到奖项开完或参与者都中奖。 - for (PlanetReward reward : missedRewards) { - final List usable = usableCandidates(poolCandidates, Collections.emptySet()); - if (usable.isEmpty()) { - break; - } - final Candidate winner = pickWeighted(usable); - if (winner == null) { - break; + // 只有名额充足时才回到原逻辑补抽,保证参与人数少于奖池名额时参与者都能中奖。 + if (!useCooldown) { + for (PlanetReward reward : missedRewards) { + final List usable = usableCandidates(poolCandidates, Collections.emptySet()); + if (usable.isEmpty()) { + break; + } + final Candidate winner = pickWeighted(usable); + if (winner == null) { + break; + } + winner.win = true; + saveWinner(record, reward, winner.ticket, now); + winnerCount++; } - winner.win = true; - saveWinner(record, reward, winner.ticket, now); - winnerCount++; } record.setWinnerCount(winnerCount); drawRecordMapper.insert(record); @@ -543,10 +546,6 @@ public class PlanetDrawServiceImpl implements PlanetDrawService { return usable; } - private boolean hasCandidate(List pool, Set cooldownUsers) { - return !usableCandidates(pool, cooldownUsers).isEmpty(); - } - private Candidate pickWeighted(List usable) { if (usable.isEmpty()) { return null; @@ -688,6 +687,14 @@ public class PlanetDrawServiceImpl implements PlanetDrawService { return total; } + private int sumRewardQuota(List rewards) { + int total = 0; + for (PlanetReward reward : rewards) { + total += reward.getQuota() == null ? 0 : reward.getQuota(); + } + return total; + } + private void refreshPoolRewardAmount(PlanetPool pool) { final List rewards = rewardMapper.selectList(new LambdaQueryWrapper() .eq(PlanetReward::getPoolId, pool.getId())); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetServiceImpl.java index ee5da3b3..d5d49d4c 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetServiceImpl.java @@ -1,10 +1,7 @@ package cc.hiver.mall.planet.service.impl; import cc.hiver.mall.planet.constant.PlanetConstant; -import cc.hiver.mall.planet.entity.PlanetPool; -import cc.hiver.mall.planet.entity.PlanetPoolTicket; -import cc.hiver.mall.planet.entity.PlanetReward; -import cc.hiver.mall.planet.entity.PlanetTicket; +import cc.hiver.mall.planet.entity.*; import cc.hiver.mall.planet.mapper.PlanetPoolTicketMapper; import cc.hiver.mall.planet.mapper.PlanetRewardMapper; import cc.hiver.mall.planet.pojo.PlanetDailyLoopVo; @@ -66,7 +63,6 @@ public class PlanetServiceImpl implements PlanetService { public PlanetHomeVo home(PlanetQuery query) { final String userId = query.getUserId(); final String regionId = query.getRegionId(); - homeReminderService.clearPlanetReminders(userId, regionId); final PlanetHomeVo vo = new PlanetHomeVo(); // 初始化/补全用户券记录 @@ -98,6 +94,11 @@ public class PlanetServiceImpl implements PlanetService { vo.setMyRankNo(rankService.userRankNo(regionId, userId)); vo.setRankList(rankService.rankTop(regionId, 10, userId)); vo.setDailyLoop(lightDailyLoop(userId, regionId, pool)); + PlanetDrawWinner unreadWinning = drawService.unreadWinning(userId, regionId); + vo.setUnreadWinning(unreadWinning); + if (unreadWinning != null) { + homeReminderService.addPlanetReward(userId, regionId, "draw", "白嫖星球中奖啦", unreadWinning.getId()); + } return vo; } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java index 2ea4da00..cdc06e8a 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java @@ -66,6 +66,18 @@ public class CreateOrderDTO { @ApiModelProperty(value = "指定配送员参数(不指定则进入抢单大厅)") private WorkerParam workerParam; + @ApiModelProperty(value = "是否选择商家自配送 0否 1是") + private Integer shopDelivery; + + @ApiModelProperty(value = "是否中转配送 0否 1是") + private Integer transferDelivery; + + @ApiModelProperty(value = "中转地点ID") + private String transferAddressId; + + @ApiModelProperty(value = "中转地点名称") + private String transferAddressName; + @ApiModelProperty(value = "使用的优惠券ID") private String userCouponId; diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallDeliveryOrderPageQuery.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallDeliveryOrderPageQuery.java index f3c6a581..e9ce2d5c 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallDeliveryOrderPageQuery.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallDeliveryOrderPageQuery.java @@ -14,6 +14,9 @@ import java.util.List; @Data public class MallDeliveryOrderPageQuery extends HiverBasePageQuery { + @ApiModelProperty("用户ID") + private String userId; + @ApiModelProperty("配送员ID") private String workerId; @@ -53,6 +56,15 @@ public class MallDeliveryOrderPageQuery extends HiverBasePageQuery { @ApiModelProperty("订单类型 1:外卖 2:快递 3:跑腿") private Integer deliveryType; + @ApiModelProperty("是否商家自配送 0否 1是") + private Integer shopDelivery; + + @ApiModelProperty("是否中转配送 0否 1是") + private Integer transferDelivery; + + @ApiModelProperty("是否只查询未送达中转点") + private Boolean transferArriveEmpty; + @ApiModelProperty("排序规则(如果有值且为deliveryFee,则按佣金降序)") private String order; diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallOrderPageQuery.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallOrderPageQuery.java index 972aa0f8..3d4f385e 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallOrderPageQuery.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallOrderPageQuery.java @@ -36,6 +36,9 @@ public class MallOrderPageQuery extends HiverBasePageQuery { @ApiModelProperty("配送方式 1:外卖配送 2:到店自取") private Integer deliveryType; + @ApiModelProperty("是否商家自配送 0否 1是") + private Integer shopDelivery; + @ApiModelProperty("订单类型 0:不区分 1:饭团 2:跑腿/快递 3:二手") private Integer searchType; diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/WorkerMatchVO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/WorkerMatchVO.java index f12bca7c..80578b82 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/WorkerMatchVO.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/WorkerMatchVO.java @@ -43,6 +43,9 @@ public class WorkerMatchVO { @ApiModelProperty("超高层额外费用") private BigDecimal highFloorFee; + @ApiModelProperty("送达地点:0送上楼 1送到宿舍门口") + private Integer deliveryLocation; + @ApiModelProperty("匹配规则的配送佣金") private BigDecimal orderBkge; diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/WorkerRealPriceVo.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/WorkerRealPriceVo.java index cd3d1af8..6f404ad0 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/WorkerRealPriceVo.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/WorkerRealPriceVo.java @@ -37,6 +37,9 @@ public class WorkerRealPriceVo implements Serializable { @ApiModelProperty(value = " 配送规则") private BigDecimal highFloorFee; + @ApiModelProperty(value = "送达地点:0送上楼 1送到宿舍门口") + private Integer deliveryLocation; + @ApiModelProperty(value = " 配送规则") private List workerRelaPriceList; } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallDeliveryOrderService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallDeliveryOrderService.java index e687cecd..e1c3cafc 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallDeliveryOrderService.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallDeliveryOrderService.java @@ -34,6 +34,11 @@ public interface MallDeliveryOrderService extends IService { */ Integer workerAccept(String workerPhone,String deliveryId, String workerId, String workerName,String groupId); + /** + * 配送员接单(可控制是否推送商家) + */ + Integer workerAccept(String workerPhone,String deliveryId, String workerId, String workerName,String groupId, Boolean notifyShop); + /** * 配送员取货 */ diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/HomeReminderServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/HomeReminderServiceImpl.java index 3fdf0d5d..42526ef8 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/HomeReminderServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/HomeReminderServiceImpl.java @@ -102,11 +102,15 @@ public class HomeReminderServiceImpl implements HomeReminderService { Integer ieUnread = parseInt(stringRedisTemplate.opsForHash().get(key, FIELD_IE_UNREAD)); Object draw = stringRedisTemplate.opsForHash().get(key, FIELD_PLANET_DRAW); Object rankTop = stringRedisTemplate.opsForHash().get(key, FIELD_PLANET_RANK_TOP); + Object drawPayload = parseJson(draw); + Object rankTopPayload = parseJson(rankTop); + boolean hasPlanetDraw = regionMatched(drawPayload, regionId); + boolean hasRankTop = regionMatched(rankTopPayload, regionId); result.put("ieUnreadCount", ieUnread); result.put("ie", ieUnread > 0); - result.put("planet", draw != null || rankTop != null); - result.put("planetDraw", parseJson(draw)); - result.put("planetRankTop", parseJson(rankTop)); + result.put("planet", hasPlanetDraw || hasRankTop); + result.put("planetDraw", hasPlanetDraw ? drawPayload : null); + result.put("planetRankTop", hasRankTop ? rankTopPayload : null); return result; } @@ -135,4 +139,12 @@ public class HomeReminderServiceImpl implements HomeReminderService { return null; } } + + private boolean regionMatched(Object payload, String regionId) { + if (!(payload instanceof cn.hutool.json.JSONObject)) { + return false; + } + String payloadRegionId = ((cn.hutool.json.JSONObject) payload).getStr("regionId"); + return StrUtil.isBlank(regionId) || StrUtil.isBlank(payloadRegionId) || regionId.equals(payloadRegionId); + } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopAreaServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopAreaServiceImpl.java index cfbf8fc4..99d1ea99 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopAreaServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopAreaServiceImpl.java @@ -40,8 +40,8 @@ public class ShopAreaServiceImpl implements ShopAreaService { @Override public List findByParentIdOrderBySortOrder(String parentId, Boolean openDataFilter) { - // 数据权限 - List depIds = securityUtil.getDeparmentIds(); + // 数据权限:匿名放行接口没有登录上下文时,不做部门过滤。 + List depIds = resolveDepartmentIds(openDataFilter); if (depIds != null && depIds.size() > 0 && openDataFilter) { return shopAreaDao.findByParentIdAndIdInOrderBySortOrder(parentId, depIds); } @@ -50,8 +50,8 @@ public class ShopAreaServiceImpl implements ShopAreaService { @Override public List findByParentIdAndTitleLikeOrderBySortOrder(String parentId, String title, Boolean openDataFilter) { - // 数据权限 - List depIds = securityUtil.getDeparmentIds(); + // 数据权限:匿名放行接口没有登录上下文时,不做部门过滤。 + List depIds = resolveDepartmentIds(openDataFilter); if (depIds != null && depIds.size() > 0 && openDataFilter) { return shopAreaDao.findByParentIdAndTitleLikeAndIdInOrderBySortOrder(parentId, title, depIds); } @@ -65,14 +65,25 @@ public class ShopAreaServiceImpl implements ShopAreaService { @Override public List findByTitleLikeOrderBySortOrder(String title, Boolean openDataFilter) { - // 数据权限 - List depIds = securityUtil.getDeparmentIds(); + // 数据权限:匿名放行接口没有登录上下文时,不做部门过滤。 + List depIds = resolveDepartmentIds(openDataFilter); if (depIds != null && depIds.size() > 0 && openDataFilter) { return shopAreaDao.findByTitleLikeAndIdInOrderBySortOrder(title, depIds); } return shopAreaDao.findByTitleLikeOrderBySortOrder(title); } + private List resolveDepartmentIds(Boolean openDataFilter) { + if (!Boolean.TRUE.equals(openDataFilter)) { + return null; + } + try { + return securityUtil.getDeparmentIds(); + } catch (Exception ignored) { + return null; + } + } + /** * 根据当前id获取顶层圈层id * diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java index 9652fcb3..4d9e99bc 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java @@ -2,13 +2,16 @@ package cc.hiver.mall.serviceimpl.mybatis; import cc.hiver.core.common.constant.DealingsRecordConstant; import cc.hiver.core.common.constant.SettingConstant; +import cc.hiver.core.common.redis.RedisTemplateHelper; import cc.hiver.core.common.sms.SmsUtil; import cc.hiver.core.entity.Worker; import cc.hiver.core.serviceimpl.JPushServiceImpl; import cc.hiver.core.serviceimpl.WorkerServiceImpl; +import cc.hiver.mall.controller.AdminSeckillGroupController; import cc.hiver.mall.dao.mapper.*; import cc.hiver.mall.entity.*; import cc.hiver.mall.mq.OrderAsyncProducer; +import cc.hiver.mall.pojo.dto.ShopCacheDTO; import cc.hiver.mall.pojo.query.MallDeliveryOrderPageQuery; import cc.hiver.mall.pojo.query.MallRefundRecordPageQuery; import cc.hiver.mall.pojo.vo.MallOrderVO; @@ -16,6 +19,7 @@ import cc.hiver.mall.service.CommentService; import cc.hiver.mall.service.ShopService; import cc.hiver.mall.service.mybatis.*; import cc.hiver.mall.utils.*; +import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; @@ -141,6 +145,9 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl pageDelivery(MallDeliveryOrderPageQuery q) { /*if (Boolean.TRUE.equals(q.getHallOnly())) { @@ -179,6 +186,54 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl orderIdsSend = new ArrayList<>(); + List cookTimeoutOrderIds = new ArrayList<>(); + MallOrder order = mallOrderService.getById(delivery.getOrderId()); + boolean transferDelivery = Integer.valueOf(1).equals(delivery.getTransferDelivery()) + || (order != null && Integer.valueOf(1).equals(order.getTransferDelivery())); + String transferPickupCode = transferDelivery ? generateTransferPickupCode() : delivery.getTransferPickupCode(); // 计算必须完成时间 - LocalDateTime now = LocalDateTime.now(); - Date mustFinishTime = Date.from(now.plusMinutes(40).atZone(ZoneId.systemDefault()).toInstant()); + Date mustFinishTime; + if (transferDelivery) { + mustFinishTime = calculateTransferMustFinishTime(delivery.getRegionId(), delivery.getShopId(), delivery.getDeliveryType()); + } else if (Boolean.FALSE.equals(notifyShop) && delivery.getMustFinishTime() != null) { + mustFinishTime = delivery.getMustFinishTime(); + } else { + mustFinishTime = calculateMustFinishTime(delivery.getRegionId(), delivery.getShopId(), delivery.getDeliveryType()); + } Date acceptTime = new Date(); + int transferArriveMinutes = 0; + if (transferDelivery) { + transferArriveMinutes = safeMinutes(getCookingTime(delivery.getRegionId(), delivery.getShopId())) + + safeMinutes(getTransferDeliveryDuration(delivery.getRegionId(), delivery.getShopId())); + String transferName = StringUtils.defaultIfBlank(delivery.getTransferAddressName(), + order == null ? "" : StringUtils.defaultString(order.getTransferAddressName())); + LocalDateTime transferArriveTime = LocalDateTime.ofInstant(acceptTime.toInstant(), ZoneId.systemDefault()) + .plusMinutes(transferArriveMinutes); + String timeText = String.format("%02d:%02d", transferArriveTime.getHour(), transferArriveTime.getMinute()); + delivery.setShopAddress("商家预计" + timeText + "配送到-" + transferName); + } // 预先更新 delivery 对象字段(后续 buildVO 和缓存复用同一对象) delivery.setWorkerPhone(workerPhone); @@ -213,6 +296,7 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl uw = new LambdaUpdateWrapper<>(); uw.eq(MallDeliveryOrder::getId, deliveryId) @@ -221,17 +305,19 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl goodsList = new ArrayList<>(); + List goodsList = delivery.getGoodsList() == null ? new ArrayList<>() : new ArrayList<>(delivery.getGoodsList()); // 提前查好 Shop,避免重复查询(后续推送复用) Shop cachedShop = null; String pushClientId = null; if (order != null && StringUtils.isNotBlank(order.getShopId())) { - cachedShop = shopService.findById(order.getShopId()); + cachedShop = getShopFromCache(order.getRegionId(), order.getShopId()); pushClientId = cachedShop != null ? cachedShop.getClientId() : null; } // 推送 orderId 列表,待事务提交后异步推送 @@ -247,9 +333,12 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl orderIds = Arrays.asList(group.getGroupOrderIds().split(",")); orderIdsSend = orderIds; + cookTimeoutOrderIds.addAll(orderIds); - // 一次批量查询所有商品明细 - goodsList = mallOrderGoodsMapper.selectByOrderIds(orderIds); + // 接单缓存优先沿用激活配送单时已装好的合单商品;缺失时按原有准确条件补齐整团商品。 + if (goodsList.isEmpty()) { + goodsList = mallOrderGoodsMapper.selectByOrderIds(orderIds); + } // 批量查询所有子订单,避免循环内 N+1 List orderInnerList = mallOrderService.listByIds(orderIds); @@ -289,6 +378,9 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl { try { @@ -358,6 +461,10 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl qw = new LambdaQueryWrapper<>(); - qw.eq(ShopTakeaway::getShopId, knownShopId); - ShopTakeaway shopTakeaway = shopTakeawayMapper.selectOne(qw); - sharedCookTimeMins = shopTakeaway != null ? shopTakeaway.getCookingTime() : 10L; + int cachedCookTime = safeMinutes(getCookingTime(knownOrder.getRegionId(), knownShopId)); + sharedCookTimeMins = cachedCookTime > 0 ? (long) cachedCookTime : 10L; } catch (Exception e) { - log.error("获取 ShopTakeaway cookingTime 异常: {}", e.getMessage()); + log.error("获取店铺出餐时间缓存异常: {}", e.getMessage()); sharedCookTimeMins = 10L; } } @@ -407,13 +512,13 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl qw = new LambdaQueryWrapper<>(); - qw.eq(ShopTakeaway::getShopId, o.getShopId()); - ShopTakeaway shopTakeaway = shopTakeawayMapper.selectOne(qw); - if (shopTakeaway != null) cookTimeMins = shopTakeaway.getCookingTime(); + int cachedCookTime = safeMinutes(getCookingTime(o.getRegionId(), o.getShopId())); + if (cachedCookTime > 0) { + cookTimeMins = cachedCookTime; + } } } catch (Exception e) { - log.error("获取 ShopTakeaway cookingTime 异常: {}", e.getMessage()); + log.error("获取店铺出餐时间缓存异常: {}", e.getMessage()); } } orderAsyncProducer.sendDelayMessage(id, "Shop_Cook_Timeout", cookTimeMins * 60 * 1000L); @@ -1015,6 +1120,11 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl waitingOrders = mallOrderService.list(oqw); boolean isFace2Face = group.getIsFace() != null && group.getIsFace() == 1; + List deliveryOrders = new ArrayList<>(); + List autoAcceptOrders = new ArrayList<>(); + List autoAcceptDeliveries = new ArrayList<>(); for (MallOrder order : waitingOrders) { int targetStatus = (order.getDeliveryType() != null && order.getDeliveryType() == 1) ? 2 : 3; @@ -154,104 +169,203 @@ public class MallOrderGroupServiceImpl extends ServiceImpl dqw = new LambdaQueryWrapper<>(); - dqw.eq(MallDeliveryOrder::getOrderId, order.getId()).last("LIMIT 1"); - MallDeliveryOrder delivery = mallDeliveryOrderMapper.selectOne(dqw); - //mq处理 - triggerDeliveryAsyncEvents(order.getId(),delivery); - if (delivery != null) { - if (StringUtils.isNotBlank(delivery.getWorkerId())) { - boolean isValid = false; - Worker worker = workerService.findById(delivery.getWorkerId()); - if (worker != null && worker.getIsOnLine() != null && worker.getIsOnLine() == 1 - && worker.getGetPushOrder() != null && worker.getGetPushOrder() == 1) { - - LambdaQueryWrapper ruleQuery = new LambdaQueryWrapper<>(); - ruleQuery.eq(WorkerRelaPrice::getWorkerId, delivery.getWorkerId()) - .eq(WorkerRelaPrice::getOrderType, 0) - .eq(WorkerRelaPrice::getGetPushOrder, 1) - .eq(WorkerRelaPrice::getGetAreaId, delivery.getGetAreaId()) - .eq(WorkerRelaPrice::getPutAreaId, delivery.getPutAreaId()) - .last("LIMIT 1"); - WorkerRelaPrice rule = workerRelaPriceMapper.selectOne(ruleQuery); - if (rule != null) { - isValid = true; - } - } + if (order.getDeliveryType() != null && order.getDeliveryType() == 1 + && (!isFace2Face || order.getId().equals(group.getHeadOrderId()))) { + deliveryOrders.add(order); + } + } - if (!isValid) { - // 配送员下线或规则关闭 -> 去掉 workerId 进入抢单大厅,保留原有补贴金 - delivery.setWorkerId(""); - delivery.setWorkerPhone(""); - delivery.setWorkerName(""); - smsUtil.sendCode(delivery.getReceiverPhone(), null, SettingConstant.SMS_TYPE.SMS_WORKEROUT.name()); + for (MallOrder order : deliveryOrders) { + // 取出预创建的运单进行校验;面对面团此时整团订单状态已全部激活。 + LambdaQueryWrapper dqw = new LambdaQueryWrapper<>(); + dqw.eq(MallDeliveryOrder::getOrderId, order.getId()).last("LIMIT 1"); + MallDeliveryOrder delivery = mallDeliveryOrderMapper.selectOne(dqw); + //mq处理 + triggerDeliveryAsyncEvents(order, delivery); + if (delivery != null) { + if (StringUtils.isNotBlank(delivery.getWorkerId())) { + boolean isValid = false; + Worker worker = workerService.findById(delivery.getWorkerId()); + if (worker != null && worker.getIsOnLine() != null && worker.getIsOnLine() == 1 + && worker.getGetPushOrder() != null && worker.getGetPushOrder() == 1) { + + LambdaQueryWrapper ruleQuery = new LambdaQueryWrapper<>(); + ruleQuery.eq(WorkerRelaPrice::getWorkerId, delivery.getWorkerId()) + .eq(WorkerRelaPrice::getOrderType, 0) + .eq(WorkerRelaPrice::getGetPushOrder, 1) + .eq(WorkerRelaPrice::getGetAreaId, delivery.getGetAreaId()) + .eq(WorkerRelaPrice::getPutAreaId, delivery.getPutAreaId()) + .last("LIMIT 1"); + WorkerRelaPrice rule = workerRelaPriceMapper.selectOne(ruleQuery); + if (rule != null) { + isValid = true; } } - // 统一修改运单状态为0使其生效 - delivery.setStatus(0); - Date createTime = new Timestamp(System.currentTimeMillis()); - delivery.setCreateTime(createTime); - // 1. 获取当前时间 (LocalDateTime) - LocalDateTime now = LocalDateTime.now(); - // 2. 加40分钟 - LocalDateTime futureTime = now.plusMinutes(40); - // 3. 如果需要转回 java.util.Date (为了兼容旧代码) - Date mustFinishTime = Date.from(futureTime.atZone(ZoneId.systemDefault()).toInstant()); - delivery.setMustFinishTime(mustFinishTime); - mallDeliveryOrderMapper.updateById(delivery); - //抢单大厅缓存 - if (isFace2Face) { - // 面对面团查所有人订单 - // 2. 一次 IN 查询所有商品明细 - goodsList = mallOrderGoodsMapper.selectByOrderIdsWait(orderIdList); - }else{ - goodsList = mallOrderGoodsMapper.selectByOrderId(order.getId()); + if (!isValid) { + // 配送员下线或规则关闭 -> 去掉 workerId 进入抢单大厅,保留原有补贴金 + delivery.setWorkerId(""); + delivery.setWorkerPhone(""); + delivery.setWorkerName(""); + smsUtil.sendCode(delivery.getReceiverPhone(), null, SettingConstant.SMS_TYPE.SMS_WORKEROUT.name()); } - delivery.setGoodsList(goodsList); - waitOrderCacheUtil.put(delivery.getRegionId(), delivery); + } + // 统一修改运单状态为0使其生效 + delivery.setStatus(0); + Date createTime = new Timestamp(System.currentTimeMillis()); + delivery.setCreateTime(createTime); + delivery.setMustFinishTime(calculateActivatedMustFinishTime(order, delivery)); + mallDeliveryOrderMapper.updateById(delivery); + //抢单大厅缓存 + if (isFace2Face) { + // 面对面团查所有人订单;保持原有查询条件,依赖上方先完成整团状态激活。 + goodsList = mallOrderGoodsMapper.selectByOrderIds(orderIdList); + }else{ + goodsList = mallOrderGoodsMapper.selectByOrderId(order.getId()); + } + delivery.setGoodsList(goodsList); + waitOrderCacheUtil.put(delivery.getRegionId(), delivery); + if (order.getShopDelivery() != null && order.getShopDelivery() == 1) { + autoAcceptOrders.add(order); + autoAcceptDeliveries.add(delivery); } } } + for (int i = 0; i < autoAcceptDeliveries.size(); i++) { + autoAcceptShopDeliveryOrder(autoAcceptOrders.get(i), autoAcceptDeliveries.get(i)); + } log.info("拼团 {} 成团成功,激活 {} 条子订单", groupId, waitingOrders.size()); } + private void notifyShopDeliveryOrder(MallOrder order) { + if (order == null || order.getShopDelivery() == null || order.getShopDelivery() != 1) { + return; + } + try { + Shop shop = getShopFromCache(order.getRegionId(), order.getShopId()); + if (shop != null && StringUtils.isNotBlank(shop.getClientId())) { + jPushService.sendPushNotification(shop.getClientId(), "您有一笔商家自配送订单,请立即处理", order.getId()); + } + } catch (Exception e) { + log.warn("商家自配送订单推送失败 orderId={}: {}", order.getId(), e.getMessage()); + } + } + + private void autoAcceptShopDeliveryOrder(MallOrder order, MallDeliveryOrder delivery) { + if (order == null || order.getShopDelivery() == null || order.getShopDelivery() != 1 || delivery == null) { + return; + } + if (StringUtils.isBlank(delivery.getWorkerId())) { + log.warn("商家自配送订单未绑定配送员,无法自动接单 orderId={}", order.getId()); + return; + } + mallDeliveryOrderService.workerAccept( + delivery.getWorkerPhone(), + delivery.getId(), + delivery.getWorkerId(), + delivery.getWorkerName(), + delivery.getGroupId() == null ? "" : delivery.getGroupId(), + false + ); + } + + private Date calculateMustFinishTime(String regionId, String shopId, Integer deliveryType) { + Integer deliveryTime = 1 == (deliveryType == null ? 0 : deliveryType) + ? AdminSeckillGroupController.getTotalDeliveryTimeByRegionIdAndShopId(redisTemplateHelper, regionId, shopId) + : 40; + LocalDateTime futureTime = LocalDateTime.now().plusMinutes(deliveryTime); + return Date.from(futureTime.atZone(ZoneId.systemDefault()).toInstant()); + } + + private Date calculateActivatedMustFinishTime(MallOrder order, MallDeliveryOrder delivery) { + Integer deliveryType = delivery == null ? 1 : delivery.getDeliveryType(); + if (order != null && Integer.valueOf(1).equals(order.getTransferDelivery())) { + return calculateTransferMustFinishTime(order.getRegionId(), order.getShopId(), deliveryType); + } + if (order != null && Integer.valueOf(1).equals(order.getShopDelivery()) && delivery != null && delivery.getMustFinishTime() != null) { + return delivery.getMustFinishTime(); + } + return calculateMustFinishTime(order == null ? null : order.getRegionId(), order == null ? null : order.getShopId(), deliveryType); + } + + private Date calculateTransferMustFinishTime(String regionId, String shopId, Integer deliveryType) { + Integer schoolDeliveryTime = 1 == (deliveryType == null ? 0 : deliveryType) + ? AdminSeckillGroupController.getTotalDeliveryTimeByRegionIdAndShopId(redisTemplateHelper, regionId, shopId) + : 40; + int totalMinutes = safeMinutes(getTransferDeliveryDuration(regionId, shopId)) + safeMinutes(schoolDeliveryTime); + LocalDateTime futureTime = LocalDateTime.now().plusMinutes(totalMinutes); + return Date.from(futureTime.atZone(ZoneId.systemDefault()).toInstant()); + } + + private Integer getTransferDeliveryDuration(String regionId, String shopId) { + Shop shop = getShopFromCache(regionId, shopId); + return shop == null || shop.getTransferDeliveryDuration() == null ? 0 : shop.getTransferDeliveryDuration(); + } + + private Integer getCookingTime(String regionId, String shopId) { + return AdminSeckillGroupController.getShopCookingTimeByRegionIdAndShopId(redisTemplateHelper, regionId, shopId); + } + + private Shop getShopFromCache(String regionId, String shopId) { + ShopCacheDTO dto = getShopCache(regionId, shopId); + return dto == null ? null : dto.getShop(); + } + + private ShopCacheDTO getShopCache(String regionId, String shopId) { + if (StringUtils.isBlank(regionId) || StringUtils.isBlank(shopId)) { + return null; + } + try { + Object value = redisTemplateHelper.hGet("SHOP_CACHE:" + regionId, shopId); + return value == null ? null : JSONUtil.toBean(value.toString(), ShopCacheDTO.class); + } catch (Exception e) { + log.warn("读取店铺缓存失败 regionId={}, shopId={}: {}", regionId, shopId, e.getMessage()); + return null; + } + } + + private int safeMinutes(Integer minutes) { + return minutes == null ? 0 : minutes; + } + private void triggerShopCookTimeoutEvent(MallOrder order) { if (order != null) { long cookTimeMins = 15; // default try { - LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); - qw.eq(cc.hiver.mall.entity.ShopTakeaway::getShopId, order.getShopId()); - List list = shopTakeawayMapper.selectList(qw); - if (list != null && !list.isEmpty() && list.get(0).getCookingTime() != null) { - cookTimeMins = list.get(0).getCookingTime(); + int cachedCookTime = safeMinutes(getCookingTime(order.getRegionId(), order.getShopId())); + if (cachedCookTime > 0) { + cookTimeMins = cachedCookTime; } } catch (Exception e) { - log.error("获取 ShopTakeaway cookingTime 异常: {}", e.getMessage()); + log.error("获取店铺出餐时间缓存异常: {}", e.getMessage()); } orderAsyncProducer.sendDelayMessage(order.getId(), "Shop_Cook_Timeout", cookTimeMins * 60 * 1000L); } @@ -273,10 +387,17 @@ public class MallOrderGroupServiceImpl extends ServiceImpl workerAccept 继续投递。 + if (Integer.valueOf(1).equals(order.getShopDelivery())) { + return; + } + // 配送单判断是否指定 if (delivery != null) { boolean isSpecified = StringUtils.isNotBlank(delivery.getWorkerId()); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java index 40e7322b..e7dae201 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java @@ -8,6 +8,7 @@ import cc.hiver.core.common.utils.SecurityUtil; import cc.hiver.core.common.utils.SnowFlakeUtil; import cc.hiver.core.entity.User; import cc.hiver.core.serviceimpl.JPushServiceImpl; +import cc.hiver.mall.controller.AdminSeckillGroupController; import cc.hiver.mall.dao.mapper.*; import cc.hiver.mall.entity.*; import cc.hiver.mall.ie.service.IeMatchService; @@ -607,32 +608,36 @@ public class MallOrderServiceImpl extends ServiceImpl 跳过待商家接单,直接进入待配送或待消费 if (DELIVERY_TYPE_EXPRESS == order.getDeliveryType()) { updateOrderStatus(orderId, STATUS_WAIT_DELIVERY); + notifyShopDeliveryOrder(order); //用户当前订单缓存 MallOrderVO orderVO = buildVO(order, null, null, null); orderVO.setStatus(STATUS_WAIT_DELIVERY); userPendingOrderCacheUtil.update(order.getUserId(), orderVO); // 激活配送单,状态改为待接单(0) Date createTime = new Timestamp(System.currentTimeMillis()); - // 1. 获取当前时间 (LocalDateTime) - LocalDateTime now = LocalDateTime.now(); - // 2. 加40分钟 - LocalDateTime futureTime = now.plusMinutes(40); - // 3. 如果需要转回 java.util.Date (为了兼容旧代码) - Date mustFinishTime = Date.from(futureTime.atZone(ZoneId.systemDefault()).toInstant()); + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(MallDeliveryOrder::getOrderId, orderId); + MallDeliveryOrder deliveryOrder = mallDeliveryOrderMapper.selectOne(qw); + Date mustFinishTime = calculateActivatedMustFinishTime(order, deliveryOrder); LambdaUpdateWrapper duw = new LambdaUpdateWrapper<>(); duw.eq(MallDeliveryOrder::getOrderId, orderId) .set(MallDeliveryOrder::getStatus, 0).set(MallDeliveryOrder::getCreateTime, createTime).set(MallDeliveryOrder::getMustFinishTime, mustFinishTime); mallDeliveryOrderMapper.update(null, duw); //抢单大厅缓存 - LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); - qw.eq(MallDeliveryOrder::getOrderId, orderId); - MallDeliveryOrder deliveryOrder = mallDeliveryOrderMapper.selectOne(qw); + if (deliveryOrder != null) { + deliveryOrder.setStatus(0); + deliveryOrder.setCreateTime(createTime); + deliveryOrder.setMustFinishTime(mustFinishTime); + } List goodsList = mallOrderGoodsMapper.selectByOrderId(orderId); - deliveryOrder.setGoodsList(goodsList); - waitOrderCacheUtil.put(order.getRegionId(), deliveryOrder); + if (deliveryOrder != null) { + deliveryOrder.setGoodsList(goodsList); + waitOrderCacheUtil.put(order.getRegionId(), deliveryOrder); + } // 触发异步推送和延迟监听流程 - triggerDeliveryAsyncEvents(orderId); + triggerDeliveryAsyncEvents(order, deliveryOrder); + autoAcceptShopDeliveryOrder(order, deliveryOrder); } else { String latestSeq = merchantOrderSeqUtil.generateOrderSequence(order.getShopId(), 2); LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); @@ -642,7 +647,10 @@ public class MallOrderServiceImpl extends ServiceImpl workerAccept 继续投递。 + if (Integer.valueOf(1).equals(order.getShopDelivery())) { + return; + } + // 获取配送单以判断是否指定 - LambdaQueryWrapper dw = new LambdaQueryWrapper<>(); - dw.eq(MallDeliveryOrder::getOrderId, orderId); - MallDeliveryOrder delivery = mallDeliveryOrderMapper.selectOne(dw); if (delivery != null) { boolean isSpecified = StringUtils.isNotBlank(delivery.getWorkerId()); if (isSpecified) { @@ -751,14 +763,12 @@ public class MallOrderServiceImpl extends ServiceImpl qw = new LambdaQueryWrapper<>(); - qw.eq(cc.hiver.mall.entity.ShopTakeaway::getShopId, order.getShopId()); - List list = shopTakeawayMapper.selectList(qw); - if (list != null && !list.isEmpty() && list.get(0).getCookingTime() != null) { - cookTimeMins = list.get(0).getCookingTime(); + int cachedCookTime = safeMinutes(getCookingTime(order.getRegionId(), order.getShopId())); + if (cachedCookTime > 0) { + cookTimeMins = cachedCookTime; } } catch (Exception e) { - log.error("获取 ShopTakeaway cookingTime 异常: {}", e.getMessage()); + log.error("获取店铺出餐时间缓存异常: {}", e.getMessage()); } orderAsyncProducer.sendDelayMessage(orderId, "Shop_Cook_Timeout", cookTimeMins * 60 * 1000L); } @@ -1149,7 +1159,10 @@ public class MallOrderServiceImpl extends ServiceImpl uw = new LambdaUpdateWrapper<>(); uw.eq(MallOrder::getId, orderId).set(MallOrder::getUserRequireMake,1); this.update(uw); - jPushService.sendPushNotification(shopService.findById(order.getShopId()).getClientId(), "您有一笔新的到店订单",orderId); + Shop shop = getShopFromCache(order.getRegionId(), order.getShopId()); + if (shop != null && StringUtils.isNotBlank(shop.getClientId())) { + jPushService.sendPushNotification(shop.getClientId(), "您有一笔新的到店订单",orderId); + } // 触发商家出餐超时监听 triggerShopCookTimeoutEvent(orderId); @@ -1241,6 +1254,10 @@ public class MallOrderServiceImpl extends ServiceImpl 0){ BigDecimal totalCoupons = coupons.stream() .map(item -> item.getDiscountAmount()) // 提取 BigDecimal 金额 .reduce(BigDecimal.ZERO, BigDecimal::add); @@ -1779,7 +1913,12 @@ public class MallOrderServiceImpl extends ServiceImpl 0){ BigDecimal totalCoupons = coupons.stream() .map(item -> item.getDiscountAmount()) // 提取 BigDecimal 金额 .reduce(BigDecimal.ZERO, BigDecimal::add); @@ -138,7 +138,7 @@ public class MallRefundRecordServiceImpl extends ServiceImpl + + + + + + @@ -51,7 +57,9 @@ d.receiver_name, d.receiver_phone, d.receiver_address, d.shop_name, d.shop_phone, d.shop_address, d.delivery_type, d.number_code, d.region_id,d.remark, d.all_count, d.phone_number, d.get_codes, d.get_pictures, d.is_big, - d.is_return,d.worker_name, d.arrive_time,d.new_worker,d.worker_phone + d.is_return,d.worker_name, d.arrive_time,d.new_worker,d.worker_phone, + d.transfer_delivery,d.transfer_address_id,d.transfer_address_name, + d.transfer_arrive_time,d.transfer_arrive_image,d.transfer_pickup_code FROM mall_delivery_order d @@ -93,28 +101,49 @@ AND d.delivery_type = #{q.deliveryType} + + AND EXISTS ( + SELECT 1 FROM mall_order o + WHERE o.id = d.order_id + AND o.shop_delivery = #{q.shopDelivery} + ) + + + AND d.transfer_delivery = #{q.transferDelivery} + + + AND d.transfer_arrive_time IS NULL + ORDER BY - - d.delivery_fee DESC, - - - (CASE - - - WHEN d.get_area_id = #{rule.getAreaId} AND d.put_area_id = #{rule.putAreaId} AND d.delivery_type = 1 THEN 1 - - - - - WHEN d.get_area_id = #{rule.getAreaId} AND d.put_area_id = #{rule.putAreaId} AND d.delivery_type = 2 THEN 1 - - - ELSE 0 - END) DESC, - - (CASE WHEN d.must_finish_time IS NULL THEN 1 ELSE 0 END) ASC, - d.must_finish_time ASC,d.delivery_fee DESC + + + (CASE WHEN d.transfer_arrive_time IS NULL THEN 0 ELSE 1 END) ASC, + d.create_time DESC + + + + d.delivery_fee DESC, + + + (CASE + + + WHEN d.get_area_id = #{rule.getAreaId} AND d.put_area_id = #{rule.putAreaId} AND d.delivery_type = 1 THEN 1 + + + + + WHEN d.get_area_id = #{rule.getAreaId} AND d.put_area_id = #{rule.putAreaId} AND d.delivery_type = 2 THEN 1 + + + ELSE 0 + END) DESC, + + (CASE WHEN d.must_finish_time IS NULL THEN 1 ELSE 0 END) ASC, + d.must_finish_time ASC,d.delivery_fee DESC + + @@ -175,7 +219,9 @@ d.receiver_name, d.receiver_phone, d.receiver_address, d.shop_name, d.shop_phone, d.shop_address, d.delivery_type, d.number_code, d.region_id,d.remark, d.all_count, d.phone_number, d.get_codes, d.get_pictures, - d.is_big,d.is_return,d.worker_name,d.arrive_time,d.new_worker,d.worker_phone + d.is_big,d.is_return,d.worker_name,d.arrive_time,d.new_worker,d.worker_phone, + d.transfer_delivery,d.transfer_address_id,d.transfer_address_name, + d.transfer_arrive_time,d.transfer_arrive_image,d.transfer_pickup_code FROM mall_delivery_order d d.status not in (-1,4) @@ -213,6 +259,19 @@ AND d.delivery_type = #{q.deliveryType} + + AND EXISTS ( + SELECT 1 FROM mall_order o + WHERE o.id = d.order_id + AND o.shop_delivery = #{q.shopDelivery} + ) + + + AND d.transfer_delivery = #{q.transferDelivery} + + + AND d.transfer_arrive_time IS NULL + ORDER BY d.finish_time desc @@ -225,7 +284,9 @@ d.receiver_name, d.receiver_phone, d.receiver_address, d.shop_name, d.shop_phone, d.shop_address, d.delivery_type, d.number_code, d.region_id,d.remark, d.all_count, d.phone_number, d.get_codes, d.get_pictures, - d.is_big,d.is_return,d.worker_name,d.arrive_time,d.new_worker,d.worker_phone + d.is_big,d.is_return,d.worker_name,d.arrive_time,d.new_worker,d.worker_phone, + d.transfer_delivery,d.transfer_address_id,d.transfer_address_name, + d.transfer_arrive_time,d.transfer_arrive_image,d.transfer_pickup_code FROM mall_delivery_order d where d.group_id = (select id from mall_order_group where 1 = 1 diff --git a/hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml b/hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml index 0a54a4ec..b434dfd5 100644 --- a/hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml +++ b/hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml @@ -34,6 +34,10 @@ + + + + @@ -99,6 +103,24 @@ AND status in (0,3) + + + + diff --git a/hiver-modules/hiver-mall/src/main/resources/mapper/WorkerRelaPriceMapper.xml b/hiver-modules/hiver-mall/src/main/resources/mapper/WorkerRelaPriceMapper.xml index 4fa05c33..623fee08 100644 --- a/hiver-modules/hiver-mall/src/main/resources/mapper/WorkerRelaPriceMapper.xml +++ b/hiver-modules/hiver-mall/src/main/resources/mapper/WorkerRelaPriceMapper.xml @@ -63,6 +63,7 @@ + @@ -80,6 +81,7 @@ w.avg_time, w.rebate_amount, w.high_floor_fee, + w.delivery_location, p.order_bkge, /* 待接单:status = 0 */ COUNT(CASE WHEN d.status = 0 THEN 1 END) AS order_wait_count, @@ -132,7 +134,7 @@ GROUP BY w.worker_id, w.worker_name, w.mobile, w.icon, - w.score, w.avg_time, w.rebate_amount, w.high_floor_fee, p.order_bkge + w.score, w.avg_time, w.rebate_amount, w.high_floor_fee, w.delivery_location, p.order_bkge ORDER BY