package service import ( "context" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/global" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqldao" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redisdao" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode" ) // LikeService 点赞业务服务。 type LikeService struct { likeDao *mysqldao.LikeDao outfitDao *mysqldao.OutfitDao } // NewLikeService 创建点赞服务。 func NewLikeService() *LikeService { return &LikeService{ likeDao: mysqldao.NewLikeDao(), outfitDao: mysqldao.NewOutfitDao(), } } // LikeCreate 点赞或取消点赞社区穿搭。 func (s *LikeService) LikeCreate(ctx context.Context, userID int64, request *dto.LikeCreateRequest) (*dto.LikeResponse, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.LikeLikeCreate") defer span.End() outfit, cerr := s.getPublicOutfit(ctx, userID, request.TargetUUID) if cerr != nil { return nil, cerr } targetID := outfit.ID existing, err := s.likeDao.LikeGetByTargetAndUser(ctx, LikeTypeOutfit, targetID, userID) notExist := false if err != nil { if isNotFound(err) { notExist = true } else { return nil, errcode.DatabaseError } } switch request.Action { case ActionDo: // 点赞 if cerr := s.likeDao.LikeUpsertOn(ctx, &mysqlmodel.Like{ LikeType: LikeTypeOutfit, TargetID: targetID, UserID: userID, Status: InteractStatusOn, }); cerr != nil { return nil, errcode.LikeFailed } _ = redisdao.LikeAdd(ctx, LikeTypeOutfit, targetID, userID) // 仅在「从取消/无记录变为点赞」时发通知,避免并发/重复点赞重复通知。 wasLiked := !notExist && existing.Status == InteractStatusOn if !wasLiked && outfit.UserID != userID { sendNotification(ctx, outfit.UserID, userID, NotificationTypeLike, "收到新的点赞", "你的穿搭「"+outfit.OutfitName+"」收到了一个赞") } case ActionCancel: // 取消点赞 if !notExist && existing.Status == InteractStatusOn { if uerr := s.likeDao.LikeUpdateStatus(ctx, existing.ID, InteractStatusOff); uerr != nil { return nil, errcode.LikeFailed } _ = redisdao.LikeRemove(ctx, LikeTypeOutfit, targetID, userID) } } return &dto.LikeResponse{}, nil } // GetByTargetID 查询穿搭点赞状态与点赞数(Redis 优先)。 func (s *LikeService) GetByTargetID(ctx context.Context, userID int64, request *dto.LikeGetByTargetIDRequest) (*dto.LikeGetByTargetIDResponse, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.LikeGetByTargetID") defer span.End() outfit, cerr := s.getPublicOutfit(ctx, userID, request.TargetUUID) if cerr != nil { return nil, cerr } targetID := outfit.ID // 是否已点赞:Redis 优先,回落 MySQL。 isLiked, err := redisdao.LikeIsMember(ctx, LikeTypeOutfit, targetID, userID) if err != nil { if rec, derr := s.likeDao.LikeGetByTargetAndUser(ctx, LikeTypeOutfit, targetID, userID); derr == nil { isLiked = rec.Status == InteractStatusOn } } // 点赞总数:Redis 优先,回落 MySQL。 count, err := redisdao.LikeCount(ctx, LikeTypeOutfit, targetID) if err != nil || count == 0 { if dbCnt, derr := s.likeDao.LikeCountByTarget(ctx, LikeTypeOutfit, targetID); derr == nil { count = dbCnt } } return &dto.LikeGetByTargetIDResponse{ UserIsLiked: isLiked, LikeCount: count, }, nil } // getPublicOutfit 按 UUID 查询社区穿搭(须存在),非本人仅可操作公开可见的穿搭。 func (s *LikeService) getPublicOutfit(ctx context.Context, userID int64, outfitUUID string) (*mysqlmodel.Outfit, *errcode.Error) { outfit, err := s.outfitDao.OutfitGetByUUID(ctx, outfitUUID) if err != nil { if isNotFound(err) { return nil, errcode.TargetNotFound } return nil, errcode.DatabaseError } if outfit.UserID != userID && !isOutfitPublicVisible(outfit) { return nil, errcode.OutfitNoPermission } return outfit, nil }