init
This commit is contained in:
248
specs/API.md
Normal file
248
specs/API.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 新项目 API 规格(/t/:tenant_code/v1)
|
||||
|
||||
## 0. 通用约定
|
||||
|
||||
### 0.1 Base URL 与租户
|
||||
|
||||
- Base:`/t/:tenant_code/v1`
|
||||
- `tenant_code` 校验:服务端对路径段做 `lower()` 后校验 `^[a-z0-9_-]+$`,并查表确认租户存在且启用
|
||||
|
||||
### 0.2 认证
|
||||
|
||||
- WeChat H5:Cookie 会话(例如 `token`),请求需携带 `withCredentials`
|
||||
- Admin:`Authorization: Bearer <token>`
|
||||
|
||||
### 0.3 响应
|
||||
|
||||
- 成功:`200`(或 201/204),JSON
|
||||
- 业务错误:`400`(含错误信息)
|
||||
- 未登录:`401`
|
||||
- 无权限:`403`
|
||||
- 不存在:`404`
|
||||
|
||||
---
|
||||
|
||||
## 1. WeChat OAuth
|
||||
|
||||
### 1.1 发起授权
|
||||
|
||||
`GET /auth/wechat?redirect=<url>`
|
||||
|
||||
- 行为:302 跳转到微信授权 URL;回调为 `/t/:tenant_code/v1/auth/login`
|
||||
|
||||
### 1.2 授权回调
|
||||
|
||||
`GET /auth/login?code=<code>&state=<state>&redirect=<url>`
|
||||
|
||||
- 行为:
|
||||
- 获取 openid 与用户资料
|
||||
- `(tenant_id, open_id)` 获取或创建 `users`
|
||||
- 写入 Cookie 会话
|
||||
- 302 回跳 `redirect`
|
||||
|
||||
---
|
||||
|
||||
## 2. WeChat H5 内容与用户
|
||||
|
||||
### 2.1 曲谱列表(仅已发布)
|
||||
|
||||
`GET /posts`
|
||||
|
||||
Query:
|
||||
- `page`(默认 1)
|
||||
- `limit`(默认 10)
|
||||
- `keyword`(可选)
|
||||
|
||||
Response(示例结构):
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "标题",
|
||||
"description": "简介",
|
||||
"price": 19900,
|
||||
"discount": 80,
|
||||
"views": 10,
|
||||
"likes": 0,
|
||||
"tags": ["tag"],
|
||||
"head_images": ["https://signed-url..."],
|
||||
"bought": false,
|
||||
"recharge_wechat": "联系微信(可选)"
|
||||
}
|
||||
],
|
||||
"total": 123,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 曲谱详情(仅已发布)
|
||||
|
||||
`GET /posts/:id/show`
|
||||
|
||||
Response:同 `PostItem`,额外含 `content`
|
||||
|
||||
### 2.3 获取播放 URL
|
||||
|
||||
`GET /posts/:id/play`
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "url": "https://signed-url..." }
|
||||
```
|
||||
|
||||
规则:未购买返回 `metas.short=true` 的视频 URL;已购买返回 `metas.short=false` 的视频 URL。
|
||||
|
||||
### 2.4 我的已购
|
||||
|
||||
`GET /posts/mine`
|
||||
|
||||
Query:`page`、`limit`、`keyword`(可选)
|
||||
|
||||
Response:同分页结构
|
||||
|
||||
### 2.5 余额购买
|
||||
|
||||
`POST /posts/:id/buy`
|
||||
|
||||
Response(余额支付成功):
|
||||
- 可直接返回 `{ "ok": true }` 或返回订单信息(由你最终 UI/交互决定)
|
||||
|
||||
错误:
|
||||
- 余额不足:`400`,message 建议为“余额不足,请联系管理员充值”
|
||||
- 已购买:`400`
|
||||
|
||||
### 2.6 用户资料
|
||||
|
||||
`GET /users/profile`
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": 1001, "created_at": "2025-01-01T00:00:00Z", "username": "xx", "avatar": "url", "balance": 10000 }
|
||||
```
|
||||
|
||||
### 2.7 修改用户名
|
||||
|
||||
`PUT /users/username`
|
||||
|
||||
Body:
|
||||
```json
|
||||
{ "username": "新昵称" }
|
||||
```
|
||||
|
||||
约束:trim 后不能为空;最大 12 个字符(按 rune 计)。
|
||||
|
||||
### 2.8 JS-SDK 配置
|
||||
|
||||
`GET /wechats/js-sdk?url=<current_page_url_without_hash>`
|
||||
|
||||
Response:JS-SDK 签名数据(结构按前端 `weixin-js-sdk` 需要输出)
|
||||
|
||||
---
|
||||
|
||||
## 3. Admin(租户后台)
|
||||
|
||||
### 3.1 登录
|
||||
|
||||
`POST /admin/auth`
|
||||
|
||||
Body:
|
||||
```json
|
||||
{ "username": "admin", "password": "******" }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "token": "..." }
|
||||
```
|
||||
|
||||
### 3.2 仪表盘统计
|
||||
|
||||
`GET /admin/statistics`
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"post_draft": 0,
|
||||
"post_published": 0,
|
||||
"media": 0,
|
||||
"order": 0,
|
||||
"user": 0,
|
||||
"amount": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 媒体库
|
||||
|
||||
`GET /admin/medias?page=&limit=&keyword=`
|
||||
|
||||
`GET /admin/medias/:id`:302 跳转到 OSS 签名 URL
|
||||
|
||||
`DELETE /admin/medias/:id`:删除 OSS 对象并软删/删 DB 记录(最终由你决定)
|
||||
|
||||
### 3.4 上传(预签名)
|
||||
|
||||
`GET /admin/uploads/pre-uploaded-check/:md5.:ext?mime=<mime>`
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "exists": false, "pre_sign": { "...": "..." } }
|
||||
```
|
||||
|
||||
说明:
|
||||
- `md5` 在租户内去重
|
||||
- OSS Key:`quyun/<tenant_uuid>/<md5>.<ext>`
|
||||
|
||||
`POST /admin/uploads/post-uploaded-action`
|
||||
|
||||
Body:
|
||||
```json
|
||||
{ "originalName": "a.mp4", "md5": "...", "mimeType": "video/mp4", "size": 123 }
|
||||
```
|
||||
|
||||
### 3.5 曲谱管理
|
||||
|
||||
`GET /admin/posts?page=&limit=&keyword=`
|
||||
|
||||
`POST /admin/posts`、`PUT /admin/posts/:id` Body(示例):
|
||||
```json
|
||||
{
|
||||
"title": "标题",
|
||||
"head_images": [1,2,3],
|
||||
"price": 19900,
|
||||
"discount": 80,
|
||||
"introduction": "简介",
|
||||
"content": "正文",
|
||||
"status": 1,
|
||||
"medias": [10,11,12]
|
||||
}
|
||||
```
|
||||
|
||||
`GET /admin/posts/:id`:返回曲谱 + medias 列表
|
||||
|
||||
`DELETE /admin/posts/:id`
|
||||
|
||||
`POST /admin/posts/:id/send-to/:userId`:赠送曲谱(写入授权记录)
|
||||
|
||||
### 3.6 用户管理
|
||||
|
||||
`GET /admin/users?page=&limit=&keyword=`
|
||||
|
||||
`GET /admin/users/:id`
|
||||
|
||||
`GET /admin/users/:id/articles?page=&limit=`
|
||||
|
||||
`POST /admin/users/:id/balance`
|
||||
|
||||
Body:
|
||||
```json
|
||||
{ "balance": 10000 }
|
||||
```
|
||||
|
||||
### 3.7 订单管理
|
||||
|
||||
`GET /admin/orders?page=&limit=&order_number=&user_id=`
|
||||
|
||||
`POST /admin/orders/:id/refund`:仅余额订单可退款
|
||||
|
||||
151
specs/DB.sql
Normal file
151
specs/DB.sql
Normal file
@@ -0,0 +1,151 @@
|
||||
-- 新项目数据库 DDL(PostgreSQL)
|
||||
-- 目标:多租户(tenant_id 全表隔离)+ tenant_uuid 用于 OSS Key:quyun/<tenant_uuid>/<md5>.<ext>
|
||||
-- 注意:tenant_uuid 由业务代码生成写入(不使用 DB 扩展默认值)
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1) 租户
|
||||
CREATE TABLE IF NOT EXISTS tenants (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_code VARCHAR(64) NOT NULL,
|
||||
tenant_uuid UUID NOT NULL,
|
||||
name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
status INT2 NOT NULL DEFAULT 0,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- tenant_code:不区分大小写(写入/查询均 lower),并限制字符集 a-z0-9_-
|
||||
ALTER TABLE tenants
|
||||
ADD CONSTRAINT tenants_tenant_code_format
|
||||
CHECK (tenant_code ~ '^[A-Za-z0-9_-]+$');
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenants_tenant_code_lower
|
||||
ON tenants (lower(tenant_code));
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenants_tenant_uuid
|
||||
ON tenants (tenant_uuid);
|
||||
|
||||
-- 2) 租户后台账号
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
username VARCHAR(64) NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role VARCHAR(32) NOT NULL DEFAULT 'admin',
|
||||
status INT2 NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_admin_users_tenant_username
|
||||
ON admin_users (tenant_id, lower(username));
|
||||
|
||||
-- 3) 用户(微信 openid 用户)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
status INT2 NOT NULL DEFAULT 0,
|
||||
open_id VARCHAR(128) NOT NULL,
|
||||
username VARCHAR(128) NOT NULL DEFAULT '',
|
||||
avatar TEXT,
|
||||
metas JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
auth_token JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
balance BIGINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_users_tenant_openid
|
||||
ON users (tenant_id, open_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_users_tenant_id
|
||||
ON users (tenant_id, id);
|
||||
|
||||
-- 4) 媒体
|
||||
CREATE TABLE IF NOT EXISTS medias (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
mime_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
size BIGINT NOT NULL DEFAULT 0,
|
||||
path VARCHAR(512) NOT NULL DEFAULT '',
|
||||
metas JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
hash VARCHAR(64) NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
-- 租户内按 md5 去重
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_medias_tenant_hash
|
||||
ON medias (tenant_id, hash);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_medias_tenant_id
|
||||
ON medias (tenant_id, id);
|
||||
|
||||
-- 5) 曲谱/内容
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
status INT2 NOT NULL DEFAULT 0,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
head_images JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
description VARCHAR(256) NOT NULL DEFAULT '',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
price BIGINT NOT NULL DEFAULT 0,
|
||||
discount INT2 NOT NULL DEFAULT 100,
|
||||
views BIGINT NOT NULL DEFAULT 0,
|
||||
likes BIGINT NOT NULL DEFAULT 0,
|
||||
tags JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
assets JSONB NOT NULL DEFAULT '[]'::jsonb
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_posts_tenant_status_deleted
|
||||
ON posts (tenant_id, status, deleted_at);
|
||||
|
||||
-- 6) 授权关系(购买/赠送)
|
||||
CREATE TABLE IF NOT EXISTS user_posts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
|
||||
price BIGINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_user_posts_tenant_user_post
|
||||
ON user_posts (tenant_id, user_id, post_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_user_posts_tenant_user
|
||||
ON user_posts (tenant_id, user_id, id);
|
||||
|
||||
-- 7) 订单(仅余额)
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
order_no VARCHAR(64) NOT NULL,
|
||||
price BIGINT NOT NULL DEFAULT 0,
|
||||
discount INT2 NOT NULL DEFAULT 100,
|
||||
currency VARCHAR(10) NOT NULL DEFAULT 'CNY',
|
||||
payment_method VARCHAR(50) NOT NULL DEFAULT 'balance',
|
||||
post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
status INT2 NOT NULL DEFAULT 0,
|
||||
meta JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_orders_tenant_order_no
|
||||
ON orders (tenant_id, order_no);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_orders_tenant_user
|
||||
ON orders (tenant_id, user_id, id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
126
specs/PRD.md
Normal file
126
specs/PRD.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# 新项目 PRD(多租户 + 微信登录/分享 + 余额支付)
|
||||
|
||||
## 1. 范围与约束
|
||||
|
||||
### 1.1 多租户(从第一天开始)
|
||||
|
||||
- 所有站点与 API 都在路径前缀下:`/t/:tenant_code/...`
|
||||
- `tenant_code` 规则:不区分大小写,允许字符集 `a-z0-9_-`
|
||||
- 系统内部以 `tenant_uuid`(UUID)作为租户在 OSS 的存储分区标识
|
||||
- OSS Key:`quyun/<tenant_uuid>/<md5>.<ext>`
|
||||
- 数据隔离:所有业务表均包含 `tenant_id`,所有查询必须带 `tenant_id` 过滤
|
||||
|
||||
### 1.2 微信能力
|
||||
|
||||
- 保留:微信网页授权登录(OAuth)、JS-SDK 签名与分享
|
||||
- 移除:微信支付/退款/回调(多租户版本完全不支持,历史也不兼容)
|
||||
|
||||
### 1.3 支付能力
|
||||
|
||||
- 仅支持:余额支付(单位:分),后台可充值余额
|
||||
- 可退款:仅余额订单(退款 = 返还余额 + 撤销授权)
|
||||
|
||||
### 1.4 技术栈约束
|
||||
|
||||
- 后端开发语言:Golang(Go)
|
||||
- 前端技术栈:Vue 3 + Vite
|
||||
- UI 组件库(推荐):PrimeVue(配合 TailwindCSS,性能较好且扩展/主题定制能力强)
|
||||
|
||||
---
|
||||
|
||||
## 2. 角色与端
|
||||
|
||||
### 2.1 WeChat H5 端(C 端用户)
|
||||
|
||||
- 访问曲谱列表、搜索
|
||||
- 查看曲谱详情
|
||||
- 播放:未购买只能播放“预览版(short=true)”,已购买播放“完整版(short=false)”
|
||||
- 余额购买
|
||||
- 查看已购列表(快速播放)
|
||||
- 查看个人资料与余额
|
||||
- 分享(JS-SDK)
|
||||
|
||||
### 2.2 Admin 管理端(租户运营人员)
|
||||
|
||||
- 登录(租户维度账号)
|
||||
- 仪表盘统计
|
||||
- 媒体库管理:上传(预签名)、列表、预览、删除
|
||||
- 曲谱管理:创建/编辑/发布/草稿、绑定媒体资源、设置封面(≤3)
|
||||
- 用户管理:列表、详情、查看已购、给用户充值余额
|
||||
- 订单管理:列表、退款(余额订单)
|
||||
- 运营操作:赠送曲谱给指定用户
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心业务对象
|
||||
|
||||
### 3.1 曲谱(Post)
|
||||
|
||||
- 字段:标题、简介、正文、价格(分)、折扣(0~100)、状态(draft/published)
|
||||
- 关联媒体:
|
||||
- `head_images`:封面媒体 ID 列表(≤3)
|
||||
- `assets`:媒体资产数组(见 MediaAsset),包含视频/音频/文件等
|
||||
|
||||
### 3.2 媒体(Media)
|
||||
|
||||
- 字段:name、mime_type、size、hash(md5)、path(OSS key)、metas
|
||||
- metas:`short`(是否预览资源)、`duration`、`parent_hash`
|
||||
- hash 去重:在租户维度内去重(同租户相同 md5 视为同一资源)
|
||||
|
||||
### 3.3 授权(UserPosts)
|
||||
|
||||
- 记录用户购买/赠送后获得的曲谱访问权
|
||||
- 唯一约束:同一租户同一用户同一曲谱只能有一条
|
||||
|
||||
### 3.4 订单(Order)
|
||||
|
||||
- 仅余额订单:`payment_method = balance`
|
||||
- 状态:`pending/completed/refund_success/cancelled`(具体枚举在技术规格中确定)
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键流程
|
||||
|
||||
### 4.1 租户识别
|
||||
|
||||
- 进入任意页面时,根据 URL 中的 `:tenant_code` 识别租户
|
||||
- 服务端需校验:
|
||||
- code 正则:`^[a-z0-9_-]+$`(不区分大小写)
|
||||
- code 存在且启用
|
||||
- 解析成功后,将 `tenant_id`、`tenant_uuid` 注入到请求上下文
|
||||
|
||||
### 4.2 微信登录
|
||||
|
||||
1) 未登录请求 API → 401(XHR)或 302 → `/t/:tenant/v1/auth/wechat?redirect=...`
|
||||
2) `/auth/wechat` 生成微信授权地址(回调到 `/t/:tenant/v1/auth/login` 并透传 redirect)
|
||||
3) `/auth/login` 换取 openid + 用户信息,按 `(tenant_id, open_id)` 获取或创建用户,签发会话 token(cookie)
|
||||
|
||||
### 4.3 播放策略(预览/完整版)
|
||||
|
||||
- assets 中选择 `type == "video/mp4"` 的资源:
|
||||
- 未购买:选择 `metas.short == true`
|
||||
- 已购买:选择 `metas.short == false`
|
||||
- 媒体 URL 通过 OSS 预签名下发
|
||||
|
||||
### 4.4 余额购买与授权
|
||||
|
||||
- 下单:创建订单 `pending`
|
||||
- 校验余额足够:
|
||||
- 足够:扣减余额 → 订单 `completed` → 写入 `user_posts`
|
||||
- 不足:返回业务错误(提示联系管理员充值)
|
||||
|
||||
### 4.5 余额退款
|
||||
|
||||
- 仅允许对 `completed + payment_method=balance` 的订单退款
|
||||
- 退款动作:
|
||||
- 返还余额
|
||||
- 删除/撤销 `user_posts` 授权
|
||||
- 订单标记为 `refund_success`
|
||||
|
||||
---
|
||||
|
||||
## 5. 非功能性要求(简版)
|
||||
|
||||
- 所有写操作必须带租户隔离与鉴权校验
|
||||
- 关键唯一约束在 DB 层实现(避免并发重复授权/重复订单号)
|
||||
- 租户 UUID 由业务代码生成并写入(不依赖 DB 扩展)
|
||||
80
specs/ROUTING.md
Normal file
80
specs/ROUTING.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 新项目路由与部署规则(多租户路径前缀)
|
||||
|
||||
## 1. 路由总览
|
||||
|
||||
### 1.1 约定
|
||||
|
||||
- 租户前缀:`/t/:tenant_code/`(不区分大小写;服务端统一按 `lower()` 识别)
|
||||
- API:`/t/:tenant_code/v1/...`
|
||||
- Admin SPA:`/t/:tenant_code/admin/...`
|
||||
- WeChat SPA:`/t/:tenant_code/...`(除 `v1` 与 `admin` 子路径)
|
||||
|
||||
### 1.2 为什么必须这样分层
|
||||
|
||||
- WeChat/后台前端都是 SPA,通常需要一个 catch-all 回退到 `index.html`
|
||||
- 同时 API 又需要精确匹配,必须确保 API 路由优先于静态路由
|
||||
|
||||
---
|
||||
|
||||
## 2. 后端(HTTP Server)路由注册顺序建议
|
||||
|
||||
以 Fiber 为例,推荐顺序:
|
||||
|
||||
1) `GET /t/:tenant_code/v1/...`:注册所有 API(并注入 TenantContext 中间件)
|
||||
2) `GET /t/:tenant_code/admin*`:回退到 Admin 的 `index.html`(并正确设置静态资源 base)
|
||||
3) `GET /t/:tenant_code/*`:回退到 WeChat 的 `index.html`
|
||||
|
||||
> 重点:不要用全局 `GET /*` 直接接管,否则会吞掉 API 与 admin。
|
||||
|
||||
---
|
||||
|
||||
## 3. 前端(Admin)
|
||||
|
||||
### 3.1 Router base
|
||||
|
||||
- `createWebHistory("/t/<tenant_code>/admin/")`
|
||||
|
||||
### 3.2 API baseURL 推导
|
||||
|
||||
- 运行时从 `location.pathname` 提取 `tenant_code`
|
||||
- axios `baseURL = "/t/<tenant_code>/v1"`
|
||||
|
||||
### 3.3 Token
|
||||
|
||||
- `Authorization: Bearer <token>`(租户后台登录返回)
|
||||
|
||||
---
|
||||
|
||||
## 4. 前端(WeChat H5)
|
||||
|
||||
### 4.1 Router base
|
||||
|
||||
- `createWebHistory("/t/<tenant_code>/")`
|
||||
|
||||
### 4.2 API baseURL 与 Cookie
|
||||
|
||||
- axios `baseURL = "/t/<tenant_code>/v1"`
|
||||
- `withCredentials = true`(携带 cookie 会话)
|
||||
|
||||
### 4.3 未登录跳转
|
||||
|
||||
- 401 时跳:`/t/<tenant_code>/v1/auth/wechat?redirect=<encodeURIComponent(currentUrl)>`
|
||||
|
||||
---
|
||||
|
||||
## 5. tenant_code 提取规则(前端共用)
|
||||
|
||||
从路径 `/t/<tenant_code>/...` 提取第二段:
|
||||
|
||||
- `tenant_code = decodeURIComponent(pathname.split('/')[2] || '')`
|
||||
- 使用时建议 `tenant_code.toLowerCase()`
|
||||
- 允许字符集:`a-z0-9_-`(不允许空)
|
||||
|
||||
---
|
||||
|
||||
## 6. OSS Key 规则
|
||||
|
||||
- 统一格式:`quyun/<tenant_uuid>/<md5>.<ext>`
|
||||
- `tenant_uuid` 来自 `tenants.tenant_uuid`
|
||||
- `md5` 来自上传文件内容 hash
|
||||
|
||||
Reference in New Issue
Block a user