From 283ac807da1c1780adcf2fd8786d6dcf152a3bc3 Mon Sep 17 00:00:00 2001 From: zhoupeng <812968653@qq.com> Date: Sat, 12 Sep 2026 14:43:54 +0800 Subject: [PATCH] =?UTF-8?q?=E9=AA=91=E8=A1=8C=E7=9B=B8=E5=85=B3=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/member/enums/MemberErrorCode.java | 8 + .../app/rideplan/AppRidePlanController.java | 81 ++++++ .../app/rideplan/AppRideTagController.java | 54 ++++ .../rideplan/vo/AppRidePlanCreateReqVO.java | 52 ++++ .../app/rideplan/vo/AppRidePlanPageReqVO.java | 27 ++ .../app/rideplan/vo/AppRidePlanRespVO.java | 76 +++++ .../app/rideplan/vo/AppRideTagRespVO.java | 16 ++ .../vo/AppRideUserTagUpdateReqVO.java | 17 ++ .../dataobject/rideplan/MemberRidePlanDO.java | 52 ++++ .../rideplan/MemberRidePlanJoinDO.java | 29 ++ .../dataobject/rideplan/MemberRideTagDO.java | 33 +++ .../rideplan/MemberRideUserTagDO.java | 29 ++ .../rideplan/MemberRidePlanJoinMapper.java | 29 ++ .../mysql/rideplan/MemberRidePlanMapper.java | 39 +++ .../mysql/rideplan/MemberRideTagMapper.java | 24 ++ .../rideplan/MemberRideUserTagMapper.java | 29 ++ .../rideplan/MemberRidePlanService.java | 67 +++++ .../rideplan/MemberRidePlanServiceImpl.java | 266 ++++++++++++++++++ 18 files changed, 928 insertions(+) create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/AppRidePlanController.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/AppRideTagController.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanCreateReqVO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanPageReqVO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanRespVO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRideTagRespVO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRideUserTagUpdateReqVO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRidePlanDO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRidePlanJoinDO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRideTagDO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRideUserTagDO.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRidePlanJoinMapper.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRidePlanMapper.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRideTagMapper.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRideUserTagMapper.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/rideplan/MemberRidePlanService.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/rideplan/MemberRidePlanServiceImpl.java diff --git a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java index 8e57b7a..fce18e6 100644 --- a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java +++ b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java @@ -102,4 +102,12 @@ public interface MemberErrorCode { ErrorCode AIRLINE_ACCOUNT_DEVICE_USED = new ErrorCode(1_004_019_006, "该手机已绑定其他航空账号"); ErrorCode AIRLINE_ACCOUNT_TIME_INVALID = new ErrorCode(1_004_019_007, "结束时间必须晚于开始时间"); ErrorCode AIRLINE_ACCOUNT_BAD_CREDENTIALS = new ErrorCode(1_004_019_008, "航空账号手机号或密码不正确"); + + // ========== 骑遇发车局 / 骑行标签 1-004-020-000 ========== + ErrorCode RIDE_PLAN_NOT_EXISTS = new ErrorCode(1_004_020_000, "发车局不存在或已下架"); + ErrorCode RIDE_PLAN_MEET_TIME_INVALID = new ErrorCode(1_004_020_001, "集合时间必须晚于当前时间"); + ErrorCode RIDE_PLAN_FULL = new ErrorCode(1_004_020_002, "该发车局已满员"); + ErrorCode RIDE_PLAN_ALREADY_JOINED = new ErrorCode(1_004_020_003, "已加入该发车局,请勿重复加入"); + ErrorCode RIDE_PLAN_NOT_JOINED = new ErrorCode(1_004_020_004, "尚未加入该发车局"); + ErrorCode RIDE_TAG_NOT_EXISTS = new ErrorCode(1_004_020_005, "骑行标签不存在或已停用"); } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/AppRidePlanController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/AppRidePlanController.java new file mode 100644 index 0000000..018325b --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/AppRidePlanController.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.member.controller.app.rideplan; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanCreateReqVO; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanPageReqVO; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanRespVO; +import cn.iocoder.yudao.module.member.service.rideplan.MemberRidePlanService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.annotation.security.PermitAll; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 骑遇发车局: + * - 查询(page/get)@PermitAll 免登录,游客可浏览, RespVO 已做字段脱敏(只暴露昵称/头像) + * - 发布/加入/取消加入/举报 需登录(/app-api/** 默认拦截,未带有效 token 返回 401) + */ +@Tag(name = "用户 APP - 骑遇发车局") +@RestController +@RequestMapping("/member/ride-plan") +@Validated +public class AppRidePlanController { + + @Resource + private MemberRidePlanService ridePlanService; + + @GetMapping("/page") + @Operation(summary = "发车局分页(免登录)", description = "游客可查;登录后返回 joinedByMe") + @PermitAll + public CommonResult> page(@Valid AppRidePlanPageReqVO reqVO) { + return success(ridePlanService.getRidePlanPage(reqVO, getLoginUserId())); + } + + @GetMapping("/get") + @Operation(summary = "发车局详情(免登录)") + @Parameter(name = "id", description = "发车局编号", required = true, example = "1") + @PermitAll + public CommonResult get(@RequestParam("id") Long id) { + return success(ridePlanService.getRidePlan(id, getLoginUserId())); + } + + @PostMapping("/create") + @Operation(summary = "发布发车局(需登录)") + public CommonResult create(@Valid @RequestBody AppRidePlanCreateReqVO reqVO) { + return success(ridePlanService.createRidePlan(getLoginUserId(), reqVO)); + } + + @PostMapping("/join") + @Operation(summary = "加入发车局(需登录)") + @Parameter(name = "planId", description = "发车局编号", required = true, example = "1") + public CommonResult join(@RequestParam("planId") Long planId) { + ridePlanService.joinRidePlan(getLoginUserId(), planId); + return success(true); + } + + @PostMapping("/cancel-join") + @Operation(summary = "取消加入发车局(需登录)") + @Parameter(name = "planId", description = "发车局编号", required = true, example = "1") + public CommonResult cancelJoin(@RequestParam("planId") Long planId) { + ridePlanService.cancelJoinRidePlan(getLoginUserId(), planId); + return success(true); + } + + @PostMapping("/report") + @Operation(summary = "举报发车局(需登录),达到阈值自动下架") + @Parameter(name = "planId", description = "发车局编号", required = true, example = "1") + public CommonResult report(@RequestParam("planId") Long planId) { + ridePlanService.reportRidePlan(getLoginUserId(), planId); + return success(true); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/AppRideTagController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/AppRideTagController.java new file mode 100644 index 0000000..8374893 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/AppRideTagController.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.member.controller.app.rideplan; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideTagRespVO; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideUserTagUpdateReqVO; +import cn.iocoder.yudao.module.member.service.rideplan.MemberRidePlanService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.annotation.security.PermitAll; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 骑遇骑行标签: + * - 标签字典 list @PermitAll 免登录(发局选标签、列表展示用) + * - 我的标签读写 需登录 + */ +@Tag(name = "用户 APP - 骑遇骑行标签") +@RestController +@RequestMapping("/member/ride-tag") +@Validated +public class AppRideTagController { + + @Resource + private MemberRidePlanService ridePlanService; + + @GetMapping("/list") + @Operation(summary = "骑行标签字典(免登录,只返回启用的)") + @PermitAll + public CommonResult> list() { + return success(ridePlanService.getRideTagList()); + } + + @GetMapping("/my") + @Operation(summary = "我的骑行标签(需登录)") + public CommonResult> my() { + return success(ridePlanService.getMyRideTags(getLoginUserId())); + } + + @PutMapping("/my") + @Operation(summary = "全量替换我的骑行标签(需登录)") + public CommonResult updateMy(@Valid @RequestBody AppRideUserTagUpdateReqVO reqVO) { + ridePlanService.updateMyRideTags(getLoginUserId(), reqVO); + return success(true); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanCreateReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanCreateReqVO.java new file mode 100644 index 0000000..ed7f654 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanCreateReqVO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.member.controller.app.rideplan.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "用户 APP - 发布发车局 Request VO") +@Data +public class AppRidePlanCreateReqVO { + + @Schema(description = "路线名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "南昆山经典环线") + @NotBlank(message = "路线名称不能为空") + @Size(max = 100, message = "路线名称长度不能超过 100") + private String routeName; + + @Schema(description = "集合地点", requiredMode = Schema.RequiredMode.REQUIRED, example = "增城广场加油站") + @NotBlank(message = "集合地点不能为空") + @Size(max = 200, message = "集合地点长度不能超过 200") + private String meetPlace; + + @Schema(description = "集合时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "集合时间不能为空") + private LocalDateTime meetTime; + + @Schema(description = "全程约多少公里", example = "50") + @DecimalMin(value = "0.0", message = "公里数不能为负数") + private BigDecimal distanceKm; + + @Schema(description = "城市(同城过滤)", example = "广州") + @Size(max = 64, message = "城市长度不能超过 64") + private String city; + + @Schema(description = "标签名列表", example = "[\"跑山\",\"新手友好\"]") + @Size(max = 5, message = "最多选择 5 个标签") + private List tags; + + @Schema(description = "人数上限", example = "10") + @NotNull(message = "人数上限不能为空") + @Min(value = 2, message = "人数上限最少 2 人") + @Max(value = 100, message = "人数上限最多 100 人") + private Integer capacity; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanPageReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanPageReqVO.java new file mode 100644 index 0000000..92b33a8 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanPageReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.member.controller.app.rideplan.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Size; + +@Schema(description = "用户 APP - 发车局分页查询 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class AppRidePlanPageReqVO extends PageParam { + + @Schema(description = "城市(同城过滤)", example = "广州") + @Size(max = 64, message = "城市长度不能超过 64") + private String city; + + @Schema(description = "关键词,匹配路线名称", example = "南昆山") + @Size(max = 100, message = "关键词长度不能超过 100") + private String keyword; + + @Schema(description = "标签名,精确匹配", example = "跑山") + @Size(max = 32, message = "标签长度不能超过 32") + private String tag; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanRespVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanRespVO.java new file mode 100644 index 0000000..88fe975 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRidePlanRespVO.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.member.controller.app.rideplan.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "用户 APP - 发车局 Response VO(免登录可查,已做字段脱敏)") +@Data +public class AppRidePlanRespVO { + + @Schema(description = "发车局编号", example = "1") + private Long id; + + @Schema(description = "路线名称", example = "南昆山经典环线") + private String routeName; + + @Schema(description = "集合地点", example = "增城广场加油站") + private String meetPlace; + + @Schema(description = "集合时间") + private LocalDateTime meetTime; + + @Schema(description = "全程约多少公里", example = "50") + private BigDecimal distanceKm; + + @Schema(description = "城市", example = "广州") + private String city; + + @Schema(description = "标签名列表", example = "[\"跑山\",\"新手友好\"]") + private List tags; + + @Schema(description = "人数上限", example = "10") + private Integer capacity; + + @Schema(description = "已报名人数", example = "3") + private Integer joinCount; + + // —— 发起人(只暴露昵称/头像,不含手机号等敏感字段)—— + + @Schema(description = "发起人编号", example = "1024") + private Long hostUserId; + + @Schema(description = "发起人昵称", example = "山路信天游") + private String hostNickname; + + @Schema(description = "发起人头像") + private String hostAvatar; + + @Schema(description = "已报名骑手列表(昵称 + 头像)") + private List joinedUsers; + + @Schema(description = "当前登录用户是否已加入(游客恒为 false)") + private Boolean joinedByMe; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "已报名骑手(只暴露昵称 + 头像)") + @Data + public static class JoinUser { + + @Schema(description = "用户编号", example = "2048") + private Long userId; + + @Schema(description = "昵称", example = "压弯不压车") + private String nickname; + + @Schema(description = "头像") + private String avatar; + + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRideTagRespVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRideTagRespVO.java new file mode 100644 index 0000000..696f508 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRideTagRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.member.controller.app.rideplan.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 骑行标签 Response VO") +@Data +public class AppRideTagRespVO { + + @Schema(description = "标签编号", example = "1") + private Long id; + + @Schema(description = "标签名", example = "跑山党") + private String name; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRideUserTagUpdateReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRideUserTagUpdateReqVO.java new file mode 100644 index 0000000..168cda8 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/rideplan/vo/AppRideUserTagUpdateReqVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.member.controller.app.rideplan.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Schema(description = "用户 APP - 设置我的骑行标签 Request VO") +@Data +public class AppRideUserTagUpdateReqVO { + + @Schema(description = "标签编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]") + @NotEmpty(message = "标签编号列表不能为空") + private List tagIds; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRidePlanDO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRidePlanDO.java new file mode 100644 index 0000000..210275d --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRidePlanDO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.member.dal.dataobject.rideplan; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 骑遇发车局 DO + */ +@TableName("member_ride_plan") +@KeySequence("member_ride_plan_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberRidePlanDO extends TenantBaseDO { + + @TableId + private Long id; + /** 发起人编号 member_user.id */ + private Long userId; + /** 路线名称 */ + private String routeName; + /** 集合地点 */ + private String meetPlace; + /** 集合时间 */ + private LocalDateTime meetTime; + /** 全程约多少公里 */ + private BigDecimal distanceKm; + /** 城市(同城过滤) */ + private String city; + /** 标签名,逗号分隔 */ + private String tags; + /** 人数上限 */ + private Integer capacity; + /** 已报名人数(冗余计数) */ + private Integer joinCount; + /** 被举报次数 */ + private Integer reportCount; + /** + * 状态:0 正常 1 已取消/下架 + */ + private Integer status; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRidePlanJoinDO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRidePlanJoinDO.java new file mode 100644 index 0000000..17d1a8a --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRidePlanJoinDO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.member.dal.dataobject.rideplan; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 骑遇发车局报名 DO + */ +@TableName("member_ride_plan_join") +@KeySequence("member_ride_plan_join_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberRidePlanJoinDO extends TenantBaseDO { + + @TableId + private Long id; + /** 发车局编号 member_ride_plan.id */ + private Long planId; + /** 报名人编号 member_user.id */ + private Long userId; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRideTagDO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRideTagDO.java new file mode 100644 index 0000000..bff2a21 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRideTagDO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.member.dal.dataobject.rideplan; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 骑遇骑行标签字典 DO(跑山党 / 新手友好 / 摩旅组队 ...) + */ +@TableName("member_ride_tag") +@KeySequence("member_ride_tag_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberRideTagDO extends TenantBaseDO { + + @TableId + private Long id; + /** 标签名 */ + private String name; + /** 显示顺序(越小越靠前) */ + private Integer sort; + /** + * 状态:0 启用 1 停用,见 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRideUserTagDO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRideUserTagDO.java new file mode 100644 index 0000000..38972a2 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/rideplan/MemberRideUserTagDO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.member.dal.dataobject.rideplan; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 骑手个人骑行标签关联 DO(Rider.tags) + */ +@TableName("member_ride_user_tag") +@KeySequence("member_ride_user_tag_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberRideUserTagDO extends TenantBaseDO { + + @TableId + private Long id; + /** 用户编号 member_user.id */ + private Long userId; + /** 标签编号 member_ride_tag.id */ + private Long tagId; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRidePlanJoinMapper.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRidePlanJoinMapper.java new file mode 100644 index 0000000..a573386 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRidePlanJoinMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.member.dal.mysql.rideplan; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRidePlanJoinDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MemberRidePlanJoinMapper extends BaseMapperX { + + default List selectListByPlanId(Long planId) { + return selectList(new LambdaQueryWrapperX() + .eq(MemberRidePlanJoinDO::getPlanId, planId)); + } + + default MemberRidePlanJoinDO selectByPlanIdAndUserId(Long planId, Long userId) { + return selectOne(new LambdaQueryWrapperX() + .eq(MemberRidePlanJoinDO::getPlanId, planId) + .eq(MemberRidePlanJoinDO::getUserId, userId)); + } + + default List selectListByUserId(Long userId) { + return selectList(new LambdaQueryWrapperX() + .eq(MemberRidePlanJoinDO::getUserId, userId)); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRidePlanMapper.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRidePlanMapper.java new file mode 100644 index 0000000..949afef --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRidePlanMapper.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.member.dal.mysql.rideplan; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanPageReqVO; +import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRidePlanDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; + +@Mapper +public interface MemberRidePlanMapper extends BaseMapperX { + + /** + * 发车局分页(免登录可查):只查未下架、未过期的,按集合时间升序 + */ + default PageResult selectPage(AppRidePlanPageReqVO reqVO) { + // 注意:LambdaQueryWrapperX 的 ifPresent 系列须在原生 eq/ge 之前调用, + // 原生方法返回值会退化为 LambdaQueryWrapper,链式后续找不到 ifPresent + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eqIfPresent(MemberRidePlanDO::getCity, reqVO.getCity()) + .likeIfPresent(MemberRidePlanDO::getRouteName, reqVO.getKeyword()); + wrapper.eq(MemberRidePlanDO::getStatus, 0); + wrapper.ge(MemberRidePlanDO::getMeetTime, LocalDateTime.now()); + if (reqVO.getTag() != null && !reqVO.getTag().isEmpty()) { + wrapper.apply("FIND_IN_SET({0}, tags) > 0", reqVO.getTag()); + } + wrapper.orderByAsc(MemberRidePlanDO::getMeetTime); + return selectPage(reqVO, wrapper); + } + + default MemberRidePlanDO selectByIdAndStatus(Long id, Integer status) { + return selectOne(new LambdaQueryWrapperX() + .eq(MemberRidePlanDO::getId, id) + .eq(MemberRidePlanDO::getStatus, status)); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRideTagMapper.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRideTagMapper.java new file mode 100644 index 0000000..a8b8e05 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRideTagMapper.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.member.dal.mysql.rideplan; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRideTagDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MemberRideTagMapper extends BaseMapperX { + + default List selectListByStatus(Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(MemberRideTagDO::getStatus, status) + .orderByAsc(MemberRideTagDO::getSort)); + } + + default MemberRideTagDO selectByName(String name) { + return selectOne(new LambdaQueryWrapperX() + .eq(MemberRideTagDO::getName, name)); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRideUserTagMapper.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRideUserTagMapper.java new file mode 100644 index 0000000..940e5e6 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/rideplan/MemberRideUserTagMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.member.dal.mysql.rideplan; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRideUserTagDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MemberRideUserTagMapper extends BaseMapperX { + + default List selectListByUserId(Long userId) { + return selectList(new LambdaQueryWrapperX() + .eq(MemberRideUserTagDO::getUserId, userId)); + } + + default void deleteByUserIdAndTagId(Long userId, Long tagId) { + delete(new LambdaQueryWrapperX() + .eq(MemberRideUserTagDO::getUserId, userId) + .eq(MemberRideUserTagDO::getTagId, tagId)); + } + + default void deleteByUserId(Long userId) { + delete(new LambdaQueryWrapperX() + .eq(MemberRideUserTagDO::getUserId, userId)); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/rideplan/MemberRidePlanService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/rideplan/MemberRidePlanService.java new file mode 100644 index 0000000..a3e30f1 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/rideplan/MemberRidePlanService.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.member.service.rideplan; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanCreateReqVO; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanPageReqVO; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanRespVO; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideTagRespVO; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideUserTagUpdateReqVO; + +import java.util.List; + +/** + * 骑遇发车局 / 骑行标签 Service + * + * 鉴权约定:查询类方法 loginUserId 可为 null(游客,@PermitAll 接口透传); + * 发布/报名/举报/设置标签必须登录,userId 由 Controller 从登录态取。 + */ +public interface MemberRidePlanService { + + /** + * 发车局分页(免登录可查) + * + * @param loginUserId 当前登录用户,游客为 null + */ + PageResult getRidePlanPage(AppRidePlanPageReqVO reqVO, Long loginUserId); + + /** + * 发车局详情(免登录可查) + */ + AppRidePlanRespVO getRidePlan(Long id, Long loginUserId); + + /** + * 发布发车局(需登录) + */ + Long createRidePlan(Long userId, AppRidePlanCreateReqVO reqVO); + + /** + * 加入发车局(需登录) + */ + void joinRidePlan(Long userId, Long planId); + + /** + * 取消加入发车局(需登录) + */ + void cancelJoinRidePlan(Long userId, Long planId); + + /** + * 举报发车局(需登录);举报达到阈值自动下架 + */ + void reportRidePlan(Long userId, Long planId); + + /** + * 骑行标签字典(免登录可查,只返回启用的) + */ + List getRideTagList(); + + /** + * 我的骑行标签(需登录) + */ + List getMyRideTags(Long userId); + + /** + * 全量替换我的骑行标签(需登录) + */ + void updateMyRideTags(Long userId, AppRideUserTagUpdateReqVO reqVO); + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/rideplan/MemberRidePlanServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/rideplan/MemberRidePlanServiceImpl.java new file mode 100644 index 0000000..0dbc697 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/rideplan/MemberRidePlanServiceImpl.java @@ -0,0 +1,266 @@ +package cn.iocoder.yudao.module.member.service.rideplan; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanCreateReqVO; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanPageReqVO; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanRespVO; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideTagRespVO; +import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideUserTagUpdateReqVO; +import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRidePlanDO; +import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRidePlanJoinDO; +import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRideTagDO; +import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRideUserTagDO; +import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO; +import cn.iocoder.yudao.module.member.dal.mysql.rideplan.MemberRidePlanJoinMapper; +import cn.iocoder.yudao.module.member.dal.mysql.rideplan.MemberRidePlanMapper; +import cn.iocoder.yudao.module.member.dal.mysql.rideplan.MemberRideTagMapper; +import cn.iocoder.yudao.module.member.dal.mysql.rideplan.MemberRideUserTagMapper; +import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.RIDE_PLAN_ALREADY_JOINED; +import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.RIDE_PLAN_FULL; +import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.RIDE_PLAN_MEET_TIME_INVALID; +import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.RIDE_PLAN_NOT_EXISTS; +import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.RIDE_PLAN_NOT_JOINED; +import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.RIDE_TAG_NOT_EXISTS; + +@Service +@Validated +public class MemberRidePlanServiceImpl implements MemberRidePlanService { + + /** 举报自动下架阈值 */ + private static final int REPORT_LIMIT = 5; + /** 状态:正常 */ + private static final int STATUS_NORMAL = 0; + /** 状态:已取消/下架 */ + private static final int STATUS_OFFLINE = 1; + + @Resource + private MemberRidePlanMapper ridePlanMapper; + @Resource + private MemberRidePlanJoinMapper ridePlanJoinMapper; + @Resource + private MemberRideTagMapper rideTagMapper; + @Resource + private MemberRideUserTagMapper rideUserTagMapper; + @Resource + private MemberUserMapper userMapper; + + @Override + public PageResult getRidePlanPage(AppRidePlanPageReqVO reqVO, Long loginUserId) { + PageResult pageResult = ridePlanMapper.selectPage(reqVO); + return new PageResult<>(convertList(pageResult.getList(), loginUserId), pageResult.getTotal()); + } + + @Override + public AppRidePlanRespVO getRidePlan(Long id, Long loginUserId) { + MemberRidePlanDO plan = ridePlanMapper.selectByIdAndStatus(id, STATUS_NORMAL); + if (plan == null) { + throw exception(RIDE_PLAN_NOT_EXISTS); + } + return convertList(Collections.singletonList(plan), loginUserId).get(0); + } + + @Override + public Long createRidePlan(Long userId, AppRidePlanCreateReqVO reqVO) { + if (!reqVO.getMeetTime().isAfter(LocalDateTime.now())) { + throw exception(RIDE_PLAN_MEET_TIME_INVALID); + } + // 校验标签名都存在于字典(保持数据干净) + List tags = reqVO.getTags() == null ? Collections.emptyList() : reqVO.getTags(); + for (String tag : tags) { + if (rideTagMapper.selectByName(tag) == null) { + throw exception(RIDE_TAG_NOT_EXISTS, tag); + } + } + MemberRidePlanDO plan = MemberRidePlanDO.builder() + .userId(userId) + .routeName(reqVO.getRouteName()) + .meetPlace(reqVO.getMeetPlace()) + .meetTime(reqVO.getMeetTime()) + .distanceKm(reqVO.getDistanceKm()) + .city(StrUtil.nullToEmpty(reqVO.getCity())) + .tags(String.join(",", tags)) + .capacity(reqVO.getCapacity()) + .joinCount(0) + .reportCount(0) + .status(STATUS_NORMAL) + .build(); + ridePlanMapper.insert(plan); + return plan.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void joinRidePlan(Long userId, Long planId) { + MemberRidePlanDO plan = ridePlanMapper.selectByIdAndStatus(planId, STATUS_NORMAL); + if (plan == null) { + throw exception(RIDE_PLAN_NOT_EXISTS); + } + if (ridePlanJoinMapper.selectByPlanIdAndUserId(planId, userId) != null) { + throw exception(RIDE_PLAN_ALREADY_JOINED); + } + if (plan.getJoinCount() >= plan.getCapacity()) { + throw exception(RIDE_PLAN_FULL); + } + MemberRidePlanJoinDO join = MemberRidePlanJoinDO.builder() + .planId(planId).userId(userId).build(); + ridePlanJoinMapper.insert(join); + plan.setJoinCount(plan.getJoinCount() + 1); + ridePlanMapper.updateById(plan); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelJoinRidePlan(Long userId, Long planId) { + MemberRidePlanDO plan = ridePlanMapper.selectByIdAndStatus(planId, STATUS_NORMAL); + if (plan == null) { + throw exception(RIDE_PLAN_NOT_EXISTS); + } + MemberRidePlanJoinDO join = ridePlanJoinMapper.selectByPlanIdAndUserId(planId, userId); + if (join == null) { + throw exception(RIDE_PLAN_NOT_JOINED); + } + ridePlanJoinMapper.deleteById(join.getId()); + plan.setJoinCount(Math.max(0, plan.getJoinCount() - 1)); + ridePlanMapper.updateById(plan); + } + + @Override + public void reportRidePlan(Long userId, Long planId) { + MemberRidePlanDO plan = ridePlanMapper.selectByIdAndStatus(planId, STATUS_NORMAL); + if (plan == null) { + throw exception(RIDE_PLAN_NOT_EXISTS); + } + plan.setReportCount(plan.getReportCount() + 1); + if (plan.getReportCount() >= REPORT_LIMIT) { + plan.setStatus(STATUS_OFFLINE); + } + ridePlanMapper.updateById(plan); + } + + @Override + public List getRideTagList() { + return rideTagMapper.selectListByStatus(STATUS_NORMAL).stream() + .map(tag -> BeanUtils.toBean(tag, AppRideTagRespVO.class)) + .collect(Collectors.toList()); + } + + @Override + public List getMyRideTags(Long userId) { + List userTags = rideUserTagMapper.selectListByUserId(userId); + if (userTags.isEmpty()) { + return Collections.emptyList(); + } + Set tagIds = userTags.stream() + .map(MemberRideUserTagDO::getTagId).collect(Collectors.toCollection(LinkedHashSet::new)); + Map tagMap = rideTagMapper.selectBatchIds(tagIds).stream() + .collect(Collectors.toMap(MemberRideTagDO::getId, Function.identity())); + return tagIds.stream() + .filter(tagMap::containsKey) + .map(id -> BeanUtils.toBean(tagMap.get(id), AppRideTagRespVO.class)) + .collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMyRideTags(Long userId, AppRideUserTagUpdateReqVO reqVO) { + // 校验标签都存在且启用 + List tags = rideTagMapper.selectBatchIds(reqVO.getTagIds()); + if (tags.size() != new LinkedHashSet<>(reqVO.getTagIds()).size()) { + throw exception(RIDE_TAG_NOT_EXISTS); + } + // 全量替换 + rideUserTagMapper.deleteByUserId(userId); + for (Long tagId : new LinkedHashSet<>(reqVO.getTagIds())) { + rideUserTagMapper.insert(MemberRideUserTagDO.builder() + .userId(userId).tagId(tagId).build()); + } + } + + // ─── 私有:DO → RespVO 批量转换(补发起人、报名人、joinedByMe)────── + + private List convertList(List plans, Long loginUserId) { + if (plans == null || plans.isEmpty()) { + return Collections.emptyList(); + } + // 发起人 + Set hostIds = plans.stream() + .map(MemberRidePlanDO::getUserId).collect(Collectors.toCollection(LinkedHashSet::new)); + Map userMap = userMapper.selectBatchIds(hostIds).stream() + .collect(Collectors.toMap(MemberUserDO::getId, Function.identity())); + // 报名记录 + List planIds = plans.stream().map(MemberRidePlanDO::getId).collect(Collectors.toList()); + Map> joinMap = ridePlanJoinMapper.selectList( + MemberRidePlanJoinDO::getPlanId, planIds).stream() + .collect(Collectors.groupingBy(MemberRidePlanJoinDO::getPlanId)); + // 报名人资料 + Set joinUserIds = joinMap.values().stream() + .flatMap(List::stream).map(MemberRidePlanJoinDO::getUserId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Map joinUserMap = joinUserIds.isEmpty() ? Collections.emptyMap() + : userMapper.selectBatchIds(joinUserIds).stream() + .collect(Collectors.toMap(MemberUserDO::getId, Function.identity())); + + List result = new ArrayList<>(plans.size()); + for (MemberRidePlanDO plan : plans) { + AppRidePlanRespVO vo = BeanUtils.toBean(plan, AppRidePlanRespVO.class); + vo.setTags(splitTags(plan.getTags())); + // 发起人(只暴露昵称/头像) + MemberUserDO host = userMap.get(plan.getUserId()); + if (host != null) { + vo.setHostUserId(host.getId()); + vo.setHostNickname(host.getNickname()); + vo.setHostAvatar(host.getAvatar()); + } + // 报名列表(只暴露昵称/头像) + List joinedUsers = joinMap + .getOrDefault(plan.getId(), Collections.emptyList()).stream() + .map(join -> { + MemberUserDO u = joinUserMap.get(join.getUserId()); + AppRidePlanRespVO.JoinUser ju = new AppRidePlanRespVO.JoinUser(); + if (u != null) { + ju.setUserId(u.getId()); + ju.setNickname(u.getNickname()); + ju.setAvatar(u.getAvatar()); + } else { + ju.setUserId(join.getUserId()); + } + return ju; + }).collect(Collectors.toList()); + vo.setJoinedUsers(joinedUsers); + // 当前用户是否已加入 + vo.setJoinedByMe(loginUserId != null && joinedUsers.stream() + .anyMatch(j -> Objects.equals(j.getUserId(), loginUserId))); + result.add(vo); + } + return result; + } + + private List splitTags(String tags) { + if (StrUtil.isEmpty(tags)) { + return Collections.emptyList(); + } + return StrUtil.split(tags, ',').stream() + .filter(StrUtil::isNotEmpty).collect(Collectors.toList()); + } + +}