diff --git a/PLAN.md b/PLAN.md index c688b03..79ca3a4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,5 +1,7 @@ # 聚玩 — 产品设计计划 +> 本文描述产品设计意图,**不代表已实现的功能**。资金托管/释放、争议仲裁、自动派单、浏览器推送等条目目前仍属于规划方向,前端可能有占位 UI 但后端尚未提供完整实现。 + ## 一、用户系统 一个账号,三种身份:消费者、打手、店主。每个身份有独立的主页,身份切换是全局的(切换后整个应用的视角和导航都随之改变)。 diff --git a/README.md b/README.md index b6fd8c3..fe5c90c 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ ### 前置要求 -- Node.js 20+ -- pnpm 9+ +- Node.js 25+ +- pnpm 10+ ### 安装依赖 @@ -140,19 +140,9 @@ async rewrites() { ## 部署 -### 构建 Docker 镜像 +本仓库作为后端仓库(`juwan-backend`)的子模块接入 dev compose,通过其中的 Envoy 网关同源对外提供。 -```bash -docker build -t juwan-frontend:latest . -docker tag juwan-frontend:latest 103.236.53.208:4418/library/juwan-frontend:latest -docker push 103.236.53.208:4418/library/juwan-frontend:latest -``` - -### 部署到 Kubernetes - -```bash -kubectl apply -f deploy/k8s/frontend.yaml -``` +`Dockerfile` 使用 Next.js standalone 输出。后端 `deploy/dev/build.py` 会扫描出 `frontend` target 一并构建为 `juwan/frontend:dev`,随 compose 拉起,浏览器从 `http://localhost:18080` 访问。 ## 开发规范 diff --git a/接口文档.md b/接口文档.md deleted file mode 100644 index 48056d0..0000000 --- a/接口文档.md +++ /dev/null @@ -1,766 +0,0 @@ -# 聚玩 (Juwan) API 设计文档 - -## 1. 文档概述 -本文档为“聚玩”游戏陪玩平台的后端 API 设计规范,用于前后端联调与上线部署。 -- **基础路径 (Base URL)**: `/api/v1` -- **数据格式**: `application/json` - -## 2. 全局约定 - -### 2.1 认证与授权 (Auth) - -- 采用 Cookie `JToken` 进行认证(Phase 2 接入后端时实现)。 -- 当前前端仍以本地数据实现为主,不涉及真实后端交互;接入后端后会删除本地数据实现。 -- 接口权限标识: - - 🔒:需要登录认证。 - - 🛡️:需要管理员权限。 - -### 2.2 错误响应格式 - -所有失败的请求均返回统一的 JSON 结构,并附带 HTTP 状态码(如 400, 401, 403, 404, 500)。 - -```json -{ - "code": 401, - "msg": "请先登录" -} -``` - -**常见错误码 (code)**: -- `401`: 未登录或 Token 失效 -- `403`: 角色权限不足或非参与者 -- `404`: 资源不存在 -- `400`: 参数校验失败、状态不合法、重复请求等 -- `500`: 服务器内部错误 - -### 2.3 分页规范 -采用 `offset` 和 `limit` 进行分页(与前端 `SearchResponse` 保持一致)。 -**请求参数**:`?offset=0&limit=20` -**响应格式**: -```json -{ - "items": [...], - "meta": { - "total": 100, - "offset": 0, - "limit": 20 - } -} -``` - -### 2.4 文件上传与访问 - -- **上传端点**: `POST /api/v1/upload` -- **Content-Type**: `multipart/form-data` -- **参数**: `file` (文件), `type` (枚举: `avatar`, `chat`, `post`, `verification`, `dispute`) -- **响应**: 返回文件的 CDN URL 或文件 ID。 -- **访问端点**: `GET /api/v1/files/:fileId` (获取文件内容) - -### 2.5 时间戳格式 -所有时间字段均采用 ISO 8601 格式的 UTC 时间字符串,例如:`2024-03-20T12:00:00.000Z`。 - -### 2.6 角色系统 (UserRole) -- `consumer`: 普通消费者(老板) -- `player`: 陪玩打手 -- `owner`: 店铺店长 -- `admin`: 平台管理员(隐藏角色,用于后台管理) - -### 2.7 ID 规范 - -所有 ID 均由后端使用 Snowflake 算法生成,类型为 `int64`。在前端交互和 JSON 序列化时,统一作为 `string` 处理,以避免精度丢失。 - ---- - -## 3. 数据模型 - -### 3.1 User (用户) -| 字段 | 类型 | 说明 | -| ------------------ | ---------- | ---------------------------------------------------- | -| id | string | 用户唯一标识 | -| username | string | 登录用户名 | -| email | string | 邮箱地址 (可选) | -| nickname | string | 昵称 | -| avatar | string | 头像 URL | -| role | UserRole | 当前激活的角色 | -| verifiedRoles | UserRole[] | 已认证的角色列表 | -| verificationStatus | object | 各角色的认证状态 (`pending`, `approved`, `rejected`) | -| bio | string | 个人简介 (可选) | -| createdAt | string | 注册时间 | - -### 3.2 Game (游戏) -| 字段 | 类型 | 说明 | -| -------- | ------ | ----------------------------------- | -| id | string | 游戏 ID | -| name | string | 游戏名称 | -| icon | string | 游戏图标 URL | -| category | string | 分类 (`MOBA`, `FPS`, `动作`, `RPG`) | - -### 3.3 Player & PlayerService (打手与服务) -**PlayerService**: -| 字段 | 类型 | 说明 | -| ------------ | -------- | --------------------------- | -| id | string | 服务 ID | -| playerId | string | 打手 ID | -| gameId | string | 游戏 ID | -| gameName | string | 游戏名称 | -| title | string | 服务标题 | -| description | string | 服务描述 | -| price | number | 价格 | -| unit | string | 计价单位 (`局`, `星`, `次`) | -| rankRange | string | 段位范围 (可选) | -| availability | string[] | 可接单时间段 | - -**Player**: -| 字段 | 类型 | 说明 | -| -------------- | --------------- | ------------------------------------- | -| id | string | 打手 ID | -| user | User | 关联的用户信息 | -| rating | number | 综合评分 | -| totalOrders | number | 总接单数 | -| completionRate | number | 完单率 (0-1) | -| status | string | 状态 (`available`, `busy`, `offline`) | -| games | string[] | 擅长游戏 ID 列表 | -| services | PlayerService[] | 提供的服务列表 | -| shopId | string | 所属店铺 ID (可选) | -| shopName | string | 所属店铺名称 (可选) | -| tags | string[] | 个人标签 | - -### 3.4 Shop (店铺) -| 字段 | 类型 | 说明 | -| ---------------------- | -------- | -------------------------------- | -| id | string | 店铺 ID | -| owner | User | 店长信息 | -| name | string | 店铺名称 | -| banner | string | 店铺招牌图 (可选) | -| description | string | 店铺简介 | -| rating | number | 综合评分 | -| totalOrders | number | 总单量 | -| playerCount | number | 打手数量 | -| commissionType | string | 抽成方式 (`fixed`, `percentage`) | -| commissionValue | number | 抽成数值 | -| allowMultiShop | boolean | 是否允许打手兼职 | -| allowIndependentOrders | boolean | 是否允许打手私下接单 | -| dispatchMode | string | 派单模式 (`manual`, `auto`) | -| announcements | string[] | 店铺公告列表 | -| templateConfig | object | 店铺主页模板配置 | - -### 3.5 Order (订单) -| 字段 | 类型 | 说明 | -| ------------ | ------------- | ------------------- | -| id | string | 订单 ID | -| consumerId | string | 消费者 ID | -| consumerName | string | 消费者昵称 | -| playerId | string | 打手 ID | -| playerName | string | 打手昵称 | -| shopId | string | 店铺 ID (可选) | -| shopName | string | 店铺名称 (可选) | -| service | PlayerService | 购买的服务快照 | -| status | OrderStatus | 订单状态 | -| totalPrice | number | 订单总价 | -| note | string | 备注 (可选) | -| createdAt | string | 创建时间 | -| acceptedAt | string | 接单时间 (可选) | -| closedAt | string | 申请结算时间 (可选) | -| completedAt | string | 完成时间 (可选) | - -**OrderStatus 枚举**: `pending_payment`, `pending_accept`, `in_progress`, `pending_close`, `pending_review`, `disputed`, `completed`, `cancelled` - -### 3.6 Review (评价) -| 字段 | 类型 | 说明 | -| -------------- | ------- | -------------------------------------- | -| id | string | 评价 ID | -| orderId | string | 关联订单 ID | -| fromUserId | string | 评价人 ID | -| fromUserName | string | 评价人昵称 | -| fromUserAvatar | string | 评价人头像 | -| toUserId | string | 被评价人 ID | -| rating | number | 评分 (1-5) | -| content | string | 评价内容 (可选) | -| sealed | boolean | 是否处于密封状态(双方未互评前不可见) | -| createdAt | string | 评价时间 | - -### 3.7 Dispute & DisputeRecord (争议) -| 字段 | 类型 | 说明 | -| ------------------ | -------- | ---------------------------------------------------------- | -| id | string | 争议 ID | -| orderId | string | 关联订单 ID | -| initiatorId | string | 发起人 ID | -| initiatorName | string | 发起人昵称 | -| reason | string | 争议原因 | -| evidence | string[] | 证据图片 URL 列表 | -| status | string | 状态 (`open`, `reviewing`, `resolved`, `appealed`) | -| result | string | 仲裁结果 (`full_refund`, `full_payment`, `partial_refund`) | -| respondentReason | string | 被诉方回应理由 (可选) | -| respondentEvidence | string[] | 被诉方证据 (可选) | -| appealReason | string | 申诉理由 (可选) | -| appealedAt | string | 申诉时间 (可选) | -| timeline | object[] | 争议处理时间线 | -| createdAt | string | 创建时间 | - -### 3.8 ChatSession & ChatMessage (聊天) -**ChatSession**: -| 字段 | 类型 | 说明 | -| ------------- | -------- | ---------------------------------- | -| id | string | 会话 ID | -| type | string | 会话类型 (`order`, `consultation`) | -| orderId | string | 关联订单 ID (仅 order 类型) | -| participants | object[] | 参与者列表 `{id, name, avatar}` | -| lastMessage | string | 最后一条消息内容 | -| lastMessageAt | string | 最后一条消息时间 | -| unreadCount | number | 未读消息数 | -| readonly | boolean | 是否只读(如订单完成/咨询超时后) | - -**ChatMessage**: -| 字段 | 类型 | 说明 | -| ------------ | ------ | ------------------------------------ | -| id | string | 消息 ID | -| sessionId | string | 会话 ID | -| senderId | string | 发送者 ID | -| senderName | string | 发送者昵称 | -| senderAvatar | string | 发送者头像 | -| type | string | 消息类型 (`text`, `image`, `system`) | -| content | string | 消息内容 | -| createdAt | string | 发送时间 | - -### 3.9 Post & Comment (社区) -**Post**: -| 字段 | 类型 | 说明 | -| ------------- | -------- | ---------------------- | -| id | string | 帖子 ID | -| author | User | 作者信息 | -| authorRole | UserRole | 作者发帖时的角色 | -| title | string | 标题 | -| content | string | 内容 | -| images | string[] | 图片列表 | -| tags | string[] | 标签列表 | -| linkedOrderId | string | 关联的订单 ID (秀单帖) | -| quotedPostId | string | 引用的帖子 ID (引用帖) | -| likeCount | number | 点赞数 | -| commentCount | number | 评论数 | -| liked | boolean | 当前用户是否已赞 | -| pinned | boolean | 是否被作者置顶 | -| createdAt | string | 发布时间 | - -**Comment**: -| 字段 | 类型 | 说明 | -| --------- | ------- | ---------------- | -| id | string | 评论 ID | -| postId | string | 关联帖子 ID | -| author | User | 评论者信息 | -| content | string | 评论内容 | -| likeCount | number | 点赞数 | -| liked | boolean | 当前用户是否已赞 | -| createdAt | string | 评论时间 | - -### 3.10 WalletTransaction (钱包流水) -| 字段 | 类型 | 说明 | -| ----------- | ------ | ----------------------------------------------------------- | -| id | string | 流水 ID | -| type | string | 类型 (`topup`, `payment`, `income`, `withdrawal`, `refund`) | -| amount | number | 金额 (正数为收入/充值/退款,负数为支出/提现) | -| description | string | 描述 | -| orderId | string | 关联订单 ID (可选,用于结构化统计,替代前端正则匹配) | -| createdAt | string | 发生时间 | - -### 3.11 Favorite (收藏) -| 字段 | 类型 | 说明 | -| ---------- | ------ | ------------------------------- | -| id | string | 收藏 ID | -| userId | string | 用户 ID | -| targetType | string | 收藏目标类型 (`player`, `shop`) | -| targetId | string | 收藏目标 ID | -| createdAt | string | 收藏时间 | - -### 3.12 Notification (通知) -| 字段 | 类型 | 说明 | -| --------- | ------- | ----------------------------------------- | -| id | string | 通知 ID | -| type | string | 通知类型 (`order`, `community`, `system`) | -| title | string | 通知标题 | -| content | string | 通知内容 | -| read | boolean | 是否已读 | -| link | string | 关联跳转链接 (可选) | -| createdAt | string | 通知时间 | ---- - -## 4. 认证与用户 - -| 方法 | 路径 | 权限 | 说明 | -| ------ | ------------------------------------- | ---- | --------------------------------- | -| POST | `/auth/register` | | 用户注册 (username + email + password + vcode) | -| POST | `/auth/login` | | 用户登录 (username + password) | -| POST | `/auth/logout` | 🔒 | 退出登录,清除 Cookie | -| POST | `/auth/forgot-password/send` | | 忘记密码(发送验证码到邮箱) | -| POST | `/auth/reset-password` | | 重置密码 (email + vcode + newPassword) | -| POST | `/email/verification-code/send` | | 发送邮箱验证码 | -| GET | `/users/me` | 🔒 | 获取当前登录用户信息 | -| PUT | `/users/me` | 🔒 | 更新个人资料 (昵称, 头像, 简介等) | -| POST | `/users/me/switch-role` | 🔒 | 切换当前激活角色 | -| POST | `/users/me/verification` | 🔒 | 提交角色认证材料 | -| GET | `/users/me/verification` | 🔒 | 获取认证状态 | -| PUT | `/users/me/preferences/notifications` | 🔒 | 更新通知偏好设置 | -| PUT | `/users/me/preferences/theme` | 🔒 | 更新主题偏好设置 | -| GET | `/users/:id` | | 获取指定用户信息 | -| POST | `/users/:id/follow` | 🔒 | 关注用户 | -| DELETE | `/users/:id/follow` | 🔒 | 取消关注用户 | - -**请求示例:登录** - -```json -// POST /api/v1/auth/login -{ - "username": "zhangsan", - "password": "..." -} -``` - -**响应示例:登录成功** - -```json -{ - "user": { - "id": "u1", - "username": "zhangsan", - "email": "zhangsan@example.com", - "nickname": "张三", - "avatar": "https://cdn.juwan.com/avatars/u1.jpg", - "role": "consumer", - "verifiedRoles": ["consumer", "player"], - "verificationStatus": { "consumer": "approved", "player": "approved" }, - "createdAt": "2024-01-15T08:00:00.000Z" - } -} -``` - -**请求示例:提交认证材料** -```json -// POST /api/v1/users/me/verification -{ - "role": "player", - "materials": { - "idCardFront": "https://cdn.juwan.com/uploads/xxx.jpg", - "idCardBack": "https://cdn.juwan.com/uploads/yyy.jpg", - "gameScreenshot": "https://cdn.juwan.com/uploads/zzz.jpg" - } -} -``` - -**请求示例:切换角色** -```json -// POST /api/v1/users/me/switch-role -{ "role": "player" } -``` - -**请求示例:更新通知偏好** -```json -// PUT /api/v1/users/me/preferences/notifications -{ "order": true, "community": true, "system": false } -``` ---- - -## 5. 游戏数据 - -| 方法 | 路径 | 权限 | 说明 | -| ---- | ------------ | ---- | ----------------------- | -| GET | `/games` | | 获取游戏列表 (支持分页) | -| GET | `/games/:id` | | 获取指定游戏详情 | - ---- - -## 6. 打手与服务 - -| 方法 | 路径 | 权限 | 说明 | -| ------ | ----------------------- | ---- | ------------------------------------------------- | -| GET | `/players` | | 获取打手列表 (支持分页、筛选) | -| GET | `/players/:id` | | 获取打手公开主页详情 | -| PUT | `/players/me/status` | 🔒 | 更新打手接单状态 (`available`, `busy`, `offline`) | -| GET | `/services` | | 获取所有服务列表 | -| GET | `/services/:id` | | 获取服务详情 | -| GET | `/players/:id/services` | | 获取指定打手的服务列表 | -| POST | `/services` | 🔒 | 创建服务 (仅 player) | -| PUT | `/services/:id` | 🔒 | 更新服务 (仅 player) | -| DELETE | `/services/:id` | 🔒 | 删除服务 (仅 player) | - ---- - -## 7. 店铺系统 - -| 方法 | 路径 | 权限 | 说明 | -| ------ | ------------------------------- | ---- | --------------------------- | -| GET | `/shops` | | 获取店铺列表 | -| GET | `/shops/:id` | | 获取店铺公开主页详情 | -| GET | `/users/:id/shop` | | 获取指定店长的店铺 | -| POST | `/shops` | 🔒 | 创建店铺 (仅 owner) | -| PUT | `/shops/:id` | 🔒 | 更新店铺基础信息及规则 | -| PUT | `/shops/:id/template` | 🔒 | 更新店铺主页模板配置 | -| PUT | `/shops/:id/announcements` | 🔒 | 更新店铺公告 | -| POST | `/shops/:id/invitations` | 🔒 | 邀请打手加入店铺 | -| POST | `/shops/invitations/:id/accept` | 🔒 | 打手接受店铺邀请 | -| DELETE | `/shops/:id/players/:playerId` | 🔒 | 将打手移出店铺 | -| GET | `/shops/:id/income-stats` | 🔒 | 获取店铺收入统计 (仅 owner) | - -| GET | `/shops/mine` | 🔒 | 获取当前登录店主的店铺详情 | -| GET | `/shops/:id/players` | | 获取店铺下的打手列表 | -| POST | `/shops/:id/announcements` | 🔒 | 新增店铺公告 | -| DELETE | `/shops/:id/announcements/:index` | 🔒 | 删除店铺公告 | -| DELETE | `/shops/invitations/:id` | 🔒 | 拒绝店铺邀请 (打手调用) | - -**请求示例:更新店铺规则** -```json -// PUT /api/v1/shops/:id -{ - "name": "星耀电竞工作室", - "description": "专业英雄联盟代练工作室", - "commissionType": "percentage", - "commissionValue": 15, - "allowMultiShop": false, - "allowIndependentOrders": true, - "dispatchMode": "auto" -} -``` -**请求示例:更新店铺模板** -```json -// PUT /api/v1/shops/:id/template -{ - "sections": [ - { "type": "banner", "enabled": true, "order": 0 }, - { "type": "intro", "enabled": true, "order": 1 }, - { "type": "services", "enabled": true, "order": 2 }, - { "type": "players", "enabled": true, "order": 3 }, - { "type": "announcements", "enabled": false, "order": 4 }, - { "type": "reviews", "enabled": true, "order": 5 } - ] -} -``` -**响应示例:店铺收入统计** -```json -// GET /api/v1/shops/:id/income-stats -{ - "monthlyIncome": 12800.00, - "pendingSettlement": 3200.00, - "totalWithdrawn": 54000.00, - "totalOrders": 156, - "completedOrders": 142 -} -``` ---- - -## 8. 订单系统 - -### 8.1 订单状态机 (Order State Machine) - -| 当前状态 | 触发动作 | 下一状态 | 允许的角色 | 副作用 | -| ----------------- | ----------------------------- | ---------------- | ---------------- | -------------------------------------------------- | -| `pending_payment` | `PAY` | `pending_accept` | consumer | CLEAR_TIMEOUT, SCHEDULE_TIMEOUT | -| `pending_accept` | `ACCEPT` | `in_progress` | player, owner | CLEAR_TIMEOUT, SYNC_CHAT_SESSION | -| `pending_accept` | `CANCEL_PRE_ACCEPT` | `cancelled` | consumer | CLEAR_TIMEOUT | -| `pending_accept` | `AUTO_TIMEOUT_PENDING_ACCEPT` | `cancelled` | system | CLEAR_TIMEOUT | -| `in_progress` | `REQUEST_CLOSE` | `pending_close` | consumer, player | CLEAR_TIMEOUT, SCHEDULE_TIMEOUT, SYNC_CHAT_SESSION | -| `in_progress` | `OPEN_DISPUTE` | `disputed` | consumer, player | CLEAR_TIMEOUT, SYNC_CHAT_SESSION | -| `pending_close` | `CONFIRM_CLOSE` | `pending_review` | consumer, player | CLEAR_TIMEOUT, SCHEDULE_TIMEOUT, SYNC_CHAT_SESSION | -| `pending_close` | `OPEN_DISPUTE` | `disputed` | consumer, player | CLEAR_TIMEOUT, SYNC_CHAT_SESSION | -| `pending_close` | `AUTO_TIMEOUT_PENDING_CLOSE` | `pending_review` | system | CLEAR_TIMEOUT, SCHEDULE_TIMEOUT, SYNC_CHAT_SESSION | -| `pending_review` | `SUBMIT_REVIEW` | `completed` | consumer, player | CLEAR_TIMEOUT, PAYOUT_INCOME, SYNC_CHAT_SESSION | -| `pending_review` | `AUTO_TIMEOUT_PENDING_REVIEW` | `completed` | system | CLEAR_TIMEOUT, PAYOUT_INCOME, SYNC_CHAT_SESSION | -| `disputed` | `RESOLVE_DISPUTE` | `pending_review` | owner, admin | CLEAR_TIMEOUT, SCHEDULE_TIMEOUT, SYNC_CHAT_SESSION | - -### 8.2 超时配置 (Timeout Configs) -后端需实现定时任务(如 Redis 延迟队列)来处理以下超时逻辑: -- `ORDER_ACCEPT_TIMEOUT_MS`: 待接单超时自动取消(默认 5 分钟)。 -- `ORDER_CLOSE_TIMEOUT_MS`: 申请结算后对方未确认,超时自动进入待评价(默认 24 小时)。 -- `ORDER_REVIEW_TIMEOUT_MS`: 待评价超时自动好评并完成订单(默认 72 小时)。 - -### 8.3 订单接口 - -| 方法 | 路径 | 权限 | 说明 | -| ---- | --------------------------- | ---- | ----------------------------------- | -| GET | `/orders` | 🔒 | 获取当前用户的订单列表 | -| GET | `/orders/:id` | 🔒 | 获取订单详情 | -| POST | `/orders` | 🔒 | 创建订单 (状态: `pending_payment`) | -| POST | `/orders/paid` | 🔒 | 创建并直接支付订单 (快捷下单) | -| POST | `/orders/:id/pay` | 🔒 | 支付订单 (`PAY`) | -| POST | `/orders/:id/accept` | 🔒 | 接单 (`ACCEPT`) | -| POST | `/orders/:id/request-close` | 🔒 | 申请结算 (`REQUEST_CLOSE`) | -| POST | `/orders/:id/confirm-close` | 🔒 | 确认结算 (`CONFIRM_CLOSE`) | -| POST | `/orders/:id/cancel` | 🔒 | 接单前取消 (`CANCEL_PRE_ACCEPT`) | -| POST | `/orders/:id/reorder` | 🔒 | 再来一单 (基于原订单快速创建新订单) | -`GET /orders` 支持查询参数:`role` (consumer/player/owner)、`status` (状态过滤)、`offset`、`limit`。后端根据 Token 中的 userId 和 role 参数自动过滤,不允许查看他人订单。 - -**请求示例:创建并支付订单 (快捷下单)** -```json -// POST /api/v1/orders/paid -{ - "playerId": "p1", - "shopId": "shop1", - "serviceId": "svc1", - "quantity": 3, - "note": "希望晚上8点后开始" -} -``` -**响应示例:订单创建成功** -```json -{ - "order": { - "id": "ord-20240320-001", - "consumerId": "u1", - "consumerName": "张三", - "playerId": "p1", - "playerName": "小明", - "shopId": "shop1", - "shopName": "星耀电竞", - "service": { "id": "svc1", "title": "英雄联盟代练", "price": 50, "unit": "局" }, - "status": "pending_accept", - "totalPrice": 150, - "createdAt": "2024-03-20T12:00:00.000Z" - } -} -``` -**请求示例:再来一单** -```json -// POST /api/v1/orders/:id/reorder -// 无请求体,后端基于原订单的 playerId、serviceId、shopId 创建新订单 -``` ---- - -## 9. 争议仲裁 - -| 方法 | 路径 | 权限 | 说明 | -| ---- | ------------------------ | ---- | ------------------------- | -| GET | `/disputes` | 🔒 | 获取当前用户的争议列表 | -| GET | `/orders/:id/dispute` | 🔒 | 获取指定订单的争议详情 | -| POST | `/orders/:id/dispute` | 🔒 | 发起争议 (`OPEN_DISPUTE`) | -| POST | `/disputes/:id/response` | 🔒 | 被诉方提交回应及证据 | -| POST | `/disputes/:id/appeal` | 🔒 | 对仲裁结果发起申诉 | - -**争议处理时间线 (Timeline)**: -- `created`: 争议发起 -- `response`: 被诉方回应 -- `reviewing`: 店长/平台介入审查 -- `resolved`: 给出仲裁结果 -- `appealed`: 发起申诉,转交平台管理员 -**请求示例:发起争议** -```json -// POST /api/v1/orders/:id/dispute -{ - "reason": "打手未按约定时间上线", - "evidence": [ - "https://cdn.juwan.com/uploads/evidence1.jpg", - "https://cdn.juwan.com/uploads/evidence2.jpg" - ] -} -``` -**请求示例:被诉方回应** -```json -// POST /api/v1/disputes/:id/response -{ - "reason": "当时网络故障,已与客户沟通并补时", - "evidence": ["https://cdn.juwan.com/uploads/response1.jpg"] -} -``` -**请求示例:申诉** -```json -// POST /api/v1/disputes/:id/appeal -{ - "reason": "仲裁结果不合理,请求平台复核" -} -``` - ---- - -## 10. 评价系统 - -**密封机制 (Sealed Mechanics)**: -评价提交后默认为 `sealed: true`。只有当双方都完成评价,或者评价超时(`ORDER_REVIEW_TIMEOUT_MS`)后,评价才会解封并公开显示。 - -| 方法 | 路径 | 权限 | 说明 | -| ---- | --------------------- | ---- | ------------------------------------ | -| POST | `/orders/:id/review` | 🔒 | 提交评价 (`SUBMIT_REVIEW`) | -| GET | `/reviews` | | 获取公开评价列表 | -| GET | `/orders/:id/reviews` | 🔒 | 获取指定订单的评价(受密封机制限制) | -| GET | `/users/:id/reviews` | | 获取指定用户收到的评价 | - ---- - -## 11. 聊天系统 - -### 11.1 会话类型 -- **`order`**: 订单会话。随订单创建而建立,订单完成后变为只读。 -- **`consultation`**: 咨询会话。用户在下单前与打手沟通。24小时无回复自动关闭。可升级为订单会话。 - -| 方法 | 路径 | 权限 | 说明 | -| ---- | ----------------------------- | ---- | ----------------------------------- | -| GET | `/chat/sessions` | 🔒 | 获取当前用户的会话列表 | -| GET | `/chat/sessions/:id` | 🔒 | 获取会话详情 | -| GET | `/chat/sessions/:id/messages` | 🔒 | 获取会话历史消息 (分页) | -| POST | `/chat/sessions/order` | 🔒 | 确保订单会话存在 (不存在则创建) | -| POST | `/chat/sessions/consultation` | 🔒 | 创建咨询会话 | -| POST | `/chat/sessions/:id/upgrade` | 🔒 | 将咨询会话升级为订单会话 | -| POST | `/chat/sessions/:id/messages` | 🔒 | 发送消息 (文本/图片) | -| GET | `/shops/:id/chat-sessions` | 🔒 | 店长查看员工的业务会话 (需员工同意) | - ---- - -## 12. 社区系统 - -| 方法 | 路径 | 权限 | 说明 | -| ------ | --------------------- | ---- | ------------------------------------- | -| GET | `/posts` | | 获取帖子列表 (支持分页、标签筛选) | -| GET | `/posts/:id` | | 获取帖子详情 | -| POST | `/posts` | 🔒 | 发布帖子 (支持普通帖、秀单帖、引用帖) | -| POST | `/posts/:id/like` | 🔒 | 点赞帖子 | -| DELETE | `/posts/:id/like` | 🔒 | 取消点赞帖子 | -| POST | `/posts/:id/pin` | 🔒 | 作者置顶帖子 (最多 N 条) | -| DELETE | `/posts/:id/pin` | 🔒 | 取消置顶 | -| GET | `/posts/:id/comments` | | 获取帖子评论列表 | -| POST | `/posts/:id/comments` | 🔒 | 发表评论 | -| POST | `/comments/:id/like` | 🔒 | 点赞评论 | -| DELETE | `/comments/:id/like` | 🔒 | 取消点赞评论 | -| GET | `/users/:id/posts` | | 获取指定用户的帖子列表 | -`GET /posts` 支持查询参数:`tags` (标签过滤)、`gameId` (游戏过滤)、`sortBy` (new/hot)、`offset`、`limit`。 -**请求示例:发布秀单帖** -```json -// POST /api/v1/posts -{ - "title": "超棒的代练体验", - "content": "从银到铂金,只用了三天!", - "images": ["https://cdn.juwan.com/uploads/post1.jpg"], - "tags": ["英雄联盟", "代练"], - "linkedOrderId": "ord-20240320-001" -} -``` - ---- - -## 13. 收藏与关注 - -| 方法 | 路径 | 权限 | 说明 | -| ------ | ---------------------------- | ---- | ---------------------------------- | -| GET | `/favorites` | 🔒 | 获取当前用户的收藏列表 (打手/店铺) | -| POST | `/favorites` | 🔒 | 添加收藏 | -| DELETE | `/favorites/:id` | 🔒 | 取消收藏 | -| GET | `/users/:id/favorites/check` | 🔒 | 检查是否已收藏指定目标 | -**请求示例:添加收藏** -```json -// POST /api/v1/favorites -{ "targetType": "player", "targetId": "p1" } -``` -**检查收藏状态** -``` -GET /api/v1/users/u1/favorites/check?targetType=player&targetId=p1 -→ { "favorited": true } -``` - ---- - -## 14. 搜索与发现 - -| 方法 | 路径 | 权限 | 说明 | -| ---- | --------- | ---- | ------------ | -| GET | `/search` | | 统一搜索接口 | - -**请求参数**: -- `q`: 搜索关键词 -- `selectedGames`: 游戏 ID 数组 -- `min`, `max`: 价格区间 -- `onlyOnline`: 是否仅看在线 (`true`/`false`) -- `minRating`: 最低评分 -- `sort`: 排序方式 (`composite`, `rating`, `orders`, `price_asc`, `price_desc`) -- `offset`, `limit`: 分页参数 -| GET | `/recommendations/home` | | 首页推荐信息流 (混合打手与店铺卡片) | -**响应示例:搜索结果** -```json -{ - "items": [ - { - "type": "player", - "player": { "id": "p1", "user": {...}, "rating": 4.8, "status": "available", ... }, - "minPrice": 30, - "unit": "局", - "rating": 4.8, - "orders": 256 - }, - { - "type": "shop", - "shop": { "id": "shop1", "name": "星耀电竞", ... }, - "minPrice": 25, - "unit": "局", - "rating": 4.6, - "orders": 1024, - "games": ["英雄联盟", "CS2"], - "hasAvailable": true - } - ], - "meta": { "total": 42, "offset": 0, "limit": 12 } -} -``` ---- - -## 15. 钱包与资金 - -### 15.1 收入计算公式 (Income Calculation) -订单完成后,系统根据店铺规则计算打手实际收入: -- **无店铺 (独立接单)**: `income = totalPrice` -- **比例抽成 (percentage)**: `income = totalPrice * (1 - commissionValue / 100)` -- **固定抽成 (fixed)**: `income = Math.max(0, totalPrice - commissionValue)` -剩余部分作为店铺收入(若有店铺)。 - -### 15.2 钱包接口 - -| 方法 | 路径 | 权限 | 说明 | -| ---- | ---------------------- | ---- | ------------------------------------- | -| GET | `/wallet/balance` | 🔒 | 获取当前余额 | -| GET | `/wallet/transactions` | 🔒 | 获取资金流水 (包含结构化的 `orderId`) | -| POST | `/wallet/topup` | 🔒 | 充值 | -| POST | `/wallet/withdraw` | 🔒 | 提现 | - ---- - -## 16. 通知系统 - -| 方法 | 路径 | 权限 | 说明 | -| ---- | ---------------------------------- | ---- | ------------------ | -| GET | `/notifications` | 🔒 | 获取通知列表 | -| PUT | `/notifications/:id/read` | 🔒 | 标记单条通知为已读 | -| PUT | `/notifications/read-all` | 🔒 | 标记所有通知为已读 | -| POST | `/notifications/push-subscription` | 🔒 | 订阅 Web Push 推送 | - ---- - -## 17. 管理后台 (Admin) - -| 方法 | 路径 | 权限 | 说明 | -| ---- | ---------------------------------- | ---- | -------------------------- | -| GET | `/admin/verifications` | 🛡️ | 获取待审核的认证申请 | -| POST | `/admin/verifications/:id/approve` | 🛡️ | 批准认证申请 | -| POST | `/admin/verifications/:id/reject` | 🛡️ | 拒绝认证申请 | -| GET | `/admin/disputes` | 🛡️ | 获取需要平台介入的争议列表 | -| POST | `/admin/disputes/:id/resolve` | 🛡️ | 平台管理员给出最终仲裁结果 | - ---- - -## 18. WebSocket 事件 - -客户端连接到 `wss://api.juwan.com/api/v1/ws`,通过 JWT 鉴权。 - -**下发事件类型**: -- `chat:message`: 收到新聊天消息。 -- `order:status_changed`: 订单状态变更(触发前端重新拉取订单详情)。 -- `notification:new`: 收到新系统通知。 -- `dispute:updated`: 争议状态或时间线更新。 -- `wallet:balance_changed`: 余额变动通知。 - ---- - -## 19. 安全与校验清单 - -1. **数据隔离 (Data Isolation)**: - - 用户只能查询自己的订单、钱包流水、私聊会话。 - - 店长只能查询本店铺的订单和员工数据。 -2. **并发控制 (Concurrency Control)**: - - 订单状态流转必须使用乐观锁或数据库事务,防止并发导致状态机错乱。 - - 钱包扣款必须保证原子性,防止超扣。 -3. **权限校验 (Authorization)**: - - 严格校验 `Actor` 的 `role`。例如,只有 `player` 才能创建服务,只有 `owner` 才能修改店铺规则。 - - 订单操作必须校验操作者是否为该订单的 `consumerId`、`playerId` 或关联的 `shopId` 的店长。 -4. **幂等性 (Idempotency)**: - - 支付、接单、结算等核心操作必须保证幂等性,重复请求返回 `IDEMPOTENT_NOOP`。 diff --git a/隐蔽未实现接口与逻辑清单.md b/隐蔽未实现接口与逻辑清单.md deleted file mode 100644 index b8b17e5..0000000 --- a/隐蔽未实现接口与逻辑清单.md +++ /dev/null @@ -1,344 +0,0 @@ -# 隐蔽未实现接口与逻辑清单 - -本报告聚焦于"看起来已实现、实际未接通或存在断层"的功能点。这些问题在 mock 演示阶段不易察觉,切换真实后端后会表现为数据丢失、页面空态、交互无效等事故。 - -与第一份报告(《静态模拟数据残留审计报告》)互补,本报告不再重复 mock 数据源、store 初始化、伪 API 层等已知问题。 - ---- - -## 一、登录态与持久化缺失 - -### 1.1 记住登录状态 — 纯展示控件 - -登录页渲染了"记住登录状态"复选框,但该控件未被表单注册,无任何读取或写入逻辑。 - -| 位置 | 内容 | -| ------------------------------ | -------------------------------------------------------------------------- | -| `app/(auth)/login/page.tsx:86` | `` — 未绑定 `register("remember")` 或 `useState` | - -### 1.2 登录态无持久化 — 刷新即丢 - -全仓库未发现 `localStorage`、`sessionStorage`、`cookie` 写入,也未使用 `zustand/middleware` 的 `persist`。`store/auth.ts` 的 `login()` 仅写内存态,页面刷新后回到未登录状态。 - -**影响**:切真实后端后,如果前端仍依赖 Zustand 内存态判断登录,刷新会导致用户被踢出。 - ---- - -## 二、身份与实体模型错位 - -### 2.1 当前用户与店铺/打手 ID 不匹配 — 后台大面积空态 - -登录固定为 `mockUsers[0]`(id=`u1`),但 mock 数据中的店铺 owner 是 `u10`/`u11`/`u12`,打手是 `u5`/`u6` 起。 - -| 位置 | 内容 | -| -------------------------------------- | ---------------------------------------------- | -| `lib/mock/users.ts:119` | `currentUser = mockUsers[0]` — 固定 `u1` | -| `lib/mock/shops.ts:7` 起 | 店铺 owner 为 `u10`、`u11`、`u12` | -| `lib/mock/players.ts:7` 起 | 打手 user 从 `u5` 开始 | -| `lib/domain/resolve-current-shop.ts:5` | `shops.find(shop => shop.owner.id === userId)` | - -**结果**:用户切换到店主身份后,`resolveOwnerShop` 始终返回 `null`,以下页面全部显示"当前账号没有可管理的店铺": - -- `app/(dashboard)/dashboard/shop/employees/page.tsx:67` -- `app/(dashboard)/dashboard/shop/rules/page.tsx:29` -- `app/(dashboard)/dashboard/shop/income/page.tsx:28` -- `app/(dashboard)/dashboard/shop/page.tsx` 同理 -- `app/(dashboard)/dashboard/shop/templates/page.tsx` 同理 -- `app/(dashboard)/dashboard/shop/orders/page.tsx` 同理 - -### 2.2 打手主页链接指向不存在的打手 - -导航栏在 player 身份下跳转 `/player/${user.id}`(`components/header.tsx:88`),但 `user.id` 是 `u1`,`mockPlayers` 中没有 id 为 `u1` 的打手,会触发 `notFound()`(`app/(main)/player/[id]/page.tsx:25`)。 - ---- - -## 三、用户动作无持久化 — 刷新即回退 - -以下交互在前端有即时反馈,但数据仅存在于 Zustand 内存态,刷新页面或换设备后全部丢失。 - -### 3.1 点赞 - -| 位置 | 行为 | -| --------------------------------------- | --------------------------------------------------------------------- | -| `components/post-like-button.tsx:21-32` | 调用 `togglePostLike` → `store/posts.ts:50-61` 本地翻转 `liked` 并 ±1 | - -### 3.2 评论 - -| 位置 | 行为 | -| ------------------------------------- | ------------------------------------------------ | -| `components/post-comments.tsx:29-52` | 调用 `addComment` → `store/comments.ts` 本地追加 | -| `components/post-comment-count.tsx:3` | 读 `useCommentStore` 本地计数 | - -### 3.3 收藏 - -| 位置 | 行为 | -| -------------------------------------- | ----------------------------------------------------- | -| `components/favorite-button.tsx:29-33` | 调用 `toggleFavorite` → `store/favorites.ts` 本地增删 | - -### 3.4 通知已读 - -| 位置 | 行为 | -| -------------------------------------- | ----------------------------------------------------------------------- | -| `app/(account)/notifications/page.tsx` | 调用 `markAsRead` / `markAllAsRead` → `store/notifications.ts` 本地标记 | -| `components/header.tsx:81-83` | 未读数通过 `.filter(n => !n.read).length` 本地计算 | - -### 3.5 设置保存 - -| 位置 | 行为 | -| --------------------------------- | ---------------------------------------------------- | -| `app/(account)/settings/page.tsx` | 昵称、简介、通知偏好等修改仅写 `useAuthStore` 内存态 | - ---- - -## 四、上传功能 — 占位或 Object URL - -所有"上传"操作要么是纯占位 UI,要么使用 `URL.createObjectURL` 生成本地临时链接,刷新后失效。 - -### 4.1 头像上传 - -| 位置 | 行为 | -| ------------------------------------ | ------------------------------------------------------------- | -| `app/(account)/settings/page.tsx:75` | `setAvatar(URL.createObjectURL(file))` — 本地预览,无上传请求 | - -### 4.2 聊天图片发送 - -| 位置 | 行为 | -| ------------------------------------ | --------------------------------------------------------------------------------- | -| `app/(order)/chat/[id]/page.tsx:152` | `sendImageMessage(session.id, URL.createObjectURL(file))` — blob URL 作为消息内容 | - -### 4.3 争议证据上传 - -| 位置 | 行为 | -| --------------------------------------- | ------------------------------------------------------------------------------- | -| `app/(order)/dispute/[id]/page.tsx:88` | `URL.createObjectURL(file)` 生成预览,提交时传入 store | -| `app/(order)/dispute/[id]/page.tsx:102` | `URL.revokeObjectURL` 移除时释放 — 说明开发者意识到了临时性,但未替换为真实上传 | - -### 4.4 身份认证证明材料 — 纯占位 - -| 位置 | 行为 | -| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `app/(account)/verify/page.tsx:170-181` | 三个 `
` 占位块(身份证正面/反面/游戏截图),有 `cursor-pointer` 样式但无 ``、无 `onClick`、无状态绑定 | - -### 4.5 发帖图片 — 假计数 + 固定路径 - -| 位置 | 行为 | -| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| `app/(main)/post/new/page.tsx:46` | `imageCount` 状态仅做数字加减 | -| `app/(main)/post/new/page.tsx:84` | 提交时 `images: Array.from({ length: imageCount }).map(() => "/posts/p1-1.jpg")` — 无论"上传"几张,全部指向同一张固定图片 | - ---- - -## 五、客户端与服务端数据隔离 - -部分页面是 Server Component(或在服务端执行的函数),通过 `lib/api/*` 读取数据;而写入操作发生在客户端 Zustand store。在 Next.js 的 SSR/RSC 模式下,服务端无法读取客户端 store 的最新状态。 - -### 5.1 发帖 → 帖子详情 - -| 写入 | 读取 | -| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -| `app/(main)/post/new/page.tsx:79` → `store/posts.ts:44` (client) | `app/(main)/post/[id]/page.tsx:17` → `lib/api/posts.ts:10` → `usePostStore.getState()` (server?) | - -### 5.2 店铺模板保存 → 店铺主页 - -| 写入 | 读取 | -| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| `app/(dashboard)/dashboard/shop/templates/page.tsx:120` → `store/shops.ts` (client) | `app/(main)/shop/[id]/page.tsx:29` → `shop.templateConfig.sections` (server) | - -### 5.3 服务发布 → 打手详情页 - -| 写入 | 读取 | -| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -| `app/(dashboard)/dashboard/services/new/page.tsx:132` → `store/services.ts` (client) | `app/(main)/player/[id]/page.tsx:29-32` — 优先读 `player.services`(mock 内嵌数据),仅当为空时才 fallback 到 `listServicesByPlayer` | - -**特别注意**:打手详情页的 `player.services && player.services.length > 0` 判断(`app/(main)/player/[id]/page.tsx:30`)意味着只要 mock 数据中打手自带了 services,新发布的服务就永远不会显示。这是一个数据遮蔽问题,不仅仅是隔离问题。 - ---- - -## 六、纯前端筛选/排序/统计 - -以下逻辑在前端内存中对全量数据做 filter/sort/slice,mock 阶段数据量小时体验正常,接后端引入分页、权限、跨端一致性后会出现偏差。 - -### 6.1 社区列表 — 内存排序与筛选 - -| 位置 | 行为 | -| ------------------------------------- | ---------------------------------------------------------- | -| `app/(main)/community/page.tsx:22-34` | 全量 `posts.filter().sort()` 实现"最新/最热"和游戏标签筛选 | - -### 6.2 订单列表 — 内存角色过滤 + Tab 过滤 - -| 位置 | 行为 | -| ------------------------------------ | ----------------------------------------------------------------- | -| `app/(order)/orders/page.tsx:91-103` | 先按 `consumerId/playerId/shopId` 过滤角色视角,再按 tab 过滤状态 | - -### 6.3 管理后台概览 — 硬取首项 + 截断 - -| 位置 | 行为 | -| --------------------------------------- | --------------------------------------------------------- | -| `app/(dashboard)/dashboard/page.tsx:17` | `listPlayers()[0]` — 始终取第一个打手的数据作为"我的"数据 | -| `app/(dashboard)/dashboard/page.tsx:18` | `listShops()[0]` — 始终取第一个店铺 | -| `app/(dashboard)/dashboard/page.tsx:19` | `listOrders().slice(0, 3)` — 最近订单截前 3 条 | - -### 6.4 首页推荐 — 全量渲染 - -| 位置 | 行为 | -| --------------------------- | ---------------------------------------------------------------------- | -| `app/(main)/page.tsx:12-14` | `listPlayers()` / `listShops()` 全量获取后直接渲染,无推荐算法、无分页 | - -### 6.5 收入统计 — 正则匹配交易描述关联订单 - -| 位置 | 行为 | -| ------------------------------------------------------ | -------------------------------------------------------------------- | -| `app/(dashboard)/dashboard/shop/income/page.tsx:51-53` | `transaction.description.match(/ord[-\d]+/)` 从描述文本中提取订单 ID | - -**风险**:后端描述格式变化时,统计数据会静默丢失,不报错。应由后端提供结构化的 `orderId` 字段。 - ---- - -## 七、消息列表未按用户过滤 - -| 位置 | 行为 | -| -------------------------------------- | ----------------------------------------------------------- | -| `app/(order)/chat/page.tsx:12` | `sessions` 直接全量渲染,未过滤当前用户是否为参与者 | -| `app/(order)/chat/page.tsx:21-23` | 仅用 `participant.id !== userId` 找"对方",但不排除无关会话 | -| `app/(order)/chat/[id]/page.tsx:52-59` | 会话详情页才做参与者校验 | - -**结果**:列表页会展示当前用户不参与的会话,点进去才提示无权查看。 - ---- - -## 八、店铺规则 — 可保存但不执行 - -### 8.1 规则字段仅做展示 - -| 字段 | 保存位置 | 执行位置 | -| ------------------------------------ | ----------------------------------------------------------------- | ----------------------------------------------------- | -| `allowMultiShop` | `app/(dashboard)/dashboard/shop/rules/page.tsx:50` → `updateShop` | 无 — 未发现任何校验逻辑 | -| `allowIndependentOrders` | 同上 | 无 — 未发现任何校验逻辑 | -| `dispatchMode` | 同上 | `store/orders.ts:407` — 仅影响前端自动派单模拟 | -| `commissionType` / `commissionValue` | 同上 | `lib/domain/income.ts:22-31` — 仅影响前端收入计算展示 | - -### 8.2 员工邀请 — 无校验直接重绑定 - -| 位置 | 行为 | -| --------------------------------------------------------- | --------------------------------------------------------- | -| `app/(dashboard)/dashboard/shop/employees/page.tsx:74-79` | 点击"邀请打手"直接调用 `assignToShop` + `playerCount + 1` | -| `store/players.ts:13-17` | `assignToShop` 仅修改 `shopId`/`shopName` 字段,无校验 | - -**缺失**:未检查打手是否已属于其他店铺、是否符合 `allowMultiShop` 规则、是否需要打手同意。 - -### 8.3 公告编辑 — `window.prompt` 无审计 - -| 位置 | 行为 | -| --------------------------------------------- | ------------------------------------------ | -| `app/(dashboard)/dashboard/shop/page.tsx:157` | `window.prompt("", announcement)` 编辑公告 | -| `app/(dashboard)/dashboard/shop/page.tsx:175` | `window.prompt("", "")` 新增公告 | - ---- - -## 九、社区列表的点赞/评论入口缺失 - -| 位置 | 行为 | -| --------------------------------------- | -------------------------------------------------------------- | -| `app/(main)/community/page.tsx:145-154` | 点赞和评论图标是 `` 内的纯展示元素,无 `onClick`,无链接 | - -帖子详情页(`components/post-like-button.tsx`、`components/post-comments.tsx`)有完整的点赞和评论交互,但社区列表页的卡片上这些图标仅做数字展示,用户无法在列表页直接操作。 - ---- - -## 十、置顶/精选 — 有字段和展示,无操作入口 - -| 位置 | 内容 | -| ----------------------------------- | ------------------------------------------------------------------------------------------- | -| `lib/types.ts:177` | `Post` 类型定义 `pinned: boolean` | -| `lib/mock/posts.ts:17` 等 | mock 帖子中有 `pinned: true` | -| `app/(main)/community/page.tsx:104` | 展示 `` 图标 | -| `store/posts.ts:40` | `createPost` 强制 `pinned: false` | -| `store/posts.ts:17-22` | store 接口仅有 `createPost` / `togglePostLike` / `incrementCommentCount`,无 pin/unpin 方法 | - -`PLAN.md:141` 规划了"用户自己置顶,最多 N 条",但目前无任何操作路径可以改变 `pinned` 状态。 - ---- - -## 十一、关注与推送 — 文案存在,实现缺席 - -### 11.1 关注 - -| 位置 | 内容 | -| ------------------------------------- | ------------------------------- | -| `PLAN.md:9` | "消费者可以收藏/关注打手或店铺" | -| `app/(account)/settings/page.tsx:234` | 通知偏好文案"点赞、评论、关注" | - -全仓库未发现 `follow`/`关注` 相关的 store、api、页面动作。 - -### 11.2 浏览器推送 - -| 位置 | 内容 | -| ------------- | ------------------------------- | -| `PLAN.md:216` | "站内通知 + 用户可选浏览器推送" | - -全仓库未发现 `Notification.requestPermission`、`serviceWorker`、`PushSubscription`、`pushManager` 等 Web Push 相关代码。现有通知体系仅为本地生成 + 本地已读。 - ---- - -## 十二、硬编码展示值 - -这些数值直接写在 JSX 中,不来自任何 store 或 API 计算。 - -| 位置 | 内容 | -| --------------------------------------- | -------------------------------------------------------- | -| `app/(account)/wallet/page.tsx:154` | `¥1,280.00`(本月收入) | -| `app/(account)/wallet/page.tsx:158` | `¥320.00`(待结算) | -| `app/(account)/wallet/page.tsx:162` | `¥5,400.00`(已提现) | -| `app/(dashboard)/dashboard/page.tsx:79` | `¥12,800`(店主本月收入) | -| `app/(auth)/layout.tsx:4-8` | `12,000+` 认证打手 / `98.6%` 好评率 / `50,000+` 完成订单 | -| `app/(order)/dispute/[id]/page.tsx:377` | UI 文案含"模拟处理结果"字样 | - ---- - -## 十三、未使用的基础设施 - -### 13.1 `requestWithAuth` — 定义未调用 - -| 位置 | 内容 | -| --------------------- | ----------------------------------------------- | -| `lib/api/client.ts:9` | 定义了 `requestWithAuth(executor, options?)` | -| 全仓库 | `rg "requestWithAuth("` 命中 0 处调用 | - -### 13.2 `usePlayerStatusStore` — 定义未调用 - -| 位置 | 内容 | -| --------------------------- | ----------------------------------------------------------- | -| `store/player-status.ts:11` | 定义了 `usePlayerStatusStore`,含 `statuses` 和 `setStatus` | -| 全仓库 | `rg "usePlayerStatusStore"` 仅命中定义处 | - -`PLAN.md:107` 规划了"打手有并发接单上限,搜索结果和打手详情页展示'可接单/忙碌'状态",该 store 疑似为此功能的未完成基础设施。 - ---- - -## 迁移优先级建议 - -### P0 — 上线前必须解决(数据安全/业务正确性) - -1. 登录态持久化(刷新丢失 = 用户无法正常使用) -2. 身份与实体 ID 对齐(否则后台全部空态) -3. 所有用户写入动作接入后端持久化(点赞/评论/收藏/设置/通知已读) -4. 上传功能替换为真实文件上传(头像/聊天图片/争议证据/认证材料/发帖图片) -5. 移除 UI 中的"模拟"字样(`dispute/[id]/page.tsx:377`) - -### P1 — 切后端时必须改造(数据一致性) - -6. 消息列表按当前用户过滤会话 -7. 筛选/排序/统计改为后端分页查询(社区/订单/首页推荐/收入统计) -8. 打手详情页移除 `player.services` 优先读取逻辑,统一从服务列表查询 -9. 店铺规则执行逻辑落地(`allowMultiShop`/`allowIndependentOrders` 校验) -10. 员工邀请增加校验流程 -11. 收入统计改用结构化字段关联订单 - -### P2 — 功能补齐或明确下线 - -12. 记住登录状态功能实现或移除控件 -13. 置顶/精选操作入口 -14. 关注功能 -15. 浏览器推送 -16. 打手在线状态(`usePlayerStatusStore` 接入或移除) -17. 公告编辑替换 `window.prompt` 为正式表单 diff --git a/静态模拟数据残留审计报告.md b/静态模拟数据残留审计报告.md deleted file mode 100644 index 234fe5c..0000000 --- a/静态模拟数据残留审计报告.md +++ /dev/null @@ -1,260 +0,0 @@ -# 静态模拟数据残留审计报告 - -## 总体结论 - -项目当前没有任何真实后端 HTTP 请求。全仓库仅 1 处 `fetch(`(`lib/api/search.ts:34`),请求的是本地 Next.js Route `/api/search`,而该 Route 本身也以 mock 数据为源。`@tanstack/react-query` 的 `QueryClientProvider` 已挂载(`app/providers.tsx`),但全仓库 0 处 `useQuery`/`useMutation` 调用。无 `.env` 文件,无 mock/real 环境切换开关。 - ---- - -## 一、静态实体数据源 `lib/mock/*.ts` - -15 个文件,定义了全部业务实体的硬编码数组,是整个模拟体系的根: - -| 文件 | 导出 | 实体类型 | -| ------------------------------ | --------------------------------------- | ------------ | -| `lib/mock/users.ts:3` | `mockUsers: User[]` | 用户 | -| `lib/mock/users.ts:119` | `currentUser = mockUsers[0]` | 当前登录用户 | -| `lib/mock/games.ts:3` | `mockGames: Game[]` | 游戏 | -| `lib/mock/services.ts:3` | `mockServices: PlayerService[]` | 陪玩服务 | -| `lib/mock/players.ts:5` | `mockPlayers: Player[]` | 打手 | -| `lib/mock/shops.ts:4` | `mockShops: Shop[]` | 店铺 | -| `lib/mock/orders.ts:4` | `mockOrders: Order[]` | 订单 | -| `lib/mock/disputes.ts:3` | `mockDisputes: Dispute[]` | 争议 | -| `lib/mock/reviews.ts:3` | `mockReviews: Review[]` | 评价 | -| `lib/mock/posts.ts:4` | `mockPosts: Post[]` | 帖子 | -| `lib/mock/comments.ts:4` | `mockComments: Comment[]` | 评论 | -| `lib/mock/chat.ts:3` | `mockChatSessions: ChatSession[]` | 聊天会话 | -| `lib/mock/chat.ts:95` | `mockChatMessages: ChatMessage[]` | 聊天消息 | -| `lib/mock/favorites.ts:3` | `mockFavorites: Favorite[]` | 收藏 | -| `lib/mock/notifications.ts:3` | `mockNotifications: Notification[]` | 通知 | -| `lib/mock/transactions.ts:3` | `mockTransactions: WalletTransaction[]` | 交易流水 | -| `lib/mock/transactions.ts:139` | `walletBalance = 275` | 钱包余额 | - -聚合出口:`lib/mock/index.ts` - ---- - -## 二、Store 层 — mock 初始化 + 前端本地状态机 - -12 个 Zustand store 全部以 mock 数据初始化,运行时在前端内存中增删改查并生成 ID/时间戳: - -| Store 文件 | 初始化行 | mock 来源 | -| --------------------------- | -------------------------------------------------------- | ------------------------------------------------------------ | -| `store/shops.ts:14` | `shops: mockShops` | `mockShops` | -| `store/players.ts:12` | `players: mockPlayers` | `mockPlayers` | -| `store/services.ts:14` | `services: mockServices` | `mockServices` | -| `store/orders.ts:315` | `orders: mockOrders` | `mockOrders` | -| `store/disputes.ts:203` | `disputes: mockDisputes.map(asRecord)` | `mockDisputes` | -| `store/reviews.ts:51` | `reviews: mockReviews` | `mockReviews` + `mockUsers`(:24) | -| `store/posts.ts:25` | `posts: mockPosts` | `mockPosts` | -| `store/comments.ts:13` | `comments: mockComments` | `mockComments` | -| `store/chat.ts:23-24` | `sessions: mockChatSessions, messages: mockChatMessages` | `mockChatSessions` + `mockChatMessages` + `mockUsers`(:15) | -| `store/favorites.ts:14` | `favorites: mockFavorites` | `mockFavorites` | -| `store/notifications.ts:21` | `notifications: mockNotifications` | `mockNotifications` | -| `store/wallet.ts:20-21` | `balance: walletBalance, transactions: mockTransactions` | `walletBalance` + `mockTransactions` | - -所有 store 还通过 `generateId()` 在前端本地生成实体 ID(`store/orders.ts:318,350`、`store/services.ts:21`、`store/wallet.ts:29,45,71,102,132`、`store/disputes.ts:231` 等),切后端后 ID 应由服务端分配。 - ---- - -## 三、伪 API 层 `lib/api/*.ts` - -15 个模块名义上是 API 层,实际全部是同步读写本地 store 或直接返回 mock: - -| 文件 | 实际行为 | 风险 | -| ---------------------------- | -------------------------------------------------- | --------------------------- | -| `lib/api/users.ts:1,4,8,12` | 直接 `import { currentUser, mockUsers }` 并 return | 高 — 直接返回 mock | -| `lib/api/games.ts:1,4,8` | 直接 `import { mockGames }` 并 return | 高 — 直接返回 mock | -| `lib/api/orders.ts:11` | `useOrderStore.getState().orders` | 中 — 读本地 store | -| `lib/api/services.ts:4` | `useServiceStore.getState().services` | 中 — 读本地 store | -| `lib/api/players.ts:4` | `usePlayerStore.getState().players` | 中 — 读本地 store | -| `lib/api/shops.ts:4` | `useShopStore.getState().shops` | 中 — 读本地 store | -| `lib/api/posts.ts:7` | `usePostStore.getState().posts` | 中 — 读写本地 store | -| `lib/api/comments.ts:8` | `useCommentStore.getState().comments` | 中 — 读写本地 store | -| `lib/api/chat.ts:6` | `useChatStore.getState().sessions` | 中 — 读写本地 store | -| `lib/api/favorites.ts:4` | `useFavoriteStore.getState().favorites` | 中 — 读本地 store | -| `lib/api/notifications.ts:7` | `useNotificationStore.getState().notifications` | 中 — 读写本地 store | -| `lib/api/transactions.ts:4` | `useWalletStore.getState().transactions` | 中 — 读本地 store | -| `lib/api/reviews.ts:6` | `useReviewStore.getState().reviews` | 中 — 读写本地 store | -| `lib/api/disputes.ts:6` | `useDisputeStore.getState().disputes` | 中 — 读写本地 store | -| `lib/api/search.ts:34` | `fetch('/api/search?...')` | 唯一 fetch,但后端仍是 mock | -| `lib/api/client.ts:9` | `requestWithAuth` 仅包装执行器做未登录拦截 | 无网络请求 | - ---- - -## 四、唯一 HTTP 链路 — 搜索 - -``` -lib/api/search.ts:34 → fetch(`/api/search?${params}`) -app/api/search/route.ts:1 → import { mockPlayers, mockServices, mockShops } from "@/lib/mock" -app/api/search/route.ts:51 → players: mockPlayers, shops: mockShops, services: mockServices -``` - -搜索是项目里唯一走了 HTTP 请求的链路,但 Next.js Route Handler 的数据源仍然是 mock。 - ---- - -## 五、前端状态自动推进(Demo 定时器) - -这是切后端时事故概率最高的部分 — 前端 `setTimeout` 自动推进业务状态,真实后端应由服务端事件驱动。 - -**定时常量:** -``` -lib/config/demo-timers.ts:1 ORDER_ACCEPT_TIMEOUT_MS = 30_000 -lib/config/demo-timers.ts:2 ORDER_CLOSE_TIMEOUT_MS = 30_000 -lib/config/demo-timers.ts:3 ORDER_REVIEW_TIMEOUT_MS = 30_000 -lib/config/demo-timers.ts:5 DISPUTE_TO_REVIEWING_MS = 5_000 -lib/config/demo-timers.ts:6 DISPUTE_TO_RESOLVED_MS = 10_000 -``` - -**订单自动流转:** -| 位置 | 行为 | -| ------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `store/orders.ts:172-206` | `scheduleOrderTimeout` — 订单进入 pending_accept/pending_close/pending_review 后启动 setTimeout 自动超时流转 | -| `store/orders.ts:405-413` | 自动派单模拟 — 当店铺 `dispatchMode === "auto"` 时,`setTimeout(3000)` 自动调用 `acceptOrder` | - -**争议自动推进:** -| 位置 | 行为 | -| --------------------------- | ----------------------------------------------------------------------------------- | -| `store/disputes.ts:142-163` | `setTimeout(DISPUTE_TO_REVIEWING_MS)` — 自动将争议从 open → reviewing | -| `store/disputes.ts:165-197` | `setTimeout(DISPUTE_TO_RESOLVED_MS)` — 自动将争议从 reviewing → resolved 并回写订单 | - ---- - -## 六、认证/表单流程模拟 - -| 位置 | 行为 | -| ---------------------------------------- | --------------------------------------------------------------------------------------- | -| `app/(auth)/login/page.tsx:35-36` | `await new Promise(r => setTimeout(r, 500))` + `login(getCurrentUserForLogin(), ...)` | -| `app/(auth)/register/page.tsx:50-51` | 同上 | -| `components/login-dialog.tsx:44-45` | 同上 | -| `app/(auth)/forgot-password/page.tsx:27` | `await new Promise(r => setTimeout(r, 500))` + toast | -| `app/(account)/verify/page.tsx:41-52` | `submitWithMockApproval` — 提交认证后 `setTimeout(3000)` 自动调用 `approveVerification` | - -`getCurrentUserForLogin()` 最终来自 `lib/api/users.ts:12` → `lib/mock/users.ts:119` 的 `currentUser = mockUsers[0]`,即无论输入什么账号密码,登录的永远是同一个硬编码用户。 - ---- - -## 七、硬编码展示值 - -| 位置 | 内容 | -| --------------------------------------- | -------------------------------------------------------------------------- | -| `app/(account)/wallet/page.tsx:154` | `¥1,280.00`(本月收入,写死) | -| `app/(account)/wallet/page.tsx:158` | `¥320.00`(待结算,写死) | -| `app/(account)/wallet/page.tsx:162` | `¥5,400.00`(已提现,写死) | -| `app/(auth)/layout.tsx:4-8` | `12,000+` 认证打手 / `98.6%` 好评率 / `50,000+` 完成订单(营销数字,写死) | -| `app/(order)/dispute/[id]/page.tsx:377` | UI 文案含"模拟处理结果"字样 | - ---- - -## 八、页面数据源分类(34 个页面) - -**Store Only(14 个)— 完全不经过 lib/api,直接读写 store:** -``` -app/(account)/notifications/page.tsx -app/(account)/settings/page.tsx -app/(account)/verify/page.tsx [含 setTimeout 模拟] -app/(account)/wallet/page.tsx -app/(dashboard)/dashboard/services/page.tsx -app/(dashboard)/dashboard/shop/employees/page.tsx -app/(dashboard)/dashboard/shop/orders/page.tsx -app/(dashboard)/dashboard/shop/page.tsx -app/(dashboard)/dashboard/shop/rules/page.tsx -app/(dashboard)/dashboard/shop/templates/page.tsx [含 setTimeout] -app/(main)/post/new/page.tsx -app/(order)/chat/page.tsx -app/(order)/order/[id]/page.tsx -app/(order)/orders/page.tsx -``` - -**API + Store 混合(9 个)— 经过 lib/api 但 lib/api 本身也是读 store:** -``` -app/(auth)/login/page.tsx [含 setTimeout 模拟登录] -app/(auth)/register/page.tsx [含 setTimeout 模拟注册] -app/(dashboard)/dashboard/page.tsx -app/(dashboard)/dashboard/services/new/page.tsx -app/(dashboard)/dashboard/shop/income/page.tsx -app/(order)/chat/[id]/page.tsx -app/(order)/dispute/[id]/page.tsx -app/(order)/order/new/page.tsx [含 setTimeout] -app/(order)/review/[id]/page.tsx -``` - -**API Only(7 个)— 仅经过 lib/api,但 lib/api 底层仍是本地数据:** -``` -app/(main)/page.tsx -app/(main)/community/page.tsx -app/(main)/player/[id]/page.tsx -app/(main)/post/[id]/page.tsx -app/(main)/search/page.tsx [唯一真正 fetch 的页面] -app/(main)/shop/[id]/page.tsx -app/(main)/user/[id]/page.tsx -``` - -**无 API/Store(4 个)— 纯静态或纯前端模拟:** -``` -app/(auth)/forgot-password/page.tsx [setTimeout 模拟] -app/(main)/help/page.tsx [纯静态内容] -app/(main)/privacy/page.tsx [纯静态内容] -app/(main)/terms/page.tsx [纯静态内容] -``` - ---- - -## 九、组件层直接读写业务 Store(绕过 lib/api) - -除 `useAuthStore`/`useLoginDialogStore` 外,以下共享组件直接操作业务 store: - -| 组件 | 直接引用的 store | -| ------------------------------------- | ----------------------------------------------- | -| `components/order-actions.tsx:14-16` | `useChatStore`, `useOrderStore`, `useShopStore` | -| `components/favorite-button.tsx:6` | `useFavoriteStore` | -| `components/post-like-button.tsx:5` | `usePostStore` | -| `components/post-comments.tsx:5` | `useCommentStore` | -| `components/post-comment-count.tsx:3` | `useCommentStore` | -| `components/header.tsx:19-20` | `useNotificationStore`, `useShopStore` | - ---- - -## 十、已排除项(确认不存在) - -- 无 `.env` / `.env.local` / `.env.development` 等环境配置文件 -- 无 mock/real 环境切换开关(`NEXT_PUBLIC_*MOCK*`、`USE_MOCK` 等) -- 无运行时 MSW 拦截代码(`msw` 仅作为 `@vitest/mocker` 的 peer dep 出现在 lockfile) -- 无 `axios`、`axios-mock-adapter`、`json-server`、`miragejs` 使用 -- 无运行时 `.json` 文件作为数据源导入 -- 无 `Promise.resolve` 模拟异步返回 -- 无 Next.js `rewrites`/`redirects`/proxy 配置 -- `useQuery`/`useMutation`/`useSWR` 全仓库 0 处调用 - ---- - -## 十一、隐蔽未实现接口(更像真功能,实际仍在前端自转) - -这些条目即使去掉了明显的 mock 字样,也会在切真实后端后造成严重偏差:UI 提示可用或可保存,实际没有任何可被后端接入的请求/数据契约/权限边界。 - -| 类型 | 现象 | 关键证据(单行) | 切后端后常见失败模式 | -| ------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | -| Client Store 与 Server Component 断层 | 管理页或发布页写入 Zustand 后,跳转到详情/公开页看不到改动 | `app/(main)/post/new/page.tsx:79`(createPost 写前端 store) / `app/(main)/post/[id]/page.tsx:17`(详情页从 server 侧读 getPostById) | 新创建内容在列表可见,详情页 404 或展示旧数据;店铺模板预览不反映保存结果 | -| 图片/文件上传未落到接口 | 多处上传只生成本地 blob URL 或占位图,刷新或换设备即失效 | `app/(account)/settings/page.tsx:75`、`app/(order)/chat/[id]/page.tsx:152`、`app/(order)/dispute/[id]/page.tsx:88`、`app/(main)/post/new/page.tsx:84`、`app/(account)/verify/page.tsx:170` | 后端接入后出现上传缺字段、图片无法复现、消息图片仅本机可见 | -| 会话列表未按参与者过滤 | 消息列表直接渲染全部会话,只在详情页才做参与者校验 | `app/(order)/chat/page.tsx:12` | 列表出现无关会话,后端接入时需要补齐按用户查询与分页 | -| 店铺规则字段缺少业务约束 | 规则页可保存 allowMultiShop 等字段,员工邀请直接改归属,不存在申请/同意流 | `app/(dashboard)/dashboard/shop/rules/page.tsx:48` / `app/(dashboard)/dashboard/shop/employees/page.tsx:75` / `store/players.ts:13` | 规则与权限不生效,导致运营规则与实际行为脱节 | -| 智能派单与流程推进为固定定时器 | 自动派单固定 3 秒自动接单;待接单/结单/评价自动超时推进;争议自动进入 reviewing/resolved | `store/orders.ts:186`、`store/orders.ts:408`、`store/disputes.ts:142` | 与真实状态机/消息队列/人工审核不一致,产生错误状态与资金流偏差 | -| 交易明细与订单关联靠字符串解析 | 收入统计用正则从 transaction.description 里提取 ord id | `app/(dashboard)/dashboard/shop/income/page.tsx:51` | 后端字段结构化后该逻辑失效,统计漏算或误算 | -| 身份切换与实体模型不对齐 | 登录永远是固定用户 u1(consumer),UI 允许切换到 player/owner;导航用 user.id 直接拼 player/shop 路由 | `app/(auth)/login/page.tsx:36` / `lib/mock/users.ts:119` / `components/header.tsx:85` / `lib/mock/players.ts:7` / `lib/mock/shops.ts:7` | 打手/店主页面长期空态或 404;店铺后台永远提示无可管理店铺 | -| 看似走 API,实际仍为 mock | `/search` 会发起 fetch,但 `/api/search` 路由以 mockPlayers/mockShops/mockServices 作为数据源 | `lib/api/search.ts:34` / `app/api/search/route.ts:1` | 接真实后端时容易遗漏替换点,导致线上仍走假数据 | - ---- - -## 复扫命令 - -后续持续监控可用以下 6 条命令覆盖主要信号: - -```bash -rg -n '@/lib/mock' app lib store components -rg -n '\bfetch\(' app lib store components -rg -n 'setTimeout\(' app lib store components -rg -n 'from "@/store/' app components -rg -n 'URL\.createObjectURL' app components -rg -n 'window\.prompt' app -```