Files
zt-closet-backend/.codebuddy/plans/衣柜分享-分享给我的_70d0547d.md
T
2026-09-11 01:41:52 +08:00

9.2 KiB
Raw Blame History

name, overview, todos
name overview todos
衣柜分享-分享给我的 新增"分享给我的"能力:通过 closet_share_receivers 关联表记录登录用户访问分享详情的接收关系(一对多、幂等),并新增 GET /closet-share/received 列表接口,返回含分享者昵称/头像的分享列表。
id content status
add-receiver-migration-model 新增 closet_share_receivers 迁移、ClosetShareReceiver 模型与 DAO(幂等 upsert、联表分页查询) completed
id content status dependencies
add-received-service 扩展 DTO 与 UserGetByIDs,详情记录接收人并实现 ListReceived 服务 completed
add-receiver-migration-model
id content status dependencies
add-received-handler-route 新增 ReceivedList handler 并注册 /closet-share/received 路由 completed
add-received-service
id content status dependencies
update-docs 更新接口文档与 README 的衣柜分享模块说明 completed
add-received-handler-route

产品概述

为后端"衣柜分享"补齐"分享给我的"数据能力。当前 closet_shares 只记录分享发起者(user_id),没有接收人概念,GET /closet-share 仅返回"我发起的分享"。本次改造采用"访问即视为收到"的语义:登录用户(非分享属主)成功访问某个分享详情时,自动建立该用户与分享的接收关系,并提供"我收到的分享"分页列表接口。

核心功能

  • 接收关系自动记录:保持现有链接分享方式不变;登录用户首次访问他人分享详情且分享状态正常时,自动幂等记录为接收人,重复访问不产生重复记录,分享属主访问不记录。
  • 一对多接收人:同一分享可被多个登录用户访问,每个接收人各生成一条关联记录,通过 share_uuid + receiver_user_id 联合唯一索引去重。
  • "分享给我的"列表接口:新增 GET /closet-share/received,按当前登录用户分页查询其收到的分享,返回分享基本信息、衣物数量、分享状态,并额外返回分享者昵称与头像(owner_name / owner_avatar),支持按状态筛选。
  • 接口兼容:现有"我的分享"列表响应结构仅新增字段,不破坏既有字段;既有创建、详情、取消分享接口行为保持不变。

技术选型

  • 语言/框架:Go + Gin(沿用现有项目)
  • ORM:GORM(含 soft_delete.DeletedAt 软删除)
  • 数据库:MySQL,迁移使用 golang-migrate(go:embed *.sql 自动打包,服务不自动执行迁移)
  • 缓存:Redis(访问计数沿用现状,接收关系落 MySQL,保证可查询与持久化)

实现方案

数据模型

新增关联表 closet_share_receivers,与现有表结构规范保持一致(含 id、created_at、updated_at、deleted_at、created_by、updated_by、deleted_by):

  • share_uuid VARCHAR(64):分享 UUID
  • receiver_user_id BIGINT:接收人用户 ID
  • UNIQUE KEY uk_closet_share_receiver (share_uuid, receiver_user_id):保证一个用户对同一分享只有一条记录
  • KEY idx_closet_share_receivers_user (receiver_user_id):支撑"按接收人分页查询"

接收关系写入采用幂等 upsert(唯一索引冲突时 DO NOTHING),避免并发访问重复插入。

接收关系记录时机

在 ClosetShareService.Detail 中,仅当以下条件全部满足时记录接收关系:

  1. 分享状态校验通过(未被取消/封禁/过期);
  2. 衣物明细加载成功,详情能够正常返回;
  3. 当前 userID > 0 且非分享属主。

记录为 best-effort:失败仅告警,不阻断详情返回,避免因关联表故障影响详情可用性。

"分享给我的"列表查询

新增 ClosetShareService.ListReceived,通过 DAO 联表 closet_share_receivers 与 closet_shares 分页查询,避免 N+1:

  1. 联表分页取出分享记录(按接收记录 id DESC 排序,支持 status 过滤);
  2. 复用 ClosetShareItemListByShareUUIDs 批量统计各分享衣物数量;
  3. 新增 UserDao.UserGetByIDs 批量查询分享者昵称/头像,头像按现有 upload.GetCosUrl 规则转换;
  4. 组装列表项返回。

路由兼容

在 GET /closet-share/:uuid 之前注册静态路由 GET /closet-share/received。gin 路由树对静态段优先于参数段,此顺序可确保 /received 不被 /:uuid 误匹配。

实施要点

  • 性能:列表查询一次联表取分享、两次批量查询(衣物明细 + 用户信息),时间复杂度与页大小线性相关;联合唯一索引 + receiver_user_id 普通索引保证写入去重与列表检索效率。
  • 幂等与并发:接收关系写入用 clause.OnConflict{DoNothing: true},并发首次访问不会报错或产生重复数据。
  • 日志:复用 slog.WarnContext 记录接收关系写入失败,仅含 share_uuid、user_id、error,不记录衣物或用户敏感大字段;避免高频详情访问造成日志刷屏,仅记录异常。
  • 安全:接收关系写入、列表查询均使用 GORM 参数绑定,防 SQL 注入;列表仅返回当前登录用户自身的接收记录,无越权读取面。
  • 爆炸半径控制:不改动既有 closet_shares 表结构,不修改创建/详情/取消接口既有返回契约,仅新增字段与新接口;迁移通过独立命令执行,服务启动不受影响。
  • 兼容性:ClosetShareItemResponse 新增 OwnerName、OwnerAvatar 字段,"我的分享"列表暂不填充(空字符串),"分享给我的"列表填充分享者信息。

架构设计

flowchart LR
    A[登录用户访问 GET /closet-share/:uuid] --> B{状态校验通过且非属主}
    B -->|是| C[UPSERT closet_share_receivers<br/>唯一键幂等]
    B -->|否| D[返回详情]
    C --> D

    E[GET /closet-share/received] --> F[JOIN receivers + shares 分页]
    F --> G[批量统计衣物数量]
    F --> H[批量查询分享者信息]
    G --> I[组装列表返回]
    H --> I

目录结构

/root/zt-closet-backend/
├── pkg/migration/
│   ├── 000018_create_closet_share_receivers.up.sql   # [NEW] 创建 closet_share_receivers 表:标准字段 + share_uuid + receiver_user_id,联合唯一索引与 receiver_user_id 普通索引
│   └── 000018_create_closet_share_receivers.down.sql # [NEW] DROP TABLE IF EXISTS closet_share_receivers
├── internal/mysqlmodel/
│   └── closet.go                                      # [MODIFY] 新增 ClosetShareReceiver 模型及 TableName,唯一索引标签与迁移一致
├── internal/mysqldao/
│   ├── closet.go                                      # [MODIFY] 新增 ClosetShareReceiverDao:NewClosetShareReceiverDao、ClosetShareReceiverUpsert(DO NOTHING 幂等)、ClosetShareReceivedList(联表分页查询 shares)
│   └── user.go                                        # [MODIFY] 新增 UserGetByIDs 批量查询,返回 []mysqlmodel.User,空 ids 直接返回空
├── internal/dto/
│   └── closet.go                                      # [MODIFY] ClosetShareItemResponse 新增 OwnerName/OwnerAvatar;接收列表复用 ClosetShareListRequest 与 ShareListResponse
├── internal/service/
│   └── closet.go                                      # [MODIFY] ClosetShareService 新增 shareReceiverDao;构造器初始化;Detail 状态校验通过后 best-effort 记录接收人;新增 ListReceived 分页查询并批量填充分享者信息
├── internal/routers/api/v1/
│   └── closet.go                                      # [MODIFY] ClosetShareHandler 新增 ReceivedList 方法,绑定 ClosetShareListRequest 并调用 svc.ListReceived
├── internal/routers/
│   └── router.go                                      # [MODIFY] 在 GET /closet-share/:uuid 前注册 auth.GET("/closet-share/received", closetHandler.ReceivedList)
├── 接口文档.md                                         # [MODIFY] 衣柜分享模块新增 GET /closet-share/received 接口说明(请求参数、响应字段、错误码)
└── README.md                                           # [MODIFY] 更新衣柜分享业务流程与接口列表,补充"访问即视为收到"与"分享给我的"说明

关键代码结构

// internal/mysqlmodel/closet.go
type ClosetShareReceiver struct {
    Model
    ShareUUID      string `gorm:"column:share_uuid;uniqueIndex:uk_closet_share_receiver,priority:1"`
    ReceiverUserID int64  `gorm:"column:receiver_user_id;uniqueIndex:uk_closet_share_receiver,priority:2"`
}

// internal/mysqldao/closet.go
func (d *ClosetShareReceiverDao) ClosetShareReceiverUpsert(ctx context.Context, shareUUID string, receiverUserID int64) error
func (d *ClosetShareReceiverDao) ClosetShareReceivedList(ctx context.Context, receiverUserID int64, status int8, page, pageSize int) ([]mysqlmodel.ClosetShare, int64, error)

// internal/mysqldao/user.go
func (d *UserDao) UserGetByIDs(ctx context.Context, ids []int64) ([]mysqlmodel.User, error)

// internal/service/closet.go
func (s *ClosetShareService) ListReceived(ctx context.Context, userID int64, request *dto.ClosetShareListRequest) (*dto.ShareListResponse, *errcode.Error)