项目:react-travel | 版本:v1.0 | 生成日期:2026-10-05 | 依据:源码分析
react-travel(小叶旅游网) 是一个面向 C 端休闲旅行者的全栈在线旅游(OTA)展示与预订站点。它提供旅游线路的浏览、搜索、筛选、收藏、评论、加购物车、下单与模拟支付等完整闭环能力,覆盖国内(三亚、云南、北京、成都、西安等)与出境(日本、泰国、马尔代夫、法国等)18 个热门目的地的精品线路。
项目的目标用户可以分为两类:
client/ 兼容层)与迁移后的新页面及后端(app/),因此文档需要特别讲清双层结构与边界,这也是本套文档的核心使命之一。从工程角度看,该项目有四个鲜明特征:
src/ 整体平移为 client/,通过 lib/router.tsx(react-router-dom → next/navigation 的同名 API 垫片)实现旧组件"零改动"运行在 App Router 下。lib/seo.ts、app/sitemap.ts、app/robots.ts、app/opengraph-image.tsx)。lib/rate-limit.ts)、HTML 净化(lib/sanitize.ts)、PostgreSQL advisory lock 防并发加购、价格快照防订单金额漂移等,均为源码中真实存在的工程细节。下表按用户旅程归纳站点功能,并标注承载页面(app/ 层路由)与关键实现文件:
| 功能域 | 功能点 | 页面路由(app/) | 关键实现 |
|---|---|---|---|
| 内容浏览 | 首页推荐(爆款/新品/国内游三个集合) | / | RSC + ISR 60s(app/page.tsx)+ client/views/home/HomePage.tsx |
| 内容浏览 | 线路详情(图廊/价格/特色/费用/须知/评价) | /detail/[touristRouteId] | RSC 动态渲染(app/detail/[touristRouteId]/page.tsx) |
| 内容浏览 | 关键词搜索 + 价格筛选 + 排序 + 分页 | /search/[keywords] | RSC 首屏 + 客户端接管(app/search/[keywords]/page.tsx) |
| 内容浏览 | 目的地攻略(国内/出境筛选) | /destinations | RSC + ISR 3600s(app/destinations/page.tsx) |
| 内容浏览 | 旅游专题(主题线路聚合) | /topics | RSC + ISR 3600s(app/topics/page.tsx) |
| 内容浏览 | 用户游记(图片预览/评分) | /reviews | RSC + ISR 3600s(app/reviews/page.tsx) |
| 内容浏览 | 关于我们 / 帮助中心(FAQ 手风琴) | /about、/help | 纯客户端静态页(client/views/about、client/views/help) |
| 搜索辅助 | 热词联想(300ms 防抖)+ 热门关键词 | Header / 首页搜索框 | client/hooks/useHotKeywords.ts + app/api/commons/hotKeywords/route.ts |
| 账号体系 | 注册 / 登录 / 退出 / 资料编辑 | /register、/signin、/personal | app/api/auth/*、app/api/user/*、client/stores/useUserStore.ts |
| 互动 | 收藏(乐观更新)/ 评论与回复 / 评论点赞 / 浏览历史 | 详情页、/favorites、/personal | app/api/favorites、app/api/comments、app/api/commentLikes、app/api/viewHistory |
| 交易 | 加入购物车 / 结算 / 模拟支付 / 订单管理 | /shoppingCart、/placeOrder、/orderSuccess、/orders、/orders/[orderId] | app/api/shoppingCart/*、app/api/orders/*、client/stores/useCartStore.ts、useOrderStore.ts |
| 站点治理 | SEO 元信息 / sitemap / robots / OG 分享图 / JSON-LD | 全站 | lib/seo.ts、app/sitemap.ts、app/robots.ts、app/opengraph-image.tsx |
| 站点治理 | 访问分析 | 全站 | 根布局内嵌 Umami 脚本(app/layout.tsx) |
所有受保护功能(购物车、下单、收藏、评论写操作、个人资料)的登录判定由三层协同完成:middleware.ts 做"是否携带凭证"的粗过滤,各 Route Handler 内 requireAuth()(lib/auth.ts)做真正的 JWT 校验,客户端 AuthGuard(app/_components/AuthGuard.tsx)做受保护页面的跳转兜底。
| 类别 | 技术 | 版本(package.json) | 选型理由(源码佐证) |
|---|---|---|---|
| 框架 | Next.js(App Router) | ^15.1.0 | 迁移目标:SSR/RSC 提升 SEO 与首屏;next.config.js 配置 output: "standalone" 支持容器独立部署 |
| UI 运行时 | React | ^18.3.1 | 与 Next 15 兼容的最低稳定线;RSC 与客户端组件混合渲染 |
| 组件库 | antd | ^5.24.2 | 原 CRA 时代即用 antd v5,迁移零成本;配套 @ant-design/icons ^5.6.1 |
| antd SSR | @ant-design/nextjs-registry | ^1.0.2 | antd v5 为 CSS-in-JS,需 AntdRegistry 在服务端抽取样式,避免首屏闪烁(app/layout.tsx) |
| 客户端状态 | Zustand | ^5.0.14 | 迁移中由 Redux Toolkit 简化而来;7 个轻量 store(client/stores/),localStorage 手工水合规避 SSR 报错 |
| ORM | Prisma | ^7.9.0 | 新版 prisma-client 生成器,产物输出到 generated/prisma;配合 @prisma/adapter-pg 驱动适配器直连 PostgreSQL |
| 数据库 | PostgreSQL | — | 原 Egg.js 用 MySQL(egg_house 库),迁移决策切换 PG(见 NEXT_MIGRATION_PLAN.md §0);prisma.config.ts + DATABASE_URL |
| HTTP 客户端 | axios | ^1.8.1 | 沿用旧前端习惯;client/lib/http.ts 安装全局拦截器自动附带 Authorization: Bearer |
| 鉴权 | jsonwebtoken + ioredis | ^9.0.2 / ^5.4.2 | JWT 签发校验;Redis 缓存 token 实现服务端注销(可选,无 Redis 时优雅降级,lib/auth.ts) |
| 密码 | bcryptjs + Node crypto md5 | ^3.0.3 | 新账户 bcrypt(12 轮),存量账户保留 md5(password+salt) 兼容,lib/password.ts 渐进迁移 |
| 内容安全 | sanitize-html | ^2.17.7 | 富文本白名单净化(详情页 features/fees/notes 走 dangerouslySetInnerHTML),纯文本字段剥标签(lib/sanitize.ts) |
| 时间 | dayjs | ^1.11.21 | antd v5 官方配套日期库,app/providers.tsx 中设置中文 locale |
| 运行环境 | Node.js ≥ 20 / pnpm ≥ 9 | engines 约束 | package.json#engines 显式声明 |
补充说明两点工程性配置的意图:next.config.js 中 images.disableStaticImages: true + webpack asset/resource 规则,是为了让 import img from "x.png" 依旧返回 URL 字符串(对齐 CRA 语义),旧组件里的 <img src={logo}> 无需改动;rewrites() 把 /auth/:path* 映射到 /api/auth/:path*,是因为旧前端 slice 的登录/注册写死了 /auth/login、/auth/register 路径,保留 rewrite 可让 client/ 层零改动。
以下是全项目目录树(省略 node_modules、.next、public/travel-img 图片资源等生成物/素材),重点标注 client/(兼容层)与 app/(新架构层)的边界:
react-travel/
├─ middleware.ts # Edge 中间件:/api/* 受保护前缀的凭证粗过滤(不验 JWT)
├─ next.config.js # standalone 输出 / 静态图片 CRA 语义 / /auth/* rewrite
├─ tsconfig.json # @/* 别名;exclude generated/
├─ prisma.config.ts # Prisma 7 配置(schema 路径 + DATABASE_URL)
├─ .env.example # DATABASE_URL / JWT_SECRET / SALT / REDIS_URL / CITYS_API
│
├─ app/ # ★ 新架构层:App Router 页面 + 后端 Route Handlers
│ ├─ layout.tsx # 根布局:AntdRegistry + Providers + 全站 metadata + JSON-LD + Umami
│ ├─ providers.tsx # 客户端 Provider:antd 中文主题、localStorage 水合、axios 拦截器、SessionLoader
│ ├─ page.tsx # / 首页(RSC,ISR 60s,服务端直取推荐集合)
│ ├─ detail/[touristRouteId]/ # 详情页(RSC 动态 + generateMetadata + loading.tsx)
│ ├─ search/[keywords]/ # 搜索页(RSC 首屏 + loading.tsx)
│ ├─ destinations|topics|reviews/ # 内容页(RSC,ISR 3600s)
│ ├─ signin|register|about|help/ # 客户端页面(signin/register 有独立 layout.tsx 提供标题)
│ ├─ shoppingCart|placeOrder|orders|orders/[orderId]|personal|favorites/ # 受保护页(AuthGuard 包裹)
│ ├─ orderSuccess/page.tsx # 支付成功页(读 useOrderStore.currentOrder,非 AuthGuard 而是状态校验)
│ ├─ _components/AuthGuard.tsx# 登录守卫组件(hydrated 后无 token → replace /signin)
│ ├─ error.tsx / global-error.tsx / not-found.tsx # 错误与 404 边界
│ ├─ robots.ts / sitemap.ts / opengraph-image.tsx # SEO 基础设施
│ └─ api/ # ★ 后端 API:28 个 route.ts(详见 03-接口设计)
│ ├─ auth/{login,register,logout}/route.ts
│ ├─ user/{detail,edit}/route.ts
│ ├─ touristRoutes/route.ts + [id]/route.ts
│ ├─ shoppingCart/{route.ts, items/route.ts, items/[ids]/route.ts, checkout/route.ts}
│ ├─ orders/{route.ts, [id]/route.ts, [id]/placeOrder/route.ts, pay/route.ts, has/route.ts}
│ ├─ comments/{route.ts, [houseId]/route.ts} + commentLikes/route.ts
│ ├─ favorites/{route.ts, ids/route.ts} + viewHistory/route.ts
│ ├─ productCollections|destinations|topics|reviews/route.ts
│ └─ commons/{citys,hotKeywords}/route.ts
│
├─ client/ # ★ 兼容层:原 CRA src/ 平移(视图/组件/store/样式),被 app/ 页面引用
│ ├─ views/ # 页面级组件(HomePage、DetailPage、SearchPage、OrdersPage 等 16 个)
│ ├─ components/ # 通用组件(Header、Footer、ProductCollection、TourCard、PaymentCard 等 14 个)
│ ├─ layouts/ # mainLayout(Header+Footer)/ userLayout(登录注册品牌卡)
│ ├─ stores/ # 7 个 Zustand store(user/cart/order/productDetail/productSearch/recommend/favorite)
│ ├─ lib/ # http.ts(axios 拦截器)、slugMap.ts(slug↔中文关键词映射)
│ ├─ hooks/useHotKeywords.ts # 搜索联想 hook(Header 与 HomePage 共用)
│ ├─ styles/tokens.css # 设计令牌(品牌青 #0e7490 等),先于 index.css 加载
│ ├─ index.css / App.module.css / assets/images/ # 全局样式与静态图片(CRA 语义 import)
│
├─ lib/ # ★ 服务端共享层:Route Handler 与 RSC 共用
│ ├─ prisma.ts # PrismaClient 单例(PrismaPg 适配器 + dev query 日志)
│ ├─ auth.ts # JWT 签发/校验 + Redis token 缓存 + requireAuth 守卫
│ ├─ password.ts # bcrypt 新账 / md5 存量兼容 + 时序对齐 DUMMY_BCRYPT_HASH
│ ├─ utils.ts # md5Password/unPick/serialize(BigInt→Number)/json/errMsg/apiError/parsePage
│ ├─ vm.ts # 视图模型:House→touristRoute、Orders→购物车项/结账单
│ ├─ sanitize.ts # sanitizeRichText / stripHtml
│ ├─ rate-limit.ts # 内存固定窗口限流(写接口 20/min,登录注册 8/min)
│ ├─ seo.ts / order.ts # SEO 常量与 JSON-LD / 订单号生成(时间戳+随机 hex)
│ ├─ router.tsx # ★ react-router-dom 垫片:useNavigate/useParams/useLocation/Link/Navigate
│ └─ services/ # 数据访问服务(user/house/comment/orders/cart/favorite/viewHistory/
│ # destination/topic/review/productCollections,共 11 个文件)
│
├─ prisma/
│ ├─ schema.prisma # 11 个模型(User/House/Imgs/Comment/Orders/Favorite/ViewHistory/
│ │ # CommentLike/Destination/Topic/TopicHouse/Review)
│ ├─ seed.ts # 种子脚本(清库→用户→线路→评论→目的地→专题→游记→收藏→浏览史→订单)
│ └─ seed-data/ # 32 条线路 / 8 个用户 / 18 个目的地 / 专题 / 游记样例
├─ generated/prisma/ # ★ Prisma 生成产物(prisma-client 生成器输出,勿手改,tsconfig 已 exclude)
├─ scripts/
│ ├─ migrate-mysql-to-pg.ts # 存量 MySQL(egg_house) → PostgreSQL 迁移脚本(B6)
│ └─ download-images.mts # 从 seed-data 提取 Unsplash 图片 id 下载到 public/travel-img/
├─ types/assets.d.ts # 静态图片模块声明(对齐 CRA 的 URL 字符串语义)
└─ public/ # favicon/logo192/travel-img(约 80 张 webp 线路图)双层边界约定(新代码放哪一层):
app/ 层负责三件事:文件系统路由与页面外壳(RSC 数据直取 + metadata)、app/api/** 后端接口、跨页面客户端守卫(_components/AuthGuard.tsx)与全局 Provider。新页面应优先落在这一层,数据获取写 RSC(await 调 lib/services/*)。client/ 层是"可渲染的 UI 组件库 + 状态仓":app/ 的 page.tsx 只做数据获取,然后把数据以 props 注入 client/views/** 的客户端组件;写操作与交互状态由 client/stores/** 承担。该层禁止直接访问数据库与 Prisma。lib/ 层是唯一允许触碰 Prisma 的地方(RSC 与 Route Handler 共用 lib/services/*),保证"同一份业务逻辑,RSC 直取与 API 回退两条链路数据一致"。app/ RSC + client/ 展示组件;纯交互页(购物车/订单/个人中心)→ app/ 薄壳 + AuthGuard + client/ 视图;任何数据访问 → lib/services/。环境要求:Node.js ≥ 20、pnpm ≥ 9、PostgreSQL(默认 5432)、Redis(可选,登录注销增强)。
# 1. 安装依赖(postinstall 自动执行 prisma generate,产出 generated/prisma)
pnpm install
# 2. 配置环境变量
cp .env.example .env
# DATABASE_URL=postgresql://user:pass@localhost:5432/travel?schema=public
# JWT_SECRET=(开发可用示例值;生产必须 openssl rand -hex 32)
# SALT=(同上,openssl rand -hex 16)
# REDIS_URL=redis://localhost:6379(可留空)
# 3. 建表 + 灌入种子数据(32 条线路、8 个 demo 账号等)
pnpm prisma:push
pnpm db:seed
# 4. 启动开发服务器 → http://localhost:3000
pnpm dev
# 生产构建与运行(产出 .next/standalone,可用 node .next/standalone/server.js 独立部署)
pnpm build && pnpm start种子账号密码统一为 123456(见 prisma/seed-data/users.ts 的 DEMO_PASSWORD),如"旅行达人小王""摄影师阿杰"等 8 个。验证后端可直连:GET /api/productCollections 返回三个推荐集合即说明 DB 链路正常;POST /auth/login(经 rewrite 到 /api/auth/login)返回 token 即说明鉴权链路正常。
开发期质量门禁:项目未配置单元测试框架(package.json 无 test 相关依赖),类型检查依赖 tsconfig.json 的 strict: true,实际门禁是 pnpm exec tsc --noEmit 零错误 + pnpm build(Next 构建内含类型检查)全绿——这也是迁移文档(NEXT_MIGRATION_PLAN.md)中每个 Phase 的验收标准。
后续六篇文档的推荐阅读顺序:01-系统架构(理解双层结构与一次请求的旅程)→ 04-数据模型(11 张表与业务对象映射)→ 03-接口设计(28 个 route.ts、39 个 Handler 契约)→ 02-页面设计(路由总表、组件树与状态管理)→ 05-核心流程(五个端到端时序)→ 06-部署运行(环境变量、构建与部署)。若你只关心"改某个页面该动哪个文件",可直接翻 02 的路由总表与 01 的边界约定一节。