Browse Source

对接拼团数据1

master
wangfukang 1 month ago
parent
commit
916d4f4c73
  1. 7
      hiver-admin/src/main/resources/application.yml
  2. 16
      hiver-admin/test-output/test-report.html
  3. 2
      hiver-core/src/main/java/cc/hiver/core/common/constant/SettingConstant.java
  4. 28
      hiver-core/src/main/java/cc/hiver/core/config/security/WebSecurityConfig.java
  5. 4
      hiver-core/src/main/java/cc/hiver/core/entity/Worker.java
  6. 92
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSeckillGroupController.java
  7. 94
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallDeliveryOrderController.java
  8. 15
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java
  9. 84
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java
  10. 1
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerController.java
  11. 2
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerRelaPriceController.java
  12. 4
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallOrderMapper.java
  13. 12
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallDeliveryOrder.java
  14. 12
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallOrder.java
  15. 38
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/Shop.java
  16. 19
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeAdminController.java
  17. 59
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeRealNameAuthController.java
  18. 19
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeRealNameVerifyDTO.java
  19. 13
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeStudentCardVerifyDTO.java
  20. 50
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserProfile.java
  21. 19
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/IeBusinessException.java
  22. 20
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/IeExceptionHandler.java
  23. 14
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/exception/UnderageUserException.java
  24. 47
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeRealNameAuthService.java
  25. 12
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeStudentCardOcrService.java
  26. 6
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java
  27. 6
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeDailyQuestionServiceImpl.java
  28. 58
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java
  29. 6
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMomentServiceImpl.java
  30. 566
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRealNameAuthServiceImpl.java
  31. 58
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeStudentCardOcrServiceImpl.java
  32. 47
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeRealNameAuthVO.java
  33. 20
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeUserProfileVO.java
  34. 53
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncProducer.java
  35. 58
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java
  36. 21
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetDrawServiceImpl.java
  37. 11
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/service/impl/PlanetServiceImpl.java
  38. 12
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java
  39. 12
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallDeliveryOrderPageQuery.java
  40. 3
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallOrderPageQuery.java
  41. 3
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/WorkerMatchVO.java
  42. 3
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/vo/WorkerRealPriceVo.java
  43. 5
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallDeliveryOrderService.java
  44. 18
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/HomeReminderServiceImpl.java
  45. 23
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopAreaServiceImpl.java
  46. 152
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java
  47. 173
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderGroupServiceImpl.java
  48. 196
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java
  49. 10
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallRefundRecordServiceImpl.java
  50. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/AliOcrUtil.java
  51. 19
      hiver-modules/hiver-mall/src/main/resources/db/ie_real_name_auth_fields.sql
  52. 30
      hiver-modules/hiver-mall/src/main/resources/db/shop_self_delivery.sql
  53. 3
      hiver-modules/hiver-mall/src/main/resources/db/worker_delivery_location.sql
  54. 69
      hiver-modules/hiver-mall/src/main/resources/mapper/MallDeliveryOrderMapper.xml
  55. 31
      hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml
  56. 154
      hiver-modules/hiver-mall/src/main/resources/mapper/ShopMapper.xml
  57. 4
      hiver-modules/hiver-mall/src/main/resources/mapper/WorkerRelaPriceMapper.xml

7
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分页获取销售历史(按商品)

16
hiver-admin/test-output/test-report.html

@ -35,7 +35,7 @@
<a href="#"><span class="badge badge-primary">Hiver</span></a>
</li>
<li class="m-r-10">
<a href="#"><span class="badge badge-primary">六月 14, 2026 18:24:58</span></a>
<a href="#"><span class="badge badge-primary">六月 19, 2026 13:55:31</span></a>
</li>
</ul>
</div>
@ -84,7 +84,7 @@
<div class="test-detail">
<span class="meta text-white badge badge-sm"></span>
<p class="name">passTest</p>
<p class="text-sm"><span>18:24:59 下午</span> / <span>0.034 secs</span></p>
<p class="text-sm"><span>13:55:31 下午</span> / <span>0.022 secs</span></p>
</div>
<div class="test-contents d-none">
<div class="detail-head">
@ -92,9 +92,9 @@
<div class="info">
<div class='float-right'><span class='badge badge-default'>#test-id=1</span></div>
<h5 class="test-status text-pass">passTest</h5>
<span class='badge badge-success'>06.14.2026 18:24:59</span>
<span class='badge badge-danger'>06.14.2026 18:24:59</span>
<span class='badge badge-default'>0.034 secs</span>
<span class='badge badge-success'>06.19.2026 13:55:31</span>
<span class='badge badge-danger'>06.19.2026 13:55:31</span>
<span class='badge badge-default'>0.022 secs</span>
</div>
<div class="m-t-10 m-l-5"></div>
</div>
@ -104,7 +104,7 @@
<tbody>
<tr class="event-row">
<td><span class="badge log pass-bg">Pass</span></td>
<td>18:24:59</td>
<td>13:55:31</td>
<td>
Test passed
</td>
@ -128,13 +128,13 @@
<div class="col-md-3">
<div class="card"><div class="card-body">
<p class="m-b-0">Started</p>
<h3>六月 14, 2026 18:24:58</h3>
<h3>六月 19, 2026 13:55:31</h3>
</div></div>
</div>
<div class="col-md-3">
<div class="card"><div class="card-body">
<p class="m-b-0">Ended</p>
<h3>六月 14, 2026 18:24:59</h3>
<h3>六月 19, 2026 13:55:31</h3>
</div></div>
</div>
<div class="col-md-3">

2
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
}

28
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<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http

4
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;
}

92
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<Integer> getDeliveryTime(@RequestParam(value = "regionId") String regionId) {
return new ResultUtil<Integer>().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) {

94
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<Object>().setData(cacheResult);
}
@ -202,6 +208,92 @@ public class MallDeliveryOrderController {
return new ResultUtil<Object>().setData(page);
}
@PostMapping("/pageShopDelivery")
@ApiOperation(value = "分页查询商家自配送单")
public Result<Object> pageShopDelivery(@RequestBody MallDeliveryOrderPageQuery q) {
q.setShopDelivery(1);
q.setDeliveryType(1);
IPage<MallDeliveryOrder> page = mallDeliveryOrderService.pageDeliveryByStatus(q);
return new ResultUtil<Object>().setData(page);
}
@PostMapping("/pageTransferDelivery")
@ApiOperation(value = "分页查询商家中转配送单")
public Result<Object> pageTransferDelivery(@RequestBody MallDeliveryOrderPageQuery q) {
q.setTransferDelivery(1);
q.setDeliveryType(1);
q.setStatus(null);
q.setTransferArriveEmpty(null);
q.setOrder("transferDelivery");
IPage<MallDeliveryOrder> page = mallDeliveryOrderService.pageDelivery(q);
return new ResultUtil<Object>().setData(page);
}
@PostMapping("/transferArrive")
@ApiOperation(value = "商家送达中转点")
public Result<Object> 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<MallDeliveryOrder> 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<Object> 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<Object> 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<Object> pagebyworkerHistory(@RequestBody MallDeliveryOrderPageQuery q) {

15
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<String> depIds = securityUtil.getDeparmentIds();
// data permission filter:匿名放行接口没有登录上下文时,不做部门过滤。
List<String> depIds = resolveDepartmentIds(openDataFilter);
if (openDataFilter && depIds != null && depIds.size() > 0) {
List<ShopArea> filteredDatas = new java.util.ArrayList<>();
for (ShopArea allData : list) {
@ -109,6 +109,17 @@ public class ShopAreaController {
return new ResultUtil<List<ShopArea>>().setData(list);
}
private List<String> 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) {

84
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<List<Shop>> 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<Object> 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<Object> 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<Map<String, Object>> findWorkerByMobile(@RequestParam String mobile) {
if (CharSequenceUtil.isBlank(mobile)) {
return ResultUtil.error("手机号不能为空");
}
Worker worker = workerService.findByMobile(mobile);
if (worker == null) {
return ResultUtil.error("未查询到配送员");
}
Map<String, Object> 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<Map<String, Object>>().setData(result);
}
@RequestMapping(value = "/delByIds", method = RequestMethod.POST)
@ResponseBody
@ApiOperation("通过id删除")
@ -374,6 +440,7 @@ public class ShopController {
}
}
}
fillGroupDeliveryTime(pageList, regionId);
Page<Shop> pageResult = new PageImpl<>(pageList, PageUtil.initPage(pageVo), allShops.size());
return new ResultUtil<Page<Shop>>().setData(pageResult);
@ -417,10 +484,25 @@ public class ShopController {
}
}
});
fillGroupDeliveryTime(page.getContent(), shop.getRegionId());
}
return new ResultUtil<Page<Shop>>().setData(page);
}
private void fillGroupDeliveryTime(List<Shop> 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) {

1
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);

2
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<WorkerRelaPrice> 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){

4
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallOrderMapper.java

@ -33,6 +33,10 @@ public interface MallOrderMapper extends BaseMapper<MallOrder> {
Integer selectRefundCount(@Param("shopId") String shopId);
Integer selectShopDeliveryPendingCount(@Param("shopId") String shopId);
Integer selectTransferDeliveryPendingCount(@Param("shopId") String shopId);
List<String> getWeChatId();
List<MallOrderVO> getPeiSongOrders(@Param("userId") String userId);

12
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 = "商品信息")

12
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)

38
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 = "模糊搜索条件")

19
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<Object> 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<java.util.Map<String, Boolean>> auditSetting() {
java.util.Map<String, Boolean> result = new java.util.HashMap<>();
result.put("realNameAuditEnabled", realNameAuthService.isRealNameAuditEnabled());
return new ResultUtil<java.util.Map<String, Boolean>>().setData(result);
}
@RequestMapping(value = "/audit-setting/save", method = RequestMethod.POST)
@ApiOperation("保存i/e审核管理配置")
public Result<Object> saveAuditSetting(@RequestParam(required = false) Boolean realNameAuditEnabled) {
realNameAuthService.setRealNameAuditEnabled(realNameAuditEnabled);
return ResultUtil.success("保存成功");
}
}

59
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<IeRealNameAuthVO> verify(@RequestBody IeRealNameVerifyDTO dto) {
return new ResultUtil<IeRealNameAuthVO>().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<IeRealNameAuthVO> verifyStudentCard(@RequestBody IeStudentCardVerifyDTO dto) {
String imageUrl = dto == null ? null : dto.getImageUrl();
return new ResultUtil<IeRealNameAuthVO>().setData(realNameAuthService.verifyStudentCard(currentUserId(), imageUrl));
}
@RequestMapping(value = "/audit-setting", method = RequestMethod.GET)
@ApiOperation("查询i/e实名认证审核开关")
public Result<java.util.Map<String, Boolean>> auditSetting() {
java.util.Map<String, Boolean> result = new java.util.HashMap<>();
result.put("realNameAuditEnabled", realNameAuthService.isRealNameAuditEnabled());
return new ResultUtil<java.util.Map<String, Boolean>>().setData(result);
}
private Long currentUserId() {
User user = securityUtil.getCurrUser();
return Long.valueOf(user.getId());
}
}

19
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;
}

13
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;
}

50
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;
}

19
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);
}
}

20
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<Object> handleIeBusinessException(IeBusinessException e) {
return ResultUtil.error(e.getMessage());
}
}

14
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);
}
}

47
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);
}

12
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);
}

6
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<IeRoomMessage>()
.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<IeRoomMessage>()
.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分钟后再试");
}
}

6
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<String, Object> todayQuestion(Long userId) {
IeDailyQuestion question = resolveTodayQuestion();
@ -76,6 +80,8 @@ public class IeDailyQuestionServiceImpl implements IeDailyQuestionService {
@Override
@Transactional
public Map<String, Object> submitAnswer(Long userId, String content) {
// 同频一题属于 i/e 互动能力,提交前按后台审核开关校验认证状态。
realNameAuthService.assertAdultVerified(userId);
if (CharSequenceUtil.isBlank(content)) {
throw new RuntimeException("先写点什么再提交吧");
}

58
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);
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;
}

6
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("发布内容不能为空");
}

566
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<String, String> 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<String, String> params) throws Exception {
List<String> 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<String, String> params) throws Exception {
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String> 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<IeUserProfile>()
.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;
}
}
}

58
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;
}
}
}

47
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;
}

20
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;
}

53
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<Long> 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<String, Object> 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;
}
}

58
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<MallDeliveryOrder> qw = new LambdaQueryWrapper<>();
qw.eq(MallDeliveryOrder::getOrderId, orderId);

21
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<PlanetReward> rewards = rewardMapper.selectList(rq);
final BigDecimal poolAmount = sumRewardAmount(rewards);
final int rewardQuotaCount = sumRewardQuota(rewards);
// 构建加权候选:投入券先按递减规则折算权重,防止大户无限堆券垄断。
final List<Candidate> poolCandidates = new ArrayList<>();
@ -273,8 +274,8 @@ public class PlanetDrawServiceImpl implements PlanetDrawService {
record.setDrawTime(now);
record.setCreateTime(now);
final Set<String> cooldownUsers = recentWinnerUserIds(pool.getRegionId(), now);
boolean useCooldown = hasCandidate(poolCandidates, cooldownUsers);
final boolean useCooldown = joins.size() > rewardQuotaCount;
final Set<String> cooldownUsers = useCooldown ? recentWinnerUserIds(pool.getRegionId(), now) : Collections.emptySet();
int winnerCount = 0;
final List<PlanetReward> missedRewards = new ArrayList<>();
for (PlanetReward reward : rewards) {
@ -295,7 +296,8 @@ public class PlanetDrawServiceImpl implements PlanetDrawService {
winnerCount++;
}
}
// 第一轮遵循原有冷却规则;若仍有奖项未开出,则从未中奖参与者中补抽,直到奖项开完或参与者都中奖。
// 只有名额充足时才回到原逻辑补抽,保证参与人数少于奖池名额时参与者都能中奖。
if (!useCooldown) {
for (PlanetReward reward : missedRewards) {
final List<Candidate> usable = usableCandidates(poolCandidates, Collections.emptySet());
if (usable.isEmpty()) {
@ -309,6 +311,7 @@ public class PlanetDrawServiceImpl implements PlanetDrawService {
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<Candidate> pool, Set<String> cooldownUsers) {
return !usableCandidates(pool, cooldownUsers).isEmpty();
}
private Candidate pickWeighted(List<Candidate> usable) {
if (usable.isEmpty()) {
return null;
@ -688,6 +687,14 @@ public class PlanetDrawServiceImpl implements PlanetDrawService {
return total;
}
private int sumRewardQuota(List<PlanetReward> rewards) {
int total = 0;
for (PlanetReward reward : rewards) {
total += reward.getQuota() == null ? 0 : reward.getQuota();
}
return total;
}
private void refreshPoolRewardAmount(PlanetPool pool) {
final List<PlanetReward> rewards = rewardMapper.selectList(new LambdaQueryWrapper<PlanetReward>()
.eq(PlanetReward::getPoolId, pool.getId()));

11
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;
}

12
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;

12
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;

3
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;

3
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;

3
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<WorkerRelaPrice> workerRelaPriceList;
}

5
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallDeliveryOrderService.java

@ -34,6 +34,11 @@ public interface MallDeliveryOrderService extends IService<MallDeliveryOrder> {
*/
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);
/**
* 配送员取货
*/

18
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);
}
}

23
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<ShopArea> findByParentIdOrderBySortOrder(String parentId, Boolean openDataFilter) {
// 数据权限
List<String> depIds = securityUtil.getDeparmentIds();
// 数据权限:匿名放行接口没有登录上下文时,不做部门过滤。
List<String> 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<ShopArea> findByParentIdAndTitleLikeOrderBySortOrder(String parentId, String title, Boolean openDataFilter) {
// 数据权限
List<String> depIds = securityUtil.getDeparmentIds();
// 数据权限:匿名放行接口没有登录上下文时,不做部门过滤。
List<String> 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<ShopArea> findByTitleLikeOrderBySortOrder(String title, Boolean openDataFilter) {
// 数据权限
List<String> depIds = securityUtil.getDeparmentIds();
// 数据权限:匿名放行接口没有登录上下文时,不做部门过滤。
List<String> depIds = resolveDepartmentIds(openDataFilter);
if (depIds != null && depIds.size() > 0 && openDataFilter) {
return shopAreaDao.findByTitleLikeAndIdInOrderBySortOrder(title, depIds);
}
return shopAreaDao.findByTitleLikeOrderBySortOrder(title);
}
private List<String> resolveDepartmentIds(Boolean openDataFilter) {
if (!Boolean.TRUE.equals(openDataFilter)) {
return null;
}
try {
return securityUtil.getDeparmentIds();
} catch (Exception ignored) {
return null;
}
}
/**
* 根据当前id获取顶层圈层id
*

152
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<MallDeliveryOrderM
@Autowired
private RefundOrderCacheUtil refundOrderCacheUtil;
@Autowired
private RedisTemplateHelper redisTemplateHelper;
@Override
public IPage<MallDeliveryOrder> pageDelivery(MallDeliveryOrderPageQuery q) {
/*if (Boolean.TRUE.equals(q.getHallOnly())) {
@ -179,6 +186,54 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
return page;
}
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 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;
}
/**
* 配送员接单
* - 抢单大厅workerId 为空的单任何配送员均可接
@ -187,6 +242,12 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
@Override
@Transactional(rollbackFor = Exception.class)
public Integer workerAccept(String workerPhone, String deliveryId, String workerId, String workerName, String groupId) {
return workerAccept(workerPhone, deliveryId, workerId, workerName, groupId, true);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Integer workerAccept(String workerPhone, String deliveryId, String workerId, String workerName, String groupId, Boolean notifyShop) {
Worker worker = workerServiceImpl.findByWorkerId(workerId);
if (worker != null && worker.getWorkerStatus() == 3) {
return 5;
@ -201,10 +262,32 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
}
List<String> orderIdsSend = new ArrayList<>();
List<String> 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<MallDeliveryOrderM
delivery.setWorkerName(workerName);
delivery.setAcceptTime(acceptTime);
delivery.setMustFinishTime(mustFinishTime);
delivery.setTransferPickupCode(transferPickupCode);
LambdaUpdateWrapper<MallDeliveryOrder> uw = new LambdaUpdateWrapper<>();
uw.eq(MallDeliveryOrder::getId, deliveryId)
@ -221,17 +305,19 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
.set(MallDeliveryOrder::getWorkerName, workerName)
.set(MallDeliveryOrder::getWorkerPhone, workerPhone)
.set(MallDeliveryOrder::getAcceptTime, acceptTime)
.set(MallDeliveryOrder::getMustFinishTime, mustFinishTime);
MallOrder order = mallOrderService.getById(delivery.getOrderId());
.set(MallDeliveryOrder::getMustFinishTime, mustFinishTime)
.set(MallDeliveryOrder::getTransferPickupCode, transferPickupCode);
if (transferDelivery) {
uw.set(MallDeliveryOrder::getShopAddress, delivery.getShopAddress());
}
String numberCode = "";
List<MallOrderGoods> goodsList = new ArrayList<>();
List<MallOrderGoods> 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<MallDeliveryOrderM
// ---- 面对面拼团 ----
List<String> orderIds = Arrays.asList(group.getGroupOrderIds().split(","));
orderIdsSend = orderIds;
cookTimeoutOrderIds.addAll(orderIds);
// 一次批量查询所有商品明细
// 接单缓存优先沿用激活配送单时已装好的合单商品;缺失时按原有准确条件补齐整团商品。
if (goodsList.isEmpty()) {
goodsList = mallOrderGoodsMapper.selectByOrderIds(orderIds);
}
// 批量查询所有子订单,避免循环内 N+1
List<MallOrder> orderInnerList = mallOrderService.listByIds(orderIds);
@ -289,6 +378,9 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
} else {
// ---- 非面对面拼团(有 groupId 但 isFace != 1)----
if (StringUtils.isNotBlank(delivery.getOrderId())) {
cookTimeoutOrderIds.add(delivery.getOrderId());
}
if (StringUtils.isNotBlank(delivery.getOrderId()) && order != null
&& order.getStatus() == ORDER_STATUS_WAIT_DELIVERY) {
orderIdsSend.add(delivery.getOrderId());
@ -303,11 +395,16 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
userPendingOrderCacheUtil.update(order.getUserId(), orderVO);
pushOrderIds.add(delivery.getOrderId());
numberCode = latestSeq;
if (goodsList.isEmpty()) {
goodsList = mallOrderGoodsMapper.selectByOrderId(delivery.getOrderId());
}
}
}
} else {
// ---- 无 groupId(普通单)----
if (StringUtils.isNotBlank(delivery.getOrderId())) {
cookTimeoutOrderIds.add(delivery.getOrderId());
}
if (StringUtils.isNotBlank(delivery.getOrderId()) && order != null
&& order.getStatus() == ORDER_STATUS_WAIT_DELIVERY) {
orderIdsSend.add(delivery.getOrderId());
@ -322,16 +419,22 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
userPendingOrderCacheUtil.update(order.getUserId(), orderVO);
pushOrderIds.add(delivery.getOrderId());
numberCode = latestSeq;
if (goodsList.isEmpty()) {
goodsList = mallOrderGoodsMapper.selectByOrderId(delivery.getOrderId());
}
}
}
// 更新配送单(含订单编号)
uw.set(MallDeliveryOrder::getNumberCode, numberCode);
this.update(uw);
// 触发商家出餐超时提醒(传入已有订单,避免重复查 DB)
triggerShopCookTimeoutEvent(orderIdsSend, order);
// 出餐超时提醒只依赖本次接单成功的配送单,不依赖订单状态更新分支是否命中。
triggerShopCookTimeoutEvent(cookTimeoutOrderIds, order);
if (transferDelivery && StringUtils.isNotBlank(delivery.getOrderId())) {
long transferDelayMillis = Math.max(1, transferArriveMinutes) * 60 * 1000L;
orderAsyncProducer.sendDelayMessage(delivery.getOrderId(), "Transfer_Arrive_Timeout", transferDelayMillis);
}
// 放进配送员缓存
delivery.setGoodsList(goodsList);
@ -343,7 +446,7 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
// 极光推送:异步执行,不阻塞主事务(clientId 已在上方查好)
final String finalPushClientId = pushClientId;
if (StringUtils.isNotBlank(finalPushClientId) && !pushOrderIds.isEmpty()) {
if (Boolean.TRUE.equals(notifyShop) && StringUtils.isNotBlank(finalPushClientId) && !pushOrderIds.isEmpty()) {
String firstOrderId = pushOrderIds.get(0);
new Thread(() -> {
try {
@ -358,6 +461,10 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
return 3;
}
private String generateTransferPickupCode() {
return String.format("%04d", new Random().nextInt(10000));
}
/**
* 构建返回 VO
*/
@ -388,12 +495,10 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
Long sharedCookTimeMins = null;
if (StringUtils.isNotBlank(knownShopId)) {
try {
LambdaQueryWrapper<ShopTakeaway> 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<MallDeliveryOrderM
try {
MallOrder o = mallOrderService.getById(id);
if (o != null && StringUtils.isNotBlank(o.getShopId())) {
LambdaQueryWrapper<ShopTakeaway> 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<MallDeliveryOrderM
.set(MallDeliveryOrder::getWorkerName, newWorkerName)
.set(MallDeliveryOrder::getWorkerPhone, newWorkerPhone);
this.update(uw);
delivery.setWorkerId(newWorkerId);
delivery.setWorkerName(newWorkerName);
delivery.setWorkerPhone(newWorkerPhone);
waitOrderCacheUtil.update(delivery.getRegionId(), delivery);
log.info("配送单 {} 重新指定配送员: workerId={}, workerName={}", deliveryId, newWorkerId, newWorkerName);
}

173
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderGroupServiceImpl.java

@ -1,18 +1,23 @@
package cc.hiver.mall.serviceimpl.mybatis;
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.service.WorkerService;
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.pojo.dto.ShopCacheDTO;
import cc.hiver.mall.pojo.vo.MallOrderVO;
import cc.hiver.mall.service.ShopService;
import cc.hiver.mall.service.mybatis.MallDeliveryOrderService;
import cc.hiver.mall.service.mybatis.MallOrderGroupService;
import cc.hiver.mall.service.mybatis.MallOrderService;
import cc.hiver.mall.service.mybatis.MallUserCouponService;
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.extension.service.impl.ServiceImpl;
@ -86,6 +91,10 @@ public class MallOrderGroupServiceImpl extends ServiceImpl<MallOrderGroupMapper,
@Autowired
private MallDeliveryOrderMapper mallDeliveryOrderMapper;
@Autowired
@Lazy
private MallDeliveryOrderService mallDeliveryOrderService;
@Autowired
private WaitOrderCacheUtil waitOrderCacheUtil;
@ -110,6 +119,9 @@ public class MallOrderGroupServiceImpl extends ServiceImpl<MallOrderGroupMapper,
@Autowired
private MallReturnOrderGoodsMapper mallReturnOrderGoodsMapper;
@Autowired
private RedisTemplateHelper redisTemplateHelper;
/**
* 检查是否成团满足则自动激活所有子订单
*/
@ -146,6 +158,9 @@ public class MallOrderGroupServiceImpl extends ServiceImpl<MallOrderGroupMapper,
List<MallOrder> waitingOrders = mallOrderService.list(oqw);
boolean isFace2Face = group.getIsFace() != null && group.getIsFace() == 1;
List<MallOrder> deliveryOrders = new ArrayList<>();
List<MallOrder> autoAcceptOrders = new ArrayList<>();
List<MallDeliveryOrder> autoAcceptDeliveries = new ArrayList<>();
for (MallOrder order : waitingOrders) {
int targetStatus = (order.getDeliveryType() != null && order.getDeliveryType() == 1) ? 2 : 3;
@ -154,33 +169,45 @@ public class MallOrderGroupServiceImpl extends ServiceImpl<MallOrderGroupMapper,
if(targetStatus == 3){
String latestSeq = merchantOrderSeqUtil.generateOrderSequence(order.getShopId(), 2);
ouw.eq(MallOrder::getId, order.getId())
.eq(MallOrder::getStatus, ORDER_STATUS_WAIT_GROUP)
.set(MallOrder::getStatus, targetStatus)
.set(MallOrder::getNumberCode, latestSeq);
if(order.getUserRequireMake() != null && order.getUserRequireMake() == 1){
jPushService.sendPushNotification(shopService.findById(order.getShopId()).getClientId(), "您有一笔新的到店订单",order.getId());
Shop shop = getShopFromCache(order.getRegionId(), order.getShopId());
if (shop != null && StringUtils.isNotBlank(shop.getClientId())) {
jPushService.sendPushNotification(shop.getClientId(), "您有一笔新的到店订单",order.getId());
}
// 触发商家出餐超时监听
triggerShopCookTimeoutEvent(order);
}
}else{
ouw.eq(MallOrder::getId, order.getId())
.eq(MallOrder::getStatus, ORDER_STATUS_WAIT_GROUP)
.set(MallOrder::getStatus, targetStatus);
}
mallOrderService.update(ouw);
if (!mallOrderService.update(ouw)) {
continue;
}
if (targetStatus == 2) {
notifyShopDeliveryOrder(order);
}
MallOrderVO orderVO = buildVO(order, null, null, null);
orderVO.setStatus(targetStatus);
userPendingOrderCacheUtil.update(order.getUserId(), orderVO);
if (order.getDeliveryType() != null && order.getDeliveryType() == 1) {
if (isFace2Face && !order.getId().equals(group.getHeadOrderId())) {
// 面对面团参团人共享团长配送单,因此参团人本身没有配送单,直接跳过
continue;
if (order.getDeliveryType() != null && order.getDeliveryType() == 1
&& (!isFace2Face || order.getId().equals(group.getHeadOrderId()))) {
deliveryOrders.add(order);
}
// 取出预创建的运单进行校验
}
for (MallOrder order : deliveryOrders) {
// 取出预创建的运单进行校验;面对面团此时整团订单状态已全部激活。
LambdaQueryWrapper<MallDeliveryOrder> dqw = new LambdaQueryWrapper<>();
dqw.eq(MallDeliveryOrder::getOrderId, order.getId()).last("LIMIT 1");
MallDeliveryOrder delivery = mallDeliveryOrderMapper.selectOne(dqw);
//mq处理
triggerDeliveryAsyncEvents(order.getId(),delivery);
triggerDeliveryAsyncEvents(order, delivery);
if (delivery != null) {
if (StringUtils.isNotBlank(delivery.getWorkerId())) {
boolean isValid = false;
@ -214,44 +241,131 @@ public class MallOrderGroupServiceImpl extends ServiceImpl<MallOrderGroupMapper,
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);
delivery.setMustFinishTime(calculateActivatedMustFinishTime(order, delivery));
mallDeliveryOrderMapper.updateById(delivery);
//抢单大厅缓存
if (isFace2Face) {
// 面对面团查所有人订单
// 2. 一次 IN 查询所有商品明细
goodsList = mallOrderGoodsMapper.selectByOrderIdsWait(orderIdList);
// 面对面团查所有人订单;保持原有查询条件,依赖上方先完成整团状态激活。
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<cc.hiver.mall.entity.ShopTakeaway> qw = new LambdaQueryWrapper<>();
qw.eq(cc.hiver.mall.entity.ShopTakeaway::getShopId, order.getShopId());
List<cc.hiver.mall.entity.ShopTakeaway> 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<MallOrderGroupMapper,
/**
* 触发配送订单异步事件微信推给骑手加入超时检查死信队列等
*/
private void triggerDeliveryAsyncEvents(String orderId,MallDeliveryOrder delivery) {
private void triggerDeliveryAsyncEvents(MallOrder order, MallDeliveryOrder delivery) {
String orderId = order.getId();
// 1. 发送微信通知(交由MQ)
orderAsyncProducer.sendWechatNotify(orderId);
// 商家自配送成团后会自动接单,不需要进入无人接单延时提醒流程;
// 出餐超时提醒由 autoAcceptShopDeliveryOrder -> workerAccept 继续投递。
if (Integer.valueOf(1).equals(order.getShopDelivery())) {
return;
}
// 配送单判断是否指定
if (delivery != null) {
boolean isSpecified = StringUtils.isNotBlank(delivery.getWorkerId());

196
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<MallOrderMapper, MallOrder
// 直购支付成功 -> 跳过待商家接单,直接进入待配送或待消费
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<MallDeliveryOrder> qw = new LambdaQueryWrapper<>();
qw.eq(MallDeliveryOrder::getOrderId, orderId);
MallDeliveryOrder deliveryOrder = mallDeliveryOrderMapper.selectOne(qw);
Date mustFinishTime = calculateActivatedMustFinishTime(order, deliveryOrder);
LambdaUpdateWrapper<MallDeliveryOrder> 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<MallDeliveryOrder> 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<MallOrderGoods> goodsList = mallOrderGoodsMapper.selectByOrderId(orderId);
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<MallOrder> uw = new LambdaUpdateWrapper<>();
@ -642,7 +647,10 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
orderVO.setStatus(STATUS_WAIT_PICKUP);
userPendingOrderCacheUtil.update(order.getUserId(), orderVO);
if(order.getUserRequireMake() != null && order.getUserRequireMake() == 1){
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);
}
@ -719,14 +727,18 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
/**
* 触发配送订单异步事件微信推给骑手加入超时检查死信队列等
*/
private void triggerDeliveryAsyncEvents(String orderId) {
private void triggerDeliveryAsyncEvents(MallOrder order, MallDeliveryOrder delivery) {
String orderId = order.getId();
// 1. 发送微信通知(交由MQ)
orderAsyncProducer.sendWechatNotify(orderId);
// 商家自配送支付成功后会自动接单,不需要进入无人接单延时提醒流程;
// 出餐超时提醒由 autoAcceptShopDeliveryOrder -> workerAccept 继续投递。
if (Integer.valueOf(1).equals(order.getShopDelivery())) {
return;
}
// 获取配送单以判断是否指定
LambdaQueryWrapper<MallDeliveryOrder> 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<MallOrderMapper, MallOrder
if (order != null) {
long cookTimeMins = 15; // default
try {
LambdaQueryWrapper<cc.hiver.mall.entity.ShopTakeaway> qw = new LambdaQueryWrapper<>();
qw.eq(cc.hiver.mall.entity.ShopTakeaway::getShopId, order.getShopId());
List<cc.hiver.mall.entity.ShopTakeaway> 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<MallOrderMapper, MallOrder
LambdaUpdateWrapper<MallOrder> 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<MallOrderMapper, MallOrder
result.put("pendingBadReviewCount", pendingBadReviewCount);
Integer refundCount = this.baseMapper.selectRefundCount(shopId);
result.put("refundCount", refundCount);
Integer shopDeliveryPendingCount = this.baseMapper.selectShopDeliveryPendingCount(shopId);
result.put("shopDeliveryPendingCount", shopDeliveryPendingCount == null ? 0 : shopDeliveryPendingCount);
Integer transferDeliveryPendingCount = this.baseMapper.selectTransferDeliveryPendingCount(shopId);
result.put("transferDeliveryPendingCount", transferDeliveryPendingCount == null ? 0 : transferDeliveryPendingCount);
return result;
}
@ -1511,6 +1528,9 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
order.setOtherOrder(dto.getOtherOrder());
order.setReceiverPhone(user.getMobile());
order.setDeliveryType(dto.getDeliveryType());
order.setShopDelivery(DELIVERY_TYPE_EXPRESS == dto.getDeliveryType()
&& dto.getShopDelivery() != null && dto.getShopDelivery() == 1 ? 1 : 0);
order.setTransferDelivery(0);
order.setAddressId(dto.getAddressId());
order.setGetAreaId(dto.getGetAreaId());
order.setPutAreaId(dto.getPutAreaId());
@ -1559,17 +1579,55 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
// ---- 快照商家信息 ----
if (StringUtils.isNotBlank(dto.getShopId())) {
Shop shop = shopMapper.selectById(dto.getShopId());
Shop shop = getShopFromCache(dto.getRegionId(), dto.getShopId());
if (shop != null) {
order.setShopName(shop.getShopName());
order.setShopPhone(shop.getContactPhone());
order.setShopAddress(shop.getShopAddress());
if (DELIVERY_TYPE_EXPRESS == dto.getDeliveryType()
&& Integer.valueOf(1).equals(shop.getSupportTransferDelivery())) {
order.setTransferDelivery(1);
order.setTransferAddressId(shop.getTransferAddressId());
order.setTransferAddressName(shop.getTransferAddressName());
}
}
}
return order;
}
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 deliveryOrder) {
if (order == null || order.getShopDelivery() == null || order.getShopDelivery() != 1 || deliveryOrder == null) {
return;
}
if (StringUtils.isBlank(deliveryOrder.getWorkerId())) {
log.warn("商家自配送订单未绑定配送员,无法自动接单 orderId={}", order.getId());
return;
}
mallDeliveryOrderService.workerAccept(
deliveryOrder.getWorkerPhone(),
deliveryOrder.getId(),
deliveryOrder.getWorkerId(),
deliveryOrder.getWorkerName(),
deliveryOrder.getGroupId() == null ? "" : deliveryOrder.getGroupId(),
false
);
}
/**
* 批量保存商品快照
*/
@ -1599,8 +1657,13 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
delivery.setDeliveryFee(overrideDeliveryFee != null ? overrideDeliveryFee : order.getDeliveryFee());
delivery.setCreateTime(new Date());
boolean transferDelivery = Integer.valueOf(1).equals(order.getTransferDelivery());
if (dto.getMustFinishTime() != null) {
delivery.setMustFinishTime(dto.getMustFinishTime());
} else if (transferDelivery) {
delivery.setMustFinishTime(calculateTransferMustFinishTime(dto.getRegionId(), dto.getShopId(), delivery.getDeliveryType()));
} else if (DELIVERY_TYPE_EXPRESS == dto.getDeliveryType()) {
delivery.setMustFinishTime(calculateMustFinishTime(dto.getRegionId(), dto.getShopId(), delivery.getDeliveryType()));
}
// 指定配送员
@ -1616,8 +1679,8 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
}
}
// 取货区域ID(商户所在区域,前端必传)
delivery.setGetAreaId(dto.getGetAreaId());
// 取货区域ID:中转单取中转点,普通单取商户所在区域。
delivery.setGetAreaId(transferDelivery ? order.getTransferAddressId() : dto.getGetAreaId());
// 送货区域ID:优先使用前端传入值,兜底从地址表查询
if (StringUtils.isNotBlank(dto.getPutAreaId())) {
@ -1636,10 +1699,81 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
delivery.setShopName(order.getShopName());
delivery.setShopPhone(order.getShopPhone());
delivery.setShopAddress(order.getShopAddress());
delivery.setTransferDelivery(transferDelivery ? 1 : 0);
delivery.setTransferAddressId(order.getTransferAddressId());
delivery.setTransferAddressName(order.getTransferAddressName());
if (transferDelivery) {
Integer duration = getTransferDeliveryDuration(order.getRegionId(), order.getShopId());
String transferName = StringUtils.defaultString(order.getTransferAddressName());
LocalDateTime arriveTime = LocalDateTime.now().plusMinutes(safeMinutes(getCookingTime(order.getRegionId(), order.getShopId())) + safeMinutes(duration));
String timeText = String.format("%02d:%02d", arriveTime.getHour(), arriveTime.getMinute());
delivery.setShopName(transferName + "取餐 - " + StringUtils.defaultString(order.getShopName()));
delivery.setShopAddress("商家预计" + timeText + "配送到-" + transferName);
}
mallDeliveryOrderMapper.insert(delivery);
}
private Date calculateTransferMustFinishTime(String regionId, String shopId, Integer deliveryType) {
Integer schoolDeliveryTime = 1 == (deliveryType == null ? 0 : deliveryType)
? AdminSeckillGroupController.getTotalDeliveryTimeByRegionIdAndShopId(redisTemplateHelper, regionId, shopId)
: 40;
Integer transferTime = getTransferDeliveryDuration(regionId, shopId);
int totalMinutes = safeMinutes(transferTime) + safeMinutes(schoolDeliveryTime);
LocalDateTime futureTime = LocalDateTime.now().plusMinutes(totalMinutes);
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 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 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;
}
/**
* 计算总配送费面对面团专用
*/
@ -1740,7 +1874,7 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
//计算已结算订单 商家同意售后时,按照商品原价-抽佣金额给商家加一条负数待结算记录,
// 未结算的订单结算时需要关联是否有售后减掉售后金额 商家需要有账单结算记录
//配送员同意售后时需要在他的账户余额减退款金额,订单配送完成给加钱
if(coupons != null){
if(coupons != null && coupons.size() > 0){
BigDecimal totalCoupons = coupons.stream()
.map(item -> item.getDiscountAmount()) // 提取 BigDecimal 金额
.reduce(BigDecimal.ZERO, BigDecimal::add);
@ -1779,7 +1913,12 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
}else if(refundTypeStatus == 3){
//商家、配送员都加一条
record.setLinkId(order.getShopId());
if(ratio != null){
record.setRefundAmount(order.getGoodsAmount().add(order.getPackageFee()).multiply(ratio).setScale(2, RoundingMode.HALF_UP));
}else{
record.setRefundAmount(order.getGoodsAmount().add(order.getPackageFee()).setScale(2, RoundingMode.HALF_UP));
}
/*if(ratio != null){
record.setRefundAmount(record.getRefundAmount()
.subtract(record.getRefundAmount().multiply(ratio)).setScale(2, RoundingMode.HALF_UP));
@ -1794,7 +1933,12 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
MallRefundRecord mallRefundRecord1 = record;
mallRefundRecord1.setId(SnowFlakeUtil.nextId().toString());
mallRefundRecord1.setLinkId(workerId);
if(ratio != null){
mallRefundRecord1.setRefundAmount(order.getDeliveryFee().multiply(ratio).setScale(2, RoundingMode.HALF_UP));
}else{
mallRefundRecord1.setRefundAmount(order.getDeliveryFee());
}
/*if(ratio != null){
mallRefundRecord1.setRefundAmount(mallRefundRecord1.getRefundAmount()
.subtract(mallRefundRecord1.getRefundAmount().multiply(ratio)).setScale(2, RoundingMode.HALF_UP));

10
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallRefundRecordServiceImpl.java

@ -121,7 +121,7 @@ public class MallRefundRecordServiceImpl extends ServiceImpl<MallRefundRecordMap
//计算已结算订单 商家同意售后时,按照商品原价-抽佣金额给商家加一条负数待结算记录,
// 未结算的订单结算时需要关联是否有售后减掉售后金额 商家需要有账单结算记录
//配送员同意售后时需要在他的账户余额减退款金额,订单配送完成给加钱
if(coupons != null){
if(coupons != null && coupons.size() > 0){
BigDecimal totalCoupons = coupons.stream()
.map(item -> item.getDiscountAmount()) // 提取 BigDecimal 金额
.reduce(BigDecimal.ZERO, BigDecimal::add);
@ -138,7 +138,7 @@ public class MallRefundRecordServiceImpl extends ServiceImpl<MallRefundRecordMap
if(mallRefundRecord.getRefundType() == 1){
// 退商品
mallRefundRecord.setLinkId(mallRefundRecord.getShopId());
if(ratio != null){
if(ratio != null && !ratio.equals(0)){
mallRefundRecord.setRefundAmount(mallRefundRecord.getRefundAmount()
.subtract(mallRefundRecord.getRefundAmount().multiply(ratio)).setScale(2, RoundingMode.HALF_UP));
}
@ -150,8 +150,10 @@ public class MallRefundRecordServiceImpl extends ServiceImpl<MallRefundRecordMap
}else if(mallRefundRecord.getRefundType() == 2){
// 退配送费
mallRefundRecord.setLinkId(mallRefundRecord.getWorkerId());
if(ratio != null){
mallRefundRecord.setRefundAmount(mallRefundRecord.getRefundAmount()
.subtract(mallRefundRecord.getRefundAmount().multiply(ratio)).setScale(2, RoundingMode.HALF_UP));
}
mallRefundRecordMapper.insert(mallRefundRecord);
//售后放到缓存
refundOrderCacheUtil.put(mallRefundRecord.getLinkId(),mallRefundRecord);
@ -177,8 +179,10 @@ public class MallRefundRecordServiceImpl extends ServiceImpl<MallRefundRecordMap
//商家、配送员都加一条
mallRefundRecord.setLinkId(mallRefundRecord.getShopId());
mallRefundRecord.setRefundAmount(mallRefundRecord.getGoodsAmount().add(mallRefundRecord.getPackageFee()));
if(ratio != null){
mallRefundRecord.setRefundAmount(mallRefundRecord.getRefundAmount()
.subtract(mallRefundRecord.getRefundAmount().multiply(ratio)).setScale(2, RoundingMode.HALF_UP));
}
mallRefundRecordMapper.insert(mallRefundRecord);
//售后放到缓存
refundOrderCacheUtil.put(mallRefundRecord.getLinkId(),mallRefundRecord);
@ -188,8 +192,10 @@ public class MallRefundRecordServiceImpl extends ServiceImpl<MallRefundRecordMap
mallRefundRecord1.setId(SnowFlakeUtil.nextId().toString());
mallRefundRecord1.setLinkId(mallRefundRecord.getWorkerId());
mallRefundRecord1.setRefundAmount(mallRefundRecord.getDeliveryFee());
if(ratio != null){
mallRefundRecord1.setRefundAmount(mallRefundRecord1.getRefundAmount()
.subtract(mallRefundRecord1.getRefundAmount().multiply(ratio)).setScale(2, RoundingMode.HALF_UP));
}
mallRefundRecordMapper.insert(mallRefundRecord1);
//售后放到缓存
refundOrderCacheUtil.put(mallRefundRecord1.getLinkId(),mallRefundRecord1);

9
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/AliOcrUtil.java

@ -43,6 +43,11 @@ import java.util.regex.Pattern;
public class AliOcrUtil {
private static final Pattern COMPILED = Pattern.compile("...");
private static final String DEFAULT_DASHSCOPE_API_KEY = "sk-bcfa4865b89548acb8225f910f13d682";
public static void prepareDashScopeApiKey(String configuredApiKey) {
Constants.apiKey = StringUtils.isNotBlank(configuredApiKey) ? configuredApiKey.trim() : DEFAULT_DASHSCOPE_API_KEY;
}
@Autowired
private static AliOcrConfig aliOcrConfig;
@ -107,7 +112,7 @@ public class AliOcrUtil {
messages.add(systemMessage);
messages.add(userMessage);
final MultiModalConversationParam param = MultiModalConversationParam.builder()
.model("qwen-vl-max")
.model("qwen3.6-flash")
.messages(messages)
.build();
stopWatch.start("一轮会话");
@ -531,7 +536,7 @@ public class AliOcrUtil {
}
})).build();
final MultiModalConversationParam param = MultiModalConversationParam.builder()
.model("qwen-vl-max")
.model("qwen3.6-flash")
.message(userMessage)
/*.stream(true)
.temperature(1F)

19
hiver-modules/hiver-mall/src/main/resources/db/ie_real_name_auth_fields.sql

@ -0,0 +1,19 @@
ALTER TABLE ie_user_profile
ADD COLUMN real_name_auth_status TINYINT NOT NULL DEFAULT 0 COMMENT '实名认证状态:0未认证,1已通过,2未成年,3失败',
ADD COLUMN real_name_encrypted VARCHAR(512) NULL COMMENT 'AES-GCM加密后的真实姓名',
ADD COLUMN id_card_encrypted VARCHAR(512) NULL COMMENT 'AES-GCM加密后的身份证号',
ADD COLUMN real_name_masked VARCHAR(64) NULL COMMENT '脱敏姓名,仅用于前端展示',
ADD COLUMN id_card_masked VARCHAR(64) NULL COMMENT '脱敏身份证号,仅用于前端展示',
ADD COLUMN birth_date VARCHAR(10) NULL COMMENT '身份证解析生日,yyyy-MM-dd',
ADD COLUMN age INT NULL COMMENT '实名校验时计算出的年龄',
ADD COLUMN real_name_auth_time DATETIME NULL COMMENT '实名通过时间',
ADD COLUMN student_card_auth_status TINYINT NOT NULL DEFAULT 0 COMMENT '学生证认证状态:0未上传,1已通过,2首次识别未通过',
ADD COLUMN student_card_image_url VARCHAR(500) NULL COMMENT '最近一次学生证图片地址',
ADD COLUMN student_card_verify_count INT NOT NULL DEFAULT 0 COMMENT '学生证识别上传次数',
ADD COLUMN student_card_auth_time DATETIME NULL COMMENT '学生证认证时间';
ALTER TABLE ie_user_profile
ADD INDEX idx_ie_profile_real_name_auth (real_name_auth_status, age);
ALTER TABLE ie_user_profile
ADD INDEX idx_ie_profile_student_card_auth (student_card_auth_status);

30
hiver-modules/hiver-mall/src/main/resources/db/shop_self_delivery.sql

@ -0,0 +1,30 @@
ALTER TABLE t_shop
ADD COLUMN support_shop_delivery TINYINT NOT NULL DEFAULT 0 COMMENT '是否支持商家自配送 0否 1是',
ADD COLUMN order_bkge DECIMAL(10,1) NOT NULL DEFAULT 0.0 COMMENT '商家自配送配送费',
ADD COLUMN shop_delivery_location TINYINT NULL COMMENT '商家自配送送达位置 1送到宿舍 2送到楼下',
ADD COLUMN shop_delivery_duration INT NULL COMMENT '商家自配送配送时长(分钟)',
ADD COLUMN worker_id VARCHAR(64) NULL COMMENT '商家自配送配送员ID',
ADD COLUMN worker_name VARCHAR(64) NULL COMMENT '商家自配送配送员姓名',
ADD COLUMN worker_phone VARCHAR(32) NULL COMMENT '商家自配送配送员电话';
ALTER TABLE mall_order
ADD COLUMN shop_delivery TINYINT NOT NULL DEFAULT 0 COMMENT '是否商家自配送 0否 1是';
ALTER TABLE t_shop
ADD COLUMN support_transfer_delivery TINYINT NOT NULL DEFAULT 0 COMMENT '是否支持中转配送 0否 1是',
ADD COLUMN transfer_delivery_duration INT NULL COMMENT '中转配送时长(分钟)',
ADD COLUMN transfer_address_id VARCHAR(64) NULL COMMENT '中转地点ID',
ADD COLUMN transfer_address_name VARCHAR(255) NULL COMMENT '中转地点名称';
ALTER TABLE mall_order
ADD COLUMN transfer_delivery TINYINT NOT NULL DEFAULT 0 COMMENT '是否中转配送 0否 1是',
ADD COLUMN transfer_address_id VARCHAR(64) NULL COMMENT '中转地点ID',
ADD COLUMN transfer_address_name VARCHAR(255) NULL COMMENT '中转地点名称';
ALTER TABLE mall_delivery_order
ADD COLUMN transfer_delivery TINYINT NOT NULL DEFAULT 0 COMMENT '是否中转配送 0否 1是',
ADD COLUMN transfer_address_id VARCHAR(64) NULL COMMENT '中转地点ID',
ADD COLUMN transfer_address_name VARCHAR(255) NULL COMMENT '中转地点名称',
ADD COLUMN transfer_arrive_time DATETIME NULL COMMENT '商家送达中转点时间',
ADD COLUMN transfer_arrive_image VARCHAR(500) NULL COMMENT '商家送达中转点图片',
ADD COLUMN transfer_pickup_code VARCHAR(8) NULL COMMENT '中转取餐码';

3
hiver-modules/hiver-mall/src/main/resources/db/worker_delivery_location.sql

@ -0,0 +1,3 @@
-- 为 t_worker 表添加配送员送达地点配置
ALTER TABLE `t_worker`
ADD COLUMN `delivery_location` TINYINT NULL DEFAULT 0 COMMENT '送达地点:0送上楼 1送到宿舍门口' AFTER `high_floor_fee`;

69
hiver-modules/hiver-mall/src/main/resources/mapper/MallDeliveryOrderMapper.xml

@ -40,6 +40,12 @@
<result column="arrive_time" property="arriveTime"/>
<result column="new_worker" property="newWorker"/>
<result column="worker_phone" property="workerPhone"/>
<result column="transfer_delivery" property="transferDelivery"/>
<result column="transfer_address_id" property="transferAddressId"/>
<result column="transfer_address_name" property="transferAddressName"/>
<result column="transfer_arrive_time" property="transferArriveTime"/>
<result column="transfer_arrive_image" property="transferArriveImage"/>
<result column="transfer_pickup_code" property="transferPickupCode"/>
</resultMap>
<!-- 分页查询配送单,支持多条件过滤(抢单大厅:hallOnly=true时只查workerId为空的) -->
@ -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
<where>
<if test="q.regionId != null and q.regionId != ''">
@ -93,8 +101,27 @@
<if test="q.deliveryType != null">
AND d.delivery_type = #{q.deliveryType}
</if>
<if test="q.shopDelivery != null">
AND EXISTS (
SELECT 1 FROM mall_order o
WHERE o.id = d.order_id
AND o.shop_delivery = #{q.shopDelivery}
)
</if>
<if test="q.transferDelivery != null">
AND d.transfer_delivery = #{q.transferDelivery}
</if>
<if test="q.transferArriveEmpty != null and q.transferArriveEmpty == true">
AND d.transfer_arrive_time IS NULL
</if>
</where>
ORDER BY
<choose>
<when test="q.transferDelivery != null and q.transferDelivery == 1 and q.order != null and q.order == 'transferDelivery'">
(CASE WHEN d.transfer_arrive_time IS NULL THEN 0 ELSE 1 END) ASC,
d.create_time DESC
</when>
<otherwise>
<if test="q.order != null and q.order == 'deliveryFee'">
d.delivery_fee DESC,
</if>
@ -115,6 +142,8 @@
</if>
(CASE WHEN d.must_finish_time IS NULL THEN 1 ELSE 0 END) ASC,
d.must_finish_time ASC,d.delivery_fee DESC
</otherwise>
</choose>
</select>
<select id="selectPageVOByStatus" resultMap="deliveryMap">
@ -125,7 +154,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.status not in (-1,4)
@ -163,6 +194,19 @@
<if test="q.deliveryType != null">
AND d.delivery_type = #{q.deliveryType}
</if>
<if test="q.shopDelivery != null">
AND EXISTS (
SELECT 1 FROM mall_order o
WHERE o.id = d.order_id
AND o.shop_delivery = #{q.shopDelivery}
)
</if>
<if test="q.transferDelivery != null">
AND d.transfer_delivery = #{q.transferDelivery}
</if>
<if test="q.transferArriveEmpty != null and q.transferArriveEmpty == true">
AND d.transfer_arrive_time IS NULL
</if>
</where>
ORDER BY (CASE WHEN d.must_finish_time IS NULL THEN 1 ELSE 0 END) ASC
</select>
@ -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
<where>
d.status not in (-1,4)
@ -213,6 +259,19 @@
<if test="q.deliveryType != null">
AND d.delivery_type = #{q.deliveryType}
</if>
<if test="q.shopDelivery != null">
AND EXISTS (
SELECT 1 FROM mall_order o
WHERE o.id = d.order_id
AND o.shop_delivery = #{q.shopDelivery}
)
</if>
<if test="q.transferDelivery != null">
AND d.transfer_delivery = #{q.transferDelivery}
</if>
<if test="q.transferArriveEmpty != null and q.transferArriveEmpty == true">
AND d.transfer_arrive_time IS NULL
</if>
</where>
ORDER BY d.finish_time desc
</select>
@ -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
<if test='orderId != null and orderId.trim() neq ""'>

31
hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml

@ -34,6 +34,10 @@
<result column="is_free_order" property="isFreeOrder"/>
<result column="free_amount" property="freeAmount"/>
<result column="is_pack" property="isPack"/>
<result column="shop_delivery" property="shopDelivery"/>
<result column="transfer_delivery" property="transferDelivery"/>
<result column="transfer_address_id" property="transferAddressId"/>
<result column="transfer_address_name" property="transferAddressName"/>
</resultMap>
<!-- 公共查询条件:店铺ID -->
@ -99,6 +103,24 @@
AND status in (0,3)
</select>
<select id="selectShopDeliveryPendingCount" parameterType="string" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM mall_order
WHERE shop_id = #{shopId}
AND shop_delivery = 1
AND delivery_type = 1
AND `status` IN (2,3,4)
</select>
<select id="selectTransferDeliveryPendingCount" parameterType="string" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM mall_delivery_order
WHERE shop_id = #{shopId}
AND transfer_delivery = 1
AND `status` = 1
AND transfer_arrive_time IS NULL
</select>
<!-- 分页查询订单(不挂载商品明细,由Service层补填) -->
<select id="selectPageVO" resultMap="mallOrderVOMap">
SELECT
@ -108,7 +130,8 @@
o.receiver_name, o.receiver_phone, o.receiver_address,
o.shop_name, o.shop_phone, o.shop_address,o.number_code,o.region_id,
o.settlement_status,o.shop_make_time,o.other_order,o.user_require_make
,o.is_free_order,o.free_amount,o.is_pack
,o.is_free_order,o.free_amount,o.is_pack,o.shop_delivery,
o.transfer_delivery,o.transfer_address_id,o.transfer_address_name
FROM mall_order o
<where>
<if test="q.userId != null and q.userId != ''">
@ -194,6 +217,9 @@
<if test="q.deliveryType != null">
AND o.delivery_type = #{q.deliveryType}
</if>
<if test="q.shopDelivery != null">
AND o.shop_delivery = #{q.shopDelivery}
</if>
<if test="q.startDate != null and q.startDate != ''">
AND DATE(o.create_time) &gt;= #{q.startDate}
</if>
@ -212,7 +238,8 @@
o.receiver_name, o.receiver_phone, o.receiver_address,
o.shop_name, o.shop_phone, o.shop_address,o.number_code,o.region_id,
o.settlement_status,o.shop_make_time,o.other_order,o.user_require_make
,o.is_free_order,o.free_amount,o.is_pack
,o.is_free_order,o.free_amount,o.is_pack,o.shop_delivery,
o.transfer_delivery,o.transfer_address_id,o.transfer_address_name
FROM mall_order o LEFT JOIN mall_order_group og ON o.id = og.head_order_id
<where>
<if test="groupId != null and groupId != ''">

154
hiver-modules/hiver-mall/src/main/resources/mapper/ShopMapper.xml

@ -44,6 +44,17 @@
<result column="is_student" property="isStudent" jdbcType="INTEGER" />
<result column="client_id" property="clientId" jdbcType="VARCHAR" />
<result column="merchant_type" property="merchantType" jdbcType="INTEGER" />
<result column="support_shop_delivery" property="supportShopDelivery" jdbcType="INTEGER" />
<result column="order_bkge" property="orderBkge" jdbcType="DECIMAL" />
<result column="shop_delivery_location" property="shopDeliveryLocation" jdbcType="INTEGER" />
<result column="shop_delivery_duration" property="shopDeliveryDuration" jdbcType="INTEGER" />
<result column="worker_id" property="workerId" jdbcType="VARCHAR" />
<result column="worker_name" property="workerName" jdbcType="VARCHAR" />
<result column="worker_phone" property="workerPhone" jdbcType="VARCHAR" />
<result column="support_transfer_delivery" property="supportTransferDelivery" jdbcType="INTEGER" />
<result column="transfer_delivery_duration" property="transferDeliveryDuration" jdbcType="INTEGER" />
<result column="transfer_address_id" property="transferAddressId" jdbcType="VARCHAR" />
<result column="transfer_address_name" property="transferAddressName" jdbcType="VARCHAR" />
</resultMap>
<resultMap id="ResultMapWithBLOBs" type="cc.hiver.mall.entity.Shop" extends="BaseResultMap" >
<result column="remark" property="remark" jdbcType="LONGVARCHAR" />
@ -53,7 +64,9 @@
region_id, shop_area, shop_area_title, shop_type, shop_type_title, shop_address, year_fee, charge_time, start_time,
end_time, status, remark, business_district_level, contact_phone, shop_icon, defaulted, ali_account, ali_name,
balance, attr_id, printing_method,shop_images,shop_lename,shop_lecard,
shop_score,isbrandflag,subtitle,shoprank,sale_count,is_student,client_id,merchant_type
shop_score,isbrandflag,subtitle,shoprank,sale_count,is_student,client_id,merchant_type,
support_shop_delivery,order_bkge,shop_delivery_location,shop_delivery_duration,worker_id,worker_name,worker_phone,
support_transfer_delivery,transfer_delivery_duration,transfer_address_id,transfer_address_name
</sql>
<sql id="Blob_Column_List" >
remark
@ -75,11 +88,15 @@ balance, attr_id, printing_method,shop_images,shop_lename,shop_lecard,
region_id, shop_area, shop_area_title, shop_type, shop_type_title, shop_address, year_fee, charge_time, start_time,
end_time, status, remark, business_district_level, contact_phone, shop_icon, defaulted, ali_account, ali_name,
balance, attr_id, printing_method, shop_images, shop_lename, shop_lecard, shop_score,
isbrandflag, subtitle,shoprank, sale_count,is_student,client_id,merchant_type)
isbrandflag, subtitle,shoprank, sale_count,is_student,client_id,merchant_type,
support_shop_delivery, order_bkge, shop_delivery_location, shop_delivery_duration, worker_id, worker_name, worker_phone,
support_transfer_delivery, transfer_delivery_duration, transfer_address_id, transfer_address_name)
values (#{id,jdbcType=VARCHAR}, #{createBy,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP}, #{delFlag,jdbcType=INTEGER}, #{updateBy,jdbcType=VARCHAR}, #{updateTime,jdbcType=TIMESTAMP}, #{shopName,jdbcType=VARCHAR}, #{shopOwnerId,jdbcType=VARCHAR}, #{shopMangerId,jdbcType=VARCHAR}, #{region,jdbcType=VARCHAR},
#{regionId,jdbcType=VARCHAR}, #{shopArea,jdbcType=VARCHAR}, #{shopAreaTitle,jdbcType=VARCHAR}, #{shopType,jdbcType=VARCHAR}, #{shopTypeTitle,jdbcType=VARCHAR}, #{shopAddress,jdbcType=VARCHAR}, #{yearFee,jdbcType=VARCHAR}, #{chargeTime,jdbcType=VARCHAR}, #{startTime,jdbcType=VARCHAR},
#{endTime,jdbcType=VARCHAR}, #{status,jdbcType=INTEGER}, #{remark,jdbcType=LONGVARCHAR}, #{businessDistrictLevel,jdbcType=VARCHAR}, #{contactPhone,jdbcType=VARCHAR}, #{shopIcon,jdbcType=VARCHAR}, #{defaulted,jdbcType=INTEGER}, #{aliAccount,jdbcType=VARCHAR}, #{aliName,jdbcType=VARCHAR},
#{balance,jdbcType=VARCHAR}, #{attrId,jdbcType=VARCHAR}, #{printingMethod,jdbcType=VARCHAR}, #{shopImages,jdbcType=VARCHAR}, #{shopLename,jdbcType=VARCHAR}, #{shopLecard,jdbcType=VARCHAR}, #{shopScore,jdbcType=DECIMAL}, #{isbrandflag,jdbcType=INTEGER}, #{subtitle,jdbcType=VARCHAR}, #{shoprank,jdbcType=INTEGER}, #{saleCount,jdbcType=INTEGER}, #{isStudent,jdbcType=INTEGER},#{clientId,jdbcType=VARCHAR}, #{merchantType,jdbcType=INTEGER})
#{balance,jdbcType=VARCHAR}, #{attrId,jdbcType=VARCHAR}, #{printingMethod,jdbcType=VARCHAR}, #{shopImages,jdbcType=VARCHAR}, #{shopLename,jdbcType=VARCHAR}, #{shopLecard,jdbcType=VARCHAR}, #{shopScore,jdbcType=DECIMAL}, #{isbrandflag,jdbcType=INTEGER}, #{subtitle,jdbcType=VARCHAR}, #{shoprank,jdbcType=INTEGER}, #{saleCount,jdbcType=INTEGER}, #{isStudent,jdbcType=INTEGER},#{clientId,jdbcType=VARCHAR}, #{merchantType,jdbcType=INTEGER},
#{supportShopDelivery,jdbcType=INTEGER}, #{orderBkge,jdbcType=DECIMAL}, #{shopDeliveryLocation,jdbcType=INTEGER}, #{shopDeliveryDuration,jdbcType=INTEGER}, #{workerId,jdbcType=VARCHAR}, #{workerName,jdbcType=VARCHAR}, #{workerPhone,jdbcType=VARCHAR},
#{supportTransferDelivery,jdbcType=INTEGER}, #{transferDeliveryDuration,jdbcType=INTEGER}, #{transferAddressId,jdbcType=VARCHAR}, #{transferAddressName,jdbcType=VARCHAR})
</insert>
<insert id="insertSelective" parameterType="cc.hiver.mall.entity.Shop" >
insert into t_shop
@ -178,13 +195,46 @@ balance, attr_id, printing_method,shop_images,shop_lename,shop_lecard,
sale_count,
</if>
<if test="isStudent != null" >
is_student
is_student,
</if>
<if test="clientId != null" >
client_id
client_id,
</if>
<if test="merchantType != null" >
merchant_type
merchant_type,
</if>
<if test="supportShopDelivery != null" >
support_shop_delivery,
</if>
<if test="orderBkge != null" >
order_bkge,
</if>
<if test="shopDeliveryLocation != null" >
shop_delivery_location,
</if>
<if test="shopDeliveryDuration != null" >
shop_delivery_duration,
</if>
<if test="workerId != null" >
worker_id,
</if>
<if test="workerName != null" >
worker_name,
</if>
<if test="workerPhone != null" >
worker_phone,
</if>
<if test="supportTransferDelivery != null" >
support_transfer_delivery,
</if>
<if test="transferDeliveryDuration != null" >
transfer_delivery_duration,
</if>
<if test="transferAddressId != null" >
transfer_address_id,
</if>
<if test="transferAddressName != null" >
transfer_address_name,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides="," >
@ -290,6 +340,39 @@ balance, attr_id, printing_method,shop_images,shop_lename,shop_lecard,
<if test="merchantType != null" >
#{merchantType,jdbcType=INTEGER},
</if>
<if test="supportShopDelivery != null" >
#{supportShopDelivery,jdbcType=INTEGER},
</if>
<if test="orderBkge != null" >
#{orderBkge,jdbcType=DECIMAL},
</if>
<if test="shopDeliveryLocation != null" >
#{shopDeliveryLocation,jdbcType=INTEGER},
</if>
<if test="shopDeliveryDuration != null" >
#{shopDeliveryDuration,jdbcType=INTEGER},
</if>
<if test="workerId != null" >
#{workerId,jdbcType=VARCHAR},
</if>
<if test="workerName != null" >
#{workerName,jdbcType=VARCHAR},
</if>
<if test="workerPhone != null" >
#{workerPhone,jdbcType=VARCHAR},
</if>
<if test="supportTransferDelivery != null" >
#{supportTransferDelivery,jdbcType=INTEGER},
</if>
<if test="transferDeliveryDuration != null" >
#{transferDeliveryDuration,jdbcType=INTEGER},
</if>
<if test="transferAddressId != null" >
#{transferAddressId,jdbcType=VARCHAR},
</if>
<if test="transferAddressName != null" >
#{transferAddressName,jdbcType=VARCHAR},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="cc.hiver.mall.entity.Shop" >
@ -394,6 +477,39 @@ balance, attr_id, printing_method,shop_images,shop_lename,shop_lecard,
<if test="merchantType != null" >
merchant_type = #{merchantType,jdbcType=INTEGER},
</if>
<if test="supportShopDelivery != null" >
support_shop_delivery = #{supportShopDelivery,jdbcType=INTEGER},
</if>
<if test="orderBkge != null" >
order_bkge = #{orderBkge,jdbcType=DECIMAL},
</if>
<if test="shopDeliveryLocation != null" >
shop_delivery_location = #{shopDeliveryLocation,jdbcType=INTEGER},
</if>
<if test="shopDeliveryDuration != null" >
shop_delivery_duration = #{shopDeliveryDuration,jdbcType=INTEGER},
</if>
<if test="workerId != null" >
worker_id = #{workerId,jdbcType=VARCHAR},
</if>
<if test="workerName != null" >
worker_name = #{workerName,jdbcType=VARCHAR},
</if>
<if test="workerPhone != null" >
worker_phone = #{workerPhone,jdbcType=VARCHAR},
</if>
<if test="supportTransferDelivery != null" >
support_transfer_delivery = #{supportTransferDelivery,jdbcType=INTEGER},
</if>
<if test="transferDeliveryDuration != null" >
transfer_delivery_duration = #{transferDeliveryDuration,jdbcType=INTEGER},
</if>
<if test="transferAddressId != null" >
transfer_address_id = #{transferAddressId,jdbcType=VARCHAR},
</if>
<if test="transferAddressName != null" >
transfer_address_name = #{transferAddressName,jdbcType=VARCHAR},
</if>
</set>
where id = #{id,jdbcType=VARCHAR}
</update>
@ -431,7 +547,18 @@ balance, attr_id, printing_method,shop_images,shop_lename,shop_lecard,
sale_count = #{saleCount,jdbcType=INTEGER},
is_student = #{isStudent,jdbcType=INTEGER},
client_id = #{clientId,jdbcType=VARCHAR},
merchant_type = #{merchantType,jdbcType=INTEGER}
merchant_type = #{merchantType,jdbcType=INTEGER},
support_shop_delivery = #{supportShopDelivery,jdbcType=INTEGER},
order_bkge = #{orderBkge,jdbcType=DECIMAL},
shop_delivery_location = #{shopDeliveryLocation,jdbcType=INTEGER},
shop_delivery_duration = #{shopDeliveryDuration,jdbcType=INTEGER},
worker_id = #{workerId,jdbcType=VARCHAR},
worker_name = #{workerName,jdbcType=VARCHAR},
worker_phone = #{workerPhone,jdbcType=VARCHAR},
support_transfer_delivery = #{supportTransferDelivery,jdbcType=INTEGER},
transfer_delivery_duration = #{transferDeliveryDuration,jdbcType=INTEGER},
transfer_address_id = #{transferAddressId,jdbcType=VARCHAR},
transfer_address_name = #{transferAddressName,jdbcType=VARCHAR}
where id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKey" parameterType="cc.hiver.mall.entity.Shop" >
@ -476,7 +603,18 @@ balance, attr_id, printing_method,shop_images,shop_lename,shop_lecard,
sale_count = #{saleCount,jdbcType=INTEGER},
is_student = #{isStudent,jdbcType=INTEGER},
client_id = #{clientId,jdbcType=VARCHAR},
merchant_type = #{merchantType,jdbcType=INTEGER}
merchant_type = #{merchantType,jdbcType=INTEGER},
support_shop_delivery = #{supportShopDelivery,jdbcType=INTEGER},
order_bkge = #{orderBkge,jdbcType=DECIMAL},
shop_delivery_location = #{shopDeliveryLocation,jdbcType=INTEGER},
shop_delivery_duration = #{shopDeliveryDuration,jdbcType=INTEGER},
worker_id = #{workerId,jdbcType=VARCHAR},
worker_name = #{workerName,jdbcType=VARCHAR},
worker_phone = #{workerPhone,jdbcType=VARCHAR},
support_transfer_delivery = #{supportTransferDelivery,jdbcType=INTEGER},
transfer_delivery_duration = #{transferDeliveryDuration,jdbcType=INTEGER},
transfer_address_id = #{transferAddressId,jdbcType=VARCHAR},
transfer_address_name = #{transferAddressName,jdbcType=VARCHAR}
where id = #{id,jdbcType=VARCHAR}
</update>
<select id="findByUserId" resultType="cc.hiver.mall.entity.Shop">

4
hiver-modules/hiver-mall/src/main/resources/mapper/WorkerRelaPriceMapper.xml

@ -63,6 +63,7 @@
<result column="rebate_amount" property="rebateAmount"/>
<result column="geolocation" property="geolocation"/>
<result column="high_floor_fee" property="highFloorFee"/>
<result column="delivery_location" property="deliveryLocation"/>
<result column="order_bkge" property="orderBkge"/>
<result column="order_wait_count" property="orderWaitCount"/>
<result column="order_get_count" property="orderGetCount"/>
@ -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 @@
</if>
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
<choose>
<when test="sortField != null and sortField == 'score'">

Loading…
Cancel
Save