feat(clothes): 支持通过社区公开穿搭只读查看衣物

新增 outfit_uuid 查询参数,非属主可通过社区公开穿搭查看衣物详情。
同时调整点赞通知逻辑,仅在首次点赞时发送通知。
This commit is contained in:
2026-09-16 02:28:59 +08:00
parent e9b9754b57
commit c2519c4b6d
5 changed files with 64 additions and 9 deletions
+1
View File
@@ -53,6 +53,7 @@ type ClothesUpdateByUUIDResponse struct{}
type ClothesGetByUUIDRequest struct { type ClothesGetByUUIDRequest struct {
ClothesUUID string `json:"clothes_uuid" binding:"required"` // 衣物UUID ClothesUUID string `json:"clothes_uuid" binding:"required"` // 衣物UUID
ShareUUID string `json:"share_uuid"` // 可选:通过穿搭分享只读查看时携带(非属主) ShareUUID string `json:"share_uuid"` // 可选:通过穿搭分享只读查看时携带(非属主)
OutfitUUID string `json:"outfit_uuid"` // 可选:通过社区公开穿搭只读查看时携带(非属主)
} }
// ClothesGetByUUIDResponse 衣物信息 // ClothesGetByUUIDResponse 衣物信息
+6 -1
View File
@@ -76,7 +76,12 @@ func (h *ClothesHandler) Get(c *gin.Context) {
response.ToErrorResponse(errcode.Unauthorized) response.ToErrorResponse(errcode.Unauthorized)
return return
} }
request := dto.ClothesGetByUUIDRequest{ClothesUUID: c.Param("uuid"), ShareUUID: c.Query("share_uuid")} request := dto.ClothesGetByUUIDRequest{
ClothesUUID: c.Param("uuid"),
// 非属主只读查看的两个入口:穿搭分享(share_uuid)与社区公开穿搭(outfit_uuid)
ShareUUID: c.Query("share_uuid"),
OutfitUUID: c.Query("outfit_uuid"),
}
if request.ClothesUUID == "" { if request.ClothesUUID == "" {
response.ToErrorResponse(errcode.InvalidParams) response.ToErrorResponse(errcode.InvalidParams)
return return
+47 -3
View File
@@ -391,7 +391,8 @@ func (s *ClothesService) ProcessStatusGetByUUID(ctx context.Context, userID int6
} }
// GetByUUID 获取衣物详情。 // GetByUUID 获取衣物详情。
// 属主始终可见;非属主仅允许携带有效穿搭分享(share_uuid)时,只读查看该分享穿搭内的衣物。 // 属主始终可见;非属主只有两个只读入口:
// 携带有效穿搭分享(share_uuid)查看该分享穿搭内的衣物,或携带社区公开穿搭(outfit_uuid)查看已公开可见穿搭内的衣物。
func (s *ClothesService) GetByUUID(ctx context.Context, userID int64, request *dto.ClothesGetByUUIDRequest) (*dto.ClothesGetByUUIDResponse, *errcode.Error) { func (s *ClothesService) GetByUUID(ctx context.Context, userID int64, request *dto.ClothesGetByUUIDRequest) (*dto.ClothesGetByUUIDResponse, *errcode.Error) {
ctx, span := global.Tracer.Start(ctx, "svc.ClothesGetByUUID") ctx, span := global.Tracer.Start(ctx, "svc.ClothesGetByUUID")
defer span.End() defer span.End()
@@ -404,18 +405,61 @@ func (s *ClothesService) GetByUUID(ctx context.Context, userID int64, request *d
return nil, errcode.DatabaseError return nil, errcode.DatabaseError
} }
if clothes.UserID != userID { if clothes.UserID != userID {
if cerr := s.assertClothesVisibleViaOutfitShare(ctx, userID, request.ShareUUID, clothes.ID); cerr != nil { if cerr := s.assertClothesVisibleToVisitor(ctx, userID, request, clothes.ID); cerr != nil {
return nil, cerr return nil, cerr
} }
} }
response := buildClothesResponse(clothes) response := buildClothesResponse(clothes)
// 非属主只读查看(穿搭分享):不暴露内部失败原因 // 非属主只读查看:不暴露内部失败原因;社区(未携带 share_uuid 的公开穿搭入口)
// 是陌生人可见场景,属主私有备注同样不下发。
if clothes.UserID != userID { if clothes.UserID != userID {
response.FailReason = "" response.FailReason = ""
if request.ShareUUID == "" {
response.Remark = ""
}
} }
return &response, nil return &response, nil
} }
// assertClothesVisibleToVisitor 校验非属主可以只读查看该衣物。
// 两个入口互斥:优先穿搭分享,其次社区公开穿搭;都未携带则直接拒绝。
func (s *ClothesService) assertClothesVisibleToVisitor(ctx context.Context, userID int64, request *dto.ClothesGetByUUIDRequest, clothesID int64) *errcode.Error {
if request.ShareUUID != "" {
return s.assertClothesVisibleViaOutfitShare(ctx, userID, request.ShareUUID, clothesID)
}
if request.OutfitUUID != "" {
return s.assertClothesVisibleViaPublicOutfit(ctx, request.OutfitUUID, clothesID)
}
return errcode.ClothesNoPermission
}
// assertClothesVisibleViaPublicOutfit 校验非属主可通过社区公开穿搭只读查看该衣物。
// 可见性口径与 OutfitService.GetByUUID 的非属主分支保持一致(公开 + 审核通过 + 正常)。
func (s *ClothesService) assertClothesVisibleViaPublicOutfit(ctx context.Context, outfitUUID string, clothesID int64) *errcode.Error {
outfit, err := s.outfitDao.OutfitGetByUUID(ctx, outfitUUID)
if err != nil {
if isNotFound(err) {
// 不区分「穿搭不存在」与「穿搭不可见」,统一按无权限返回,避免探测他人穿搭
return errcode.ClothesNoPermission
}
return errcode.DatabaseError
}
if !isOutfitPublicVisible(outfit) {
return errcode.ClothesNoPermission
}
// 该衣物必须属于该公开穿搭的明细。
items, ierr := s.outfitItemDao.OutfitItemListByOutfitID(ctx, outfit.ID)
if ierr != nil {
return errcode.DatabaseError
}
for _, it := range items {
if it.ClothesID == clothesID {
return nil
}
}
return errcode.ClothesNoPermission
}
// assertClothesVisibleViaOutfitShare 校验非属主可通过某穿搭分享只读查看该衣物。 // assertClothesVisibleViaOutfitShare 校验非属主可通过某穿搭分享只读查看该衣物。
func (s *ClothesService) assertClothesVisibleViaOutfitShare(ctx context.Context, userID int64, shareUUID string, clothesID int64) *errcode.Error { func (s *ClothesService) assertClothesVisibleViaOutfitShare(ctx context.Context, userID int64, shareUUID string, clothesID int64) *errcode.Error {
if shareUUID == "" { if shareUUID == "" {
+4 -3
View File
@@ -64,9 +64,10 @@ func (s *LikeService) LikeCreate(ctx context.Context, userID int64, request *dto
"target_id", targetID, "user_id", userID, "error", rerr) "target_id", targetID, "user_id", userID, "error", rerr)
} }
// 仅在「从取消/无记录变为点赞」时发通知,避免并发/重复点赞重复通知。 // 仅在「首次点赞」时发通知(likes 表此前无记录)。
wasLiked := !notExist && existing.Status == InteractStatusOn // 取消点赞只把 status 置为已取消、不删行,因此 notExist 可稳定表达
if !wasLiked && outfit.UserID != userID { // 「此前从未赞过该穿搭」;取消后再点赞不再重复通知,避免频繁点赞/取消把拥有者刷屏。
if notExist && outfit.UserID != userID {
sendNotification(ctx, outfit.UserID, userID, NotificationTypeLike, "收到新的点赞", "你的穿搭「"+outfit.OutfitName+"」收到了一个赞") sendNotification(ctx, outfit.UserID, userID, NotificationTypeLike, "收到新的点赞", "你的穿搭「"+outfit.OutfitName+"」收到了一个赞")
} }
+6 -2
View File
@@ -817,15 +817,19 @@ Authorization: Bearer <access_token>
| --- | --- | --- | --- | | --- | --- | --- | --- |
| uuid | string | ✅ | 衣物 UUID | | uuid | string | ✅ | 衣物 UUID |
| share_uuid | string | 否 | 穿搭分享 UUID:非属主通过穿搭分享只读查看该分享穿搭内的衣物时必传 | | share_uuid | string | 否 | 穿搭分享 UUID:非属主通过穿搭分享只读查看该分享穿搭内的衣物时必传 |
| outfit_uuid | string | 否 | 穿搭 UUID:非属主通过**社区公开穿搭**只读查看该穿搭内的衣物时必传 |
**响应** `data` = [衣物对象](#42-衣物对象-clothesitem) **响应** `data` = [衣物对象](#42-衣物对象-clothesitem)
**业务逻辑(可见性)** **业务逻辑(可见性)**
- 属主:始终可见。 - 属主:始终可见。
- 非属主:**仅当携带有效 `share_uuid`**(该分享正常、穿搭未被删除 / 封禁、且该衣物属于该分享的穿搭明细)时返回**只读**衣物信息;否则 `50005`。 - 非属主:两个只读入口,**二选一携带即可**,都不携带则 `50005`。
1. `share_uuid`:该分享正常、穿搭未被删除 / 封禁、且该衣物属于该分享的穿搭明细 → 返回**只读**衣物信息。
2. `outfit_uuid`:该穿搭**公开可见**(`is_public=2` 且 `audit_status=2` 且 `status=1`,与 `GET /outfit/:uuid` 非属主口径一致)且该衣物属于该穿搭明细 → 返回**只读**衣物信息。
- 只读场景下响应不含失败原因(`fail_reason` 恒为空);通过 `outfit_uuid`(社区公开穿搭)进入时,属主**私有备注 `remark` 也不下发**,避免暴露给陌生人(`share_uuid` 好友分享场景保持原样)。
**错误码**:`10000` 参数错误(uuid 为空)· `50000` 衣物不存在 · `50005` 无权操作该衣物 · `90103` 穿搭分享不存在(含穿搭已删除 / 已封禁)· `90104` 穿搭分享已取消 **错误码**:`10000` 参数错误(uuid 为空)· `50000` 衣物不存在 · `50005` 无权操作该衣物(含未携带凭证 / 穿搭不可见 / 衣物不属于该穿搭)· `90103` 穿搭分享不存在(含穿搭已删除 / 已封禁)· `90104` 穿搭分享已取消
--- ---