feat(interaction): 支持游客互动与登录合并

This commit is contained in:
Zhou Weidong committed 2026-10-26 22:34:14 +08:00
1 parent 2fe24b9db7
commit 4b0fbd73d3
41 files changed
+1207 -90

No files matched your search

@@ -20,6 +20,7 @@ public class UserFavorite {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long userId;
private String deviceId;
private Long dramaId;
private Integer sort;
private LocalDateTime createdAt;
@@ -17,4 +17,11 @@ public interface UserFavoriteMapper extends BaseMapper<UserFavorite> {
*/
@Delete("DELETE FROM user_favorite WHERE user_id = #{userId} AND drama_id = #{dramaId}")
int physicalDelete(@Param("userId") Long userId, @Param("dramaId") Long dramaId);
@Delete("DELETE FROM user_favorite WHERE user_id IS NULL AND device_id = #{deviceId} "
+ "AND drama_id = #{dramaId}")
int physicalDeleteDevice(@Param("deviceId") String deviceId, @Param("dramaId") Long dramaId);
@Delete("DELETE FROM user_favorite WHERE id = #{id}")
int physicalDeleteById(@Param("id") Long id);
}
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.interaction.InteractionIdentity;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.enums.ContentStatus;
import com.fruitshorts.content.service.DramaService;
@@ -37,21 +38,28 @@ public class FavoriteService {
/** 返回切换后的状态:true 表示已收藏。 */
@Transactional(rollbackFor = Exception.class)
public boolean toggle(Long userId, Long dramaId) {
return toggle(new InteractionIdentity(userId, ""), dramaId);
}
@Transactional(rollbackFor = Exception.class)
public boolean toggle(InteractionIdentity identity, Long dramaId) {
dramaService.getPublished(dramaId);
UserFavorite existing = favoriteMapper.selectOne(Wrappers.<UserFavorite>lambdaQuery()
.eq(UserFavorite::getUserId, userId)
UserFavorite existing = favoriteMapper.selectOne(identityQuery(identity)
.eq(UserFavorite::getDramaId, dramaId)
.last("limit 1"));
if (existing != null) {
// 必须物理删除:唯一索引 (user_id, drama_id) 会挡住"取消后再收藏"
int removed = favoriteMapper.physicalDelete(userId, dramaId);
int removed = identity.authenticated()
? favoriteMapper.physicalDelete(identity.userId(), dramaId)
: favoriteMapper.physicalDeleteDevice(identity.deviceId(), dramaId);
if (removed > 0) {
dramaService.adjustFavoriteCount(dramaId, -1);
}
return false;
}
UserFavorite favorite = new UserFavorite();
favorite.setUserId(userId);
favorite.setUserId(identity.userId());
favorite.setDeviceId(identity.authenticated() ? "" : identity.deviceId());
favorite.setDramaId(dramaId);
favorite.setSort(0);
favoriteMapper.insert(favorite);
@@ -68,7 +76,13 @@ public class FavoriteService {
/** 批量判断用户是否已收藏,一次查询覆盖多部剧。 */
public Set<Long> favoriteDramaIds(Long userId, Collection<Long> dramaIds) {
if (userId == null || dramaIds == null || dramaIds.isEmpty()) {
return favoriteDramaIds(userId, "", dramaIds);
}
/** 登录用户优先;匿名状态按设备号查询。 */
public Set<Long> favoriteDramaIds(Long userId, String deviceId,
Collection<Long> dramaIds) {
if (dramaIds == null || dramaIds.isEmpty()) {
return Set.of();
}
Set<Long> normalized = new HashSet<>();
@@ -80,14 +94,62 @@ public class FavoriteService {
if (normalized.isEmpty()) {
return Set.of();
}
return favoriteMapper.selectList(Wrappers.<UserFavorite>lambdaQuery()
.eq(UserFavorite::getUserId, userId)
if (userId == null && (deviceId == null || deviceId.isBlank())) {
return Set.of();
}
return favoriteMapper.selectList(identityQuery(new InteractionIdentity(userId, deviceId))
.in(UserFavorite::getDramaId, normalized))
.stream()
.map(UserFavorite::getDramaId)
.collect(java.util.stream.Collectors.toSet());
}
/** 登录时把设备收藏归并到账号;重复收藏删除并回退剧级计数。 */
@Transactional(rollbackFor = Exception.class)
public MergeResult mergeDeviceIntoUser(Long userId, String deviceId) {
if (userId == null || deviceId == null || deviceId.isBlank()) {
return new MergeResult(0, 0);
}
List<UserFavorite> deviceFavorites = favoriteMapper.selectList(
Wrappers.<UserFavorite>lambdaQuery()
.isNull(UserFavorite::getUserId)
.eq(UserFavorite::getDeviceId, deviceId));
int migrated = 0;
int duplicates = 0;
for (UserFavorite deviceFavorite : deviceFavorites) {
UserFavorite existing = favoriteMapper.selectOne(
Wrappers.<UserFavorite>lambdaQuery()
.eq(UserFavorite::getUserId, userId)
.eq(UserFavorite::getDramaId, deviceFavorite.getDramaId())
.last("limit 1"));
if (existing != null) {
if (favoriteMapper.physicalDeleteById(deviceFavorite.getId()) > 0) {
dramaService.adjustFavoriteCount(deviceFavorite.getDramaId(), -1);
}
duplicates++;
continue;
}
deviceFavorite.setUserId(userId);
favoriteMapper.updateById(deviceFavorite);
migrated++;
}
return new MergeResult(migrated, duplicates);
}
private com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<UserFavorite>
identityQuery(InteractionIdentity identity) {
if (identity.authenticated()) {
return Wrappers.<UserFavorite>lambdaQuery()
.eq(UserFavorite::getUserId, identity.userId());
}
return Wrappers.<UserFavorite>lambdaQuery()
.isNull(UserFavorite::getUserId)
.eq(UserFavorite::getDeviceId, identity.deviceId());
}
public record MergeResult(int migrated, int duplicates) {
}
/** 分页查询收藏短剧,保持收藏时间与 dramaId 稳定排序。 */
public PageResult<Drama> page(Long userId, int pageIndex, int pageSize) {
int safeIndex = Math.max(pageIndex, 1);
@@ -130,6 +192,14 @@ public class FavoriteService {
}
}
@Transactional(rollbackFor = Exception.class)
public void removeDevice(String deviceId, Long dramaId) {
int removed = favoriteMapper.physicalDeleteDevice(deviceId, dramaId);
if (removed > 0) {
dramaService.adjustFavoriteCount(dramaId, -1);
}
}
@Transactional(rollbackFor = Exception.class)
public void removeBatch(Long userId, List<Long> dramaIds) {
if (dramaIds == null || dramaIds.isEmpty()) {
@@ -2,6 +2,8 @@
package com.fruitshorts.playback.service;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.entity.DramaEpisode;
import com.fruitshorts.content.service.DramaService;
@@ -57,6 +59,10 @@ public class PlaybackService {
boolean free = episode.getIsFree() != null && episode.getIsFree() == 1;
boolean entitled = userId != null && entitlementPort.isUnlocked(userId, drama.getId(), episode.getId());
if (!free && !entitled && userId == null) {
// 锁定集正片必须登录;即使暂时没有片源,也不能把游客引到匿名取票分支。
throw new ServiceException(ErrorCode.UNAUTHORIZED, "登录后才能解锁并观看该集");
}
String source = playableSource(episode);
PlayRespVO vo = new PlayRespVO();
@@ -11,4 +11,7 @@ public class FavoriteToggleReqVO {
@NotNull(message = "缺少 dramaId")
private Long dramaId;
/** 兼容旧调用;正常客户端通过 X-Device-Id 下发。 */
private String deviceId;
}