diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/api/feign/RemoteCanteenDeliveryOrderService.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/api/feign/RemoteCanteenDeliveryOrderService.java new file mode 100644 index 000000000..c8d58265b --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/api/feign/RemoteCanteenDeliveryOrderService.java @@ -0,0 +1,26 @@ +package com.bsd.pjcloud.canteen.api.feign; + +import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryOrderSyncDTO; +import com.bsd.pjcloud.common.core.constant.ServiceNameConstants; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.feign.annotation.NoToken; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 订单服务调用:同步外送单 / 取消外送单 + */ +@FeignClient(contextId = "remoteCanteenDeliveryOrderService", value = ServiceNameConstants.CANTEEN_SERVICE) +public interface RemoteCanteenDeliveryOrderService { + + + @PostMapping("/canteenDeliveryOrder/inner/sync-from-order") + @NoToken + R syncFromOrder(@RequestBody CanteenDeliveryOrderSyncDTO dto); + + @PostMapping("/canteenDeliveryOrder/inner/cancel-by-source-id") + @NoToken + R cancelBySourceOrderId(@RequestParam("sourceOrderId") String sourceOrderId); +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/api/feign/RemoteCanteenProductionTemplateService.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/api/feign/RemoteCanteenProductionTemplateService.java index 0db85939b..cae5d36c8 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/api/feign/RemoteCanteenProductionTemplateService.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/api/feign/RemoteCanteenProductionTemplateService.java @@ -19,12 +19,14 @@ public interface RemoteCanteenProductionTemplateService { /** * 根据菜品id列表获取加工模版详情 */ + @NoToken @PostMapping("/canteenProductionTemplate/inner/getDetailsByDishIds") R> getDetailsByDishIds(@RequestBody List dishIds); /** * 根据菜品id列表获取线上加工模版详情(仅 template_type=2,用于线上订餐统计) */ + @NoToken @PostMapping("/canteenProductionTemplate/inner/getDetailsByDishIdsForOnline") R> getDetailsByDishIdsForOnline(@RequestBody List dishIds); } diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryAssignType.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryAssignType.java new file mode 100644 index 000000000..ee6671a3e --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryAssignType.java @@ -0,0 +1,11 @@ +package com.bsd.pjcloud.canteen.constants; + +public final class CanteenDeliveryAssignType { + + private CanteenDeliveryAssignType() { + } + + public static final String NONE = "NONE"; + public static final String BIND = "BIND"; + public static final String ROUND_ROBIN = "ROUND_ROBIN"; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryOrderStatus.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryOrderStatus.java new file mode 100644 index 000000000..043be566a --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryOrderStatus.java @@ -0,0 +1,21 @@ +package com.bsd.pjcloud.canteen.constants; + +/** + * 外送单状态:待分配 → 待取餐 → 配送中 → 完成/取消 + */ +public final class CanteenDeliveryOrderStatus { + + private CanteenDeliveryOrderStatus() { + } + + /** 待分配骑手 */ + public static final int PENDING_ASSIGN = 0; + /** 已指派,待到店取餐(上传取餐照后进入配送中) */ + public static final int WAIT_PICKUP = 1; + /** 配送中 */ + public static final int DELIVERING = 2; + /** 已完成 */ + public static final int COMPLETED = 3; + /** 已取消 */ + public static final int CANCELLED = 4; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryPushConstants.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryPushConstants.java new file mode 100644 index 000000000..aae230889 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryPushConstants.java @@ -0,0 +1,18 @@ +package com.bsd.pjcloud.canteen.constants; + +/** + * 食堂外送 — 推送给骑手的 WebSocket 业务类型(messageText JSON 内字段 bizType) + */ +public final class CanteenDeliveryPushConstants { + + private CanteenDeliveryPushConstants() { + } + + /** + * 新外送单已指派给当前骑手(支付同步成功且默认池派单成功) + */ + public static final String BIZ_TYPE_ORDER_ASSIGNED = "CANTEEN_DELIVERY_ORDER_ASSIGNED"; + + /** messageText 内 schema 版本,前端兼容用 */ + public static final int PAYLOAD_VERSION = 1; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryCartItemDTO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryCartItemDTO.java new file mode 100644 index 000000000..400de08a7 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryCartItemDTO.java @@ -0,0 +1,24 @@ +package com.bsd.pjcloud.canteen.dto.delivery; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@Schema(description = "外送购物车行") +public class CanteenDeliveryCartItemDTO { + + private String id; + private String parentCarId; + /** 商品/SPU id */ + private String productId; + private String productName; + private String productPic; + /** 单价,分 */ + private Integer productPrice; + private Integer productCount; + private BigDecimal productWeight; + /** 行总价,分(兼容字符串) */ + private String totalPrice; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryOrderSyncDTO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryOrderSyncDTO.java new file mode 100644 index 000000000..58b0d6d94 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryOrderSyncDTO.java @@ -0,0 +1,62 @@ +package com.bsd.pjcloud.canteen.dto.delivery; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * 订单支付成功后由订单服务 Inner 调用同步外送单。 + *

+ * 派单规则: + * 1) 若传入 buildingId,则优先按「单位 + 楼栋绑定」指派骑手; + * 2) 否则(或绑定无效)按单位默认骑手池轮询指派。 + *

+ * 收货展示用 fullAddress 快照;性别匹配用 {@link #buyerUserId} 查询用户表,不要求订单侧传 gender。 + */ +@Data +@Schema(description = "外送单同步(订单回调)") +public class CanteenDeliveryOrderSyncDTO { + + @NotBlank + @Schema(description = "来源订单号(业务唯一)", requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + + @Schema(description = "商品摘要") + private String productDesc; + + @Schema(description = "收货人姓名") + private String username; + + @Schema(description = "收货人手机") + private String userMobile; + + @Schema(description = "商户名称") + private String merchantName; + + @Schema(description = "商户logo") + private String logoPic; + + @Schema(description = "购物车") + private List carList; + + // --------- 骑手/派单补充字段(由订单服务尽量传入)--------- + + @NotNull + @Schema(description = "所属单位部门ID(仅该单位下骑手可配送)", requiredMode = Schema.RequiredMode.REQUIRED) + private Long deptId; + + @Schema(description = "下单用户ID") + private Long buyerUserId; + + @Schema(description = "商户ID(字符串)") + private String merchantId; + + @Schema(description = "配送楼栋ID(可选;有则优先按楼栋绑定骑手)") + private Long buildingId; + + @Schema(description = "完整收货地址(展示用)") + private String fullAddress; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryPhotoDTO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryPhotoDTO.java new file mode 100644 index 000000000..802f8b475 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryPhotoDTO.java @@ -0,0 +1,18 @@ +package com.bsd.pjcloud.canteen.dto.delivery; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "外送上传照片") +public class CanteenDeliveryPhotoDTO { + + @NotNull + private Long id; + + @NotBlank + @Schema(description = "图片URL(先走你们统一上传拿地址)") + private String photoUrl; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDeliveryAssignSeqEntity.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDeliveryAssignSeqEntity.java new file mode 100644 index 000000000..12b17d8d3 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDeliveryAssignSeqEntity.java @@ -0,0 +1,21 @@ +package com.bsd.pjcloud.canteen.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 按单位轮询派单序号(无外键,不按租户拆分时以 dept_id 唯一) + */ +@Data +@TableName("canteen_delivery_assign_seq") +@EqualsAndHashCode(callSuper = true) +public class CanteenDeliveryAssignSeqEntity extends Model { + + @TableId(value = "dept_id", type = IdType.INPUT) + private Long deptId; + private Long seq; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDeliveryOrderEntity.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDeliveryOrderEntity.java new file mode 100644 index 000000000..c9ec46c45 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDeliveryOrderEntity.java @@ -0,0 +1,60 @@ +package com.bsd.pjcloud.canteen.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import com.bsd.pjcloud.common.core.util.TenantTable; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Data +@TenantTable +@TableName("canteen_delivery_order") +@EqualsAndHashCode(callSuper = true) +@Schema(description = "食堂外送单") +public class CanteenDeliveryOrderEntity extends Model { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private String sourceOrderId; + private Long deptId; + private Long buyerUserId; + private String merchantId; + private String productDesc; + private String receiverName; + private String receiverMobile; + private String merchantName; + private String merchantLogo; + private Long buildingId; + private Long unitId; + private Long roomId; + private String fullAddress; + private Integer buyerGender; + private String carListJson; + private String assignType; + private Long riderUserId; + private Integer status; + private String pickupPhotoUrl; + private String deliveryPhotoUrl; + + @TableField(fill = FieldFill.INSERT) + private String createBy; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + @TableLogic + @TableField(fill = FieldFill.INSERT) + private String delFlag; + + private Long tenantId; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenRiderBuildingBindEntity.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenRiderBuildingBindEntity.java new file mode 100644 index 000000000..0bbc8de33 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenRiderBuildingBindEntity.java @@ -0,0 +1,33 @@ +package com.bsd.pjcloud.canteen.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import com.bsd.pjcloud.common.core.util.TenantTable; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Data +@TenantTable +@TableName("canteen_rider_building_bind") +@EqualsAndHashCode(callSuper = true) +public class CanteenRiderBuildingBindEntity extends Model { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + private Long deptId; + private Long buildingId; + private Long riderUserId; + private Long tenantId; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + @TableLogic + @TableField(fill = FieldFill.INSERT) + private String delFlag; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenRiderPoolEntity.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenRiderPoolEntity.java new file mode 100644 index 000000000..955515666 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenRiderPoolEntity.java @@ -0,0 +1,33 @@ +package com.bsd.pjcloud.canteen.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import com.bsd.pjcloud.common.core.util.TenantTable; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Data +@TenantTable +@TableName("canteen_rider_pool") +@EqualsAndHashCode(callSuper = true) +public class CanteenRiderPoolEntity extends Model { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + private Long deptId; + private Long riderUserId; + private Integer sortOrder; + private Long tenantId; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + @TableLogic + @TableField(fill = FieldFill.INSERT) + private String delFlag; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/CanteenDeliveryOrderAppVO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/CanteenDeliveryOrderAppVO.java new file mode 100644 index 000000000..10fc94ae6 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/CanteenDeliveryOrderAppVO.java @@ -0,0 +1,30 @@ +package com.bsd.pjcloud.canteen.vo.app; + +import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryCartItemDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Schema(description = "骑手端外送单摘要") +public class CanteenDeliveryOrderAppVO { + + private Long id; + private String sourceOrderId; + private Long deptId; + private String productDesc; + private String receiverName; + private String receiverMobile; + private String merchantName; + private String merchantLogo; + private String fullAddress; + private Integer status; + private String pickupPhotoUrl; + private String deliveryPhotoUrl; + private LocalDateTime createTime; + + @Schema(description = "购物车行项目(由 car_list_json 反序列化)") + private List carList; +} diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/resources/META-INF/spring/org.springframework.cloud.openfeign.FeignClient.imports b/pjcloud-canteen/pjcloud-canteen-api/src/main/resources/META-INF/spring/org.springframework.cloud.openfeign.FeignClient.imports index 31ad18645..cb1edbde2 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/resources/META-INF/spring/org.springframework.cloud.openfeign.FeignClient.imports +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/resources/META-INF/spring/org.springframework.cloud.openfeign.FeignClient.imports @@ -3,4 +3,6 @@ com.bsd.pjcloud.canteen.api.feign.RemoteCanteenOnlineCategoryService com.bsd.pjcloud.canteen.api.feign.RemoteCanteenSpuService com.bsd.pjcloud.canteen.api.feign.RemoteOnlineCanteenSpuService com.bsd.pjcloud.canteen.api.feign.RemoteDailyMenuService +com.bsd.pjcloud.canteen.api.feign.RemoteCanteenDeliveryOrderService +com.bsd.pjcloud.canteen.api.feign.RemoteCanteenProductionTemplateService diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDeliveryOrderController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDeliveryOrderController.java new file mode 100644 index 000000000..543fbd124 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDeliveryOrderController.java @@ -0,0 +1,98 @@ +package com.bsd.pjcloud.canteen.controller; + +import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryOrderSyncDTO; +import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryPhotoDTO; +import com.bsd.pjcloud.canteen.push.CanteenDeliveryRiderPushPublisher; +import com.bsd.pjcloud.canteen.service.CanteenDeliveryOrderService; +import com.bsd.pjcloud.canteen.vo.app.CanteenDeliveryOrderAppVO; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.log.annotation.SysLog; +import com.bsd.pjcloud.common.security.annotation.Inner; +import com.bsd.pjcloud.common.security.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 食堂外送:订单同步 + 骑手 App + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/canteenDeliveryOrder") +@Tag(name = "食堂外送订单") +@SecurityRequirement(name = HttpHeaders.AUTHORIZATION) +public class CanteenDeliveryOrderController { + + private final CanteenDeliveryOrderService canteenDeliveryOrderService; + + private final CanteenDeliveryRiderPushPublisher riderPushPublisher; + + /** + * 联调:模拟「新单已指派」的 WebSocket 推送(与正式推送同一 Redis 频道与 messageText 结构)。 + * 骑手端须已用相同 userId 建立 WS;需管理端权限 {@code canteen_canteenDelivery_config}。 + */ + @SysLog("测试骑手WS推送") + @PostMapping("/test/mock-rider-push") + @PreAuthorize("@pms.hasPermission('canteen_canteenDelivery_config')") + @Operation(summary = "测试-模拟向骑手推送新单 WS 消息", description = "参数 riderUserId 为骑手用户主键;deliveryOrderId 可选,不传则用时间戳占位(详情页可能 404,仅测消息与跳转)") + public R mockRiderPush(@RequestParam Long riderUserId, + @RequestParam(required = false) Long deliveryOrderId) { + long fakeId = deliveryOrderId != null ? deliveryOrderId : System.currentTimeMillis(); + riderPushPublisher.publishOrderAssigned(riderUserId, fakeId, "MOCK-TEST-" + fakeId, "【测试】食堂", "【测试】收货地址"); + return R.ok(); + } + + @Inner + @PostMapping("/inner/sync-from-order") + @Operation(summary = "订单支付成功回调(内部服务)", + description = "由订单服务 Feign 调用:落库、按单位默认骑手池指派骑手;指派成功且事务提交后,经 Redis 向骑手 WebSocket 下发新单通知(见食堂文档「骑手推送与联调规范」)。未指派(无池)则不推送。") + public R syncFromOrder(@Valid @RequestBody CanteenDeliveryOrderSyncDTO dto) { + return canteenDeliveryOrderService.syncFromOrder(dto); + } + + @Inner + @PostMapping("/inner/cancel-by-source-id") + @Operation(summary = "订单取消回调(内部服务)") + public R cancelBySourceOrderId(@RequestParam("sourceOrderId") String sourceOrderId) { + return canteenDeliveryOrderService.cancelBySourceOrderId(sourceOrderId); + } + + @GetMapping("/app/list") + @Operation(summary = "骑手端-我的配送单", description = "tab: pending待配送 | all全部 | completed已完成 | cancelled已取消") + public R> appList(@RequestParam(defaultValue = "pending") String tab) { + Long uid = SecurityUtils.getUser().getId(); + return R.ok(canteenDeliveryOrderService.listForRider(uid, tab)); + } + + @GetMapping("/app/{id}") + @Operation(summary = "骑手端-配送单详情") + public R appDetail(@PathVariable Long id) { + Long uid = SecurityUtils.getUser().getId(); + CanteenDeliveryOrderAppVO vo = canteenDeliveryOrderService.detailForRider(uid, id); + if (vo == null) { + return R.failed("订单不存在或无权查看"); + } + return R.ok(vo); + } + + @SysLog("取餐拍照") + @PostMapping("/app/pickup") + @Operation(summary = "骑手端-到店取餐拍照") + public R pickup(@Valid @RequestBody CanteenDeliveryPhotoDTO dto) { + return canteenDeliveryOrderService.pickup(SecurityUtils.getUser().getId(), dto); + } + + @SysLog("送达拍照") + @PostMapping("/app/deliver") + @Operation(summary = "骑手端-送达拍照") + public R deliver(@Valid @RequestBody CanteenDeliveryPhotoDTO dto) { + return canteenDeliveryOrderService.deliver(SecurityUtils.getUser().getId(), dto); + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenRiderConfigController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenRiderConfigController.java new file mode 100644 index 000000000..e93876755 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenRiderConfigController.java @@ -0,0 +1,72 @@ +package com.bsd.pjcloud.canteen.controller; + +import com.bsd.pjcloud.canteen.entity.CanteenRiderBuildingBindEntity; +import com.bsd.pjcloud.canteen.entity.CanteenRiderPoolEntity; +import com.bsd.pjcloud.canteen.service.CanteenRiderConfigService; +import com.bsd.pjcloud.common.core.util.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 骑手与楼栋绑定、默认骑手池(管理端) + *

权限码 canteen_canteenDelivery_config 需在 sys_menu/sys_role_menu 中配置

+ */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/canteenRiderConfig") +@Tag(name = "食堂骑手配置") +@SecurityRequirement(name = HttpHeaders.AUTHORIZATION) +public class CanteenRiderConfigController { + + private final CanteenRiderConfigService canteenRiderConfigService; + + @GetMapping("/building-bind/list") + @PreAuthorize("@pms.hasPermission('canteen_canteenDelivery_config')") + @Operation(summary = "楼栋绑定列表") + public R> listBind(@RequestParam Long deptId) { + return R.ok(canteenRiderConfigService.listBuildingBind(deptId)); + } + + @PostMapping("/building-bind") + @PreAuthorize("@pms.hasPermission('canteen_canteenDelivery_config')") + @Operation(summary = "新增楼栋-骑手绑定") + public R saveBind(@RequestParam Long deptId, @RequestParam Long buildingId, @RequestParam Long riderUserId) { + return canteenRiderConfigService.saveBuildingBind(deptId, buildingId, riderUserId); + } + + @DeleteMapping("/building-bind/{id}") + @PreAuthorize("@pms.hasPermission('canteen_canteenDelivery_config')") + @Operation(summary = "删除楼栋绑定") + public R removeBind(@PathVariable Long id) { + return canteenRiderConfigService.removeBuildingBind(id); + } + + @GetMapping("/pool/list") + @PreAuthorize("@pms.hasPermission('canteen_canteenDelivery_config')") + @Operation(summary = "默认骑手池列表") + public R> listPool(@RequestParam Long deptId) { + return R.ok(canteenRiderConfigService.listPool(deptId)); + } + + @PostMapping("/pool") + @PreAuthorize("@pms.hasPermission('canteen_canteenDelivery_config')") + @Operation(summary = "新增默认骑手(轮询顺序)") + public R savePool(@RequestParam Long deptId, @RequestParam Long riderUserId, + @RequestParam(required = false, defaultValue = "0") Integer sortOrder) { + return canteenRiderConfigService.savePool(deptId, riderUserId, sortOrder); + } + + @DeleteMapping("/pool/{id}") + @PreAuthorize("@pms.hasPermission('canteen_canteenDelivery_config')") + @Operation(summary = "从默认池移除骑手") + public R removePool(@PathVariable Long id) { + return canteenRiderConfigService.removePool(id); + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenDeliveryAssignSeqMapper.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenDeliveryAssignSeqMapper.java new file mode 100644 index 000000000..52e5abfa2 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenDeliveryAssignSeqMapper.java @@ -0,0 +1,14 @@ +package com.bsd.pjcloud.canteen.mapper; + +import com.bsd.pjcloud.canteen.entity.CanteenDeliveryAssignSeqEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +@Mapper +public interface CanteenDeliveryAssignSeqMapper extends BaseMapper { + + @Update("UPDATE canteen_delivery_assign_seq SET seq = seq + 1 WHERE dept_id = #{deptId}") + int incrSeq(@Param("deptId") Long deptId); +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenDeliveryOrderMapper.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenDeliveryOrderMapper.java new file mode 100644 index 000000000..e81471230 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenDeliveryOrderMapper.java @@ -0,0 +1,9 @@ +package com.bsd.pjcloud.canteen.mapper; + +import com.bsd.pjcloud.canteen.entity.CanteenDeliveryOrderEntity; +import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface CanteenDeliveryOrderMapper extends PjcloudBaseMapper { +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenRiderBuildingBindMapper.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenRiderBuildingBindMapper.java new file mode 100644 index 000000000..c6419a404 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenRiderBuildingBindMapper.java @@ -0,0 +1,9 @@ +package com.bsd.pjcloud.canteen.mapper; + +import com.bsd.pjcloud.canteen.entity.CanteenRiderBuildingBindEntity; +import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface CanteenRiderBuildingBindMapper extends PjcloudBaseMapper { +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenRiderPoolMapper.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenRiderPoolMapper.java new file mode 100644 index 000000000..8361ef39c --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenRiderPoolMapper.java @@ -0,0 +1,9 @@ +package com.bsd.pjcloud.canteen.mapper; + +import com.bsd.pjcloud.canteen.entity.CanteenRiderPoolEntity; +import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface CanteenRiderPoolMapper extends PjcloudBaseMapper { +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/push/CanteenDeliveryRiderPushPublisher.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/push/CanteenDeliveryRiderPushPublisher.java new file mode 100644 index 000000000..48f1fa8a8 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/push/CanteenDeliveryRiderPushPublisher.java @@ -0,0 +1,75 @@ +package com.bsd.pjcloud.canteen.push; + +import com.bsd.pjcloud.canteen.constants.CanteenDeliveryPushConstants; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * 向骑手 WebSocket 会话发通知:通过 Redis 频道广播,由已连接 WebSocket 的网关服务(通常为 UPMS)订阅并下发。 + * 频道名与 {@code com.bsd.pjcloud.common.websocket.distribute.RedisWebsocketMessageListener#CHANNEL} 一致。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class CanteenDeliveryRiderPushPublisher { + + /** 与 pjcloud-common-websocket 中 RedisWebsocketMessageListener.CHANNEL 保持一致 */ + public static final String REDIS_CHANNEL_WEBSOCKET_SEND = "websocket-send"; + + private final ObjectProvider stringRedisTemplate; + private final ObjectMapper objectMapper; + + /** + * 新外送单已指派:推给指定骑手 userId(与 WS SessionKey 一致,为字符串形式的用户主键) + */ + public void publishOrderAssigned(Long riderUserId, + Long deliveryOrderId, + String sourceOrderId, + String merchantName, + String fullAddress) { + if (riderUserId == null) { + return; + } + StringRedisTemplate redis = stringRedisTemplate.getIfAvailable(); + if (redis == null) { + log.debug("StringRedisTemplate 不可用,跳过外送骑手 WS 推送"); + return; + } + try { + ObjectNode envelope = objectMapper.createObjectNode(); + envelope.put("bizType", CanteenDeliveryPushConstants.BIZ_TYPE_ORDER_ASSIGNED); + envelope.put("version", CanteenDeliveryPushConstants.PAYLOAD_VERSION); + envelope.put("title", "新配送任务"); + envelope.put("body", "您有一单新的食堂外送,请及时处理"); + envelope.set("payload", objectMapper.valueToTree(Map.of( + "deliveryOrderId", deliveryOrderId, + "sourceOrderId", sourceOrderId != null ? sourceOrderId : "", + "merchantName", merchantName != null ? merchantName : "", + "fullAddress", fullAddress != null ? fullAddress : "" + ))); + ObjectNode route = objectMapper.createObjectNode(); + route.put("type", "navigate"); + route.put("path", "/pages/canteen/rider/delivery-detail"); + route.set("query", objectMapper.valueToTree(Map.of("id", String.valueOf(deliveryOrderId)))); + envelope.set("route", route); + + String messageText = objectMapper.writeValueAsString(envelope); + Map messageDo = Map.of( + "needBroadcast", false, + "sessionKeys", List.of(String.valueOf(riderUserId)), + "messageText", messageText + ); + redis.convertAndSend(REDIS_CHANNEL_WEBSOCKET_SEND, objectMapper.writeValueAsString(messageDo)); + } catch (Exception e) { + log.warn("外送单指派推送 Redis 发布失败 riderUserId={} deliveryOrderId={}", riderUserId, deliveryOrderId, e); + } + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenDeliveryOrderService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenDeliveryOrderService.java new file mode 100644 index 000000000..2ab8ceee3 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenDeliveryOrderService.java @@ -0,0 +1,31 @@ +package com.bsd.pjcloud.canteen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryOrderSyncDTO; +import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryPhotoDTO; +import com.bsd.pjcloud.canteen.entity.CanteenDeliveryOrderEntity; +import com.bsd.pjcloud.canteen.vo.app.CanteenDeliveryOrderAppVO; +import com.bsd.pjcloud.common.core.util.R; + +import java.util.List; + +public interface CanteenDeliveryOrderService extends IService { + + /** + * 订单服务支付成功回调,落库并自动派单 + */ + R syncFromOrder(CanteenDeliveryOrderSyncDTO dto); + + R cancelBySourceOrderId(String sourceOrderId); + + /** + * @param tab pending | all | completed | cancelled + */ + List listForRider(Long riderUserId, String tab); + + CanteenDeliveryOrderAppVO detailForRider(Long riderUserId, Long id); + + R pickup(Long riderUserId, CanteenDeliveryPhotoDTO dto); + + R deliver(Long riderUserId, CanteenDeliveryPhotoDTO dto); +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenRiderConfigService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenRiderConfigService.java new file mode 100644 index 000000000..c2525f7fb --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenRiderConfigService.java @@ -0,0 +1,22 @@ +package com.bsd.pjcloud.canteen.service; + +import com.bsd.pjcloud.canteen.entity.CanteenRiderBuildingBindEntity; +import com.bsd.pjcloud.canteen.entity.CanteenRiderPoolEntity; +import com.bsd.pjcloud.common.core.util.R; + +import java.util.List; + +public interface CanteenRiderConfigService { + + R saveBuildingBind(Long deptId, Long buildingId, Long riderUserId); + + R removeBuildingBind(Long id); + + List listBuildingBind(Long deptId); + + R savePool(Long deptId, Long riderUserId, Integer sortOrder); + + R removePool(Long id); + + List listPool(Long deptId); +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDeliveryOrderServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDeliveryOrderServiceImpl.java new file mode 100644 index 000000000..9d317edd6 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDeliveryOrderServiceImpl.java @@ -0,0 +1,331 @@ +package com.bsd.pjcloud.canteen.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bsd.pjcloud.admin.api.dto.UserInfo; +import com.bsd.pjcloud.admin.api.entity.SysDept; +import com.bsd.pjcloud.admin.api.entity.SysUser; +import com.bsd.pjcloud.admin.api.feign.RemoteDeptService; +import com.bsd.pjcloud.admin.api.feign.RemoteUserService; +import com.bsd.pjcloud.canteen.constants.CanteenDeliveryAssignType; +import com.bsd.pjcloud.canteen.constants.CanteenDeliveryOrderStatus; +import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryCartItemDTO; +import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryOrderSyncDTO; +import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryPhotoDTO; +import com.bsd.pjcloud.canteen.entity.CanteenDeliveryAssignSeqEntity; +import com.bsd.pjcloud.canteen.entity.CanteenDeliveryOrderEntity; +import com.bsd.pjcloud.canteen.entity.CanteenRiderBuildingBindEntity; +import com.bsd.pjcloud.canteen.entity.CanteenRiderPoolEntity; +import com.bsd.pjcloud.canteen.mapper.CanteenDeliveryAssignSeqMapper; +import com.bsd.pjcloud.canteen.mapper.CanteenDeliveryOrderMapper; +import com.bsd.pjcloud.canteen.mapper.CanteenRiderBuildingBindMapper; +import com.bsd.pjcloud.canteen.mapper.CanteenRiderPoolMapper; +import com.bsd.pjcloud.canteen.push.CanteenDeliveryRiderPushPublisher; +import com.bsd.pjcloud.canteen.service.CanteenDeliveryOrderService; +import com.bsd.pjcloud.canteen.vo.app.CanteenDeliveryOrderAppVO; +import com.bsd.pjcloud.common.core.util.BeanUtils; +import com.bsd.pjcloud.common.core.util.R; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class CanteenDeliveryOrderServiceImpl extends ServiceImpl + implements CanteenDeliveryOrderService { + + private final CanteenRiderBuildingBindMapper riderBuildingBindMapper; + private final CanteenRiderPoolMapper riderPoolMapper; + private final CanteenDeliveryAssignSeqMapper assignSeqMapper; + private final RemoteUserService remoteUserService; + private final RemoteDeptService remoteDeptService; + private final ObjectMapper objectMapper; + private final CanteenDeliveryRiderPushPublisher riderPushPublisher; + + @Override + @Transactional(rollbackFor = Exception.class) + public R syncFromOrder(CanteenDeliveryOrderSyncDTO dto) { + long exists = count(Wrappers.lambdaQuery() + .eq(CanteenDeliveryOrderEntity::getSourceOrderId, dto.getId())); + if (exists > 0) { + return R.failed("订单已同步过"); + } + CanteenDeliveryOrderEntity entity = new CanteenDeliveryOrderEntity(); + entity.setSourceOrderId(dto.getId()); + entity.setDeptId(dto.getDeptId()); + entity.setBuyerUserId(dto.getBuyerUserId()); + entity.setMerchantId(dto.getMerchantId()); + entity.setProductDesc(dto.getProductDesc()); + // 收货人信息:优先使用订单侧回传;缺失时根据 buyerUserId 自动补齐 + entity.setReceiverName(dto.getUsername()); + entity.setReceiverMobile(dto.getUserMobile()); + if ((StrUtil.isBlank(entity.getReceiverName()) || StrUtil.isBlank(entity.getReceiverMobile())) + && dto.getBuyerUserId() != null) { + R r = remoteUserService.getInnerByUserId(dto.getBuyerUserId()); + if (r != null && r.isOk() && r.getData() != null && r.getData().getSysUser() != null) { + SysUser u = r.getData().getSysUser(); + if (StrUtil.isBlank(entity.getReceiverName())) { + entity.setReceiverName(u.getName()); + } + if (StrUtil.isBlank(entity.getReceiverMobile())) { + entity.setReceiverMobile(u.getPhone()); + } + } + } + entity.setMerchantName(dto.getMerchantName()); + entity.setMerchantLogo(dto.getLogoPic()); + entity.setBuildingId(dto.getBuildingId()); + entity.setFullAddress(dto.getFullAddress()); + try { + entity.setCarListJson(objectMapper.writeValueAsString(dto.getCarList())); + } catch (JsonProcessingException e) { + return R.failed("购物车序列化失败"); + } + assignRiderOnCreate(entity); + save(entity); + scheduleRiderPushAfterAssign(entity); + return R.ok(entity.getId()); + } + + /** + * 事务提交后再推送,避免回滚后骑手仍收到通知。 + */ + private void scheduleRiderPushAfterAssign(CanteenDeliveryOrderEntity entity) { + if (entity.getRiderUserId() == null + || !Objects.equals(entity.getStatus(), CanteenDeliveryOrderStatus.WAIT_PICKUP)) { + return; + } + final Long riderId = entity.getRiderUserId(); + final Long deliveryId = entity.getId(); + final String sourceOrderId = entity.getSourceOrderId(); + final String merchantName = entity.getMerchantName(); + final String fullAddress = entity.getFullAddress(); + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + riderPushPublisher.publishOrderAssigned(riderId, deliveryId, sourceOrderId, merchantName, fullAddress); + } + }); + } else { + riderPushPublisher.publishOrderAssigned(riderId, deliveryId, sourceOrderId, merchantName, fullAddress); + } + } + + private void assignRiderOnCreate(CanteenDeliveryOrderEntity entity) { + Long deptId = entity.getDeptId(); + entity.setAssignType(CanteenDeliveryAssignType.NONE); + entity.setRiderUserId(null); + entity.setStatus(CanteenDeliveryOrderStatus.PENDING_ASSIGN); + + // 1) 优先楼栋绑定(有 buildingId 才有意义) + if (entity.getBuildingId() != null) { + CanteenRiderBuildingBindEntity bind = riderBuildingBindMapper.selectOne( + Wrappers.lambdaQuery() + .eq(CanteenRiderBuildingBindEntity::getDeptId, deptId) + .eq(CanteenRiderBuildingBindEntity::getBuildingId, entity.getBuildingId())); + if (bind != null && verifyRiderInUnit(bind.getRiderUserId(), deptId)) { + entity.setRiderUserId(bind.getRiderUserId()); + entity.setAssignType(CanteenDeliveryAssignType.BIND); + entity.setStatus(CanteenDeliveryOrderStatus.WAIT_PICKUP); + return; + } + } + + // 2) 默认骑手池轮询 + List pool = riderPoolMapper.selectList( + Wrappers.lambdaQuery() + .eq(CanteenRiderPoolEntity::getDeptId, deptId) + .orderByAsc(CanteenRiderPoolEntity::getSortOrder) + .orderByAsc(CanteenRiderPoolEntity::getId)); + if (pool.isEmpty()) { + return; + } + Integer buyerGender = fetchUserGender(entity.getBuyerUserId()); + List filtered = filterPoolByBuyerGender(pool, buyerGender); + long seq = nextSeq(deptId); + int idx = (int) ((seq - 1) % filtered.size()); + if (idx < 0) { + idx = 0; + } + CanteenRiderPoolEntity chosen = filtered.get(idx); + if (!verifyRiderInUnit(chosen.getRiderUserId(), deptId)) { + for (CanteenRiderPoolEntity p : filtered) { + if (verifyRiderInUnit(p.getRiderUserId(), deptId)) { + chosen = p; + break; + } + } + } + if (!verifyRiderInUnit(chosen.getRiderUserId(), deptId)) { + return; + } + entity.setRiderUserId(chosen.getRiderUserId()); + entity.setAssignType(CanteenDeliveryAssignType.ROUND_ROBIN); + entity.setStatus(CanteenDeliveryOrderStatus.WAIT_PICKUP); + } + + private List filterPoolByBuyerGender(List pool, Integer buyerGender) { + if (buyerGender == null || (buyerGender != 1 && buyerGender != 2)) { + return pool; + } + List matched = new ArrayList<>(); + for (CanteenRiderPoolEntity p : pool) { + Integer g = fetchUserGender(p.getRiderUserId()); + if (buyerGender.equals(g)) { + matched.add(p); + } + } + return matched.isEmpty() ? pool : matched; + } + + private Integer fetchUserGender(Long userId) { + if (userId == null) { + return 0; + } + R r = remoteUserService.getInnerByUserId(userId); + if (r == null || !r.isOk() || r.getData() == null || r.getData().getSysUser() == null) { + return 0; + } + Integer g = r.getData().getSysUser().getGender(); + return g == null ? 0 : g; + } + + private boolean verifyRiderInUnit(Long riderUserId, Long unitDeptId) { + if (riderUserId == null || unitDeptId == null) { + return false; + } + R r = remoteUserService.getInnerByUserId(riderUserId); + if (r == null || !r.isOk() || r.getData() == null || r.getData().getSysUser() == null) { + return false; + } + SysUser u = r.getData().getSysUser(); + Long riderDept = u.getDeptId(); + if (riderDept == null) { + return false; + } + if (unitDeptId.equals(riderDept)) { + return true; + } + R> parents = remoteDeptService.getParentDeptList(riderDept); + if (parents == null || !parents.isOk() || parents.getData() == null) { + return false; + } + return parents.getData().stream().anyMatch(d -> unitDeptId.equals(d.getDeptId())); + } + + private long nextSeq(Long deptId) { + CanteenDeliveryAssignSeqEntity row = assignSeqMapper.selectById(deptId); + if (row == null) { + row = new CanteenDeliveryAssignSeqEntity(); + row.setDeptId(deptId); + row.setSeq(0L); + assignSeqMapper.insert(row); + } + assignSeqMapper.incrSeq(deptId); + CanteenDeliveryAssignSeqEntity after = assignSeqMapper.selectById(deptId); + return after != null && after.getSeq() != null ? after.getSeq() : 1L; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R cancelBySourceOrderId(String sourceOrderId) { + if (StrUtil.isBlank(sourceOrderId)) { + return R.failed("订单号不能为空"); + } + CanteenDeliveryOrderEntity one = getOne(Wrappers.lambdaQuery() + .eq(CanteenDeliveryOrderEntity::getSourceOrderId, sourceOrderId)); + if (one == null) { + return R.ok(); + } + one.setStatus(CanteenDeliveryOrderStatus.CANCELLED); + updateById(one); + return R.ok(); + } + + @Override + public List listForRider(Long riderUserId, String tab) { + String t = StrUtil.emptyToDefault(tab, "all"); + var q = Wrappers.lambdaQuery() + .eq(CanteenDeliveryOrderEntity::getRiderUserId, riderUserId); + switch (t) { + case "pending" -> + q.in(CanteenDeliveryOrderEntity::getStatus, CanteenDeliveryOrderStatus.WAIT_PICKUP, CanteenDeliveryOrderStatus.DELIVERING); + case "completed" -> q.eq(CanteenDeliveryOrderEntity::getStatus, CanteenDeliveryOrderStatus.COMPLETED); + case "cancelled" -> q.eq(CanteenDeliveryOrderEntity::getStatus, CanteenDeliveryOrderStatus.CANCELLED); + default -> { + } + } + q.orderByDesc(CanteenDeliveryOrderEntity::getCreateTime); + List list = list(q); + return list.stream().map(this::toAppVoWithCart).toList(); + } + + @Override + public CanteenDeliveryOrderAppVO detailForRider(Long riderUserId, Long id) { + CanteenDeliveryOrderEntity e = getById(id); + if (e == null || !Objects.equals(e.getRiderUserId(), riderUserId)) { + return null; + } + return toAppVoWithCart(e); + } + + private CanteenDeliveryOrderAppVO toAppVoWithCart(CanteenDeliveryOrderEntity e) { + CanteenDeliveryOrderAppVO vo = BeanUtils.toBean(e, CanteenDeliveryOrderAppVO.class); + vo.setCarList(parseCartList(e.getCarListJson())); + return vo; + } + + private List parseCartList(String carListJson) { + if (StrUtil.isBlank(carListJson)) { + return List.of(); + } + try { + return objectMapper.readValue(carListJson, new TypeReference>() {}); + } catch (Exception ex) { + return List.of(); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R pickup(Long riderUserId, CanteenDeliveryPhotoDTO dto) { + CanteenDeliveryOrderEntity e = getById(dto.getId()); + if (e == null || !Objects.equals(e.getRiderUserId(), riderUserId)) { + return R.failed("订单不存在或无权操作"); + } + if (e.getStatus() != CanteenDeliveryOrderStatus.WAIT_PICKUP) { + return R.failed("当前状态不可上传取餐照"); + } + e.setPickupPhotoUrl(dto.getPhotoUrl()); + e.setStatus(CanteenDeliveryOrderStatus.DELIVERING); + updateById(e); + return R.ok(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R deliver(Long riderUserId, CanteenDeliveryPhotoDTO dto) { + CanteenDeliveryOrderEntity e = getById(dto.getId()); + if (e == null || !Objects.equals(e.getRiderUserId(), riderUserId)) { + return R.failed("订单不存在或无权操作"); + } + if (e.getStatus() != CanteenDeliveryOrderStatus.DELIVERING) { + return R.failed("当前状态不可上传送达照"); + } + e.setDeliveryPhotoUrl(dto.getPhotoUrl()); + e.setStatus(CanteenDeliveryOrderStatus.COMPLETED); + updateById(e); + return R.ok(); + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenRiderConfigServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenRiderConfigServiceImpl.java new file mode 100644 index 000000000..7dc273aae --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenRiderConfigServiceImpl.java @@ -0,0 +1,74 @@ +package com.bsd.pjcloud.canteen.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bsd.pjcloud.canteen.entity.CanteenRiderBuildingBindEntity; +import com.bsd.pjcloud.canteen.entity.CanteenRiderPoolEntity; +import com.bsd.pjcloud.canteen.mapper.CanteenRiderBuildingBindMapper; +import com.bsd.pjcloud.canteen.mapper.CanteenRiderPoolMapper; +import com.bsd.pjcloud.canteen.service.CanteenRiderConfigService; +import com.bsd.pjcloud.common.core.util.R; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CanteenRiderConfigServiceImpl implements CanteenRiderConfigService { + + private final CanteenRiderBuildingBindMapper buildingBindMapper; + private final CanteenRiderPoolMapper poolMapper; + + @Override + public R saveBuildingBind(Long deptId, Long buildingId, Long riderUserId) { + long dup = buildingBindMapper.selectCount(Wrappers.lambdaQuery() + .eq(CanteenRiderBuildingBindEntity::getDeptId, deptId) + .eq(CanteenRiderBuildingBindEntity::getBuildingId, buildingId)); + if (dup > 0) { + return R.failed("该单位下楼栋已绑定骑手,请先解绑"); + } + CanteenRiderBuildingBindEntity e = new CanteenRiderBuildingBindEntity(); + e.setDeptId(deptId); + e.setBuildingId(buildingId); + e.setRiderUserId(riderUserId); + buildingBindMapper.insert(e); + return R.ok(e.getId()); + } + + @Override + public R removeBuildingBind(Long id) { + buildingBindMapper.deleteById(id); + return R.ok(); + } + + @Override + public List listBuildingBind(Long deptId) { + return buildingBindMapper.selectList(Wrappers.lambdaQuery() + .eq(CanteenRiderBuildingBindEntity::getDeptId, deptId) + .orderByAsc(CanteenRiderBuildingBindEntity::getId)); + } + + @Override + public R savePool(Long deptId, Long riderUserId, Integer sortOrder) { + CanteenRiderPoolEntity e = new CanteenRiderPoolEntity(); + e.setDeptId(deptId); + e.setRiderUserId(riderUserId); + e.setSortOrder(sortOrder == null ? 0 : sortOrder); + poolMapper.insert(e); + return R.ok(e.getId()); + } + + @Override + public R removePool(Long id) { + poolMapper.deleteById(id); + return R.ok(); + } + + @Override + public List listPool(Long deptId) { + return poolMapper.selectList(Wrappers.lambdaQuery() + .eq(CanteenRiderPoolEntity::getDeptId, deptId) + .orderByAsc(CanteenRiderPoolEntity::getSortOrder) + .orderByAsc(CanteenRiderPoolEntity::getId)); + } +} diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/controller/CanteenDeliveryOrderControllerTest.java b/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/controller/CanteenDeliveryOrderControllerTest.java new file mode 100644 index 000000000..4d8ff1ac7 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/test/java/com/bsd/pjcloud/canteen/controller/CanteenDeliveryOrderControllerTest.java @@ -0,0 +1,185 @@ +package com.bsd.pjcloud.canteen.controller; + +import com.bsd.pjcloud.canteen.service.CanteenDeliveryOrderService; +import com.bsd.pjcloud.canteen.vo.app.CanteenDeliveryOrderAppVO; +import com.bsd.pjcloud.common.core.constant.SecurityConstants; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.security.service.PjcloudUser; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * 本地可跑:不连库、不连 Nacos,仅校验 Controller 入参与路由。 + *
+ *   cd pjcloud-canteen/pjcloud-canteen-biz
+ *   mvn test -Dtest=CanteenDeliveryOrderControllerTest
+ * 
+ */ +@WebMvcTest(controllers = CanteenDeliveryOrderController.class) +@AutoConfigureMockMvc(addFilters = false) +class CanteenDeliveryOrderControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private CanteenDeliveryOrderService canteenDeliveryOrderService; + + private static final long RIDER_USER_ID = 9001L; + + @BeforeEach + void loginRider() { + PjcloudUser rider = new PjcloudUser( + RIDER_USER_ID, + "rider_test", + 1L, + "13800000000", + null, + null, + "测试骑手", + null, + 1L, + "nop", + true, + true, + null, + true, + true, + Collections.emptyList()); + UsernamePasswordAuthenticationToken auth = + new UsernamePasswordAuthenticationToken(rider, rider.getPassword(), rider.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + } + + @AfterEach + void clearSecurity() { + SecurityContextHolder.clearContext(); + } + + @Test + void innerSyncFromOrder_ok() throws Exception { + when(canteenDeliveryOrderService.syncFromOrder(any())).thenReturn(R.ok(12345L)); + + String body = """ + {"id":"TEST-ORDER-001","deptId":100} + """; + + mockMvc.perform(post("/canteenDeliveryOrder/inner/sync-from-order") + .header(SecurityConstants.FROM, SecurityConstants.FROM_IN) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.ok").value(true)) + .andExpect(jsonPath("$.data").value(12345)); + + verify(canteenDeliveryOrderService).syncFromOrder(any()); + } + + @Test + void innerSyncFromOrder_validationFail() throws Exception { + mockMvc.perform(post("/canteenDeliveryOrder/inner/sync-from-order") + .header(SecurityConstants.FROM, SecurityConstants.FROM_IN) + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andExpect(status().isBadRequest()); + } + + @Test + void innerCancel_ok() throws Exception { + when(canteenDeliveryOrderService.cancelBySourceOrderId("TEST-ORDER-001")).thenReturn(R.ok()); + + mockMvc.perform(post("/canteenDeliveryOrder/inner/cancel-by-source-id") + .header(SecurityConstants.FROM, SecurityConstants.FROM_IN) + .param("sourceOrderId", "TEST-ORDER-001")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.ok").value(true)); + + verify(canteenDeliveryOrderService).cancelBySourceOrderId("TEST-ORDER-001"); + } + + @Test + void appList_delegatesToService() throws Exception { + CanteenDeliveryOrderAppVO vo = new CanteenDeliveryOrderAppVO(); + vo.setId(1L); + vo.setSourceOrderId("SO-1"); + when(canteenDeliveryOrderService.listForRider(RIDER_USER_ID, "pending")).thenReturn(List.of(vo)); + + mockMvc.perform(get("/canteenDeliveryOrder/app/list").param("tab", "pending")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.ok").value(true)) + .andExpect(jsonPath("$.data[0].id").value(1)) + .andExpect(jsonPath("$.data[0].sourceOrderId").value("SO-1")); + + verify(canteenDeliveryOrderService).listForRider(RIDER_USER_ID, "pending"); + } + + @Test + void appDetail_ok() throws Exception { + CanteenDeliveryOrderAppVO vo = new CanteenDeliveryOrderAppVO(); + vo.setId(99L); + when(canteenDeliveryOrderService.detailForRider(eq(RIDER_USER_ID), eq(99L))).thenReturn(vo); + + mockMvc.perform(get("/canteenDeliveryOrder/app/99")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.ok").value(true)) + .andExpect(jsonPath("$.data.id").value(99)); + } + + @Test + void appDetail_notFound() throws Exception { + when(canteenDeliveryOrderService.detailForRider(eq(RIDER_USER_ID), eq(404L))).thenReturn(null); + + mockMvc.perform(get("/canteenDeliveryOrder/app/404")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.ok").value(false)); + } + + @Test + void pickup_ok() throws Exception { + when(canteenDeliveryOrderService.pickup(eq(RIDER_USER_ID), any())).thenReturn(R.ok()); + + mockMvc.perform(post("/canteenDeliveryOrder/app/pickup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString( + java.util.Map.of("id", 1L, "photoUrl", "https://example.com/a.jpg")))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.ok").value(true)); + } + + @Test + void deliver_ok() throws Exception { + when(canteenDeliveryOrderService.deliver(eq(RIDER_USER_ID), any())).thenReturn(R.ok()); + + mockMvc.perform(post("/canteenDeliveryOrder/app/deliver") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString( + java.util.Map.of("id", 1L, "photoUrl", "https://example.com/b.jpg")))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.ok").value(true)); + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppTakeOutAddress.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppTakeOutAddress.java index cd161051d..2b01250a0 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppTakeOutAddress.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppTakeOutAddress.java @@ -1,6 +1,7 @@ package com.bsd.pjcloud.hard.api.dto.order.app; import cn.hutool.core.util.ObjectUtil; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data @@ -46,6 +47,11 @@ public class HardAppTakeOutAddress { */ private String roomName; + /** + * 详细地址 + */ + private String fullAddress; + public String getAddressName() { String addressName = ""; if (ObjectUtil.isNotEmpty(buildingName)) { diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderAddressEntity.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderAddressEntity.java index a86202f37..8e3eaaad2 100755 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderAddressEntity.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderAddressEntity.java @@ -92,6 +92,13 @@ public class HardOrderAddressEntity extends Model { @Schema(description = "房间名") private String roomName; + + /** + * 详细地址 + */ + @Schema(description = "详细地址") + private String fullAddress; + /** * 创建人 */ diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java index f14e7a0c8..83dd93844 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java @@ -17,8 +17,11 @@ import com.bsd.pjcloud.admin.api.feign.RemoteDeptService; import com.bsd.pjcloud.admin.api.feign.RemoteUserService; import com.bsd.pjcloud.admin.api.utils.DataUtil; import com.bsd.pjcloud.admin.api.vo.UserVO; +import com.bsd.pjcloud.canteen.api.feign.RemoteCanteenDeliveryOrderService; import com.bsd.pjcloud.canteen.api.feign.RemoteOnlineCanteenSpuService; import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO; +import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryCartItemDTO; +import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryOrderSyncDTO; import com.bsd.pjcloud.common.core.constant.CacheConstants; import com.bsd.pjcloud.common.core.constant.CommonConstants; import com.bsd.pjcloud.common.core.constant.CommonRoleConstant; @@ -30,6 +33,7 @@ import com.bsd.pjcloud.common.core.util.CollectionUtils; import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.data.datascope.DataScope; import com.bsd.pjcloud.common.security.service.PjcloudUser; +import com.bsd.pjcloud.common.security.util.SecurityUtils; import com.bsd.pjcloud.hard.api.constant.HardProductNatureEnum; import com.bsd.pjcloud.hard.api.constant.HardThirdMiStatusEnum; import com.bsd.pjcloud.hard.api.constant.HardTypeEnum; @@ -162,6 +166,7 @@ public class HardOrderServiceImpl extends ServiceImpl pagejoin(Page page, HardOrderDto dto) { @@ -679,8 +684,24 @@ public class HardOrderServiceImpl extends ServiceImpl orderCarList = BeanUtils.toBean(carList, CanteenDeliveryCartItemDTO.class); + canteenDeliveryOrderSyncDTO.setCarList(orderCarList); + canteenDeliveryOrderSyncDTO.setDeptId(SecurityUtils.getUser().getDeptId()); + canteenDeliveryOrderSyncDTO.setId(String.valueOf(orderObj.getId())); + canteenDeliveryOrderSyncDTO.setFullAddress(dto.getAddress().getFullAddress()); + canteenDeliveryOrderSyncDTO.setBuildingId(dto.getAddress().getBuildingId()); + remoteCanteenDeliveryOrderService.syncFromOrder(canteenDeliveryOrderSyncDTO); + return orderObj.getId(); } diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysUser.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysUser.java index bfc447d6c..b770f9115 100644 --- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysUser.java +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysUser.java @@ -157,6 +157,13 @@ public class SysUser extends Model implements Serializable { @Schema(description = "手机号") private String phone; + /** + * 性别:0未知 1男 2女(骑手派单等) + */ + @Schema(description = "性别:0未知 1男 2女") + @TableField("gender") + private Integer gender; + /** * 头像 */ diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java index ed07e253b..9ef93898c 100644 --- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java @@ -164,6 +164,12 @@ public class UserVO implements BaseFacesInterface, Serializable { @Schema(description = "手机号") private String phone; + /** + * 性别:0未知 1男 2女 + */ + @Schema(description = "性别:0未知 1男 2女") + private Integer gender; + /** * 头像 */ diff --git a/pjcloud-upms/pjcloud-upms-biz/pom.xml b/pjcloud-upms/pjcloud-upms-biz/pom.xml index 4ec10d336..809c3e63b 100644 --- a/pjcloud-upms/pjcloud-upms-biz/pom.xml +++ b/pjcloud-upms/pjcloud-upms-biz/pom.xml @@ -33,6 +33,11 @@ pjcloud 通用用户权限管理系统业务处理模块 + + + com.bsd + pjcloud-common-websocket + com.qiniu qiniu-java-sdk diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java index a48be06f1..2ef85c183 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java @@ -74,6 +74,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.Objects; /** * @author pjcloud @@ -569,7 +570,7 @@ public class SysUserController { public R> getUserListByRoleCode(@RequestParam("code") String code) { SysRole sysRole = sysRoleService.getOne(Wrappers.lambdaQuery().eq(SysRole::getRoleCode, code)); if (sysRole == null) { - return R.failed("不存在共富商城商户角色的用户!"); + return R.failed("不存在角色用户!"); } List list = sysUserRoleService.list(Wrappers.lambdaQuery().eq(SysUserRole::getRoleId, sysRole.getRoleId())); List userIds = list.stream().map(sysUserRole -> sysUserRole.getUserId()).collect(Collectors.toList()); @@ -585,4 +586,41 @@ public class SysUserController { }).collect(Collectors.toList()); return R.ok(userVOList); } + + /** + * 根据角色编码与单位部门(含其子部门)获取用户列表,用于下拉等场景 + */ + @Operation(summary = "根据角色编码与单位部门查询用户简要列表", description = "仅返回 deptId 为该单位或其下级部门内的用户,返回字段同 getUserListByRoleCode") + @GetMapping("/getUserListByRoleCodeAndDept") + public R> getUserListByRoleCodeAndDept(@RequestParam("code") String code, + @RequestParam("deptId") Long deptId) { + if (deptId == null) { + return R.failed("deptId不能为空"); + } + SysRole sysRole = sysRoleService.getOne(Wrappers.lambdaQuery().eq(SysRole::getRoleCode, code)); + if (sysRole == null) { + return R.failed("不存在该角色!"); + } + List roleRows = sysUserRoleService.list(Wrappers.lambdaQuery() + .eq(SysUserRole::getRoleId, sysRole.getRoleId())); + List userIds = roleRows.stream().map(SysUserRole::getUserId).collect(Collectors.toList()); + if (CollUtil.isEmpty(userIds)) { + return R.ok(new ArrayList<>()); + } + Set scopeDeptIds = sysDeptService.listDescendant(deptId).stream() + .map(SysDept::getDeptId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + List listData = userService.list(Wrappers.lambdaQuery().in(SysUser::getUserId, userIds)); + List userVOList = listData.stream() + .filter(u -> u.getDeptId() != null && scopeDeptIds.contains(u.getDeptId())) + .map(sysUser -> { + UserSimpleVo userVO = new UserSimpleVo(); + userVO.setUserId(sysUser.getUserId()); + userVO.setPhone(sysUser.getPhone()); + userVO.setName(sysUser.getName()); + return userVO; + }).collect(Collectors.toList()); + return R.ok(userVOList); + } } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/stats/AppAccessAggDayMapper.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/stats/AppAccessAggDayMapper.java index fa0f869b6..396a527ca 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/stats/AppAccessAggDayMapper.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/stats/AppAccessAggDayMapper.java @@ -1,9 +1,21 @@ package com.bsd.pjcloud.admin.mapper.stats; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; import com.bsd.pjcloud.admin.api.entity.stats.AppAccessAggDayEntity; import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper; +import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDate; @Mapper public interface AppAccessAggDayMapper extends PjcloudBaseMapper { + + /** + * 物理删除该统计日下所有维度行(同分钟聚合,避免逻辑删除导致 uk_day_dim 重复)。 + */ + @InterceptorIgnore(tenantLine = "true") + @Delete("DELETE FROM app_access_agg_day WHERE stat_date = #{statDate}") + int deletePhysicallyByStatDate(@Param("statDate") LocalDate statDate); } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/stats/AppAccessAggMinuteMapper.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/stats/AppAccessAggMinuteMapper.java index b0e5d23fe..9cde9cda7 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/stats/AppAccessAggMinuteMapper.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/stats/AppAccessAggMinuteMapper.java @@ -1,9 +1,21 @@ package com.bsd.pjcloud.admin.mapper.stats; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; import com.bsd.pjcloud.admin.api.entity.stats.AppAccessAggMinuteEntity; import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper; +import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; @Mapper public interface AppAccessAggMinuteMapper extends PjcloudBaseMapper { + + /** + * 物理删除该分钟桶下所有维度行。聚合任务不可用 {@link #delete},否则逻辑删除后唯一键仍冲突。 + */ + @InterceptorIgnore(tenantLine = "true") + @Delete("DELETE FROM app_access_agg_minute WHERE bucket_time = #{bucketTime}") + int deletePhysicallyByBucketTime(@Param("bucketTime") LocalDateTime bucketTime); } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/stats/AppAccessStatsServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/stats/AppAccessStatsServiceImpl.java index 730711ebe..7593bf5d0 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/stats/AppAccessStatsServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/stats/AppAccessStatsServiceImpl.java @@ -285,8 +285,7 @@ public class AppAccessStatsServiceImpl implements AppAccessStatsService { queryWrapper.groupBy("tenant_id", "unit_id", "dept_id", "page_code"); List> rows = appAccessEventMapper.selectMaps(queryWrapper); - appAccessAggMinuteMapper.delete(new LambdaQueryWrapper() - .eq(AppAccessAggMinuteEntity::getBucketTime, start)); + appAccessAggMinuteMapper.deletePhysicallyByBucketTime(start); if (CollUtil.isEmpty(rows)) { return; } @@ -336,8 +335,7 @@ public class AppAccessStatsServiceImpl implements AppAccessStatsService { queryWrapper.groupBy("tenant_id", "unit_id", "dept_id", "page_code"); List> rows = appAccessEventMapper.selectMaps(queryWrapper); - appAccessAggDayMapper.delete(new LambdaQueryWrapper() - .eq(AppAccessAggDayEntity::getStatDate, date)); + appAccessAggDayMapper.deletePhysicallyByStatDate(date); for (Map row : rows) { AppAccessAggDayEntity entity = new AppAccessAggDayEntity(); entity.setStatDate(date); diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/application.yml b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/application.yml index c8c768d9b..c390d0b78 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/application.yml +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/application.yml @@ -23,4 +23,14 @@ qn: accessKey: IVrAjjdrTeZi14pqDd2s5kb_iAIGXATEMtOgmQUF secretKey: SNhPscR3_xqX3E_B1XtMMWmLMrz7ipcguzNykQvo bucketName: bizcabinet - domain: https://bizcabinet.zianxin.com \ No newline at end of file + domain: https://bizcabinet.zianxin.com + +pjcloud: + websocket: + path: /ws/info # WebSocket 端点路径 (默认: /ws/info) + allow-origins: "*" # 允许的跨域源 (默认: *) + heartbeat: true # 是否启用心跳 (默认: true) + map-session: true # 是否启用 Session 映射 (默认: true) + message-distributor: redis # 消息分发器类型: local | redis (默认: local) + send-time-limit: 10000 # 发送时间限制,单位毫秒 (默认: 10000) + send-buffer-size-limit: 64000 # 发送缓冲区大小限制 (默认: 64000) \ No newline at end of file diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml index 054ecbbad..a20dcc142 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml @@ -30,6 +30,7 @@ + @@ -69,6 +70,7 @@ u.password, u.salt, u.phone, + u.gender, u.avatar, u.wx_openid, u.qq_openid, @@ -102,6 +104,7 @@ u.password, u.salt, u.phone, + u.gender, u.avatar, u.wx_openid, u.qq_openid, @@ -157,6 +160,7 @@ u.password, u.salt, u.phone, + u.gender, u.avatar, u.wx_openid, u.qq_openid,