Files
quyun-v2/docs/PROJECT_FUNCTIONS_AND_DB_DICTIONARY.md
2025-12-15 17:55:32 +08:00

524 lines
22 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.
# QuyUn 项目功能与数据库字典(用于多租户改造)
本文档基于当前仓库代码静态分析整理(`frontend/` + `backend/`),目标是帮助你将“单实例/单运营方(单用户后台)”改造为“多租户(多运营方)”。
---
## 1. 仓库结构与技术栈
### 1.1 目录结构
- `backend/`Go 后端(同时包含少量 `package.json` 依赖,但核心为 Go 服务)
- `frontend/admin/`后台管理端Vue3 + Vite
- `frontend/wechat/`:微信 H5 端Vue3 + Vite
### 1.2 关键技术栈
**后端**
- Web 框架:`gofiber/fiber/v3`
- DI / 代码生成:`go.ipao.vip/atom`(路由文件为 `routes.gen.go`
- 数据库PostgreSQL`lib/pq``pgx`),迁移:`pressly/goose`
- SQL Builder/ORM`go-jet/jet``backend/database/table/*``backend/app/model/*.gen.go`
- 任务队列:`riverqueue/river`(依赖表:`river_job` 等)
- 对象存储:阿里云 OSS签名上传、签名下载、删除
- 微信网页授权OAuth、JS-SDK 签名、支付/退款回调(微信支付 v3
- 监控链路OpenTelemetry存在 provider 但本文档不展开)
**前端**
- Vue 3 + Vite
- AdminPrimeVue + Tailwind并用少量 DaisyUI
- WeChat H5Tailwind + `weixin-js-sdk` + `xgplayer`(视频播放)
---
## 2. 运行时架构与路由总览
### 2.1 HTTP 服务入口与静态资源托管
后端启动入口:`backend/main.go`,命令为 `serve``backend/app/service/http/http.go`)。
HTTP 路由前缀固定为 `/v1`
- API统一挂载在 `/v1/*`
- 静态资源:
- 后台管理端:`GET /admin*``App.DistAdmin` 指向的 `frontend/admin/dist`
- 微信端:`GET /*``App.DistWeChat` 指向的 `frontend/wechat/dist`
这意味着项目典型部署形态是:**一个后端进程同时提供 API + 两套前端静态文件**。
### 2.2 认证/授权(非常影响多租户改造)
#### 2.2.1 微信端用户认证Cookie Token + 重定向)
中间件:`backend/app/middlewares/mid_auth.go`
- 受保护范围:除以下路径外,均要求登录
- `/v1/pay/callback/*`(支付回调免登录)
- `/v1/auth/*`(授权流程免登录)
- `/v1/admin/*`(后台 API 走另一套鉴权)
- 机制:
- 读取 Cookie`token`
- 解析 JWT`providers/jwt`),拿到 `UserID`
- 查询用户(`users` 表)
- 校验 `users.auth_token.expires_at`(微信 access token 过期则强制重登)
- 未登录时:非 XHR 请求会重定向到 `/v1/auth/wechat?redirect=当前完整URL`XHR 请求直接返回 401
- 认证通过后写入:`ctx.Locals("user", *model.Users)`
> 重要:当前系统的“用户”是**微信 OpenID 用户**,并且 `users.open_id` 在 DB 层为 `UNIQUE`(单租户假设)。
#### 2.2.2 后台管理端鉴权(硬编码账号 + 固定 UserID
登录接口:`POST /v1/admin/auth``backend/app/http/admin/auth.go`
- 用户名/密码硬编码在后端:`pl.yang` / `Xixi@0202`
- 登录成功签发 JWT`UserID` 固定写死为 `-20140202`
中间件:`backend/app/middlewares/mid_auth_admin.go`
-`/v1/admin/*` 生效(除 `/v1/admin/auth`
- 从 Header `Authorization` 或 Query `token` 读取 JWT
- 校验 `jwt.UserID == -20140202`,否则 403
> 重要:这套后台体系是“单运营方/单管理员”的实现方式,多租户必须重构为“租户维度的后台账号体系”。
### 2.3 API 路由清单(从 `routes.gen.go` 汇总)
#### 2.3.1 微信端(非 adminAPI
路由文件:`backend/app/http/routes.gen.go`
- `GET /v1/auth/wechat`:发起微信网页授权(重定向到微信授权页)
- `GET /v1/auth/login`:微信回调,换取 openid + 用户信息,写入 Cookie `token`,再重定向回 `redirect`
- `GET /v1/posts`:曲谱列表(仅已发布),支持 keyword 搜索,返回是否已购买、封面图 OSS 签名 URL
- `GET /v1/posts/:id/show`:曲谱详情(仅已发布),返回购买态、封面图 URL
- `GET /v1/posts/:id/play`:获取可播放视频 URL未购买返回“短视频/试听”,已购买返回“完整版”)
- `GET /v1/posts/mine`:我的已购曲谱列表
- `POST /v1/posts/:id/buy`:购买(当前实现主路径为“余额支付”)
- `GET /v1/users/profile`:当前用户资料 + 余额
- `PUT /v1/users/username`:修改用户名(最多 12 字符)
- `GET /v1/wechats/js-sdk`:获取 JS-SDK 签名配置
- `POST /v1/pay/callback/:channel`:支付/退款回调入口(免登录),内部将回调入队列异步处理
#### 2.3.2 后台管理端adminAPI
路由文件:`backend/app/http/admin/routes.gen.go`
- `POST /v1/admin/auth`:后台登录(硬编码账号),返回 Token
媒体库:
- `GET /v1/admin/medias`:媒体列表(分页 + keyword
- `GET /v1/admin/medias/:id`媒体预览302 跳转到 OSS 签名 URL
- `DELETE /v1/admin/medias/:id`:删除 OSS 文件 + DB 记录
上传:
- `GET /v1/admin/uploads/pre-uploaded-check/:md5.:ext?mime=...`:按 md5 判断是否已存在;不存在则返回 OSS 预签名 PUT URL
- `POST /v1/admin/uploads/post-uploaded-action`:上传完成回调:写入 `medias`;若为 `video/mp4` 触发下载/转码类任务
曲谱:
- `GET /v1/admin/posts`:曲谱列表(分页 + keyword附带销量购买次数
- `POST /v1/admin/posts`:创建曲谱(包含封面 head_images 与媒体 assets
- `PUT /v1/admin/posts/:id`:编辑曲谱
- `DELETE /v1/admin/posts/:id`:硬删除
- `GET /v1/admin/posts/:id`:曲谱详情(附带 medias 列表)
- `POST /v1/admin/posts/:id/send-to/:userId`:赠送曲谱(写入 `user_posts`price=-1
用户:
- `GET /v1/admin/users`:用户列表(分页 + keyword
- `GET /v1/admin/users/:id`:用户详情
- `GET /v1/admin/users/:id/articles`:用户已购曲谱(分页)
- `POST /v1/admin/users/:id/balance`:后台给用户充值余额(单位:分)
订单:
- `GET /v1/admin/orders`:订单列表(分页 + 按订单号/用户过滤),返回附带 `post_title``username`
- `POST /v1/admin/orders/:id/refund`:退款(余额支付直接退余额 + 撤销权限;微信支付走退款 API 并等待回调)
统计:
- `GET /v1/admin/statistics`:仪表盘统计(草稿/已发布、媒体数、已完成订单数、用户数、已完成订单金额汇总)
---
## 3. 前端功能梳理
### 3.1 Admin`frontend/admin`
**路由(`frontend/admin/src/router.js`base 为 `/admin/`**
- `/`Dashboard统计
- `/medias`:媒体库列表(预览/下载/删除)
- `/medias/uploads`媒体上传MD5 去重 + OSS 预签名上传 + 上传后回调)
- `/posts`:曲谱列表(创建/编辑/删除/赠送)
- `/posts/create`:创建曲谱(选择封面图 ≤3、选择媒体资源、设置价格/折扣/状态)
- `/posts/edit/:id`:编辑曲谱
- `/users`:用户列表(可充值)
- `/users/:id`:用户详情 + 已购列表
- `/orders`:订单列表 + 退款
- `/login`:后台登录页
**API 调用方式**
- Axios baseURL`/v1``frontend/admin/src/api/httpClient.js`
- Token本地存储 `__token`,请求头写入 `Authorization: Bearer <token>`
### 3.2 WeChat H5`frontend/wechat`
**路由(`frontend/wechat/src/router.js`**
- `/`:曲谱列表(无限滚动 + 搜索)
- `/posts/:id`:曲谱详情(视频播放 + 购买按钮 + 分享)
- `/purchased`:已购列表(顶部固定播放器 + 列表点播)
- `/profile`:个人信息 + 余额
**API 调用方式**
- Axios baseURL`/v1`,并且 `withCredentials: true`(携带 Cookie `token`
- 401 时前端自动跳转 `/v1/auth/wechat?redirect=<当前URL>``frontend/wechat/src/api/client.js`
**注意:接口路径不一致**
`frontend/wechat/src/api/userApi.js` 的更新接口调用 `PUT /users/profile`,但后端实际是 `PUT /v1/users/username`
---
## 4. 后端业务流程(核心用例)
### 4.1 微信登录/注册流程
1) 前端/中间件发现未登录 → 重定向到 `/v1/auth/wechat?redirect=...`
2) `/v1/auth/wechat` 生成微信授权 URL回调到 `/v1/auth/login`,并透传 redirect
3) `/v1/auth/login`
- `code` 换取 `openid``access_token`
- 获取“稳定版 token”stable_token
- 拉取用户信息(失败则生成随机昵称/头像)
- `users` 表按 `open_id` 查询,不存在则创建;存在则更新用户名/头像/metas/auth_token
- 生成 JWTclaims 里只使用了 `UserID`),写 Cookie`token`
### 4.2 曲谱内容模型与呈现
曲谱(`posts`)本质是一个“商品/内容条目”,它的媒体资产在 `posts.assets` 里记录JSON 数组),每个 asset 指向 `medias.id`
- 封面图:`posts.head_images`(媒体 ID 数组)→ 后端转为 OSS 签名 URL 数组返回
- 播放:`GET /v1/posts/:id/play`
- 若未购买:`preview=true`
-`posts.assets` 中选择 `Type=="video/mp4"``asset.Metas.Short == preview` 的媒体作为播放源
- 生成带过期时间的 OSS 签名 URL预览与正式片过期时间不同
### 4.3 购买/订单/权限授予
核心表:
- `orders`:订单记录(支付状态 + 支付/退款回调内容写入 `orders.meta`
- `user_posts`:用户与曲谱的授予关系(购买、赠送)
购买接口:`POST /v1/posts/:id/buy`
- 先检查 `user_posts` 是否已有记录(避免重复购买)
- 创建 `orders`(状态 pending
- 当前主路径:余额足够则走余额支付,写入 `orders.meta.cost_balance` 并投递 `BalancePayNotify` 任务,随后返回一个特殊响应(`appId: "balance"`
- 代码中存在“余额不足时走微信 JSAPI 支付”的逻辑,但当前版本在余额不足时提前 `return`,导致后续微信支付逻辑不可达(这属于现状问题,改多租户时建议一并梳理修正)。
### 4.4 支付/退款回调(异步任务)
回调入口:`POST /v1/pay/callback/:channel`
- `TRANSACTION.SUCCESS` → 入队 `WechatPayNotify`
- `REFUND.*` → 入队 `WechatRefundNotify`
任务处理(均运行在 River 队列里):
- `WechatPayNotify`:校验金额、更新订单状态为 completed、扣减余额若 cost_balance>0、写入 `user_posts` 授权
- `BalancePayNotify`:余额支付完成,更新订单状态 completed、扣减余额、写入 `user_posts`
- `WechatRefundNotify`:订单状态按退款状态更新;退款成功则撤销 `user_posts`
---
## 5. 数据库字典PostgreSQL
### 5.1 迁移与生成代码来源
- Goose 迁移:`backend/database/migrations/*.sql`
- Jet 表定义(由 DB 反射/生成):`backend/database/table/*.go`
- ModelCRUD + 部分自定义逻辑):`backend/app/model/*`
- 类型映射jsonb → struct、int2 → enum`backend/database/transform.yaml` + `backend/database/fields/*`
### 5.2 业务表一览
| 表 | 作用 |
|---|---|
| `users` | 微信用户主体 + 余额 + 微信 token 信息 |
| `posts` | 曲谱/内容商品 |
| `medias` | 媒体资源OSS 路径、hash 去重、媒体元信息) |
| `user_posts` | 用户-曲谱的授予关系(购买/赠送) |
| `orders` | 订单记录(支付/退款状态与元数据) |
此外还有 `migrations`Goose 使用)以及 River 队列表(`river_job` 等,属于基础设施表)。
### 5.3 枚举int2/int取值定义
**posts.status`fields.PostStatus`**
- `0`draft
- `1`published
**users.status`fields.UserStatus`**
- `0`ok
- `1`banned
- `2`blocked
**orders.status`fields.OrderStatus`**
- `0`pending
- `1`paid当前代码主要使用 `pending/completed/refund_*`,该值可能历史遗留)
- `2`refund_success
- `3`refund_closed
- `4`refund_processing
- `5`refund_abnormal
- `6`cancelled
- `7`completed
### 5.4 `users` 表
迁移:`backend/database/migrations/20250322103119_create_users.sql``20250430014015_alter_user.sql``20250512113213_alter_user.sql`
字段(按迁移语义整理):
- `id int8`:主键(序列起始被设置为 1000
- `created_at timestamp not null default now()`
- `updated_at timestamp not null default now()`
- `deleted_at timestamp null`:软删除
- `status int2 not null default 0`:见 `UserStatus`
- `open_id varchar(128) not null unique`:微信 OpenID单租户假设的关键约束
- `username varchar(128) not null`
- `avatar text null`
- `metas jsonb not null default '{}'`:用户资料(见下方 JSON 结构)
- `auth_token jsonb not null default '{}'`:微信 token见下方 JSON 结构)
- `balance int8 not null default 0`:余额(单位:分)
`users.metas``fields.UserMetas`
- `city/country/province`:地区
- `head_image_url`:头像
- `nickname`:昵称
- `sex`:性别
- `privilege[]`:特权列表
`users.auth_token``fields.UserAuthToken`
- `stable_access_token``stable_expires_at`
- `access_token``expires_at`
- `refresh_token``scope``is_snapshotuser`
### 5.5 `medias` 表
迁移:`backend/database/migrations/20250321112535_create_medias.sql`
- `id int8`:主键
- `created_at timestamp not null default now()`
- `name varchar(255) not null default ''`:原始文件名
- `mime_type varchar(128) not null default ''`
- `size int8 not null default 0`
- `path varchar(255) not null default ''`OSS 对象 key实际业务中通常为 `quyun/<md5>.<ext>`
- `metas jsonb not null default '{}'`:媒体元信息
- `hash varchar(64) not null default ''`:上传 md5用于去重
`medias.metas``fields.MediaMetas`
- `parent_hash`:关联源文件(例如转码后的视频/封面与原视频关联)
- `short bool`:是否为“试听/预览版”
- `duration int64`:时长(秒)
### 5.6 `posts` 表
迁移:`backend/database/migrations/20250322100215_create_posts.sql`
- `id int8`:主键
- `created_at timestamp not null default now()`
- `updated_at timestamp not null default now()`
- `deleted_at timestamp null`:软删除
- `status int2 not null default 0`:见 `PostStatus`
- `title varchar(128) not null`
- `head_images jsonb not null default '[]'`:封面媒体 ID 数组int64
- `description varchar(256) not null`简介admin 表单里叫 introduction
- `content text not null`:正文(微信端详情里展示在 `content`
- `price int8 not null default 0`:单位:分
- `discount int2 not null default 100`折扣百分比0~100
- `views int8 not null default 0`
- `likes int8 not null default 0`
- `tags jsonb default '{}'`:标签(代码期望是 `[]string`
- `assets jsonb default '{}'`:媒体资产(代码期望是 `[]MediaAsset`
`posts.assets``fields.MediaAsset[]`)元素结构:
- `type string`mime_type例如 `video/mp4`
- `media int64`:对应 `medias.id`
- `metas MediaMetas`:冗余一份媒体 metas用于直接判断 short/duration
- `mark *string`:预留字段(当前业务未见强依赖)
> 注意:迁移里 `tags/assets` 默认值是 `{}`object但代码将其当作数组解析。真实运行依赖于写入时覆盖该字段多租户改造时建议顺手把默认值修正为 `[]` 并清洗历史数据。
### 5.7 `user_posts` 表
迁移:`backend/database/migrations/20250322103243_create_user_posts.sql`
- `id int8`:主键
- `created_at timestamp not null default now()`
- `updated_at timestamp not null default now()`
- `user_id int8 not null`
- `post_id int8 not null`
- `price int8 not null`:购买/赠送时记录(赠送场景 `price=-1`
> 注意:无外键、无唯一约束(同一用户重复 post 的防重由代码实现);多租户/一致性增强时建议加唯一索引与外键。
### 5.8 `orders` 表
迁移:`backend/database/migrations/20250410130530_create_orders.sql`
- `id int8`:主键
- `created_at timestamp not null default now()`
- `updated_at timestamp not null default now()`
- `order_no varchar(64) not null`:系统订单号(当前用时间戳字符串)
- `sub_order_no varchar(64) not null default ''`:子订单号(当前等于 order_no
- `transaction_id varchar(64) not null default ''`:微信支付交易号
- `refund_transaction_id varchar(64) not null default ''`:微信退款单号
- `price int8 not null default 0`:单位:分
- `discount int2 not null default 100`
- `currency varchar(10) not null default 'CNY'`
- `payment_method varchar(50) not null default 'wechatpay'`:实际可能为 `balance` 或微信回调的 trade_type
- `post_id int8 not null`
- `user_id int8 not null`
- `status int2 not null`:见 `OrderStatus`
- `meta jsonb not null default '{}'`
`orders.meta``fields.OrderMeta`
- `pay_notify`:微信支付回调解密内容(结构体较大)
- `refund_resp`:发起退款 API 响应
- `refund_notify`:微信退款回调解密内容
- `cost_balance int64`:混合支付时使用余额金额(单位:分)
---
## 6. 单租户假设与“多租户化”切入点清单
### 6.1 当前显式/隐式单租户点
- DB 约束:`users.open_id UNIQUE`(同一 openid 只能存在一条用户记录)
- Admin 登录:硬编码账号/密码JWT `UserID` 写死 `-20140202`(没有“租户管理员表”)
- OSS 路径:`providers/ali/oss_client.go` 固定把对象放到 `quyun/` 前缀;上传模块也固定 `UPLOAD_PATH = "quyun"`
- 业务表均缺少租户字段:`posts/medias/orders/user_posts/users` 都没有 `tenant_id`
- 业务查询没有任何“租户过滤”(例如 `admin/posts` 列表直接全表扫描)
### 6.2 值得注意JWT Claims 已预留租户字段
`backend/providers/jwt/jwt.go``BaseClaims` 已包含:
- `tenant string`
- `tenant_id int64`
但当前业务仅用到 `user_id`,并未实现“解析租户上下文 → 写入 claims → 数据隔离”。
---
## 7. 多租户改造建议(面向本项目的可落地方案)
下面给的是“从当前代码出发”的推荐路线,优先保证:改动面可控、隔离强、后续可扩展。
### 7.1 明确租户边界与识别方式(建议先定这个)
对该项目而言,最常见的租户识别方式:
1) **按域名**(推荐):`tenantA.example.com``tenantB.example.com`
2) **按路径前缀**`/t/:tenant/...`(会影响前端路由与静态文件托管)
3) **按请求头**`X-Tenant-ID`(适合 B 端 API但微信 H5 场景常常不便)
4) **按二维码/推广链接参数**:首次进入携带 tenant再写入 cookie/localStorage需要防篡改与校验
微信场景强依赖“回跳 redirect”因此建议**域名识别租户** 或 “二维码/链接识别租户 + 服务端签名 state 防篡改”。
### 7.2 数据库层:新增租户表 + 为业务表补 `tenant_id`
建议新增:
- `tenants``id``code``name``status``created_at``updated_at`、配置项(可 JSONB
- `admin_users`(或 `tenant_users`):租户后台账号体系(账号/密码 hash/角色/tenant_id
为现有业务表增加:
- `users.tenant_id`
- `posts.tenant_id`
- `medias.tenant_id`
- `orders.tenant_id`
- `user_posts.tenant_id`
并建立必要约束/索引(至少):
- `users`: `UNIQUE(tenant_id, open_id)`(替代当前全局唯一)
- `posts`: `(tenant_id, id)` 常用过滤索引;如有需要加 `(tenant_id, status, deleted_at)`
- `medias`: `UNIQUE(tenant_id, hash)`(上传去重在租户内进行)
- `orders`: `UNIQUE(tenant_id, order_no)`(避免多租户下时间戳订单号冲突)
- `user_posts`: `UNIQUE(tenant_id, user_id, post_id)`(彻底防重)
### 7.3 应用层:租户上下文注入与强制过滤
建议新增一个最先执行的 middleware
- 解析租户host/path/header
- 写入 `ctx.Locals("tenant", tenant)`(包含 `tenant_id`
- 后续所有 model 查询都必须带 tenant 条件
落点示例(按当前代码组织方式):
- `middlewares` 新增 `TenantResolve` 并在 `Group("v1").Use(...)` 的最前面插入
- `Auth` 中间件里查用户时改为:`WHERE tenant_id = ? AND id = ?`
- 微信登录 `GetUserByOpenIDOrCreate` 改为 tenant 维度:`WHERE tenant_id=? AND open_id=?`
- Admin 路由统一加 tenant 过滤(例如 `/admin/posts` 只看当前租户)
### 7.4 OAuth/支付:租户与微信配置关系
必须提前决策:**每个租户是否独立微信 AppID/支付商户**
- 若每租户独立:`tenants` 里存微信配置AppID/AppSecret/MchID/ApiV3Key/...),并按租户动态初始化 client当前 providers 是“全局单例配置”)
- 若共享同一个公众号/商户:仍然要做数据隔离,但需要在订单号、回调处理里带上 tenant 信息(例如 `order_no` 编码 tenant 前缀,或在 `meta` 中存 tenant_id 并保证可反查)
### 7.5 OSS对象 Key 增加租户前缀
当前固定前缀 `quyun/`。多租户建议改为:
- `tenants/<tenant_code>/quyun/<md5>.<ext>``tenants/<tenant_id>/...`
并同步改动:
- 预签名上传:`PreUploadCheck` 返回的 Key
- 上传完成回调写入 `medias.path`
- 后续签名下载/删除统一使用新 path
### 7.6 队列任务:必须携带 tenant 上下文
目前任务参数里不包含 tenant。多租户后建议
- Job Args 增加 `TenantID`(或可解析的 `TenantCode`
- Worker 执行时所有 DB 查询都加 tenant 过滤
- OSS 操作也按 tenant 前缀
否则会出现:回调/任务在多租户环境下“写错库、扣错余额、发错权限”的高危问题。
---
## 8. 建议你下一步提供的信息(能让多租户设计更准确)
为了把“多租户模式”落到你期望的形态,建议你确认并告诉我:
1) 租户识别方式:域名 / 路径前缀 / header / 邀请链接?
2) 微信与支付配置:租户独立还是共享?
3) 租户隔离级别仅逻辑隔离tenant_id还是需要“库/Schema 隔离”?
4) 你期望的后台账号体系:每租户多个管理员?是否需要角色权限?
确认后我可以按本仓库的结构直接给出:迁移脚本 + middleware + model 查询改造点 + 前端配套改造清单。