---
url: /docs/modules/community.md
---
# 模块手册 · 社区域（community）

> 收录 **4** 个模块的权威开发手册（源自各模块 `CLAUDE.md`，随仓库同步）。

## 本文目录

* 1. feed
* 2. follow
* 3. favorite
* 4. review

***

## 1. feed 模块

> 源：`src/app/feed/CLAUDE.md`（随仓库同步）

## src/app/feed/

> feed 模块 — V1 运动动态 9 action 迁移（V2 DDD 4 层 + InMemory + 行为兼容），L3-A 社交域。

### 模块职责

`feed` 承接 C 端运动动态域（对齐 V1 `server/src/modules/feed/`），**路由风格 = action 协议**：

* 入口：`POST /api/v1/feed` + `body {action, payload}`（V1 `POST /api/feed` 路径平移，协议 100% 兼容）
* 9 action（routes.ts switch 权威 — 比 V1 CLAUDE.md 记载的 7 action 多 2）：
  `list` / `myFeeds` / `publish` / `like` / `unlike` / `comment` / `listComments` /
  `hotTopics` / `shoesForPicker`
* 鉴权：全部需已登录（`Authorization: Bearer <access_token>`）
* 错误码区段：**40420xxx**（与 upload 40419xxx / follow 40421xxx 相邻隔离）
  * ⚠️ 2026-09-16 撞码治理时从 `40413xxx` **迁出**（该段现属 **group\_buy**）
* HTTP 状态：400 / 401 / 404（V1 行为 100% 兼容）

### DDD 4 层结构

```
src/app/feed/
├── domain/                          # 零 web 依赖
│   ├── entities/feed.py             # Feed / FeedLike / FeedComment + CZ_FEED_NOTIFY_CONTENT_MAX(50)
│   └── exceptions.py                # FeedErrorCode (40420xxx) + cz_feed_error_to_http_status
│
├── application/
│   ├── ports/repositories.py        # FeedRepositoryPort + FeedUser/FeedShoe/FeedFollow 最小端口
│   ├── ports/services.py            # AccessTokenDecoderPort + NotificationPort
│   ├── usecase/feed_timeline.py     # list / myFeeds / listComments / hotTopics（4）
│   ├── usecase/feed_publish.py      # publish / shoesForPicker（2）
│   ├── usecase/feed_interactions.py # like / unlike / comment（3）
│   └── factories/create_feed_usecases.py
│
├── infrastructure/persistence/inmemory_feed_repositories.py  # 主仓储 + 4 适配器 + 种子
└── interface/{api/feed_actions.py, handlers.py}
```

### 对外接口（9 action 一览）

| action | payload | 返回（data） | 说明 |
|---|---|---|---|
| `list` | `{page=1, pageSize=20(1..50), sort=latest\|hot, topic?, userId?}` | `{list, total, page, pageSize, hasMore}` | 动态流；hot=likeCount desc；含作者 + shoe + liked |
| `myFeeds` | `{page, pageSize}` | 同上 | 我的动态（createdAt desc） |
| `publish` | `{content 1..500, images ≤9, checkinId?, distanceKm? 0..200, topic? ≤30, videoUrl?(url), shoeId?}` | `{id}` | 发布（shoeId 归属校验不属则忽略 + fan-out 粉丝通知） |
| `like` | `{feedId}` | `{ok: true, liked: true}` | 幂等点赞（事务计数 +1；notify type=like） |
| `unlike` | `{feedId}` | `{ok: true, liked: false}` | 幂等取消（**不校验 feed 存在** — V1 口径） |
| `comment` | `{feedId, content 1..200}` | `{id}` | 评论（事务计数 +1；notify content 50 字截断 + '…'） |
| `listComments` | `{feedId, page=1, pageSize=50}` | `{list, total}` | 评论列表（createdAt desc + user） |
| `hotTopics` | — | `{topics: [{topic, count}]}` | groupBy topic（非空）count desc take 10 |
| `shoesForPicker` | — | `{shoes: [...]}` | active 跑鞋（createdAt desc — 发动态 picker） |

### 关键行为约束（V1 对拍）

#### $transaction 计数原子（like/unlike/comment — 02 §6 铁律）

* `cz_like_feed` / `cz_unlike_feed` / `cz_add_comment`：create|delete + 计数 ±1 **收敛到同一
  async 方法内**（无 await 间隙 → asyncio 单线程原子 — V1 $transaction 回调等价）
* 幂等：like 先 findUnique 检查（已赞不重复计数）；unlike 未赞也返 ok；重复 PublishLike 由
  @@unique(\[feedId, userId]) 语义兜底（仓储 upsert 式跳过）

#### fan-out（publish + V0.2.125）

publish 后给作者**所有粉丝**推 `type=new_post` 通知：粉丝列表查询失败 → 静默（外层）；
单条 notify 失败 → 独立吞错（`Promise.allSettled` 等价 — 不影响其他粉丝与主返回）。

#### notify 集成（V1 notification.notify 对拍）

* 自己触发自己跳过（`user_id === actor_id` → 不写通知 — **在端口实现内**，V1 notify 首行）
* 调用方 `contextlib.suppress(Exception)` 包裹（通知失败不阻塞点赞/评论/发布主链路）
* V1 另有 realtime 推送（publishToUser）— V2 InMemory 阶段不模拟（与缓存同决策）

#### list 过滤组合（V1 where spread 语义）

`where = {...topic?, ...userId?}` — topic 空串/缺省不过滤（`topic ? {topic} : {}`）；sort=hot
按 likeCount desc 否则 createdAt desc；liked = viewer 对 feed 的点赞集合（批量查询等价 V1
include likes.where(userId)）。

#### 评论 notify 截断（50 字）

V1 `content.length > 50 → slice(0, 50) + '…'`（**>50 才截** — 恰好 50 不截）。
⚠️ V1 `length` 为 UTF-16 code units；V2 用 Python len（code points）— 中文/拉丁文一致，
增补平面 emoji 边缘差异已接受。

#### postman/videoUrl 校验

V1 `z.string().url()`（JS `new URL()` try/catch — 绝对 URL）→ V2 pydantic field\_validator
（`urlparse().scheme` 非空判定 — 语义近似）。

### 跨模块最小端口（对应模块未接真实现 — 生产阶段替换）

| 端口 | InMemory 实现 | 将来接入 |
|---|---|---|
| `FeedUserPort`（作者 id/nickname/avatarUrl） | InMemoryFeedUserAdapter | user 模块共库 |
| `FeedShoePort`（归属校验 / picker / include 批量） | InMemoryFeedShoeAdapter | shoes 模块真实现（共读 shoes 表） |
| `FeedFollowPort`（粉丝 userIds — fan-out） | InMemoryFeedFollowAdapter | follow 模块真实现（共读 follows 表） |
| `NotificationPort`（notify 集成） | InMemoryFeedNotificationAdapter（记录 + skip-self） | notification 模块迁移时接真实现 |

### 与 V1 行为差异（已接受）

| 项 | V1 | V2 |
|---|---|---|
| 时间序列化 | `toISOString()`（UTC 带 Z） | naive 本地 isoformat（全局口径 — 显示层等价） |
| 缓存 | list 无缓存；V1 部分读走 Cache.wrap | 不模拟（与 goal/shoes/training/mall 同决策） |
| realtime 推送 | notify 后 publishToUser（WS） | 不模拟（InMemory 阶段无 WS 通道） |
| listComments 入参校验 | route 层无 zod（TS 仅类型断言 — 垃圾入参未定义行为） | pydantic 宽容归一（feedId 必传 + 默认 1/50 — 防御性增强） |
| 截断口径（评论 notify） | UTF-16 code units | Python len（code points — emoji 边缘差异） |
| 事务 | Prisma $transaction 回调 | InMemory 单方法原子（生产 Prisma 阶段经端口恢复真事务） |

### 未迁移项（标注）

* V1 feed 的 redis 缓存/热度排行（如有）：本批未涉及（V1 侦察确认无）
* 跨模块调用方（frontend hot/topic 页）只消费 HTTP 契约 — 无后端缺口

### 测试与质量

| 文件 | 范围 |
|---|---|
| `tests/integration/test_feed_actions.py` | 9 action E2E（鉴权 / 契约 / publish→like→comment 链 / 404） |
| `tests/unit/application/test_feed_usecases.py` | 9 action 全分支（幂等 / fan-out / 截断 / hot 排序） |

```bash
PYTHONPATH=src python3 -m pytest tests/integration/test_feed_actions.py \
  tests/unit/application/test_feed_usecases.py -q
```

### 相关文件

```
src/app/feed/                       ← 本文件所在模块
tests/integration/test_feed_actions.py
tests/unit/application/test_feed_usecases.py
src/main.py                         # 装配（"── 13. L3-A 社交域"段）
```

### 变更记录 (Asia/Shanghai)

| 日期 | 版本 | 变更 |
| --- | --- | --- |
| 2026-09-16 | 1.0.0 | 首版 — V1 运动动态 9 action 迁移（DDD 4 层 + 40413xxx + 原子计数 + fan-out + 4 最小端口） |

***

## 2. follow 模块

> 源：`src/app/follow/CLAUDE.md`（随仓库同步）

## src/app/follow/

> follow 模块 — V1 关注/粉丝 6 action 迁移（V2 DDD 4 层 + InMemory + 行为兼容），L3-A 社交域。

### 模块职责

`follow` 承接 C 端关注关系域（对齐 V1 `server/src/modules/follow/`），**路由风格 = action 协议**：

* 入口：`POST /api/v1/follow` + `body {action, payload}`（V1 `POST /api/follow` 路径平移，协议 100% 兼容）
* 6 action：`follow` / `unfollow` / `isFollowing` / `myFollowing` / `myFollowers` / `myCounts`
* 鉴权：全部需已登录（`Authorization: Bearer <access_token>`）
* 错误码区段：**40421xxx**（与 feed 40420xxx 相邻隔离）
  * ⚠️ 2026-09-16 撞码治理时从 `40414xxx` **迁出**（该段现属 **distribution**）
* HTTP 状态：400 / 401 / 404（V1 行为 100% 兼容）

### DDD 4 层结构

```
src/app/follow/
├── domain/                          # 零 web 依赖
│   ├── entities/follow.py           # Follow（followerId / followeeId / createdAt）
│   └── exceptions.py                # FollowErrorCode (40421xxx) + cz_follow_error_to_http_status
│
├── application/
│   ├── ports/repositories.py        # FollowRepositoryPort + FollowUserPort
│   ├── ports/services.py            # AccessTokenDecoderPort + NotificationPort
│   ├── usecase/follow_relations.py  # follow / unfollow / isFollowing（3）
│   ├── usecase/follow_queries.py    # myFollowing / myFollowers / myCounts（3）
│   └── factories/create_follow_usecases.py
│
├── infrastructure/persistence/inmemory_follow_repositories.py  # 主仓储 + 2 适配器 + 种子
└── interface/{api/follow_actions.py, handlers.py}
```

### 对外接口（6 action 一览）

| action | payload | 返回（data） | 说明 |
|---|---|---|---|
| `follow` | `{userId}` | `{ok: true, following: true}` | upsert 幂等；'不能关注自己' 400 / '用户不存在' 404；notify type=follow |
| `unfollow` | `{userId}` | `{ok: true, following: false}` | deleteMany 幂等（不存在也 ok） |
| `isFollowing` | `{userIds 1..50}` | `{results: [{userId, following}]}` | 批量（保 input 顺序） |
| `myFollowing` | `{page, pageSize}` | `{list: [{userId, createdAt, user}], total, page, pageSize, hasMore}` | 我关注的人（createdAt desc） |
| `myFollowers` | `{page, pageSize}` | 同上 | 我的粉丝（createdAt desc） |
| `myCounts` | `{userId}` | `{user, followingCount, followerCount, isFollowing, isSelf}` | 用户主页一次拿全（可查任意 userId） |

### 关键行为约束（V1 对拍）

#### follow 校验链（顺序严格）

① 自己关注自己 → 400 `不能关注自己`（**先于**目标存在校验）；② 目标不存在 → 404 `用户不存在`；
③ upsert（已存在则 update {} 不重复建 — @@unique(\[followerId, followeeId]) 语义）。

#### notify（fail-safe）

`follow` 成功后 notify 被关注者（type=follow — 无 targetType/targetId）；通知失败静默
（`contextlib.suppress` — 不阻塞关注主链路）；自己触发自己跳过在端口内（V1 notify 首行）。
⚠️ 幂等重复 follow 依然调 notify（V1 无重查短路 — 逐字对齐）。

#### myCounts 视图逻辑（V1 三元表达式逐字）

`viewerId && viewerId !== userId ? findUnique : null` — viewerId 恒存在（已登录）→
`viewer === user` 时**跳过查询**（isFollowing 恒 false，isSelf true）；user 不存在 →
404 `用户不存在`（Promise.all 后判空 — V1 顺序）。

#### 分页

myFollowing / myFollowers：createdAt desc + `hasMore = page * pageSize < total`；
user include select {id, nickname, avatarUrl}。

### 跨模块最小端口（对应模块未接真实现 — 生产阶段替换）

| 端口 | InMemory 实现 | 将来接入 |
|---|---|---|
| `FollowUserPort`（目标校验 + user include） | InMemoryFollowUserAdapter | user 模块共库 |
| `NotificationPort`（notify 集成） | InMemoryFollowNotificationAdapter（记录 + skip-self） | notification 模块迁移时接真实现 |

### 与 V1 行为差异（已接受）

| 项 | V1 | V2 |
|---|---|---|
| 时间序列化 | `toISOString()`（UTC 带 Z） | naive 本地 isoformat（全局口径） |
| realtime 推送 | notify 后 publishToUser（WS） | 不模拟（InMemory 阶段无 WS 通道） |
| 事务 | Prisma upsert/deleteMany 原子 | InMemory 单方法（等价） |

### 测试与质量

| 文件 | 范围 |
|---|---|
| `tests/integration/test_follow_actions.py` | 6 action E2E（鉴权 / 幂等 / 400+401+404） |
| `tests/unit/application/test_follow_usecases.py` | 6 action 全分支（校验收敛 / 保序 / isSelf） |

```bash
PYTHONPATH=src python3 -m pytest tests/integration/test_follow_actions.py \
  tests/unit/application/test_follow_usecases.py -q
```

### 相关文件

```
src/app/follow/                     ← 本文件所在模块
tests/integration/test_follow_actions.py
tests/unit/application/test_follow_usecases.py
src/main.py                         # 装配（"── 13. L3-A 社交域"段）
```

### 变更记录 (Asia/Shanghai)

| 日期 | 版本 | 变更 |
| --- | --- | --- |
| 2026-09-16 | 1.0.0 | 首版 — V1 关注/粉丝 6 action 迁移（DDD 4 层 + 40414xxx + 最小端口 ×2） |

***

## 3. favorite 模块

> 源：`src/app/favorite/CLAUDE.md`（随仓库同步）

## src/app/favorite/

> favorite 模块 — V1 收藏 4 action 迁移（V2 DDD 4 层 + InMemory + 行为兼容），L3-A 社交域。

### 模块职责

`favorite` 承接 C 端 Content/Product 收藏域（对齐 V1 `server/src/modules/favorite/`），
**路由风格 = action 协议**：

* 入口：`POST /api/v1/favorite` + `body {action, payload}`（V1 `POST /api/favorite` 路径平移，协议 100% 兼容）
* 4 action：`list` / `add` / `remove` / `isFavorited`
* 鉴权：全部需已登录（`Authorization: Bearer <access_token>`）
* 错误码区段：**40415xxx**（与 follow 40421xxx / review 40416xxx 相邻隔离）
* HTTP 状态：400 / 401（4 action 无业务抛出 — V1 口径）

### DDD 4 层结构

```
src/app/favorite/
├── domain/                          # 零 web 依赖
│   ├── entities/favorite.py         # Favorite + CZ_FAVORITE_TYPES（content | product）
│   ├── services/money.py            # cz_js_decimal_string（product price — 各模块自带一份）
│   └── exceptions.py                # FavoriteErrorCode (40415xxx) + cz_favorite_error_to_http_status
│
├── application/
│   ├── ports/repositories.py        # FavoriteRepositoryPort + FavoriteContent/FavoriteProduct 最小端口
│   ├── ports/services.py            # AccessTokenDecoderPort
│   ├── usecase/favorite_actions.py  # list / add / remove / isFavorited（4）
│   └── factories/create_favorite_usecases.py
│
├── infrastructure/persistence/inmemory_favorite_repositories.py  # 主仓储 + 2 适配器 + 种子
└── interface/{api/favorite_actions.py, handlers.py}
```

### 对外接口（4 action 一览）

| action | payload | 返回（data） | 说明 |
|---|---|---|---|
| `list` | `{targetType?: content\|product}` | `{favorites: [{id, targetType, targetId, createdAt, detail}]}` | 我的收藏（createdAt desc，**无分页** — V1 口径）；detail 为精确 select 或 null |
| `add` | `{targetType, targetId}` | `{ok: true}` | upsert 幂等；**不校验目标存在** — V1 口径 |
| `remove` | `{targetType, targetId}` | `{ok: true}` | deleteMany 幂等（不存在也 ok） |
| `isFavorited` | `{items 1..50: [{targetType, targetId}]}` | `{results: [{targetType, targetId, favorited}]}` | 批量红心（保 input 顺序） |

### 关键行为约束（V1 对拍）

#### list 批量关联（避免 N+1）

先查 favorites → 拆 content/product ids → 批量查详情（Map 关联）；**id 列表为空时短路不查**
（V1 `length === 0 ? [] : findMany` 逐字）；目标已删除 → `detail: null`（前端据此隐藏）。

#### detail 精确键集（V1 select）

* content：`{id, title, cover, summary, type, location, date}`（可空字段 null 直通）
* product：`{id, name, price, images, category, status}` — **price → decimal.js toString 字符串**
  （`Decimal('199.00')` → `'199'` 去尾随零 — cz\_js\_decimal\_string）

#### add 不校验目标存在（V1 口径）

V1 upsert 直写不校验 content/product 存在 — 幽灵目标可收藏（list detail=null）。

#### isFavorited key 格式

`'type:id'` 集合匹配（V1 `${targetType}:${targetId}`）— results 保 input 顺序。

### 跨模块最小端口（对应模块未接真实现 — 生产阶段替换）

| 端口 | InMemory 实现 | 将来接入 |
|---|---|---|
| `FavoriteContentPort`（内容详情 select） | InMemoryFavoriteContentAdapter | content 模块迁移时接真实现 |
| `FavoriteProductPort`（商品详情 select） | InMemoryFavoriteProductAdapter | mall 模块真实现（共读 products 表） |

### 与 V1 行为差异（已接受）

| 项 | V1 | V2 |
|---|---|---|
| 时间序列化 | `toISOString()`（UTC 带 Z） | naive 本地 isoformat（全局口径） |
| 可选键 null | JS `undefined` 键被丢弃 | 输出 `null`（JS falsy 等价 — detail 显式 null 是 V1 行为） |

### 测试与质量

| 文件 | 范围 |
|---|---|
| `tests/integration/test_favorite_actions.py` | 4 action E2E（price 字符串契约 / detail null / 校验 400） |
| `tests/unit/application/test_favorite_usecases.py` | 4 action 全分支（幂等 / 幽灵目标 / 保序） |
| `tests/unit/domain/test_favorite_money.py` | decimal.js toString 纯函数 |

```bash
PYTHONPATH=src python3 -m pytest tests/integration/test_favorite_actions.py \
  tests/unit/application/test_favorite_usecases.py tests/unit/domain/test_favorite_money.py -q
```

### 相关文件

```
src/app/favorite/                   ← 本文件所在模块
tests/{integration/test_favorite_actions.py, unit/application/test_favorite_usecases.py, unit/domain/test_favorite_money.py}
src/main.py                         # 装配（"── 13. L3-A 社交域"段）
```

### 变更记录 (Asia/Shanghai)

| 日期 | 版本 | 变更 |
| --- | --- | --- |
| 2026-09-16 | 1.0.0 | 首版 — V1 收藏 4 action 迁移（DDD 4 层 + 40415xxx + 最小端口 ×2 + Decimal price 口径） |

***

## 4. review 模块

> 源：`src/app/review/CLAUDE.md`（随仓库同步）

## src/app/review/

> review 模块 — V1 C 端评价域 7 action 迁移（V2 DDD 4 层 + InMemory + 行为兼容），L3-A 社交域。

### 模块职责

`review` 承接 C 端评价域（对齐 V1 `server/src/modules/review/` — V0.1.113 电商评价闭环 +
V0.1.137 鞋评），**路由风格 = action 协议**：

* 入口：`POST /api/v1/review` + `body {action, payload}`（V1 `POST /api/review` 路径平移，协议 100% 兼容）
* 7 action：`create` / `list` / `listByTarget` / `stats` / `targetStats` / `myReviews` / `remove`
* 鉴权：全部需已登录（`Authorization: Bearer <access_token>`）
* 错误码区段：**40416xxx**（避开 admin 侧 review 管理 40201500-599 — 后台审核链路独立）
* HTTP 状态：400 / 401 / 403 / 404（V1 行为 100% 兼容）

### DDD 4 层结构

```
src/app/review/
├── domain/                          # 零 web 依赖
│   ├── entities/review.py           # Review + CZ_REVIEWABLE_ORDER_STATUS + CZ_SHOE_REVIEW_PREFIX
│   ├── services/review_calc.py      # syntheticId / tag 前缀 / 剥前缀 / cz_js_round1
│   └── exceptions.py                # ReviewErrorCode (40416xxx) + cz_review_error_to_http_status
│
├── application/
│   ├── ports/repositories.py        # ReviewRepositoryPort + ReviewOrder/Shoe/User/Product 最小端口
│   ├── ports/services.py            # AccessTokenDecoderPort
│   ├── usecase/review_create.py     # create（product 7 步链 / shoe 路径）
│   ├── usecase/review_queries.py    # list / listByTarget / stats / targetStats / myReviews（5）
│   ├── usecase/review_remove.py     # remove（1）
│   └── factories/create_review_usecases.py
│
├── infrastructure/persistence/inmemory_review_repositories.py  # 主仓储 + 4 适配器 + 种子
└── interface/{api/review_actions.py, handlers.py}
```

### 对外接口（7 action 一览）

| action | payload | 返回（data） | 说明 |
|---|---|---|---|
| `create` | `{targetType=product\|shoe, targetId, productId?, orderId?, rating 1..5, content? ≤500, images? ≤9}` | product: `{id}` / shoe: `{id, reviewType: 'shoe'}` | 商品（已购校验）/ 鞋评（归属校验）双路径 |
| `list` | `{productId, page=1, pageSize=10}` | `{list, total, page, pageSize}` | 旧商品入口（**不剥** \[shoe-review] 前缀 — V1 口径） |
| `listByTarget` | `{targetType, targetId, page, pageSize}` | 同上 | V0.1.137 通用（syntheticId 查询 + **剥**前缀） |
| `stats` | `{productId}` | `{avg, count, distribution{1..5}}` | 商品评分汇总（= targetStats(productId)） |
| `targetStats` | `{targetType, targetId}` | 同上 | 通用汇总（shoe → syntheticId） |
| `myReviews` | `{page, pageSize}` | `{list, total, page, pageSize}` | 我的评价（含 product select；**无 user 键** — V1 口径） |
| `remove` | `{id}` | `{ok: true}` | 仅本人可删（'评价不存在' 404 / '不是你的评价' 403） |

### 关键行为约束（V1 对拍）

#### create — product 校验链（**顺序严格不可调换**）

① `productId = input.productId ?? input.targetId` 缺 → 400 `productId required`（防御 — zod 拦截在前）；
② `orderId` 缺 → 400 `orderId required`；③ 订单不存在 → 404 `订单不存在`；④ 非本人 →
403 `不是你的订单`；⑤ status ∉ {paid, shipped, done} → 400 `订单未支付，不可评价`；
⑥ 商品不在订单 → 400 `该商品不在此订单`；⑦ 三元组防重 → 400 `已评价过该商品`。

#### create — shoe 路径（V0.1.137）

归属校验（'跑鞋不存在或不属于你' 404）→ syntheticId `shoe:{shoeId}` 防重（'已评价过该跑鞋' 400）
→ content 存 tag `` `[shoe-review] ${content ?? ''}`.trim() ``（V1 模板拼接语义 — content=None →
`'[shoe-review]'`（去尾空格）；content=' 缓震一流 ' → `'[shoe-review]  缓震一流'`（**中间双空格保留**））。

#### 前缀剥除不对称（V1 生产行为 — 逐字复刻）

* `listByTarget`：`content.startsWith('[shoe-review] ')` 才剥（**含尾空格**；裸 tag `'[shoe-review]'`
  不满足 → 不剥）
* `list`（旧 productId 入口）/ `myReviews`：**不剥前缀**（V1 实现差异）

#### 评分汇总（JS Math.round 语义）

`avg = agg._avg.rating ? Math.round(x * 10) / 10 : 0` — 空态 0；无评价 distribution 全 0 键
`{1..5}`。⚠️ **不可用 Python round**（银行家舍入）→ `cz_js_round1 = floor(v*10 + 0.5)/10`
（4.25 → 4.3 — 单测锁定）。

#### 输出键集（V1 include 差异）

* `list` / `listByTarget`：`{id, userId, productId, orderId, rating, content, images, createdAt,
  replyContent, repliedAt, user}`（user=include select {id, nickname, avatarUrl}；repliedAt 恒输出 null/ISO）
* `myReviews`：同字段集但 **无 user 键** + `product: {id, name, images} | null`（鞋评 → null）

### 跨模块最小端口（对应模块未接真实现 — 生产阶段替换）

| 端口 | InMemory 实现 | 将来接入 |
|---|---|---|
| `ReviewOrderPort`（订单存在/归属/状态/商品在单） | InMemoryReviewOrderAdapter | order 模块共库 |
| `ReviewShoePort`（跑鞋归属） | InMemoryReviewShoeAdapter | shoes 模块真实现 |
| `ReviewUserPort`（评价列表 user include） | InMemoryReviewUserAdapter | user 模块共库 |
| `ReviewProductPort`（myReviews product include） | InMemoryReviewProductAdapter | mall 模块真实现 |

### 与 V1 行为差异（已接受）

| 项 | V1 | V2 |
|---|---|---|
| 时间序列化 | `toISOString()`（UTC 带 Z） | naive 本地 isoformat（全局口径） |
| content 截断口径 | zod max(500) 按 UTF-16 | pydantic max\_length 按 code points（emoji 边缘差异） |
| distribution 键类型 | JS 对象数字键（JSON 序列化为字符串键） | Python int 键（JSON 序列化同为字符串键 — 等价） |

### 未迁移项（标注）

* admin 侧评价审核（P2-A `src/app/admin/application/usecase/review_mgmt.py` — 40201500-599）
  与本模块独立；admin 审核的 \[shoe-review] tag 识别由 admin 迁移批承载

### 测试与质量

| 文件 | 范围 |
|---|---|
| `tests/integration/test_review_actions.py` | 7 action E2E（错误链 HTTP 状态 400/403/404 / 鞋评 tag / 统计契约） |
| `tests/unit/application/test_review_usecases.py` | 7 action 全分支（校验链逐项 / avg JS 语义 4.25→4.3 / 前缀不对称） |
| `tests/unit/domain/test_review_calc.py` | syntheticId / tag / 剥前缀 / round1 纯函数 |

```bash
PYTHONPATH=src python3 -m pytest tests/integration/test_review_actions.py \
  tests/unit/application/test_review_usecases.py tests/unit/domain/test_review_calc.py -q
```

### 相关文件

```
src/app/review/                     ← 本文件所在模块
tests/{integration/test_review_actions.py, unit/application/test_review_usecases.py, unit/domain/test_review_calc.py}
src/main.py                         # 装配（"── 13. L3-A 社交域"段 — InMemoryReviewRepository 别名防 admin 同名遮蔽）
```

### 变更记录 (Asia/Shanghai)

| 日期 | 版本 | 变更 |
| --- | --- | --- |
| 2026-09-16 | 1.0.0 | 首版 — V1 C 端评价域 7 action 迁移（DDD 4 层 + 40416xxx + 鞋评 syntheticId/tag + 最小端口 ×4） |
