9.2 KiB
9.2 KiB
name, overview, todos
| name | overview | todos | |||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 衣柜分享-分享给我的 | 新增"分享给我的"能力:通过 closet_share_receivers 关联表记录登录用户访问分享详情的接收关系(一对多、幂等),并新增 GET /closet-share/received 列表接口,返回含分享者昵称/头像的分享列表。 |
|
产品概述
为后端"衣柜分享"补齐"分享给我的"数据能力。当前 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):分享 UUIDreceiver_user_id BIGINT:接收人用户 IDUNIQUE KEY uk_closet_share_receiver (share_uuid, receiver_user_id):保证一个用户对同一分享只有一条记录KEY idx_closet_share_receivers_user (receiver_user_id):支撑"按接收人分页查询"
接收关系写入采用幂等 upsert(唯一索引冲突时 DO NOTHING),避免并发访问重复插入。
接收关系记录时机
在 ClosetShareService.Detail 中,仅当以下条件全部满足时记录接收关系:
- 分享状态校验通过(未被取消/封禁/过期);
- 衣物明细加载成功,详情能够正常返回;
- 当前
userID > 0且非分享属主。
记录为 best-effort:失败仅告警,不阻断详情返回,避免因关联表故障影响详情可用性。
"分享给我的"列表查询
新增 ClosetShareService.ListReceived,通过 DAO 联表 closet_share_receivers 与 closet_shares 分页查询,避免 N+1:
- 联表分页取出分享记录(按接收记录
id DESC排序,支持status过滤); - 复用
ClosetShareItemListByShareUUIDs批量统计各分享衣物数量; - 新增
UserDao.UserGetByIDs批量查询分享者昵称/头像,头像按现有upload.GetCosUrl规则转换; - 组装列表项返回。
路由兼容
在 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)