骑行模块代码

This commit is contained in:
周鹏 committed 2026-09-13 08:30:58 +08:00
1 parent 283ac807da
commit 377f9f56c7
11 files changed
+733

No files matched your search

@@ -110,4 +110,5 @@ public interface MemberErrorCode {
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, "骑行标签不存在或已停用");
ErrorCode RIDE_RECORD_NOT_EXISTS = new ErrorCode(1_004_020_006, "骑行战报不存在或已下架");
}
@@ -0,0 +1,96 @@
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.AppRideRecordCreateReqVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideRecordPageReqVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideRecordRespVO;
import cn.iocoder.yudao.module.member.service.rideplan.MemberRideRecordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.validation.Valid;
import javax.validation.constraints.Size;
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;
/**
* 骑遇骑行记录(战报):
* - 战报墙 page / 详情 get / 天梯榜 rank @PermitAll 免登录,游客可浏览,RespVO 已做字段脱敏(只暴露昵称/头像)
* - 上报 create / 点赞 like / 举报 report 需登录(/app-api/** 默认拦截,未带有效 token 返回 401)
*/
@Tag(name = "用户 APP - 骑遇骑行记录")
@RestController
@RequestMapping("/member/ride-record")
@Validated
public class AppRideRecordController {
@Resource
private MemberRideRecordService rideRecordService;
@PostMapping("/create")
@Operation(summary = "上报骑行战报(需登录)", description = "完骑后提交;cornerName 有值则参与天梯榜")
public CommonResult<Long> create(@Valid @RequestBody AppRideRecordCreateReqVO reqVO) {
return success(rideRecordService.createRideRecord(getLoginUserId(), reqVO));
}
@GetMapping("/page")
@Operation(summary = "战报墙分页(免登录)", description = "游客可查;登录后返回 likedByMe")
@PermitAll
public CommonResult<PageResult<AppRideRecordRespVO>> page(@Valid AppRideRecordPageReqVO reqVO) {
return success(rideRecordService.getRideRecordPage(reqVO, getLoginUserId()));
}
@GetMapping("/my")
@Operation(summary = "我的骑行记录分页(需登录)")
public CommonResult<PageResult<AppRideRecordRespVO>> my(@Valid AppRideRecordPageReqVO reqVO) {
return success(rideRecordService.getMyRideRecordPage(getLoginUserId(), reqVO));
}
@GetMapping("/get")
@Operation(summary = "战报详情(免登录,含轨迹)")
@Parameter(name = "id", description = "战报编号", required = true, example = "1")
@PermitAll
public CommonResult<AppRideRecordRespVO> get(@RequestParam("id") Long id) {
return success(rideRecordService.getRideRecord(id, getLoginUserId()));
}
@GetMapping("/rank")
@Operation(summary = "弯道天梯榜(免登录)", description = "按路线+弯道分组,组内按压弯角度降序;支持过滤")
@PermitAll
public CommonResult<List<AppRideRecordRespVO.CornerBoard>> rank(
@RequestParam(value = "roadName", required = false)
@Size(max = 50, message = "路线名长度不能超过 50") String roadName,
@RequestParam(value = "cornerName", required = false)
@Size(max = 50, message = "弯道名长度不能超过 50") String cornerName) {
return success(rideRecordService.getCornerRank(roadName, cornerName));
}
@PostMapping("/like")
@Operation(summary = "点赞/取消点赞(需登录)", description = "幂等切换,返回操作后是否处于点赞状态")
@Parameter(name = "recordId", description = "战报编号", required = true, example = "1")
public CommonResult<Boolean> like(@RequestParam("recordId") Long recordId) {
return success(rideRecordService.toggleLike(getLoginUserId(), recordId));
}
@PostMapping("/report")
@Operation(summary = "举报战报(需登录)", description = "达到阈值自动下架")
@Parameter(name = "recordId", description = "战报编号", required = true, example = "1")
public CommonResult<Boolean> report(@RequestParam("recordId") Long recordId) {
rideRecordService.reportRideRecord(getLoginUserId(), recordId);
return success(true);
}
}
@@ -0,0 +1,65 @@
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.DecimalMax;
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;
@Schema(description = "用户 APP - 骑行战报上报 Request VO(完骑后提交,需登录)")
@Data
public class AppRideRecordCreateReqVO {
@Schema(description = "战报标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "南昆山 63 连弯,今天左弯终于不虚了")
@NotBlank(message = "战报标题不能为空")
@Size(max = 100, message = "战报标题不能超过 100 字")
private String title;
@Schema(description = "路线名(可选)", example = "南昆山")
@Size(max = 50, message = "路线名不能超过 50 字")
private String roadName;
@Schema(description = "弯道名(可选,有值参与天梯榜)", example = "观音潭右弯")
@Size(max = 50, message = "弯道名不能超过 50 字")
private String cornerName;
@Schema(description = "里程(公里)", requiredMode = Schema.RequiredMode.REQUIRED, example = "87.3")
@NotNull(message = "里程不能为空")
@DecimalMin(value = "0.1", message = "里程至少 0.1 公里")
@DecimalMax(value = "5000", message = "里程异常")
private BigDecimal distanceKm;
@Schema(description = "骑行时长(秒)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5760")
@NotNull(message = "骑行时长不能为空")
@Min(value = 30, message = "骑行时长太短")
private Integer durationSec;
@Schema(description = "极速(km/h)", requiredMode = Schema.RequiredMode.REQUIRED, example = "138")
@NotNull(message = "极速不能为空")
@DecimalMin(value = "0", message = "极速不能为负")
@DecimalMax(value = "500", message = "极速异常")
private BigDecimal topSpeedKmh;
@Schema(description = "过弯次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "63")
@NotNull(message = "过弯次数不能为空")
@Min(value = 0, message = "过弯次数不能为负")
@Max(value = 10000, message = "过弯次数异常")
private Integer curveCount;
@Schema(description = "最大压弯角度(度)", requiredMode = Schema.RequiredMode.REQUIRED, example = "47.2")
@NotNull(message = "最大压弯角度不能为空")
@DecimalMin(value = "0", message = "压弯角度不能为负")
@DecimalMax(value = "90", message = "压弯角度异常")
private BigDecimal maxLean;
@Schema(description = "轨迹点 JSON:[{\"lat\":23.1,\"lng\":113.3,\"speed\":88}],可选,仅详情接口回显")
@Size(max = 2_000_000, message = "轨迹数据过大")
private String trackJson;
}
@@ -0,0 +1,22 @@
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 AppRideRecordPageReqVO extends PageParam {
@Schema(description = "按骑手过滤(查某人战报)", example = "1024")
private Long userId;
@Schema(description = "路线名过滤", example = "南昆山")
@Size(max = 50, message = "路线名长度不能超过 50")
private String roadName;
}
@@ -0,0 +1,115 @@
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 AppRideRecordRespVO {
@Schema(description = "战报编号", example = "1")
private Long id;
@Schema(description = "战报标题", example = "南昆山 63 连弯,今天左弯终于不虚了")
private String title;
@Schema(description = "路线名", example = "南昆山")
private String roadName;
@Schema(description = "弯道名", example = "观音潭右弯")
private String cornerName;
@Schema(description = "里程(公里)", example = "87.3")
private BigDecimal distanceKm;
@Schema(description = "骑行时长(秒)", example = "5760")
private Integer durationSec;
@Schema(description = "极速(km/h)", example = "138")
private BigDecimal topSpeedKmh;
@Schema(description = "过弯次数", example = "63")
private Integer curveCount;
@Schema(description = "最大压弯角度(度)", example = "47.2")
private BigDecimal maxLean;
@Schema(description = "轨迹点 JSON,仅详情接口返回")
private String trackJson;
@Schema(description = "点赞数", example = "128")
private Integer likeCount;
@Schema(description = "当前登录用户是否已点赞(游客恒为 false)")
private Boolean likedByMe;
// —— 骑手(只暴露昵称/头像,不含手机号等敏感字段)——
@Schema(description = "骑手编号", example = "1024")
private Long userId;
@Schema(description = "骑手昵称", example = "山路信天游")
private String nickname;
@Schema(description = "骑手头像")
private String avatar;
@Schema(description = "发布时间")
private LocalDateTime createTime;
@Schema(description = "天梯榜条目(rank 接口专用)")
@Data
public static class RankItem {
@Schema(description = "名次,从 1 开始", example = "1")
private Integer rank;
@Schema(description = "战报编号", example = "5")
private Long recordId;
@Schema(description = "路线名", example = "南昆山")
private String roadName;
@Schema(description = "弯道名", example = "观音潭右弯")
private String cornerName;
@Schema(description = "最大压弯角度(度)", example = "52.0")
private BigDecimal maxLean;
@Schema(description = "极速(km/h)", example = "62")
private BigDecimal topSpeedKmh;
@Schema(description = "骑手编号", example = "1024")
private Long userId;
@Schema(description = "骑手昵称", example = "压弯不压车")
private String nickname;
@Schema(description = "骑手头像")
private String avatar;
@Schema(description = "骑行时间")
private LocalDateTime createTime;
}
@Schema(description = "弯道榜单(rank 接口返回,按弯道分组)")
@Data
public static class CornerBoard {
@Schema(description = "路线名", example = "南昆山")
private String roadName;
@Schema(description = "弯道名", example = "观音潭右弯")
private String cornerName;
@Schema(description = "该弯道上榜条目(已按压弯角度降序)")
private List<RankItem> entries;
}
}
@@ -0,0 +1,55 @@
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;
/**
* 骑遇骑行记录(战报)DO
*/
@TableName("member_ride_record")
@KeySequence("member_ride_record_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberRideRecordDO extends TenantBaseDO {
@TableId
private Long id;
/** 骑手编号 member_user.id */
private Long userId;
/** 战报标题 */
private String title;
/** 路线名(如:南昆山) */
private String roadName;
/** 弯道名(如:观音潭右弯),为空表示非跑山记录 */
private String cornerName;
/** 里程(公里) */
private BigDecimal distanceKm;
/** 骑行时长(秒) */
private Integer durationSec;
/** 极速(km/h) */
private BigDecimal topSpeedKmh;
/** 过弯次数 */
private Integer curveCount;
/** 最大压弯角度(度) */
private BigDecimal maxLean;
/** 轨迹点 JSON([{"lat":..,"lng":..,"speed":..}]),仅详情接口返回 */
private String trackJson;
/** 点赞数(冗余计数) */
private Integer likeCount;
/** 被举报次数 */
private Integer reportCount;
/**
* 状态:0 正常 1 已下架
*/
private Integer status;
}
@@ -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_record_like")
@KeySequence("member_ride_record_like_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberRideRecordLikeDO extends TenantBaseDO {
@TableId
private Long id;
/** 战报编号 member_ride_record.id */
private Long recordId;
/** 点赞人编号 member_user.id */
private Long userId;
}
@@ -0,0 +1,26 @@
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.MemberRideRecordLikeDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface MemberRideRecordLikeMapper extends BaseMapperX<MemberRideRecordLikeDO> {
default MemberRideRecordLikeDO selectByRecordIdAndUserId(Long recordId, Long userId) {
return selectOne(new LambdaQueryWrapperX<MemberRideRecordLikeDO>()
.eq(MemberRideRecordLikeDO::getRecordId, recordId)
.eq(MemberRideRecordLikeDO::getUserId, userId));
}
default List<MemberRideRecordLikeDO> selectListByUserIdAndRecordIds(Long userId, Collection<Long> recordIds) {
return selectList(new LambdaQueryWrapperX<MemberRideRecordLikeDO>()
.eq(MemberRideRecordLikeDO::getUserId, userId)
.in(MemberRideRecordLikeDO::getRecordId, recordIds));
}
}
@@ -0,0 +1,59 @@
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.AppRideRecordPageReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRideRecordDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MemberRideRecordMapper extends BaseMapperX<MemberRideRecordDO> {
/**
* 战报墙分页(免登录可查):只查正常状态,按发布时间倒序
*/
default PageResult<MemberRideRecordDO> selectPage(AppRideRecordPageReqVO reqVO) {
LambdaQueryWrapperX<MemberRideRecordDO> wrapper = new LambdaQueryWrapperX<MemberRideRecordDO>()
.eqIfPresent(MemberRideRecordDO::getUserId, reqVO.getUserId())
.eqIfPresent(MemberRideRecordDO::getRoadName, reqVO.getRoadName());
wrapper.eq(MemberRideRecordDO::getStatus, 0);
wrapper.orderByDesc(MemberRideRecordDO::getId);
return selectPage(reqVO, wrapper);
}
/**
* 我的历史骑行(分页)
*/
default PageResult<MemberRideRecordDO> selectMyPage(Long userId, AppRideRecordPageReqVO reqVO) {
LambdaQueryWrapperX<MemberRideRecordDO> wrapper = new LambdaQueryWrapperX<MemberRideRecordDO>()
.eq(MemberRideRecordDO::getUserId, userId)
.eq(MemberRideRecordDO::getStatus, 0);
wrapper.orderByDesc(MemberRideRecordDO::getId);
return selectPage(reqVO, wrapper);
}
/**
* 弯道天梯榜:取最大压弯角度最高的前 N 条(有弯道名的跑山记录)
*/
default List<MemberRideRecordDO> selectRank(String roadName, String cornerName, int limit) {
LambdaQueryWrapperX<MemberRideRecordDO> wrapper = new LambdaQueryWrapperX<MemberRideRecordDO>()
.eqIfPresent(MemberRideRecordDO::getRoadName, roadName)
.eqIfPresent(MemberRideRecordDO::getCornerName, cornerName);
wrapper.eq(MemberRideRecordDO::getStatus, 0);
wrapper.isNotNull(MemberRideRecordDO::getCornerName);
wrapper.ne(MemberRideRecordDO::getCornerName, "");
wrapper.orderByDesc(MemberRideRecordDO::getMaxLean);
wrapper.last("LIMIT " + limit);
return selectList(wrapper);
}
default MemberRideRecordDO selectByIdAndStatus(Long id, Integer status) {
return selectOne(new LambdaQueryWrapperX<MemberRideRecordDO>()
.eq(MemberRideRecordDO::getId, id)
.eq(MemberRideRecordDO::getStatus, status));
}
}
@@ -0,0 +1,50 @@
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.AppRideRecordCreateReqVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideRecordPageReqVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideRecordRespVO;
import java.util.List;
/**
* 骑遇骑行记录(战报)Service
*/
public interface MemberRideRecordService {
/**
* 上报一次骑行战报(需登录)
*/
Long createRideRecord(Long userId, AppRideRecordCreateReqVO reqVO);
/**
* 战报墙分页(免登录;登录后返回 likedByMe)
*/
PageResult<AppRideRecordRespVO> getRideRecordPage(AppRideRecordPageReqVO reqVO, Long loginUserId);
/**
* 我的骑行记录分页(需登录)
*/
PageResult<AppRideRecordRespVO> getMyRideRecordPage(Long userId, AppRideRecordPageReqVO reqVO);
/**
* 战报详情(免登录,含轨迹)
*/
AppRideRecordRespVO getRideRecord(Long id, Long loginUserId);
/**
* 弯道天梯榜(免登录):按 road+corner 分组,组内按压弯角度降序
*/
List<AppRideRecordRespVO.CornerBoard> getCornerRank(String roadName, String cornerName);
/**
* 点赞/取消点赞(需登录,幂等切换),返回操作后是否处于点赞状态
*/
boolean toggleLike(Long userId, Long recordId);
/**
* 举报战报(需登录),达到阈值自动下架
*/
void reportRideRecord(Long userId, Long recordId);
}
@@ -0,0 +1,215 @@
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.AppRideRecordCreateReqVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideRecordPageReqVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideRecordRespVO;
import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRideRecordDO;
import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRideRecordLikeDO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.mysql.rideplan.MemberRideRecordLikeMapper;
import cn.iocoder.yudao.module.member.dal.mysql.rideplan.MemberRideRecordMapper;
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.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
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_RECORD_NOT_EXISTS;
@Service
@Validated
public class MemberRideRecordServiceImpl implements MemberRideRecordService {
/** 举报自动下架阈值 */
private static final int REPORT_LIMIT = 5;
/** 状态:正常 */
private static final int STATUS_NORMAL = 0;
/** 状态:已下架 */
private static final int STATUS_OFFLINE = 1;
/** 天梯榜单弯道最多展示条数 */
private static final int RANK_LIMIT = 100;
/** 每条弯道榜单最多人数 */
private static final int CORNER_BOARD_SIZE = 5;
@Resource
private MemberRideRecordMapper rideRecordMapper;
@Resource
private MemberRideRecordLikeMapper rideRecordLikeMapper;
@Resource
private MemberUserMapper userMapper;
@Override
public Long createRideRecord(Long userId, AppRideRecordCreateReqVO reqVO) {
MemberRideRecordDO record = MemberRideRecordDO.builder()
.userId(userId)
.title(reqVO.getTitle())
.roadName(StrUtil.nullToEmpty(reqVO.getRoadName()))
.cornerName(StrUtil.nullToEmpty(reqVO.getCornerName()))
.distanceKm(reqVO.getDistanceKm())
.durationSec(reqVO.getDurationSec())
.topSpeedKmh(reqVO.getTopSpeedKmh())
.curveCount(reqVO.getCurveCount())
.maxLean(reqVO.getMaxLean())
.trackJson(reqVO.getTrackJson())
.likeCount(0)
.reportCount(0)
.status(STATUS_NORMAL)
.build();
rideRecordMapper.insert(record);
return record.getId();
}
@Override
public PageResult<AppRideRecordRespVO> getRideRecordPage(AppRideRecordPageReqVO reqVO, Long loginUserId) {
PageResult<MemberRideRecordDO> pageResult = rideRecordMapper.selectPage(reqVO);
return new PageResult<>(convertList(pageResult.getList(), loginUserId, false), pageResult.getTotal());
}
@Override
public PageResult<AppRideRecordRespVO> getMyRideRecordPage(Long userId, AppRideRecordPageReqVO reqVO) {
reqVO.setUserId(null); // my 接口以登录身份为准,忽略过滤参数
PageResult<MemberRideRecordDO> pageResult = rideRecordMapper.selectMyPage(userId, reqVO);
return new PageResult<>(convertList(pageResult.getList(), userId, false), pageResult.getTotal());
}
@Override
public AppRideRecordRespVO getRideRecord(Long id, Long loginUserId) {
MemberRideRecordDO record = rideRecordMapper.selectByIdAndStatus(id, STATUS_NORMAL);
if (record == null) {
throw exception(RIDE_RECORD_NOT_EXISTS);
}
return convertList(Collections.singletonList(record), loginUserId, true).get(0);
}
@Override
public List<AppRideRecordRespVO.CornerBoard> getCornerRank(String roadName, String cornerName) {
List<MemberRideRecordDO> records = rideRecordMapper.selectRank(roadName, cornerName, RANK_LIMIT);
if (records.isEmpty()) {
return Collections.emptyList();
}
// 骑手资料
Set<Long> userIds = records.stream()
.map(MemberRideRecordDO::getUserId).collect(Collectors.toCollection(LinkedHashSet::new));
Map<Long, MemberUserDO> userMap = userMapper.selectBatchIds(userIds).stream()
.collect(Collectors.toMap(MemberUserDO::getId, u -> u));
// 按 road+corner 分组(保持 maxLean 降序,selectRank 已排序)
Map<String, List<MemberRideRecordDO>> grouped = new LinkedHashMap<>();
for (MemberRideRecordDO record : records) {
grouped.computeIfAbsent(record.getRoadName() + "§" + record.getCornerName(),
k -> new ArrayList<>()).add(record);
}
List<AppRideRecordRespVO.CornerBoard> boards = new ArrayList<>(grouped.size());
for (List<MemberRideRecordDO> group : grouped.values()) {
MemberRideRecordDO first = group.get(0);
AppRideRecordRespVO.CornerBoard board = new AppRideRecordRespVO.CornerBoard();
board.setRoadName(first.getRoadName());
board.setCornerName(first.getCornerName());
board.setEntries(group.stream().limit(CORNER_BOARD_SIZE)
.map(r -> {
AppRideRecordRespVO.RankItem item = new AppRideRecordRespVO.RankItem();
item.setRecordId(r.getId());
item.setRoadName(r.getRoadName());
item.setCornerName(r.getCornerName());
item.setMaxLean(r.getMaxLean());
item.setTopSpeedKmh(r.getTopSpeedKmh());
item.setUserId(r.getUserId());
item.setCreateTime(r.getCreateTime());
MemberUserDO u = userMap.get(r.getUserId());
if (u != null) {
item.setNickname(u.getNickname());
item.setAvatar(u.getAvatar());
}
return item;
}).collect(Collectors.toList()));
for (int i = 0; i < board.getEntries().size(); i++) {
board.getEntries().get(i).setRank(i + 1);
}
boards.add(board);
}
return boards;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean toggleLike(Long userId, Long recordId) {
MemberRideRecordDO record = rideRecordMapper.selectByIdAndStatus(recordId, STATUS_NORMAL);
if (record == null) {
throw exception(RIDE_RECORD_NOT_EXISTS);
}
MemberRideRecordLikeDO like = rideRecordLikeMapper.selectByRecordIdAndUserId(recordId, userId);
if (like != null) {
rideRecordLikeMapper.deleteById(like.getId());
record.setLikeCount(Math.max(0, record.getLikeCount() - 1));
rideRecordMapper.updateById(record);
return false;
}
rideRecordLikeMapper.insert(MemberRideRecordLikeDO.builder()
.recordId(recordId).userId(userId).build());
record.setLikeCount(record.getLikeCount() + 1);
rideRecordMapper.updateById(record);
return true;
}
@Override
public void reportRideRecord(Long userId, Long recordId) {
MemberRideRecordDO record = rideRecordMapper.selectByIdAndStatus(recordId, STATUS_NORMAL);
if (record == null) {
throw exception(RIDE_RECORD_NOT_EXISTS);
}
record.setReportCount(record.getReportCount() + 1);
if (record.getReportCount() >= REPORT_LIMIT) {
record.setStatus(STATUS_OFFLINE);
}
rideRecordMapper.updateById(record);
}
// ─── 私有:DO → RespVO 批量转换(补骑手资料、likedByMe)──────
private List<AppRideRecordRespVO> convertList(List<MemberRideRecordDO> records, Long loginUserId,
boolean withTrack) {
if (records == null || records.isEmpty()) {
return Collections.emptyList();
}
// 骑手资料
Set<Long> userIds = records.stream()
.map(MemberRideRecordDO::getUserId).collect(Collectors.toCollection(LinkedHashSet::new));
Map<Long, MemberUserDO> userMap = userMapper.selectBatchIds(userIds).stream()
.collect(Collectors.toMap(MemberUserDO::getId, u -> u));
// 当前用户对这批战报的点赞
Set<Long> likedRecordIds = Collections.emptySet();
if (loginUserId != null) {
List<Long> recordIds = records.stream().map(MemberRideRecordDO::getId).collect(Collectors.toList());
likedRecordIds = rideRecordLikeMapper.selectListByUserIdAndRecordIds(loginUserId, recordIds).stream()
.map(MemberRideRecordLikeDO::getRecordId).collect(Collectors.toSet());
}
Set<Long> finalLiked = likedRecordIds;
return records.stream().map(record -> {
AppRideRecordRespVO vo = BeanUtils.toBean(record, AppRideRecordRespVO.class);
if (!withTrack) {
vo.setTrackJson(null); // 列表不带轨迹,减小响应体
}
MemberUserDO user = userMap.get(record.getUserId());
if (user != null) {
vo.setNickname(user.getNickname());
vo.setAvatar(user.getAvatar());
}
vo.setLikedByMe(loginUserId != null && finalLiked.contains(record.getId()));
return vo;
}).collect(Collectors.toList());
}
}