airpredict/空气质量预测/源码/docs/superpowers/specs/2026-07-07-forum-and-eco-ma...

223 lines
11 KiB
Markdown
Raw 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.

# 设计文档:导航改版 + 环保建材 + 健康装修大家谈论坛
- 日期2026-07-07
- 目标项目:`C:\code\空气质量预测\源码`(用户端 iapip-web / 服务端 iapip-svr
- 工作目录:前端 `源码/用户端/iapip-web`,后端 `源码/服务端/iapip-svr`
- 状态:已通过需求评审,待写实现计划
---
## 1. 背景与目标
用户端首页landing`用户端/iapip-web/src/pages/landing/index.tsx`)当前顶部导航为 5 项(资讯科普 / 治理案例 / 如何使用 / 污染源识别 / 专业看板),前三项为页面内锚点滚动、内容全为写死的静态数据。
本次改动:
1. 顶部导航改为 4 项:**健康装修科普、预测案例、环保建材、健康装修大家谈**。
2. 「环保建材」为新区块,从后端真实公共材料库拉取 E0 低释放材料展示。
3. 「健康装修大家谈」为新的**真功能论坛**(进阶版:板块、发帖、楼层回帖、点赞、分页;公开浏览、手机登录后发帖)。
前两项(健康装修科普、预测案例)仅为现有区块的**标签改名**,内容不变。
---
## 2. 范围
### 2.1 纳入范围
- 导航 4 项改版与两处区块标签改名(纯前端)。
- 环保建材:新增 1 个免认证后端接口 + landing 新区块(真实数据)。
- 论坛:新增 3 张 Prisma 表 + 1 个后端 controller + 2 个前端页面 + 1 个前端 API 封装。
### 2.2 不纳入范围YAGNI
- 论坛板块的后台增删(板块用代码常量固定)。
- 帖子/回帖的富文本、@提及、通知、搜索。
- 管理端对论坛的审核后台(作者可自删自己的帖/回帖已满足基本治理;管理端审核为后续可选项)。
- 环保建材的收藏、跳转到材料详情等交互(仅展示)。
---
## 3. A. 导航栏与 landing 文案(前端)
文件:`用户端/iapip-web/src/pages/landing/index.tsx`
| 新导航项 | 行为 | 实现 |
|---|---|---|
| 健康装修科普 | 滚动到 `#news` | 现「资讯科普」区块改名;`nav-links` 文案改,`#news` 区块 `sec-tag` 由「资讯 · 科普」改为「健康装修科普」 |
| 预测案例 | 滚动到 `#cases` | 现「治理案例」区块改名;`sec-tag` 由「治理案例」改为「预测案例」内容before/after 卡片)不变 |
| 环保建材 | 滚动到新 `#eco` | 见第 4 节 |
| 健康装修大家谈 | `history.push('/forum')` | 见第 57 节 |
其他:
- 「免费试算」主按钮(`openPredict`)保留不变。
- 原「如何使用」`#how` 区块**保留在页面**,仅从导航移除。
- 原「专业看板」入口从导航移除CTA 区块的「查看专业看板」按钮(`goPro`)与登录入口仍可到达工作台。
---
## 4. B. 环保建材(后端免认证接口 + 前端区块)
### 4.1 后端接口
- 路由:`GET /api/mtrl/eco`**免认证**(不挂 `authn`),加在 `material.ts` 中。
- 逻辑:查询 `PublicMaterial``deleted=0 且 display=1`、`eco_level='E0'` 的材料;若不足 8 条,补 `E1`。按 `created_at` 倒序,最多返回 8 条。
- 返回字段(精简,不返回释放参数三件套等敏感/无关字段):
`material_id, name, category, brand, factory, eco_level, methanal`(甲醛标称值,若有)。
- 空结果返回 `[]`,前端优雅处理。
### 4.2 前端区块
- landing 新增 `#eco` 区块,位置置于 `#cases``#how` 之间顺序news → cases → eco → how
- 组件挂载时通过 `api.material.getEcoMaterials()` 拉取,`useState` 保存。
- 展示:卡片网格(复用现有 `.cases-grid` / `.case` 或新增等价样式每张卡显示材料名、品牌、类别、环保等级标签E0/E1
- 加载中不阻塞页面其他区块;返回空数组时该区块整体隐藏(`data.length === 0` 则不渲染 `<section>`)。
- 前端 API`src/services/api/material.ts` 新增 `getEcoMaterials`,走 `/api/mtrl/eco`
---
## 5. C. 论坛数据模型Prisma
文件:`服务端/iapip-svr/prisma/schema.prisma`。新增 3 张表。板块board**不建表**,用后端常量固定。
### 5.1 板块常量
`服务端/iapip-svr/src/common/constants.ts` 定义:
```
FORUM_BOARDS = ['甲醛治理', '异味TVOC', '环保材料', '装修避坑', '求助问答']
```
帖子的 `board` 字段值必须属于该列表(后端校验)。
### 5.2 表结构
```prisma
model ForumThread {
id Int @id @default(autoincrement())
board String
title String
content String @db.Text
author_id String
author_name String
like_count Int @default(0)
reply_count Int @default(0)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
deleted Int @default(0)
replies ForumReply[]
likes ForumThreadLike[]
@@map("forum_thread")
}
model ForumReply {
id Int @id @default(autoincrement())
thread_id Int
thread ForumThread @relation(fields: [thread_id], references: [id], onDelete: Cascade)
floor Int
content String @db.Text
author_id String
author_name String
created_at DateTime @default(now())
deleted Int @default(0)
@@map("forum_reply")
}
model ForumThreadLike {
thread_id Int
thread ForumThread @relation(fields: [thread_id], references: [id], onDelete: Cascade)
user_id String
created_at DateTime @default(now())
@@id([thread_id, user_id])
@@map("forum_thread_like")
}
```
- `author_name` 冗余存 username列表/详情展示不连表。
- 软删 `deleted` 沿用项目惯例;`ForumThreadLike` 无软删toggle 直接删行)。
- 迁移:`npm run dbpush:dev`prisma db push
---
## 6. D. 论坛后端 API
新建 controller `服务端/iapip-svr/src/controllers/forum.ts``prefix('/api/forum')`,在 `src/index.ts``app.use(forumRoutes)` 注册。登录鉴权复用 `authn(CERT_TYPE.ACCOUNT)`,作者身份取 `ctx.state.acct_cert``id`、`username`)。
| 方法 路径 | 鉴权 | 功能 | 说明 |
|---|---|---|---|
| `GET /api/forum/boards` | 公开 | 板块列表 + 各板块帖数 | 返回 `[{board, count}]`count 统计 `deleted=0` |
| `GET /api/forum/threads` | 公开 | 分页帖子列表 | query`board?`(缺省全部)、`page`(默认1)、`size`(默认20)。按 `created_at desc`。返回列表项 + 总数 |
| `GET /api/forum/threads/:id` | 公开 | 帖子详情 + 分页回帖 | 回帖按 `floor asc`,跳过 `deleted=1` 的回帖。best-effort若请求带有效 token 则解析出用户并附 `liked` 布尔,否则省略该字段 |
| `POST /api/forum/threads` | 登录 | 发帖 | body`{board,title,content}`。board 必须在 `FORUM_BOARDS`title/content 非空且限长 |
| `POST /api/forum/threads/:id/replies` | 登录 | 回帖 | body`{content}`。事务内 `floor = 当前 reply_count + 1`,同时 `reply_count++` |
| `POST /api/forum/threads/:id/like` | 登录 | 点赞/取消 toggle | 有 like 行则删并 `like_count--`;无则插并 `like_count++`。返回最新 `{liked, like_count}` |
| `DELETE /api/forum/threads/:id` | 登录 | 软删自己的帖 | 仅 `author_id === cert.id`,否则 403 |
| `DELETE /api/forum/replies/:id` | 登录 | 软删自己的回帖 | 同上;不回退 floor其余楼层号保持不变。详情接口跳过 `deleted=1` 的回帖,前端不展示已删楼层 |
- 校验:沿用项目 AJV `validate` 中间件风格(参照 `material.ts`)。
- 错误码:非法 board / 空标题用 `1000 INVALID_REQ_DATA`;越权删除用 `3000 FORBIDDEN`;帖子不存在用 `4000`(沿用现有粗粒度约定)。
- 并发:回帖 floor 与 reply_count、点赞计数均在 `db.$transaction` 内完成,避免竞态。
---
## 7. E. 论坛前端
前端 API 封装:新建 `用户端/iapip-web/src/services/api/forum.ts`,聚合进 `src/services/api/index.ts``api.forum.*`)。
新增两个**公开路由**(无需登录即可访问,路由风格参照 landing 在 `.umirc.ts` 中的注册方式):
### 7.1 `/forum` — 论坛首页
- 板块 Tab含「全部」点击切换 `board` 过滤。
- 帖子列表:标题、作者名、时间、回帖数、点赞数;点击行 → `/forum/thread/:id`
- 分页控件Antd `Pagination`)。
- 右上「发帖」按钮:未登录 → 弹 `PhoneAuthModal`;已登录 → 打开发帖表单Modal板块选择 + 标题 + 正文)。发帖成功后刷新列表。
### 7.2 `/forum/thread/:id` — 帖子详情
- 帖子正文(作者、时间、板块)。
- 点赞按钮:显示 `like_count` 与当前用户 `liked` 状态;未登录点则弹登录框,登录后 toggle。
- 楼层回帖列表floor + 作者 + 时间 + 内容),分页。
- 底部回帖框:未登录点则弹登录框;已登录提交后刷新回帖列表并楼层递增。
- 若为作者本人,帖子/自己的回帖显示「删除」入口(软删)。
### 7.3 登录复用
- 统一复用现有 `PhoneAuthModal``src/components/phone-auth-modal`)。登录态判断沿用 `localStorage[LOC_STOR_KEY.AUTH_TOKEN]`
- 已登录请求自动带 `Authorization`(沿用 `services/api` 现有请求拦截)。
---
## 8. 测试
### 8.1 后端
- 发帖 → 该板块帖数 +1非法 board 被拒1000
- 连续回帖 → floor 依次 1、2、3`reply_count` 同步未登录回帖被拒401/2000
- 点赞 toggle首次 +1 且 `liked=true`,再次 -1 且 `liked=false`;同一用户不重复计数。
- 越权删除他人帖 → 403删除自己的 → `deleted=1`,列表不再出现。
- 分页:`page/size` 边界正确,返回总数一致。
### 8.2 环保建材
- 有 E0 材料时返回 ≤8 条且字段精简;无任何公共材料时返回 `[]`,前端 `#eco` 区块隐藏、不报错。
### 8.3 前端
- 未登录浏览 `/forum` 与详情正常;触发发帖/回帖/点赞时弹登录框,登录后动作继续完成。
- 导航 4 项滚动/跳转正确;`#news`/`#cases` 改名后内容不变。
---
## 9. 涉及文件清单
### 后端 `服务端/iapip-svr`
- `prisma/schema.prisma`(新增 3 表)
- `src/common/constants.ts`(新增 `FORUM_BOARDS`
- `src/controllers/forum.ts`(新建)
- `src/controllers/material.ts`(新增 `GET /eco`
- `src/index.ts`(注册 `forumRoutes`
### 前端 `用户端/iapip-web`
- `src/pages/landing/index.tsx`(导航 4 项、区块改名、新增 `#eco` 区块及取数)
- `src/pages/forum/index.tsx`(新建,论坛首页)
- `src/pages/forum/thread.tsx`(新建,帖子详情)
- `src/services/api/forum.ts`(新建)
- `src/services/api/material.ts`(新增 `getEcoMaterials`
- `src/services/api/index.ts`(聚合 `api.forum`
- `.umirc.ts`(新增 `/forum`、`/forum/thread/:id` 公开路由)
---
## 10. 风险与注意
- **数据库迁移**:改 schema 需 `npm run dbpush:dev`;生产库需谨慎(本地开发用 Docker MySQL
- **landing 首个真实接口**:环保建材使 landing 依赖后端,需保证后端未起/空数据时页面不崩。
- **楼层稳定性**:回帖软删后不回退 floor避免楼层错位。
- **登录态一致性**:论坛与现有测算入口共用手机登录 token行为需一致。