# 数据库与 API 设计 ## 1. 关系概览 订单模型和接口的最新增量见 [首轮需求访谈决策](./07-requirements-interview-decisions.md)。实现时必须包含客户微信身份、套餐价格快照、标签、未读状态和客户订单查询;与本文早期枚举冲突时,以该文档为准。 ```text BrandProfile 1 ├── Services 1..n └── PortfolioItems 1..n Service 1 ── n Booking Booking 1 ── 0..n Feedback AdminUser 1 ── n AuditLog ``` 第一版数据库只有一个品牌记录,但所有内容表仍通过 `brand_id` 关联,便于后续扩展。 ## 2. 表与约束 ### brand_profiles - `id` UUID 主键 - `name` 必填 - `is_published` 默认 true - `created_at`、`updated_at` 必填 ### services - `id` UUID 主键 - `brand_id` 外键 - `name` 必填 - `price_text` 可空 - `price_amount` 可空,单位为分,第一版不用于支付 - `booking_enabled` 默认 true - `is_enabled` 默认 true - `sort_order` 默认 0 - 对 `(brand_id, sort_order)` 建索引 ### portfolio_items - `id` UUID 主键 - `brand_id` 外键 - `image_urls` 可使用 JSON 数组,或拆分为 media_assets 表 - `is_visible` 默认 true - `sort_order` 默认 0 ### bookings - `id` UUID 主键 - `booking_no` 唯一 - `brand_id` 外键 - `service_id` 可空,防止服务删除后历史预约无法读取 - `status` 枚举:`pending/contacted/completed/cancelled` - `extra_data` JSON,可存储受控的行业扩展字段 - 对 `(brand_id, status, created_at)` 建索引 - 不存支付状态 `extra_data` 不是任意键值存储。字段定义必须由品牌行业配置决定,API 需要校验键名、类型和必填规则;通用字段仍保持结构化。 ### feedbacks - `id` UUID 主键 - `brand_id` 外键 - `booking_id` 可空 - `status` 枚举:`pending/approved/hidden` - 对 `(brand_id, status, created_at)` 建索引 ### admin_users - `id` UUID 主键 - `username` 唯一 - `password_hash` 必填 - `role` 枚举:`admin/operator` - `is_enabled` 默认 true ### audit_logs - `id` UUID 主键 - `admin_user_id` 外键 - `action`、`resource_type`、`resource_id` 必填 - `metadata` JSON,可记录变更摘要,不记录密码和完整联系方式 ## 3. API 约定 ### 响应格式 ```json { "data": {}, "error": null, "requestId": "..." } ``` 失败时: ```json { "data": null, "error": { "code": "VALIDATION_ERROR", "message": "请填写有效的联系方式", "fields": { "phone": "格式不正确" } }, "requestId": "..." } ``` ### 公开接口 ```text GET /api/public/brand GET /api/public/services GET /api/public/services/:id GET /api/public/portfolio GET /api/public/portfolio/:id GET /api/public/feedbacks POST /api/public/bookings POST /api/public/feedbacks ``` 公开接口只能返回启用、可见和已审核数据。 ### 管理接口 ```text POST /api/auth/login POST /api/auth/logout GET /api/auth/me GET /api/admin/brand PUT /api/admin/brand GET /api/admin/services POST /api/admin/services PUT /api/admin/services/:id DELETE /api/admin/services/:id GET /api/admin/portfolio POST /api/admin/portfolio PUT /api/admin/portfolio/:id DELETE /api/admin/portfolio/:id GET /api/admin/bookings GET /api/admin/bookings/:id PATCH /api/admin/bookings/:id/status GET /api/admin/feedbacks PATCH /api/admin/feedbacks/:id/status POST /api/admin/uploads/presign ``` ## 4. 预约接口规则 - 服务不存在或已停用时拒绝提交。 - 期望日期早于当天时拒绝提交。 - 联系方式至少一项非空。 - 服务端再次执行所有校验,不能只依赖小程序校验。 - 使用请求幂等键或短时间重复检测避免重复预约。 - 创建成功后返回 `booking_no`,不返回管理员数据。 ## 5. 文件上传流程 ## 5.1 微信客户会话 小程序调用 `wx.login` 获取临时 `code`,提交到 `/api/customer/session/wechat`。服务端使用微信官方 `jscode2session` 接口换取 OpenID 和 session key,并在服务端建立 `CustomerUser` 映射和登录态。当前代码将微信调用封装在 `WechatAuthService`,使用 Node 原生 `fetch`,不引入第三方微信 SDK;后续订单接口必须从服务端登录态解析当前 OpenID,不能由客户端提交或选择 `customer_user_id`。生产实现不得把 OpenID 或 session key 返回给小程序,应改为 HttpOnly 会话 Cookie 或短期签名令牌。 1. 后台请求上传凭证。 2. API 校验管理员权限和文件元数据。 3. 前端直接上传对象存储。 4. 上传完成后提交文件 URL 和元数据。 5. 内容实体引用该资源。 不得让浏览器上传任意路径或任意 MIME 文件。图片最大尺寸和大小由环境变量配置。 ## 6. 远期支付边界 未来新增: ```text orders payments refunds ``` `Booking` 表示客户意向或服务预约;`Order` 表示应收业务;`Payment` 表示微信支付交易;三者必须独立建模。支付成功不能直接等价于预约完成,预约仍由业务流程决定。