fix:分享
This commit is contained in:
@@ -32,6 +32,7 @@ type ClosetShareItemResponse struct {
|
|||||||
Status int8 `json:"status"` // 1=正常 2=已取消 3=已过期 4=已封禁
|
Status int8 `json:"status"` // 1=正常 2=已取消 3=已过期 4=已封禁
|
||||||
OwnerName string `json:"owner_name"` // 分享者昵称("分享给我的"列表返回)
|
OwnerName string `json:"owner_name"` // 分享者昵称("分享给我的"列表返回)
|
||||||
OwnerAvatar string `json:"owner_avatar"` // 分享者头像("分享给我的"列表返回)
|
OwnerAvatar string `json:"owner_avatar"` // 分享者头像("分享给我的"列表返回)
|
||||||
|
CreatedAt int64 `json:"created_at"` // 分享创建时间(秒,前端合并排序用)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShareListRequest 我的分享列表
|
// ShareListRequest 我的分享列表
|
||||||
|
|||||||
@@ -52,6 +52,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"` // 可选:通过穿搭分享只读查看时携带(非属主)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClothesGetByUUIDResponse 衣物信息
|
// ClothesGetByUUIDResponse 衣物信息
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
// OutfitShareCreateRequest 创建穿搭分享
|
||||||
|
type OutfitShareCreateRequest struct {
|
||||||
|
OutfitUUID string `json:"outfit_uuid" binding:"required"` // 穿搭UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareCreateResponse 创建穿搭分享响应
|
||||||
|
type OutfitShareCreateResponse struct {
|
||||||
|
ShareUUID string `json:"share_uuid"` // 分享UUID,供他人访问分享详情
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareDetailResponse 穿搭分享详情(访客 / 属主视角)
|
||||||
|
type OutfitShareDetailResponse struct {
|
||||||
|
ShareUUID string `json:"share_uuid"`
|
||||||
|
OwnerName string `json:"owner_name"`
|
||||||
|
OwnerAvatar string `json:"owner_avatar"`
|
||||||
|
Status int8 `json:"status"` // 1=正常 2=已取消
|
||||||
|
IsOwner bool `json:"is_owner"` // 是否本人分享
|
||||||
|
Outfit OutfitCreateResponse `json:"outfit"` // 穿搭内容(含 items 的 processed_image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareItemResponse 穿搭分享列表项
|
||||||
|
type OutfitShareItemResponse struct {
|
||||||
|
ShareUUID string `json:"share_uuid"`
|
||||||
|
OutfitUUID string `json:"outfit_uuid"`
|
||||||
|
OutfitName string `json:"outfit_name"`
|
||||||
|
CoverImage string `json:"cover_image"` // 封面(首件衣物抠图 URL,无则空)
|
||||||
|
ItemsCount int `json:"items_count"` // 包含衣物数量
|
||||||
|
Status int8 `json:"status"` // 1=正常 2=已取消
|
||||||
|
OwnerName string `json:"owner_name"` // 分享者昵称("分享给我的"列表返回)
|
||||||
|
OwnerAvatar string `json:"owner_avatar"`
|
||||||
|
CreatedAt int64 `json:"created_at"` // 分享创建时间(秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareListRequest 穿搭分享列表请求
|
||||||
|
type OutfitShareListRequest struct {
|
||||||
|
PageRequest
|
||||||
|
Status int8 `form:"status" json:"status"` // 状态 1=正常 2=已取消(0 不筛选)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareListResponse 穿搭分享列表响应
|
||||||
|
type OutfitShareListResponse struct {
|
||||||
|
PageResponse
|
||||||
|
List []OutfitShareItemResponse `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareCancelRequest 取消穿搭分享
|
||||||
|
type OutfitShareCancelRequest struct {
|
||||||
|
ShareUUID string `json:"share_uuid" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareCancelResponse 取消穿搭分享响应
|
||||||
|
type OutfitShareCancelResponse struct{}
|
||||||
@@ -108,6 +108,22 @@ func (d *OutfitDao) OutfitGetByID(ctx context.Context, id int64) (*mysqlmodel.Ou
|
|||||||
return &outfit, nil
|
return &outfit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OutfitGetByIDs 按主键 ID 批量查询穿搭
|
||||||
|
func (d *OutfitDao) OutfitGetByIDs(ctx context.Context, ids []int64) ([]mysqlmodel.Outfit, error) {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitGetByIDs")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
var list []mysqlmodel.Outfit
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
if err := d.db.WithContext(ctx).Where("id IN ?", ids).Find(&list).Error; err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
// OutfitGetByUUIDs 按 UUID 批量查询穿搭
|
// OutfitGetByUUIDs 按 UUID 批量查询穿搭
|
||||||
func (d *OutfitDao) OutfitGetByUUIDs(ctx context.Context, uuids []string) ([]mysqlmodel.Outfit, error) {
|
func (d *OutfitDao) OutfitGetByUUIDs(ctx context.Context, uuids []string) ([]mysqlmodel.Outfit, error) {
|
||||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitGetByUUIDs")
|
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitGetByUUIDs")
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package mysqldao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||||
|
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OutfitShareDao 穿搭分享数据访问
|
||||||
|
type OutfitShareDao struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOutfitShareDao 创建穿搭分享 DAO
|
||||||
|
func NewOutfitShareDao() *OutfitShareDao {
|
||||||
|
return &OutfitShareDao{db: global.MysqlEngine}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareCreate 创建穿搭分享
|
||||||
|
func (d *OutfitShareDao) OutfitShareCreate(ctx context.Context, share *mysqlmodel.OutfitShare) error {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitShareCreate")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
if err := d.db.WithContext(ctx).Create(share).Error; err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareGetByUUID 按 UUID 查询穿搭分享
|
||||||
|
func (d *OutfitShareDao) OutfitShareGetByUUID(ctx context.Context, shareUUID string) (*mysqlmodel.OutfitShare, error) {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitShareGetByUUID")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
var share mysqlmodel.OutfitShare
|
||||||
|
if err := d.db.WithContext(ctx).Where("share_uuid = ?", shareUUID).First(&share).Error; err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &share, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareUpdateStatus 更新穿搭分享状态(取消)
|
||||||
|
func (d *OutfitShareDao) OutfitShareUpdateStatus(ctx context.Context, shareUUID string, status int8) error {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitShareUpdateStatus")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
if err := d.db.WithContext(ctx).Model(&mysqlmodel.OutfitShare{}).
|
||||||
|
Where("share_uuid = ?", shareUUID).
|
||||||
|
UpdateColumn("status", status).Error; err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareListByUserID 分页查询我的穿搭分享(status 为 0 不筛选)
|
||||||
|
func (d *OutfitShareDao) OutfitShareListByUserID(ctx context.Context, userID int64, status int8, page, pageSize int) ([]mysqlmodel.OutfitShare, int64, error) {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitShareListByUserID")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
var list []mysqlmodel.OutfitShare
|
||||||
|
var total int64
|
||||||
|
q := d.db.WithContext(ctx).Model(&mysqlmodel.OutfitShare{}).Where("user_id = ?", userID)
|
||||||
|
if status != 0 {
|
||||||
|
q = q.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
if err := q.Count(&total).Error; err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if err := q.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&list).Error; err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareReceiverDao 穿搭分享接收人数据访问
|
||||||
|
type OutfitShareReceiverDao struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOutfitShareReceiverDao 创建穿搭分享接收人 DAO
|
||||||
|
func NewOutfitShareReceiverDao() *OutfitShareReceiverDao {
|
||||||
|
return &OutfitShareReceiverDao{db: global.MysqlEngine}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareReceiverUpsert 幂等记录接收关系:唯一索引冲突时不做任何事,避免重复记录。
|
||||||
|
func (d *OutfitShareReceiverDao) OutfitShareReceiverUpsert(ctx context.Context, shareUUID string, receiverUserID int64) error {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitShareReceiverUpsert")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
receiver := &mysqlmodel.OutfitShareReceiver{
|
||||||
|
ShareUUID: shareUUID,
|
||||||
|
ReceiverUserID: receiverUserID,
|
||||||
|
}
|
||||||
|
if err := d.db.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(receiver).Error; err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareReceivedList 联表分页查询某接收人收到的穿搭分享(status 为 0 不筛选)。
|
||||||
|
func (d *OutfitShareReceiverDao) OutfitShareReceivedList(ctx context.Context, receiverUserID int64, status int8, page, pageSize int) ([]mysqlmodel.OutfitShare, int64, error) {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitShareReceivedList")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
var list []mysqlmodel.OutfitShare
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
q := d.db.WithContext(ctx).
|
||||||
|
Model(&mysqlmodel.OutfitShare{}).
|
||||||
|
Joins("JOIN outfit_share_receivers r ON r.share_uuid = outfit_shares.share_uuid").
|
||||||
|
Where("r.receiver_user_id = ?", receiverUserID)
|
||||||
|
if status != 0 {
|
||||||
|
q = q.Where("outfit_shares.status = ?", status)
|
||||||
|
}
|
||||||
|
if err := q.Count(&total).Error; err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if err := q.Select("outfit_shares.*").
|
||||||
|
Order("r.id DESC").
|
||||||
|
Offset((page - 1) * pageSize).
|
||||||
|
Limit(pageSize).
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package mysqlmodel
|
||||||
|
|
||||||
|
// OutfitShare 穿搭分享。
|
||||||
|
type OutfitShare struct {
|
||||||
|
Model
|
||||||
|
ShareUUID string `gorm:"column:share_uuid"` // 分享UUID
|
||||||
|
UserID int64 `gorm:"column:user_id"` // 所属用户ID
|
||||||
|
OutfitID int64 `gorm:"column:outfit_id"` // 穿搭ID
|
||||||
|
Status int8 `gorm:"column:status"` // 分享状态 1=正常 2=用户取消
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OutfitShare) TableName() string {
|
||||||
|
return "outfit_shares"
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutfitShareReceiver 穿搭分享接收人关联(访问即视为收到,幂等)。
|
||||||
|
type OutfitShareReceiver struct {
|
||||||
|
Model
|
||||||
|
ShareUUID string `gorm:"column:share_uuid;uniqueIndex:uk_outfit_share_receiver,priority:1"` // 分享UUID
|
||||||
|
ReceiverUserID int64 `gorm:"column:receiver_user_id;uniqueIndex:uk_outfit_share_receiver,priority:2"` // 接收人用户ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OutfitShareReceiver) TableName() string {
|
||||||
|
return "outfit_share_receivers"
|
||||||
|
}
|
||||||
@@ -76,7 +76,7 @@ func (h *ClothesHandler) Get(c *gin.Context) {
|
|||||||
response.ToErrorResponse(errcode.Unauthorized)
|
response.ToErrorResponse(errcode.Unauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
request := dto.ClothesGetByUUIDRequest{ClothesUUID: c.Param("uuid")}
|
request := dto.ClothesGetByUUIDRequest{ClothesUUID: c.Param("uuid"), ShareUUID: c.Query("share_uuid")}
|
||||||
if request.ClothesUUID == "" {
|
if request.ClothesUUID == "" {
|
||||||
response.ToErrorResponse(errcode.InvalidParams)
|
response.ToErrorResponse(errcode.InvalidParams)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
|
||||||
|
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
|
||||||
|
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
|
||||||
|
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
|
||||||
|
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OutfitShareHandler 穿搭分享相关接口。
|
||||||
|
type OutfitShareHandler struct {
|
||||||
|
svc *service.OutfitShareService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOutfitShareHandler 创建穿搭分享 handler。
|
||||||
|
func NewOutfitShareHandler() *OutfitShareHandler {
|
||||||
|
return &OutfitShareHandler{svc: service.NewOutfitShareService()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建穿搭分享。
|
||||||
|
func (h *OutfitShareHandler) Create(c *gin.Context) {
|
||||||
|
response := app.NewResponse(c)
|
||||||
|
userID, ok := middleware.GetUserID(c.Request.Context())
|
||||||
|
if !ok {
|
||||||
|
response.ToErrorResponse(errcode.Unauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request dto.OutfitShareCreateRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, e := h.svc.Create(c.Request.Context(), userID, &request)
|
||||||
|
if e != nil {
|
||||||
|
response.ToErrorResponse(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.ToResponse(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 分页查询我的穿搭分享。
|
||||||
|
func (h *OutfitShareHandler) List(c *gin.Context) {
|
||||||
|
response := app.NewResponse(c)
|
||||||
|
userID, ok := middleware.GetUserID(c.Request.Context())
|
||||||
|
if !ok {
|
||||||
|
response.ToErrorResponse(errcode.Unauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request dto.OutfitShareListRequest
|
||||||
|
if err := c.ShouldBindQuery(&request); err != nil {
|
||||||
|
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, e := h.svc.List(c.Request.Context(), userID, &request)
|
||||||
|
if e != nil {
|
||||||
|
response.ToErrorResponse(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.ToResponse(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReceivedList 分页查询分享给我的穿搭分享。
|
||||||
|
func (h *OutfitShareHandler) ReceivedList(c *gin.Context) {
|
||||||
|
response := app.NewResponse(c)
|
||||||
|
userID, ok := middleware.GetUserID(c.Request.Context())
|
||||||
|
if !ok {
|
||||||
|
response.ToErrorResponse(errcode.Unauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request dto.OutfitShareListRequest
|
||||||
|
if err := c.ShouldBindQuery(&request); err != nil {
|
||||||
|
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, e := h.svc.ListReceived(c.Request.Context(), userID, &request)
|
||||||
|
if e != nil {
|
||||||
|
response.ToErrorResponse(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.ToResponse(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail 查看穿搭分享详情(uuid 来自路径参数)。
|
||||||
|
func (h *OutfitShareHandler) Detail(c *gin.Context) {
|
||||||
|
response := app.NewResponse(c)
|
||||||
|
userID, ok := middleware.GetUserID(c.Request.Context())
|
||||||
|
if !ok {
|
||||||
|
response.ToErrorResponse(errcode.Unauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shareUUID := c.Param("uuid")
|
||||||
|
if shareUUID == "" {
|
||||||
|
response.ToErrorResponse(errcode.InvalidParams)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, e := h.svc.Detail(c.Request.Context(), userID, shareUUID)
|
||||||
|
if e != nil {
|
||||||
|
response.ToErrorResponse(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.ToResponse(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel 取消穿搭分享(uuid 来自路径参数)。
|
||||||
|
func (h *OutfitShareHandler) Cancel(c *gin.Context) {
|
||||||
|
response := app.NewResponse(c)
|
||||||
|
userID, ok := middleware.GetUserID(c.Request.Context())
|
||||||
|
if !ok {
|
||||||
|
response.ToErrorResponse(errcode.Unauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request := dto.OutfitShareCancelRequest{ShareUUID: c.Param("uuid")}
|
||||||
|
if request.ShareUUID == "" {
|
||||||
|
response.ToErrorResponse(errcode.InvalidParams)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, e := h.svc.Cancel(c.Request.Context(), userID, &request)
|
||||||
|
if e != nil {
|
||||||
|
response.ToErrorResponse(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.ToResponse(out)
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ func NewRouter() *gin.Engine {
|
|||||||
clothesHandler := v1.NewClothesHandler()
|
clothesHandler := v1.NewClothesHandler()
|
||||||
outfitHandler := v1.NewOutfitHandler()
|
outfitHandler := v1.NewOutfitHandler()
|
||||||
closetHandler := v1.NewClosetShareHandler()
|
closetHandler := v1.NewClosetShareHandler()
|
||||||
|
outfitShareHandler := v1.NewOutfitShareHandler()
|
||||||
collectHandler := v1.NewCollectHandler()
|
collectHandler := v1.NewCollectHandler()
|
||||||
likeHandler := v1.NewLikeHandler()
|
likeHandler := v1.NewLikeHandler()
|
||||||
notificationHandler := v1.NewNotificationHandler()
|
notificationHandler := v1.NewNotificationHandler()
|
||||||
@@ -99,6 +100,13 @@ func NewRouter() *gin.Engine {
|
|||||||
auth.GET("/closet-share/:uuid", closetHandler.Detail)
|
auth.GET("/closet-share/:uuid", closetHandler.Detail)
|
||||||
auth.POST("/closet-share/:uuid/cancel", closetHandler.Cancel)
|
auth.POST("/closet-share/:uuid/cancel", closetHandler.Cancel)
|
||||||
|
|
||||||
|
// 穿搭分享
|
||||||
|
auth.POST("/outfit-share", outfitShareHandler.Create)
|
||||||
|
auth.GET("/outfit-share", outfitShareHandler.List)
|
||||||
|
auth.GET("/outfit-share/received", outfitShareHandler.ReceivedList)
|
||||||
|
auth.GET("/outfit-share/:uuid", outfitShareHandler.Detail)
|
||||||
|
auth.POST("/outfit-share/:uuid/cancel", outfitShareHandler.Cancel)
|
||||||
|
|
||||||
// 收藏
|
// 收藏
|
||||||
auth.POST("/collect", collectHandler.Collect)
|
auth.POST("/collect", collectHandler.Collect)
|
||||||
auth.GET("/collect", collectHandler.List)
|
auth.GET("/collect", collectHandler.List)
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ func (s *ClosetShareService) ListReceived(ctx context.Context, userID int64, req
|
|||||||
Status: sh.Status,
|
Status: sh.Status,
|
||||||
OwnerName: ownerName,
|
OwnerName: ownerName,
|
||||||
OwnerAvatar: ownerAvatar,
|
OwnerAvatar: ownerAvatar,
|
||||||
|
CreatedAt: sh.CreatedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,15 +27,21 @@ var imageProcessSem = make(chan struct{}, 10)
|
|||||||
|
|
||||||
// ClothesService 衣物业务服务。
|
// ClothesService 衣物业务服务。
|
||||||
type ClothesService struct {
|
type ClothesService struct {
|
||||||
clothesDao *mysqldao.ClothesDao
|
clothesDao *mysqldao.ClothesDao
|
||||||
userDao *mysqldao.UserDao
|
userDao *mysqldao.UserDao
|
||||||
|
outfitShareDao *mysqldao.OutfitShareDao
|
||||||
|
outfitDao *mysqldao.OutfitDao
|
||||||
|
outfitItemDao *mysqldao.OutfitItemDao
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClothesService 创建衣物服务。
|
// NewClothesService 创建衣物服务。
|
||||||
func NewClothesService() *ClothesService {
|
func NewClothesService() *ClothesService {
|
||||||
return &ClothesService{
|
return &ClothesService{
|
||||||
clothesDao: mysqldao.NewClothesDao(),
|
clothesDao: mysqldao.NewClothesDao(),
|
||||||
userDao: mysqldao.NewUserDao(),
|
userDao: mysqldao.NewUserDao(),
|
||||||
|
outfitShareDao: mysqldao.NewOutfitShareDao(),
|
||||||
|
outfitDao: mysqldao.NewOutfitDao(),
|
||||||
|
outfitItemDao: mysqldao.NewOutfitItemDao(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,18 +306,66 @@ func (s *ClothesService) ProcessStatusGetByUUID(ctx context.Context, userID int6
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetByUUID 获取衣物详情。
|
// GetByUUID 获取衣物详情。
|
||||||
|
// 属主始终可见;非属主仅允许携带有效穿搭分享(share_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()
|
||||||
|
|
||||||
clothes, cerr := s.getOwnedClothes(ctx, request.ClothesUUID, userID)
|
clothes, err := s.clothesDao.ClothesGetByUUID(ctx, request.ClothesUUID)
|
||||||
if cerr != nil {
|
if err != nil {
|
||||||
return nil, cerr
|
if isNotFound(err) {
|
||||||
|
return nil, errcode.ClothesNotFound
|
||||||
|
}
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
if clothes.UserID != userID {
|
||||||
|
if cerr := s.assertClothesVisibleViaOutfitShare(ctx, userID, request.ShareUUID, clothes.ID); cerr != nil {
|
||||||
|
return nil, cerr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
response := buildClothesResponse(clothes)
|
response := buildClothesResponse(clothes)
|
||||||
return &response, nil
|
return &response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assertClothesVisibleViaOutfitShare 校验非属主可通过某穿搭分享只读查看该衣物。
|
||||||
|
func (s *ClothesService) assertClothesVisibleViaOutfitShare(ctx context.Context, userID int64, shareUUID string, clothesID int64) *errcode.Error {
|
||||||
|
if shareUUID == "" {
|
||||||
|
return errcode.ClothesNoPermission
|
||||||
|
}
|
||||||
|
share, err := s.outfitShareDao.OutfitShareGetByUUID(ctx, shareUUID)
|
||||||
|
if err != nil {
|
||||||
|
if isNotFound(err) {
|
||||||
|
return errcode.OutfitShareNotFound
|
||||||
|
}
|
||||||
|
return errcode.DatabaseError
|
||||||
|
}
|
||||||
|
if share.Status == ShareStatusCancelled {
|
||||||
|
return errcode.OutfitShareCancelled
|
||||||
|
}
|
||||||
|
outfit, oerr := s.outfitDao.OutfitGetByID(ctx, share.OutfitID)
|
||||||
|
if oerr != nil {
|
||||||
|
if isNotFound(oerr) {
|
||||||
|
return errcode.OutfitShareNotFound
|
||||||
|
}
|
||||||
|
return errcode.DatabaseError
|
||||||
|
}
|
||||||
|
// 访客查看要求穿搭仍公开可见(属主访问自己的分享始终可查看)。
|
||||||
|
if share.UserID != userID && !isOutfitPublicVisible(outfit) {
|
||||||
|
return errcode.OutfitShareNotFound
|
||||||
|
}
|
||||||
|
// 该衣物必须属于该分享的穿搭明细。
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateByUUID 修改衣物属性(仅更新非空字段)。
|
// UpdateByUUID 修改衣物属性(仅更新非空字段)。
|
||||||
func (s *ClothesService) UpdateByUUID(ctx context.Context, userID int64, request *dto.ClothesUpdateByUUIDRequest) (*dto.ClothesUpdateByUUIDResponse, *errcode.Error) {
|
func (s *ClothesService) UpdateByUUID(ctx context.Context, userID int64, request *dto.ClothesUpdateByUUIDRequest) (*dto.ClothesUpdateByUUIDResponse, *errcode.Error) {
|
||||||
ctx, span := global.Tracer.Start(ctx, "svc.ClothesUpdateByUUID")
|
ctx, span := global.Tracer.Start(ctx, "svc.ClothesUpdateByUUID")
|
||||||
|
|||||||
@@ -325,7 +325,12 @@ func (s *OutfitService) createItems(ctx context.Context, outfitID int64, clothes
|
|||||||
|
|
||||||
// itemResponses 获取某穿搭的衣物明细(含图片 URL)。
|
// itemResponses 获取某穿搭的衣物明细(含图片 URL)。
|
||||||
func (s *OutfitService) itemResponses(ctx context.Context, outfitID int64) ([]dto.OutfitItemResponse, error) {
|
func (s *OutfitService) itemResponses(ctx context.Context, outfitID int64) ([]dto.OutfitItemResponse, error) {
|
||||||
items, err := s.outfitItemDao.OutfitItemListByOutfitID(ctx, outfitID)
|
return loadOutfitItemResponses(ctx, s.outfitItemDao, s.clothesDao, outfitID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadOutfitItemResponses 获取某穿搭的衣物明细(含图片 URL),供穿搭详情与穿搭分享复用。
|
||||||
|
func loadOutfitItemResponses(ctx context.Context, outfitItemDao *mysqldao.OutfitItemDao, clothesDao *mysqldao.ClothesDao, outfitID int64) ([]dto.OutfitItemResponse, error) {
|
||||||
|
items, err := outfitItemDao.OutfitItemListByOutfitID(ctx, outfitID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -333,13 +338,13 @@ func (s *OutfitService) itemResponses(ctx context.Context, outfitID int64) ([]dt
|
|||||||
for _, it := range items {
|
for _, it := range items {
|
||||||
ids = append(ids, it.ClothesID)
|
ids = append(ids, it.ClothesID)
|
||||||
}
|
}
|
||||||
clothesList, err := s.clothesDao.ClothesGetByIDs(ctx, ids)
|
clothesList, err := clothesDao.ClothesGetByIDs(ctx, ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
clothesByID := make(map[int64]mysqlmodel.Clothes, len(clothesList))
|
clothesByID := make(map[int64]mysqlmodel.Clothes, len(clothesList))
|
||||||
for _, c := range clothesList {
|
for i := range clothesList {
|
||||||
clothesByID[c.ID] = c
|
clothesByID[clothesList[i].ID] = clothesList[i]
|
||||||
}
|
}
|
||||||
resps := make([]dto.OutfitItemResponse, 0, len(items))
|
resps := make([]dto.OutfitItemResponse, 0, len(items))
|
||||||
for _, it := range items {
|
for _, it := range items {
|
||||||
@@ -402,11 +407,15 @@ func (s *OutfitService) buildOutfitListResponses(ctx context.Context, list []mys
|
|||||||
|
|
||||||
// buildOutfitItemResponse 组装穿搭明细(processed_image 转为可访问 URL)。
|
// buildOutfitItemResponse 组装穿搭明细(processed_image 转为可访问 URL)。
|
||||||
func buildOutfitItemResponse(c *mysqlmodel.Clothes) dto.OutfitItemResponse {
|
func buildOutfitItemResponse(c *mysqlmodel.Clothes) dto.OutfitItemResponse {
|
||||||
img := c.ProcessedImage
|
return dto.OutfitItemResponse{ClothesUUID: c.ClothesUUID, ProcessedImage: clothesImageURL(c.ProcessedImage)}
|
||||||
if img != "" {
|
}
|
||||||
img = upload.GetCosUrl(img)
|
|
||||||
|
// clothesImageURL 将衣物抠图相对路径转为可访问 URL(空则返回空)。
|
||||||
|
func clothesImageURL(processed string) string {
|
||||||
|
if processed == "" {
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
return dto.OutfitItemResponse{ClothesUUID: c.ClothesUUID, ProcessedImage: img}
|
return upload.GetCosUrl(processed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildOutfitCreateResponse 组装穿搭响应。
|
// buildOutfitCreateResponse 组装穿搭响应。
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"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/pkg/errcode"
|
||||||
|
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/upload"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OutfitShareService 穿搭分享业务服务。
|
||||||
|
type OutfitShareService struct {
|
||||||
|
shareDao *mysqldao.OutfitShareDao
|
||||||
|
receiverDao *mysqldao.OutfitShareReceiverDao
|
||||||
|
outfitDao *mysqldao.OutfitDao
|
||||||
|
outfitItemDao *mysqldao.OutfitItemDao
|
||||||
|
clothesDao *mysqldao.ClothesDao
|
||||||
|
userDao *mysqldao.UserDao
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOutfitShareService 创建穿搭分享服务。
|
||||||
|
func NewOutfitShareService() *OutfitShareService {
|
||||||
|
return &OutfitShareService{
|
||||||
|
shareDao: mysqldao.NewOutfitShareDao(),
|
||||||
|
receiverDao: mysqldao.NewOutfitShareReceiverDao(),
|
||||||
|
outfitDao: mysqldao.NewOutfitDao(),
|
||||||
|
outfitItemDao: mysqldao.NewOutfitItemDao(),
|
||||||
|
clothesDao: mysqldao.NewClothesDao(),
|
||||||
|
userDao: mysqldao.NewUserDao(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建穿搭分享。仅能分享本人且已公开的穿搭;不消耗额度。
|
||||||
|
func (s *OutfitShareService) Create(ctx context.Context, userID int64, request *dto.OutfitShareCreateRequest) (*dto.OutfitShareCreateResponse, *errcode.Error) {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "svc.OutfitShareCreate")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
outfit, err := s.outfitDao.OutfitGetByUUID(ctx, request.OutfitUUID)
|
||||||
|
if err != nil {
|
||||||
|
if isNotFound(err) {
|
||||||
|
return nil, errcode.OutfitNotFound
|
||||||
|
}
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
if outfit.UserID != userID {
|
||||||
|
return nil, errcode.OutfitNoPermission
|
||||||
|
}
|
||||||
|
// 分享前提:穿搭已公开可见(否则对方打开后无法查看)。
|
||||||
|
if !isOutfitPublicVisible(outfit) {
|
||||||
|
return nil, errcode.InvalidParams.WithDetails("请先公开该穿搭再分享")
|
||||||
|
}
|
||||||
|
|
||||||
|
share := &mysqlmodel.OutfitShare{
|
||||||
|
ShareUUID: genUUID(),
|
||||||
|
UserID: userID,
|
||||||
|
OutfitID: outfit.ID,
|
||||||
|
Status: ShareStatusNormal,
|
||||||
|
}
|
||||||
|
if err := s.shareDao.OutfitShareCreate(ctx, share); err != nil {
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
return &dto.OutfitShareCreateResponse{ShareUUID: share.ShareUUID}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail 查看穿搭分享详情(访客 / 属主视角)。访客访问成功即记录接收关系。
|
||||||
|
func (s *OutfitShareService) Detail(ctx context.Context, userID int64, shareUUID string) (*dto.OutfitShareDetailResponse, *errcode.Error) {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "svc.OutfitShareDetail")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
share, err := s.shareDao.OutfitShareGetByUUID(ctx, shareUUID)
|
||||||
|
if err != nil {
|
||||||
|
if isNotFound(err) {
|
||||||
|
return nil, errcode.OutfitShareNotFound
|
||||||
|
}
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
if share.Status == ShareStatusCancelled {
|
||||||
|
return nil, errcode.OutfitShareCancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
outfit, oerr := s.outfitDao.OutfitGetByID(ctx, share.OutfitID)
|
||||||
|
if oerr != nil {
|
||||||
|
// 穿搭已删除:分享视为失效
|
||||||
|
if isNotFound(oerr) {
|
||||||
|
return nil, errcode.OutfitShareNotFound
|
||||||
|
}
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
|
||||||
|
isOwner := share.UserID == userID
|
||||||
|
// 访客视角:穿搭必须仍处于公开可见状态,否则分享失效
|
||||||
|
if !isOwner && !isOutfitPublicVisible(outfit) {
|
||||||
|
return nil, errcode.OutfitShareNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
items, lerr := loadOutfitItemResponses(ctx, s.outfitItemDao, s.clothesDao, outfit.ID)
|
||||||
|
if lerr != nil {
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录接收关系:登录且非属主用户成功访问即视为收到,幂等,失败不阻断详情返回。
|
||||||
|
if userID > 0 && !isOwner {
|
||||||
|
if rerr := s.receiverDao.OutfitShareReceiverUpsert(ctx, shareUUID, userID); rerr != nil {
|
||||||
|
slog.WarnContext(ctx, "record outfit share receiver failed", "share_uuid", shareUUID, "user_id", userID, "error", rerr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerName, ownerAvatar := "", ""
|
||||||
|
if owner, uerr := s.userDao.UserGetByID(ctx, share.UserID); uerr == nil {
|
||||||
|
ownerName = owner.Username
|
||||||
|
ownerAvatar = upload.AvatarURL(owner.Avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dto.OutfitShareDetailResponse{
|
||||||
|
ShareUUID: shareUUID,
|
||||||
|
OwnerName: ownerName,
|
||||||
|
OwnerAvatar: ownerAvatar,
|
||||||
|
Status: share.Status,
|
||||||
|
IsOwner: isOwner,
|
||||||
|
Outfit: buildOutfitCreateResponse(outfit, items),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 分页查询我的穿搭分享。
|
||||||
|
func (s *OutfitShareService) List(ctx context.Context, userID int64, request *dto.OutfitShareListRequest) (*dto.OutfitShareListResponse, *errcode.Error) {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "svc.OutfitShareList")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
page, pageSize := normalizePage(request.Page, request.PageSize)
|
||||||
|
list, total, err := s.shareDao.OutfitShareListByUserID(ctx, userID, request.Status, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
items, berr := s.buildShareItems(ctx, list)
|
||||||
|
if berr != nil {
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
return &dto.OutfitShareListResponse{
|
||||||
|
PageResponse: dto.PageResponse{Total: total, Page: page, PageSize: pageSize},
|
||||||
|
List: items,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListReceived 分页查询"分享给我的"穿搭分享。
|
||||||
|
func (s *OutfitShareService) ListReceived(ctx context.Context, userID int64, request *dto.OutfitShareListRequest) (*dto.OutfitShareListResponse, *errcode.Error) {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "svc.OutfitShareReceivedList")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
page, pageSize := normalizePage(request.Page, request.PageSize)
|
||||||
|
list, total, err := s.receiverDao.OutfitShareReceivedList(ctx, userID, request.Status, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
items, berr := s.buildShareItems(ctx, list)
|
||||||
|
if berr != nil {
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量填充分享者信息。
|
||||||
|
ownerIDs := make([]int64, 0, len(list))
|
||||||
|
for _, sh := range list {
|
||||||
|
ownerIDs = append(ownerIDs, sh.UserID)
|
||||||
|
}
|
||||||
|
ownerByID := make(map[int64]mysqlmodel.User, len(ownerIDs))
|
||||||
|
if owners, oerr := s.userDao.UserGetByIDs(ctx, ownerIDs); oerr == nil {
|
||||||
|
for _, u := range owners {
|
||||||
|
ownerByID[u.ID] = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range items {
|
||||||
|
if owner, ok := ownerByID[list[i].UserID]; ok {
|
||||||
|
items[i].OwnerName = owner.Username
|
||||||
|
items[i].OwnerAvatar = upload.AvatarURL(owner.Avatar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dto.OutfitShareListResponse{
|
||||||
|
PageResponse: dto.PageResponse{Total: total, Page: page, PageSize: pageSize},
|
||||||
|
List: items,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel 取消穿搭分享(校验归属)。
|
||||||
|
func (s *OutfitShareService) Cancel(ctx context.Context, userID int64, request *dto.OutfitShareCancelRequest) (*dto.OutfitShareCancelResponse, *errcode.Error) {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "svc.OutfitShareCancel")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
share, err := s.shareDao.OutfitShareGetByUUID(ctx, request.ShareUUID)
|
||||||
|
if err != nil {
|
||||||
|
if isNotFound(err) {
|
||||||
|
return nil, errcode.OutfitShareNotFound
|
||||||
|
}
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
if share.UserID != userID {
|
||||||
|
return nil, errcode.Forbidden
|
||||||
|
}
|
||||||
|
if err := s.shareDao.OutfitShareUpdateStatus(ctx, request.ShareUUID, ShareStatusCancelled); err != nil {
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
return &dto.OutfitShareCancelResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 内部辅助 ----
|
||||||
|
|
||||||
|
// buildShareItems 批量组装分享列表项(一次性加载穿搭与首件衣物,避免 N+1)。
|
||||||
|
func (s *OutfitShareService) buildShareItems(ctx context.Context, list []mysqlmodel.OutfitShare) ([]dto.OutfitShareItemResponse, error) {
|
||||||
|
items := make([]dto.OutfitShareItemResponse, 0, len(list))
|
||||||
|
if len(list) == 0 {
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
outfitIDs := make([]int64, 0, len(list))
|
||||||
|
for _, sh := range list {
|
||||||
|
outfitIDs = append(outfitIDs, sh.OutfitID)
|
||||||
|
}
|
||||||
|
|
||||||
|
outfits, err := s.outfitDao.OutfitGetByIDs(ctx, outfitIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
outfitByID := make(map[int64]mysqlmodel.Outfit, len(outfits))
|
||||||
|
for i := range outfits {
|
||||||
|
outfitByID[outfits[i].ID] = outfits[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
allItems, err := s.outfitItemDao.OutfitItemListByOutfitIDs(ctx, outfitIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
countByOutfit := make(map[int64]int, len(outfitIDs))
|
||||||
|
firstClothesByOutfit := make(map[int64]int64, len(outfitIDs))
|
||||||
|
for _, it := range allItems {
|
||||||
|
countByOutfit[it.OutfitID]++
|
||||||
|
if _, ok := firstClothesByOutfit[it.OutfitID]; !ok {
|
||||||
|
firstClothesByOutfit[it.OutfitID] = it.ClothesID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clothesIDs := make([]int64, 0, len(firstClothesByOutfit))
|
||||||
|
for _, cid := range firstClothesByOutfit {
|
||||||
|
clothesIDs = append(clothesIDs, cid)
|
||||||
|
}
|
||||||
|
coverByClothesID := make(map[int64]string, len(clothesIDs))
|
||||||
|
if clothesList, cerr := s.clothesDao.ClothesGetByIDs(ctx, clothesIDs); cerr == nil {
|
||||||
|
for i := range clothesList {
|
||||||
|
coverByClothesID[clothesList[i].ID] = clothesImageURL(clothesList[i].ProcessedImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sh := range list {
|
||||||
|
o := outfitByID[sh.OutfitID]
|
||||||
|
cover := ""
|
||||||
|
if cid, ok := firstClothesByOutfit[sh.OutfitID]; ok {
|
||||||
|
cover = coverByClothesID[cid]
|
||||||
|
}
|
||||||
|
items = append(items, dto.OutfitShareItemResponse{
|
||||||
|
ShareUUID: sh.ShareUUID,
|
||||||
|
OutfitUUID: o.OutfitUUID,
|
||||||
|
OutfitName: o.OutfitName,
|
||||||
|
CoverImage: cover,
|
||||||
|
ItemsCount: countByOutfit[sh.OutfitID],
|
||||||
|
Status: sh.Status,
|
||||||
|
CreatedAt: sh.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
@@ -10,6 +10,10 @@ var (
|
|||||||
ShareExpired = NewError(90101, "分享已过期")
|
ShareExpired = NewError(90101, "分享已过期")
|
||||||
ShareCancelled = NewError(90102, "分享已取消")
|
ShareCancelled = NewError(90102, "分享已取消")
|
||||||
|
|
||||||
|
// 穿搭分享(90103 - 90109)
|
||||||
|
OutfitShareNotFound = NewError(90103, "穿搭分享不存在")
|
||||||
|
OutfitShareCancelled = NewError(90104, "穿搭分享已取消")
|
||||||
|
|
||||||
// 点赞/收藏/社区(90200 - 90299)
|
// 点赞/收藏/社区(90200 - 90299)
|
||||||
TargetNotFound = NewError(90200, "目标内容不存在")
|
TargetNotFound = NewError(90200, "目标内容不存在")
|
||||||
LikeFailed = NewError(90201, "点赞操作失败")
|
LikeFailed = NewError(90201, "点赞操作失败")
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE IF EXISTS `outfit_share_receivers`;
|
||||||
|
DROP TABLE IF EXISTS `outfit_shares`;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
CREATE TABLE `outfit_shares` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||||
|
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||||
|
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||||
|
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||||
|
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||||
|
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属用户ID',
|
||||||
|
`outfit_id` BIGINT NOT NULL DEFAULT 0 COMMENT '穿搭ID',
|
||||||
|
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '分享状态 1=正常 2=用户取消',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_outfit_shares_uuid` (`share_uuid`),
|
||||||
|
KEY `idx_outfit_shares_user_status` (`user_id`, `status`)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭分享表';
|
||||||
|
|
||||||
|
CREATE TABLE `outfit_share_receivers` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||||
|
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||||
|
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||||
|
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||||
|
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||||
|
`receiver_user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '接收人用户ID',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_outfit_share_receiver` (`share_uuid`, `receiver_user_id`),
|
||||||
|
KEY `idx_outfit_share_receivers_user` (`receiver_user_id`)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭分享接收人关联表';
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
- [2.6 幂等与重试](#26-幂等与重试)
|
- [2.6 幂等与重试](#26-幂等与重试)
|
||||||
- [2.7 需要轮询的场景](#27-需要轮询的场景)
|
- [2.7 需要轮询的场景](#27-需要轮询的场景)
|
||||||
- [2.8 CORS 与链路追踪](#28-cors-与链路追踪)
|
- [2.8 CORS 与链路追踪](#28-cors-与链路追踪)
|
||||||
- [3. 接口总表(43 个)](#3-接口总表43-个)
|
- [3. 接口总表(48 个)](#3-接口总表48-个)
|
||||||
- [4. 公共数据结构](#4-公共数据结构)
|
- [4. 公共数据结构](#4-公共数据结构)
|
||||||
- [5. 接口详解](#5-接口详解)
|
- [5. 接口详解](#5-接口详解)
|
||||||
- [5.1 用户模块](#51-用户模块)
|
- [5.1 用户模块](#51-用户模块)
|
||||||
@@ -31,10 +31,11 @@
|
|||||||
- [5.5 穿搭模块](#55-穿搭模块)
|
- [5.5 穿搭模块](#55-穿搭模块)
|
||||||
- [5.6 AI 推荐模块](#56-ai-推荐模块)
|
- [5.6 AI 推荐模块](#56-ai-推荐模块)
|
||||||
- [5.7 衣柜分享模块](#57-衣柜分享模块)
|
- [5.7 衣柜分享模块](#57-衣柜分享模块)
|
||||||
- [5.8 收藏与点赞模块](#58-收藏与点赞模块)
|
- [5.8 穿搭分享模块](#58-穿搭分享模块)
|
||||||
- [5.9 通知模块](#59-通知模块)
|
- [5.9 收藏与点赞模块](#59-收藏与点赞模块)
|
||||||
- [5.10 商品模块](#510-商品模块)
|
- [5.10 通知模块](#510-通知模块)
|
||||||
- [5.11 订单与退款模块](#511-订单与退款模块)
|
- [5.11 商品模块](#511-商品模块)
|
||||||
|
- [5.12 订单与退款模块](#512-订单与退款模块)
|
||||||
- [6. 业务逻辑与状态机](#6-业务逻辑与状态机)
|
- [6. 业务逻辑与状态机](#6-业务逻辑与状态机)
|
||||||
- [7. 核心调用时序图](#7-核心调用时序图)
|
- [7. 核心调用时序图](#7-核心调用时序图)
|
||||||
- [8. 错误码全表](#8-错误码全表)
|
- [8. 错误码全表](#8-错误码全表)
|
||||||
@@ -239,7 +240,7 @@ Authorization: Bearer <access_token>
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 接口总表(43 个)
|
## 3. 接口总表(48 个)
|
||||||
|
|
||||||
### 健康检查(1)
|
### 健康检查(1)
|
||||||
|
|
||||||
@@ -310,6 +311,16 @@ Authorization: Bearer <access_token>
|
|||||||
| GET | `/closet-share/:uuid` | 分享详情(属主 / 访客视角) |
|
| GET | `/closet-share/:uuid` | 分享详情(属主 / 访客视角) |
|
||||||
| POST | `/closet-share/:uuid/cancel` | 取消分享 |
|
| POST | `/closet-share/:uuid/cancel` | 取消分享 |
|
||||||
|
|
||||||
|
**穿搭分享(5)**
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| POST | `/outfit-share` | 创建穿搭分享(不扣额度,需穿搭已公开) |
|
||||||
|
| GET | `/outfit-share` | 我的穿搭分享列表 |
|
||||||
|
| GET | `/outfit-share/received` | 分享给我的穿搭分享 |
|
||||||
|
| GET | `/outfit-share/:uuid` | 穿搭分享详情(属主 / 访客视角) |
|
||||||
|
| POST | `/outfit-share/:uuid/cancel` | 取消穿搭分享 |
|
||||||
|
|
||||||
**收藏 / 点赞(5)**
|
**收藏 / 点赞(5)**
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
@@ -771,15 +782,21 @@ Authorization: Bearer <access_token>
|
|||||||
|
|
||||||
#### GET `/clothes/:uuid` — 衣物详情(鉴权)
|
#### GET `/clothes/:uuid` — 衣物详情(鉴权)
|
||||||
|
|
||||||
**请求**(path)
|
**请求**(path + query)
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| uuid | string | ✅ | 衣物 UUID |
|
| uuid | string | ✅ | 衣物 UUID |
|
||||||
|
| share_uuid | string | 否 | 穿搭分享 UUID:非属主通过穿搭分享只读查看该分享穿搭内的衣物时必传 |
|
||||||
|
|
||||||
**响应** `data` = [衣物对象](#42-衣物对象-clothesitem)
|
**响应** `data` = [衣物对象](#42-衣物对象-clothesitem)
|
||||||
|
|
||||||
**错误码**:`10000` 参数错误(uuid 为空)· `50000` 衣物不存在 · `50005` 无权操作该衣物
|
**业务逻辑(可见性)**
|
||||||
|
|
||||||
|
- 属主:始终可见。
|
||||||
|
- 非属主:**仅当携带有效 `share_uuid`**(该分享正常、穿搭仍公开可见、且该衣物属于该分享的穿搭明细)时返回**只读**衣物信息;否则 `50005`。
|
||||||
|
|
||||||
|
**错误码**:`10000` 参数错误(uuid 为空)· `50000` 衣物不存在 · `50005` 无权操作该衣物 · `90103` 穿搭分享不存在(含穿搭已删除 / 取消公开)· `90104` 穿搭分享已取消
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1125,6 +1142,7 @@ Authorization: Bearer <access_token>
|
|||||||
| status | int | `1`=正常 `2`=已取消 `3`=已过期 `4`=已封禁 |
|
| status | int | `1`=正常 `2`=已取消 `3`=已过期 `4`=已封禁 |
|
||||||
| owner_name | string | 分享者昵称(本接口暂不返回,恒为空) |
|
| owner_name | string | 分享者昵称(本接口暂不返回,恒为空) |
|
||||||
| owner_avatar | string | 分享者头像(本接口暂不返回,恒为空) |
|
| owner_avatar | string | 分享者头像(本接口暂不返回,恒为空) |
|
||||||
|
| created_at | int64 | 分享创建时间(秒,前端合并排序用) |
|
||||||
|
|
||||||
> ⚠️ **过期是"惰性"的**:`expire_time` 到了但没人访问时,列表里 `status` 可能仍是 `1`,直到有人访问详情才变成 `3`。**前端应同时判断 `status` 与 `expire_time`** 来展示"已过期"。
|
> ⚠️ **过期是"惰性"的**:`expire_time` 到了但没人访问时,列表里 `status` 可能仍是 `1`,直到有人访问详情才变成 `3`。**前端应同时判断 `status` 与 `expire_time`** 来展示"已过期"。
|
||||||
|
|
||||||
@@ -1214,7 +1232,122 @@ Authorization: Bearer <access_token>
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.8 收藏与点赞模块
|
### 5.8 穿搭分享模块
|
||||||
|
|
||||||
|
**状态枚举** `status`:`1`=正常 `2`=用户取消
|
||||||
|
|
||||||
|
> 穿搭分享用于把「自己的公开穿搭」以链接形式分享给好友。与衣柜分享不同,**创建分享不消耗任何额度**,但**要求穿搭已公开可见**(`is_public=2` 且 `audit_status=2` 且 `status=1`);一旦穿搭取消公开或被删除,相关分享即视为失效。
|
||||||
|
|
||||||
|
#### POST `/outfit-share` — 创建穿搭分享(鉴权)
|
||||||
|
|
||||||
|
**业务逻辑**
|
||||||
|
|
||||||
|
1. 仅能分享**本人**的穿搭(他人 → `40004`)。
|
||||||
|
2. 穿搭必须已公开可见,否则 `10000` + `details:["请先公开该穿搭再分享"]`。
|
||||||
|
3. **不扣额度**;创建成功后返回 `share_uuid`,分享链接携带该值。
|
||||||
|
|
||||||
|
**请求**(json)
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| outfit_uuid | string | ✅ | 穿搭 UUID |
|
||||||
|
|
||||||
|
**响应** `data`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| share_uuid | string | 分享 UUID,供他人访问分享详情 |
|
||||||
|
|
||||||
|
**错误码**:`10000` 参数错误 · `40000` 穿搭不存在 · `40004` 无权操作该穿搭 · `19001` 数据库错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### GET `/outfit-share` — 我的穿搭分享列表(鉴权)
|
||||||
|
|
||||||
|
**请求**(query)
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| page | int | 否 | 页码,默认 1 |
|
||||||
|
| page_size | int | 否 | 每页数量,默认 10,最大 50 |
|
||||||
|
| status | int | 否 | `1`=正常 `2`=已取消;不传 = 全部 |
|
||||||
|
|
||||||
|
**响应** `data.list[]`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| share_uuid | string | 分享 UUID |
|
||||||
|
| outfit_uuid | string | 穿搭 UUID |
|
||||||
|
| outfit_name | string | 穿搭名称 |
|
||||||
|
| cover_image | string | 封面(首件衣物抠图 URL,无则空) |
|
||||||
|
| items_count | int | 包含衣物数量 |
|
||||||
|
| status | int | `1`=正常 `2`=已取消 |
|
||||||
|
| owner_name | string | 分享者昵称(本接口不返回,恒为空) |
|
||||||
|
| owner_avatar | string | 分享者头像(本接口不返回,恒为空) |
|
||||||
|
| created_at | int64 | 分享创建时间(秒) |
|
||||||
|
|
||||||
|
**错误码**:`10000` 参数错误 · `19001` 数据库错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### GET `/outfit-share/received` — 分享给我的穿搭分享(鉴权)
|
||||||
|
|
||||||
|
**业务逻辑**
|
||||||
|
|
||||||
|
- "访问即视为收到":登录用户(非分享属主)成功访问他人穿搭分享详情时,自动记录接收关系;重复访问不重复记录,属主访问不记录。
|
||||||
|
- 列表按接收记录倒序分页返回,支持按 `status` 筛选。
|
||||||
|
- 字段同「我的穿搭分享列表」,并额外返回 `owner_name` / `owner_avatar`。
|
||||||
|
|
||||||
|
**错误码**:`10000` 参数错误 · `19001` 数据库错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### GET `/outfit-share/:uuid` — 穿搭分享详情(鉴权,属主 / 访客视角)
|
||||||
|
|
||||||
|
**业务逻辑**
|
||||||
|
|
||||||
|
- 属主与访客都可访问(需已登录,`is_owner` 区分视角)。
|
||||||
|
- **访客访问成功即记录接收关系**;属主访问不记录。
|
||||||
|
- 分享已取消 → `90104`;分享不存在或对应穿搭已删除 / 取消公开 → `90103`。
|
||||||
|
|
||||||
|
**请求**(path)
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| uuid | string | ✅ | 分享 UUID |
|
||||||
|
|
||||||
|
**响应** `data`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| share_uuid | string | 分享 UUID |
|
||||||
|
| owner_name | string | 分享者昵称 |
|
||||||
|
| owner_avatar | string | 分享者头像 |
|
||||||
|
| status | int | `1`=正常 `2`=已取消 |
|
||||||
|
| is_owner | bool | 是否本人分享 |
|
||||||
|
| outfit | object | [穿搭对象](#43-穿搭对象-outfititem),`items[]` 含 `processed_image`(可直接展示) |
|
||||||
|
|
||||||
|
**错误码**:`10000` 参数错误 · `90103` 穿搭分享不存在(含穿搭已删除 / 取消公开)· `90104` 穿搭分享已取消
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### POST `/outfit-share/:uuid/cancel` — 取消穿搭分享(鉴权)
|
||||||
|
|
||||||
|
**业务逻辑**:**仅属主可操作**,他人 → `12000`(HTTP 403);置 `status=2`,不可逆。
|
||||||
|
|
||||||
|
**请求**(path)
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| uuid | string | ✅ | 分享 UUID |
|
||||||
|
|
||||||
|
**响应** `data` = `{}`
|
||||||
|
|
||||||
|
**错误码**:`10000` 参数错误 · `12000` 无权限访问(403)· `90103` 穿搭分享不存在 · `19001` 数据库错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.9 收藏与点赞模块
|
||||||
|
|
||||||
**动作枚举** `action`:`1`=执行(点赞/收藏)`2`=取消。两个接口都**幂等**,重复提交无副作用。
|
**动作枚举** `action`:`1`=执行(点赞/收藏)`2`=取消。两个接口都**幂等**,重复提交无副作用。
|
||||||
|
|
||||||
@@ -1309,7 +1442,7 @@ Authorization: Bearer <access_token>
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.9 通知模块
|
### 5.10 通知模块
|
||||||
|
|
||||||
**`is_read` 取值**:⚠️ `1`=**未读** `2`=**已读**(非常规的 0/1,也不是布尔值)
|
**`is_read` 取值**:⚠️ `1`=**未读** `2`=**已读**(非常规的 0/1,也不是布尔值)
|
||||||
**`msg_type` 取值**:`1`=点赞通知 `2`=订单通知 `3`=系统通知(当前代码无发送点,预留)
|
**`msg_type` 取值**:`1`=点赞通知 `2`=订单通知 `3`=系统通知(当前代码无发送点,预留)
|
||||||
@@ -1387,7 +1520,7 @@ Authorization: Bearer <access_token>
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.10 商品模块(公开,无需登录)
|
### 5.11 商品模块(公开,无需登录)
|
||||||
|
|
||||||
**`goods_type` 枚举**:`1`=会员包 `2`=上传包 `3`=AI 推荐包 `4`=衣柜分享包 `5`=搭配分享包
|
**`goods_type` 枚举**:`1`=会员包 `2`=上传包 `3`=AI 推荐包 `4`=衣柜分享包 `5`=搭配分享包
|
||||||
**`status`**:`1`=上架 `2`=下架(列表只返回上架商品)
|
**`status`**:`1`=上架 `2`=下架(列表只返回上架商品)
|
||||||
@@ -1421,7 +1554,7 @@ Authorization: Bearer <access_token>
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.11 订单与退款模块
|
### 5.12 订单与退款模块
|
||||||
|
|
||||||
**`order_status` 枚举**:`1`=待支付 `2`=已支付 `3`=已取消 `4`=已过期 `5`=已退款
|
**`order_status` 枚举**:`1`=待支付 `2`=已支付 `3`=已取消 `4`=已过期 `5`=已退款
|
||||||
**退款 `status` 枚举**:`0`=待处理 `1`=退款中 `2`=退款成功 `3`=退款失败
|
**退款 `status` 枚举**:`0`=待处理 `1`=退款中 `2`=退款成功 `3`=退款失败
|
||||||
@@ -2039,6 +2172,8 @@ sequenceDiagram
|
|||||||
| 90100 | 分享不存在 | 200 |
|
| 90100 | 分享不存在 | 200 |
|
||||||
| 90101 | 分享已过期 | 200 |
|
| 90101 | 分享已过期 | 200 |
|
||||||
| 90102 | 分享已取消 | 200 |
|
| 90102 | 分享已取消 | 200 |
|
||||||
|
| 90103 | 穿搭分享不存在 | 200 |
|
||||||
|
| 90104 | 穿搭分享已取消 | 200 |
|
||||||
| 90200 | 目标内容不存在 | 200 |
|
| 90200 | 目标内容不存在 | 200 |
|
||||||
| 90201 | 点赞操作失败 | 200 |
|
| 90201 | 点赞操作失败 | 200 |
|
||||||
| 90202 | 收藏操作失败 | 200 |
|
| 90202 | 收藏操作失败 | 200 |
|
||||||
|
|||||||
Reference in New Issue
Block a user