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

147 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: 衣柜分享-分享给我的
overview: 新增"分享给我的"能力:通过 closet_share_receivers 关联表记录登录用户访问分享详情的接收关系(一对多、幂等),并新增 GET /closet-share/received 列表接口,返回含分享者昵称/头像的分享列表。
todos:
- id: add-receiver-migration-model
content: 新增 closet_share_receivers 迁移、ClosetShareReceiver 模型与 DAO(幂等 upsert、联表分页查询)
status: completed
- id: add-received-service
content: 扩展 DTO 与 UserGetByIDs,详情记录接收人并实现 ListReceived 服务
status: completed
dependencies:
- add-receiver-migration-model
- id: add-received-handler-route
content: 新增 ReceivedList handler 并注册 /closet-share/received 路由
status: completed
dependencies:
- add-received-service
- id: update-docs
content: 更新接口文档与 README 的衣柜分享模块说明
status: completed
dependencies:
- 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` 字段,"我的分享"列表暂不填充(空字符串),"分享给我的"列表填充分享者信息。
## 架构设计
```mermaid
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)
```