Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffac56b219 |
@@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/im_manager.iml" filepath="$PROJECT_DIR$/.idea/im_manager.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,960 +0,0 @@
|
||||
# HotGo 项目开发规范
|
||||
|
||||
**生成或修改任何代码前,必须完整阅读本文档。**
|
||||
本规范是项目唯一事实来源,所有新增代码必须严格对齐现有模块风格,不得自创新的代码模式。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层 | 技术 |
|
||||
|----|----------------------------------------------------------------|
|
||||
| 后端语言 | Go,module 名 `hotgo` |
|
||||
| 后端框架 | GoFrame v2(gdb、ghttp、gcron、gerror、gctx、gerror、gjson、gvar) |
|
||||
| 前端框架 | Vue 3 + Vite + TypeScript |
|
||||
| UI 组件库 | **Naive UI**(优先使用,未经允许不得引入其他 UI 库) |
|
||||
| 状态管理 | Pinia |
|
||||
| HTTP 客户端 | 项目封装 `@/utils/http/axios`(`http.request`),禁止直接使用 axios |
|
||||
|
||||
---
|
||||
|
||||
## 后端目录结构
|
||||
|
||||
```
|
||||
server/
|
||||
├── addons/ # 插件模块(每个插件独立微架构,见"插件开发"章节)
|
||||
│ ├── modules/ # 隐式注册文件(每个插件一个 .go,仅含 import)
|
||||
│ └── <addonName>/ # 插件目录,与主模块结构镜像
|
||||
├── api/admin/<module>/ # 主模块 HTTP 契约:Req/Res + g.Meta 路由声明
|
||||
├── internal/
|
||||
│ ├── consts/ # 全局常量与字典选项(枚举在此定义并注册)
|
||||
│ ├── global/ # 全局公用变量(谨慎添加,尽量少用)
|
||||
│ ├── controller/admin/sys/ # 薄控制器,只做参数透传,不含业务逻辑
|
||||
│ ├── crons/ # 主模块定时任务注册与处理
|
||||
│ ├── queues/ # 主模块消息队列消费者注册与处理
|
||||
│ ├── logic/sys/ # 业务实现(sSysXxx),init() 注册到 service
|
||||
│ ├── library/ # 内部基础库(addons、dict、hgorm、cache、queue、cron 等)
|
||||
│ ├── service/ # 接口声明(ISysXxx)+ 注册函数
|
||||
│ ├── dao/ # 对外 DAO;internal/dao/internal/ 为 CLI 生成,禁止手改
|
||||
│ ├── model/
|
||||
│ │ ├── entity/ # 表结构体(CLI 生成,禁止手改)
|
||||
│ │ ├── do/ # ORM 写入结构(CLI 生成,禁止手改)
|
||||
│ │ └── input/sysin/ # 业务入参、出参、过滤结构
|
||||
│ └── router/
|
||||
│ ├── admin.go # 主路由,末尾调用 genrouter.Register()
|
||||
│ └── genrouter/ # 每个生成模块一个文件,init() 追加到 LoginRequiredRouter
|
||||
├── resource/
|
||||
│ ├── generate/default/curd/ # 主模块 CRUD 代码生成模板(参考,禁止直接修改)
|
||||
│ └── generate/default/addon/ # 插件脚手架代码生成模板(参考,禁止直接修改)
|
||||
└── utility/ # 通用工具:convert、excel、validate 等
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 后端分层约定
|
||||
|
||||
### 分层职责(禁止跨层调用)
|
||||
|
||||
```
|
||||
api(HTTP契约) → controller(参数透传) → service(接口) → logic(业务实现) → dao(数据访问)
|
||||
```
|
||||
|
||||
- **controller 层**:不写任何业务逻辑,只转调 service
|
||||
- **logic 层**:不出现 HTTP 相关代码,所有校验通过 `Filter()` 或 `g.Validator()` 完成
|
||||
- **dao 层**:`internal/dao/internal/` 由 CLI 自动生成,禁止手改
|
||||
|
||||
---
|
||||
|
||||
### 1. API 层 `api/admin/<module>/<module>.go`
|
||||
|
||||
- 每个接口一对 `XxxReq` / `XxxRes`,`g.Meta` 声明路由
|
||||
- GET 接口用 `method:"get"`,写操作用 `method:"post"`
|
||||
- 请求体嵌入 `sysin.XxxInp`,响应体嵌入对应 Model 或空 `struct{}`
|
||||
|
||||
```go
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/xxxModule/list" method:"get" tags:"XXX模块" summary:"获取XXX列表"`
|
||||
sysin.XxxListInp
|
||||
}
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.XxxListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Controller 层 `internal/controller/admin/sys/<module>.go`
|
||||
|
||||
- 变量:`var XxxModule = cXxxModule{}`,类型:`type cXxxModule struct{}`
|
||||
- 只做参数转发和结果组装,禁止直接操作 dao 或写业务逻辑
|
||||
|
||||
```go
|
||||
func (c *cXxxModule) List(ctx context.Context, req *xxxmodule.ListReq) (res *xxxmodule.ListRes, err error) {
|
||||
list, totalCount, err := service.SysXxxModule().List(ctx, &req.XxxListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.XxxListModel{}
|
||||
}
|
||||
res = new(xxxmodule.ListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Service 层 `internal/service/sys.go`
|
||||
|
||||
- 在已有文件中**追加**接口声明和注册函数,禁止新建文件
|
||||
- 声明 `ISysXxxModule` 接口,提供 `SysXxxModule()` 获取函数和 `RegisterSysXxxModule()` 注册函数
|
||||
|
||||
### 4. Logic 层 `internal/logic/sys/<module>.go`
|
||||
|
||||
- 结构体:`type sSysXxxModule struct{}`
|
||||
- `init()` 里调用 `service.RegisterSysXxxModule(NewSysXxxModule())`
|
||||
- `Model()` 方法:`return handler.Model(dao.XxxTable.Ctx(ctx), option...)`
|
||||
- 列名引用必须用 `dao.XxxTable.Columns().FieldName`,禁止硬编码字符串字段名
|
||||
- `Edit` 方法以 `in.Id > 0` 区分更新/新增,更新用 `XxxUpdateFields`,新增用 `XxxInsertFields`
|
||||
- 字段校验在 `sysin` 的 `Filter(ctx)` 方法完成,logic 层不重复校验
|
||||
|
||||
### 5. Model 层 `internal/model/input/sysin/<module>.go`
|
||||
|
||||
| 类型 | 用途 |
|
||||
|------|------|
|
||||
| `XxxUpdateFields` | 更新时字段白名单 |
|
||||
| `XxxInsertFields` | 新增时字段白名单 |
|
||||
| `XxxEditInp` | 嵌入 `entity.XxxTable`,含 `Filter()` 校验方法 |
|
||||
| `XxxListInp` | 列表查询入参,嵌入 `form.PageInp` |
|
||||
| `XxxListModel` | 列表返回字段 |
|
||||
| `XxxViewInp` / `XxxViewModel` | 详情入参/出参 |
|
||||
| `XxxDeleteInp` | 删除入参(通常含 `Ids []int64`) |
|
||||
| `XxxStatusInp`、`XxxMaxSortInp` 等 | 其他操作 |
|
||||
|
||||
### 6. 路由注册 `internal/router/genrouter/<module>.go`
|
||||
|
||||
```go
|
||||
package genrouter
|
||||
|
||||
import "hotgo/internal/controller/admin/sys"
|
||||
|
||||
func init() {
|
||||
LoginRequiredRouter = append(LoginRequiredRouter, sys.XxxModule) // XXX模块
|
||||
}
|
||||
```
|
||||
|
||||
新建此文件即可,`genrouter/init.go` 通过 `init()` 链自动加载,无需修改 `admin.go`。
|
||||
|
||||
---
|
||||
|
||||
## 字典与枚举规范
|
||||
|
||||
**字典由后端统一维护**,前端通过固定接口拉取,禁止在前端硬编码枚举值。
|
||||
|
||||
### 后端:定义与注册(`internal/consts/` 下对应文件)
|
||||
|
||||
```go
|
||||
// 1. 定义常量
|
||||
const (
|
||||
XxxStatusEnabled = 1 // 启用
|
||||
XxxStatusDisable = 2 // 禁用
|
||||
)
|
||||
|
||||
// 2. 定义选项(选择合适的样式函数)
|
||||
var XxxStatusOptions = []*model.Option{
|
||||
dict.GenSuccessOption(XxxStatusEnabled, "启用"),
|
||||
dict.GenWarningOption(XxxStatusDisable, "禁用"),
|
||||
}
|
||||
|
||||
// 3. 在 init() 中注册(同文件)
|
||||
func init() {
|
||||
dict.RegisterEnums("XxxStatusOptions", "XXX状态选项", XxxStatusOptions)
|
||||
}
|
||||
```
|
||||
|
||||
**样式函数选择规则:**
|
||||
|
||||
| 函数 | 适用场景 |
|
||||
|------|---------|
|
||||
| `dict.GenSuccessOption` | 正常、启用、成功 |
|
||||
| `dict.GenWarningOption` | 待处理、禁用、警告 |
|
||||
| `dict.GenErrorOption` | 失败、封禁、危险 |
|
||||
| `dict.GenInfoOption` | 信息类、中性状态 |
|
||||
| `dict.GenPrimaryOption` | 主要操作、强调 |
|
||||
| `dict.GenDefaultOption` | 默认、无特殊含义 |
|
||||
| `dict.GenHashOption` | 不确定数量的动态枚举 |
|
||||
|
||||
### 后端:Logic 层中使用字典
|
||||
|
||||
```go
|
||||
// 获取标签文本
|
||||
label := dict.GetOptionLabel(consts.XxxStatusOptions, in.Status)
|
||||
|
||||
// 校验是否为合法值
|
||||
if !dict.HasOptionKey(consts.XxxStatusOptions, in.Status) {
|
||||
return gerror.New("状态值不合法")
|
||||
}
|
||||
```
|
||||
|
||||
### 前端:加载与使用字典
|
||||
|
||||
**加载**(在 `model.ts` 的 `loadOptions()` 中):
|
||||
|
||||
```ts
|
||||
export function loadOptions() {
|
||||
dict.loadOptions(['XxxStatusOptions', 'sys_normal_disable']);
|
||||
}
|
||||
```
|
||||
|
||||
**在 `model.ts` 的 columns 中渲染字典标签:**
|
||||
|
||||
```ts
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
const dict = useDictStore();
|
||||
|
||||
// columns 中
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
render(row) {
|
||||
return dict.getLabel('XxxStatusOptions', row.status);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**在搜索表单 schemas 中使用字典下拉:**
|
||||
|
||||
```ts
|
||||
{
|
||||
field: 'status',
|
||||
component: 'NSelect',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
options: computed(() => dict.getOptions('XxxStatusOptions')),
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 插件(Addon)开发规范
|
||||
|
||||
> 定位:独立、临时性、工具类型的功能推荐插件化开发,例如小游戏、广告管理、文章管理、小程序、微商城等。插件之间完全隔离,方便多项目复用。
|
||||
|
||||
### 插件目录结构
|
||||
|
||||
每个插件拥有完整的独立微架构,与主模块结构镜像,但 **包路径前缀为 `hotgo/addons/<addonName>/`**:
|
||||
|
||||
```
|
||||
server/addons/<addonName>/
|
||||
├── main.go # 插件入口:定义 module 结构体,实现 Module 接口,init() 注册
|
||||
├── global/
|
||||
│ ├── global.go # 插件级全局变量
|
||||
│ └── init.go # Init(ctx, skeleton),GetSkeleton() 方法
|
||||
├── api/
|
||||
│ ├── admin/ # 后台接口契约(同主模块 api 写法)
|
||||
│ ├── api/ # 前台 API 接口契约
|
||||
│ └── home/ # 首页接口契约(如有)
|
||||
├── controller/
|
||||
│ ├── admin/sys/ # 后台薄控制器
|
||||
│ ├── api/ # 前台控制器
|
||||
│ └── home/ # 首页控制器(如有)
|
||||
├── logic/
|
||||
│ ├── logic.go # 匿名导入各 logic 子包触发 init()
|
||||
│ └── sys/ # 业务实现(同主模块 logic 写法)
|
||||
├── service/ # 插件内接口声明与注册(独立,不与主模块 service 混用)
|
||||
├── model/
|
||||
│ └── input/sysin/ # 插件内入参/出参(调用主模块服务时继承主模块 input 结构)
|
||||
├── router/
|
||||
│ ├── admin.go # 注册后台路由,使用 addons.RouterPrefix() 获取路由前缀
|
||||
│ ├── api.go # 注册前台路由
|
||||
│ ├── home.go # 注册首页路由(如有)
|
||||
│ ├── websocket.go # 注册 WebSocket 路由(如有)
|
||||
│ └── genrouter/
|
||||
│ ├── init.go # 插件版 genrouter:Register() 使用插件前缀
|
||||
│ └── *.go # 每个生成模块一个文件,追加到插件 LoginRequiredRouter
|
||||
├── consts/ # 插件内常量与字典(同主模块 consts 写法)
|
||||
├── crons/ # 插件定时任务(可选)
|
||||
├── queues/ # 插件消息队列消费者(可选)
|
||||
└── resource/ # 插件静态资源与模板(可选)
|
||||
```
|
||||
|
||||
**对应前端目录:**
|
||||
|
||||
```
|
||||
web/src/
|
||||
├── api/addons/<addonName>/ # 插件前端 API 封装
|
||||
└── views/addons/<addonName>/ # 插件前端页面
|
||||
```
|
||||
|
||||
### 插件入口 `main.go`
|
||||
|
||||
```go
|
||||
package <addonName>
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
_ "hotgo/addons/<addonName>/crons"
|
||||
"hotgo/addons/<addonName>/global"
|
||||
_ "hotgo/addons/<addonName>/logic"
|
||||
_ "hotgo/addons/<addonName>/queues"
|
||||
"hotgo/addons/<addonName>/router"
|
||||
"hotgo/internal/library/addons"
|
||||
"hotgo/internal/service"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
skeleton *addons.Skeleton
|
||||
ctx context.Context
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func init() { newModule() }
|
||||
|
||||
func newModule() {
|
||||
m := &module{
|
||||
skeleton: &addons.Skeleton{
|
||||
Label: "插件显示名",
|
||||
Name: "<addonName>", // 与目录名一致
|
||||
Group: 1,
|
||||
Brief: "简介",
|
||||
Description: "详细描述",
|
||||
Author: "作者",
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
ctx: gctx.New(),
|
||||
}
|
||||
addons.RegisterModule(m)
|
||||
}
|
||||
|
||||
func (m *module) Start(option *addons.Option) (err error) {
|
||||
global.Init(m.ctx, m.skeleton)
|
||||
option.Server.Group("/", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(service.Middleware().Addon)
|
||||
router.Admin(m.ctx, group)
|
||||
router.Api(m.ctx, group)
|
||||
})
|
||||
return
|
||||
}
|
||||
// 其余方法:Stop、Ctx、GetSkeleton、Install、Upgrade、UnInstall(默认留空)
|
||||
```
|
||||
|
||||
### 插件路由前缀规则
|
||||
|
||||
```
|
||||
后台:/admin/<addonName>/接口路径 → 对应 controller/admin/sys/
|
||||
前台:/api/<addonName>/接口路径 → 对应 controller/api/
|
||||
首页:/home/<addonName>/接口路径 → 对应 controller/home/
|
||||
WebSocket:/socket/<addonName>/接口路径
|
||||
```
|
||||
|
||||
路由前缀通过 `addons.RouterPrefix(ctx, consts.AppAdmin, global.GetSkeleton().Name)` 自动生成,**禁止在路由文件里硬编码路径前缀**。
|
||||
|
||||
### 插件 genrouter 与主模块的区别
|
||||
|
||||
插件 `genrouter/init.go` 的 `Register()` 函数需要传入 `ctx` 和 `group`,并使用插件路由前缀:
|
||||
|
||||
```go
|
||||
// 插件 genrouter/init.go
|
||||
func Register(ctx context.Context, group *ghttp.RouterGroup) {
|
||||
prefix := addons.RouterPrefix(ctx, consts.AppAdmin, global.GetSkeleton().Name)
|
||||
group.Group(prefix, func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(service.Middleware().AdminAuth)
|
||||
if len(LoginRequiredRouter) > 0 {
|
||||
group.Bind(LoginRequiredRouter...)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 插件 genrouter/<module>.go
|
||||
func init() {
|
||||
LoginRequiredRouter = append(LoginRequiredRouter, sys.XxxModule)
|
||||
}
|
||||
```
|
||||
|
||||
参考实现:`server/addons/hgexample/router/genrouter/`
|
||||
|
||||
### 插件调用主模块服务
|
||||
|
||||
**在插件 input 层继承主模块 input 结构,解耦参数依赖**,避免 import cycle:
|
||||
|
||||
```go
|
||||
// 插件 model/input/sysin/config.go
|
||||
package sysin
|
||||
|
||||
import "hotgo/internal/model/input/sysin"
|
||||
|
||||
type UpdateConfigInp struct {
|
||||
sysin.UpdateAddonsConfigInp
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// 插件 logic 中调用主模块服务
|
||||
import isc "hotgo/internal/service"
|
||||
|
||||
func (s *sSysConfig) UpdateConfigByGroup(ctx context.Context, in sysin.UpdateConfigInp) error {
|
||||
in.UpdateAddonsConfigInp.AddonName = global.GetSkeleton().Name
|
||||
return isc.SysAddonsConfig().UpdateConfigByGroup(ctx, in.UpdateAddonsConfigInp)
|
||||
}
|
||||
```
|
||||
|
||||
### 插件获取自身信息
|
||||
|
||||
```go
|
||||
// 插件内部
|
||||
global.GetSkeleton()
|
||||
|
||||
// 任意位置判断当前请求是否为插件请求
|
||||
contexts.IsAddonRequest(ctx)
|
||||
contexts.GetAddonName(ctx)
|
||||
```
|
||||
|
||||
### 插件隐式注册
|
||||
|
||||
在 `server/addons/modules/` 下新建 `<addonName>.go`,内容仅含一行匿名导入:
|
||||
|
||||
```go
|
||||
package modules
|
||||
|
||||
import _ "hotgo/addons/<addonName>"
|
||||
```
|
||||
|
||||
### 新增插件检查清单
|
||||
|
||||
**后端:**
|
||||
- [ ] `addons/<addonName>/main.go` — 插件入口,实现 Module 接口
|
||||
- [ ] `addons/<addonName>/global/` — global.go + init.go
|
||||
- [ ] `addons/modules/<addonName>.go` — 隐式注册
|
||||
- [ ] `addons/<addonName>/service/` — 插件内接口声明(独立包)
|
||||
- [ ] `addons/<addonName>/logic/logic.go` — 匿名导入触发 init()
|
||||
- [ ] `addons/<addonName>/logic/sys/<module>.go` — 业务实现
|
||||
- [ ] `addons/<addonName>/model/input/sysin/<module>.go` — 入参/出参
|
||||
- [ ] `addons/<addonName>/api/admin/<module>/<module>.go` — API 契约
|
||||
- [ ] `addons/<addonName>/controller/admin/sys/<module>.go` — 薄控制器
|
||||
- [ ] `addons/<addonName>/router/admin.go` — 路由注册
|
||||
- [ ] `addons/<addonName>/router/genrouter/init.go` — 插件 genrouter
|
||||
- [ ] `addons/<addonName>/router/genrouter/<module>.go` — 模块路由追加
|
||||
|
||||
**前端:**
|
||||
- [ ] `web/src/api/addons/<addonName>/index.ts` — API 封装
|
||||
- [ ] `web/src/views/addons/<addonName>/model.ts` — State、schemas、columns
|
||||
- [ ] `web/src/views/addons/<addonName>/index.vue` — 列表页
|
||||
- [ ] `web/src/views/addons/<addonName>/edit.vue` — 编辑页
|
||||
|
||||
参考实现:`server/addons/hgexample/`(完整插件示例)、`server/resource/generate/default/addon/`(生成模板)
|
||||
|
||||
---
|
||||
|
||||
## 标准 CRUD 接口清单
|
||||
|
||||
| 接口 | method | 说明 |
|
||||
|------|--------|------|
|
||||
| `/xxx/list` | GET | 分页列表 |
|
||||
| `/xxx/view` | GET | 详情 |
|
||||
| `/xxx/edit` | POST | 新增/编辑(id>0 为编辑) |
|
||||
| `/xxx/delete` | POST | 删除(支持批量,传 ids) |
|
||||
| `/xxx/status` | POST | 启用/禁用状态切换 |
|
||||
| `/xxx/switch` | POST | 开关字段切换 |
|
||||
| `/xxx/maxSort` | GET | 获取最大排序值 |
|
||||
| `/xxx/export` | GET | 导出列表 |
|
||||
|
||||
按实际需求裁剪,不强制全部实现。
|
||||
|
||||
---
|
||||
|
||||
## 前端目录结构
|
||||
|
||||
```
|
||||
web/src/
|
||||
├── api/<module>/index.ts # HTTP 请求封装
|
||||
├── views/<module>/
|
||||
│ ├── index.vue # 列表页
|
||||
│ ├── edit.vue # 新增/编辑弹窗
|
||||
│ └── model.ts # State 类、rules、schemas、columns、loadOptions
|
||||
├── components/ # 通用组件(BasicTable、BasicForm、BasicModal 等)
|
||||
├── store/modules/dict.ts # 字典 store(loadOptions、getLabel、getOptions)
|
||||
└── utils/
|
||||
├── http/axios.ts # HTTP 封装(http.request / jumpExport)
|
||||
└── dateUtil.ts # 日期工具(defRangeShortcuts 等)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前端编码约定
|
||||
|
||||
### API 文件 `api/<module>/index.ts`
|
||||
|
||||
- 使用 `http.request`,禁止直接使用 axios
|
||||
- 函数名与接口语义一致:`List`、`Edit`、`Delete`、`View`、`Status`、`Switch`、`Export`、`MaxSort`
|
||||
- 导出接口用 `jumpExport('/xxx/export', params)`
|
||||
|
||||
```ts
|
||||
import { http, jumpExport } from '@/utils/http/axios';
|
||||
|
||||
export function List(params) {
|
||||
return http.request({ url: '/xxxModule/list', method: 'get', params });
|
||||
}
|
||||
export function Edit(params) {
|
||||
return http.request({ url: '/xxxModule/edit', method: 'POST', params });
|
||||
}
|
||||
export function Export(params) {
|
||||
jumpExport('/xxxModule/export', params);
|
||||
}
|
||||
```
|
||||
|
||||
### model.ts 结构
|
||||
|
||||
- `class State`:属性与后端 entity 字段对应,提供合理默认值
|
||||
- `newState()`:工厂函数,支持深拷贝(`cloneDeep`)
|
||||
- `rules`:Naive UI 表单校验规则
|
||||
- `schemas`:搜索栏 `FormSchema[]`(`ref` 包装)
|
||||
- `columns`:表格列定义,含权限守卫的操作用 `hasPermission(['/xxx/edit'])`
|
||||
- `loadOptions()`:调用 `dict.loadOptions([...])`,预加载字典数据
|
||||
|
||||
### 组件使用规范
|
||||
|
||||
**必须优先使用 Naive UI 原生组件**,能用 Naive UI 实现的禁止自行封装替代组件:
|
||||
|
||||
| 场景 | 必须使用的组件 |
|
||||
|------|--------------|
|
||||
| 表格 | 项目封装 `BasicTable`(基于 Naive UI) |
|
||||
| 表单 | 项目封装 `BasicForm`(基于 Naive UI) |
|
||||
| 弹窗 | `n-modal` 或项目封装 `BasicModal` |
|
||||
| 按钮 | `n-button`,类型用 `primary`/`default`/`error` 等语义值 |
|
||||
| 选择器 | `n-select`(字典下拉)、`n-tree-select`(树形) |
|
||||
| 开关 | `n-switch`,配合 Switch 接口 |
|
||||
| 输入框 | `n-input` |
|
||||
| 日期选择 | `n-date-picker`,范围选择配合 `defRangeShortcuts()` |
|
||||
| 图片渲染 | `renderImage(url)` |
|
||||
| 文件渲染 | `renderFile(url)` |
|
||||
| 成员信息 | `renderPopoverMemberSumma(summa)` |
|
||||
|
||||
**禁止行为:**
|
||||
- 禁止引入 Element Plus、Ant Design Vue 等其他 UI 库
|
||||
- 禁止使用内联 style(使用 Naive UI 的 props 或 class)
|
||||
- 禁止在模板里硬编码字典值文本,必须通过 `dict.getLabel()` 渲染
|
||||
|
||||
### 权限控制
|
||||
|
||||
```ts
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
// 模板中
|
||||
v-if="hasPermission(['/xxxModule/edit'])"
|
||||
|
||||
// columns 中开关操作
|
||||
disabled: !hasPermission(['/xxxModule/switch'])
|
||||
```
|
||||
|
||||
权限标识与后端路由路径保持一致,**路由由后端菜单数据驱动,禁止在源码里硬编码路由路径**。
|
||||
|
||||
### Vue 3 代码规范
|
||||
|
||||
- 使用 Composition API(`<script setup>`)
|
||||
- 响应式数据用 `ref` / `reactive`,计算属性用 `computed`
|
||||
- 字典选项传给组件时用 `computed(() => dict.getOptions('XxxOptions'))`,保持响应式
|
||||
|
||||
---
|
||||
|
||||
## 新增 CRUD 模块检查清单
|
||||
|
||||
**后端(必须全部完成,顺序建议从下往上):**
|
||||
|
||||
- [ ] `internal/consts/<module>.go` — 常量定义、选项变量、`init()` 注册字典
|
||||
- [ ] 使用 goframe CLI 生成:`entity`、`do`、`dao/internal`、`dao`(禁止手写)
|
||||
- [ ] `internal/model/input/sysin/<module>.go` — 入参/出参/过滤模型
|
||||
- [ ] `internal/service/sys.go` — 追加接口声明与注册函数
|
||||
- [ ] `internal/logic/sys/<module>.go` — 业务实现
|
||||
- [ ] `internal/controller/admin/sys/<module>.go` — 薄控制器
|
||||
- [ ] `api/admin/<module>/<module>.go` — API 契约
|
||||
- [ ] `internal/router/genrouter/<module>.go` — 路由注册(`LoginRequiredRouter` 追加)
|
||||
|
||||
**前端(必须全部完成):**
|
||||
|
||||
- [ ] `web/src/api/<module>/index.ts` — HTTP 封装
|
||||
- [ ] `web/src/views/<module>/model.ts` — State、schemas、columns、rules、loadOptions
|
||||
- [ ] `web/src/views/<module>/index.vue` — 列表页
|
||||
- [ ] `web/src/views/<module>/edit.vue` — 编辑页
|
||||
|
||||
---
|
||||
|
||||
## 参考实现
|
||||
|
||||
**所有新模块必须严格对齐以下文件的风格,生成前请逐一阅读:**
|
||||
|
||||
**主模块 CRUD 参考:**
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `server/internal/consts/status.go` | 字典常量定义与注册标准写法 |
|
||||
| `server/api/admin/curddemo/curddemo.go` | API 契约标准写法 |
|
||||
| `server/internal/controller/admin/sys/curd_demo.go` | 薄控制器标准写法 |
|
||||
| `server/internal/logic/sys/curd_demo.go` | Logic 标准写法(含关联查询、导出、字段过滤) |
|
||||
| `server/internal/model/input/sysin/curd_demo.go` | 入参/出参模型标准写法 |
|
||||
| `server/internal/router/genrouter/curd_demo.go` | 主模块路由注册标准写法 |
|
||||
| `web/src/api/curdDemo/index.ts` | 前端 API 封装标准写法 |
|
||||
| `web/src/views/curdDemo/model.ts` | 前端 model 标准写法(含字典加载) |
|
||||
| `web/src/views/curdDemo/index.vue` | 列表页标准写法 |
|
||||
| `web/src/views/curdDemo/edit.vue` | 编辑页标准写法 |
|
||||
|
||||
**插件参考(以 hgexample 为标准):**
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `server/addons/hgexample/main.go` | 插件入口标准写法 |
|
||||
| `server/addons/hgexample/global/init.go` | 插件 global 标准写法 |
|
||||
| `server/addons/hgexample/router/admin.go` | 插件路由注册标准写法 |
|
||||
| `server/addons/hgexample/router/genrouter/init.go` | 插件 genrouter 标准写法 |
|
||||
| `server/addons/hgexample/router/genrouter/tenant_order.go` | 插件模块路由追加标准写法 |
|
||||
| `server/addons/hgexample/logic/sys/tenant_order.go` | 插件业务逻辑标准写法 |
|
||||
| `server/addons/hgexample/logic/sys/config.go` | 插件调用主模块服务标准写法 |
|
||||
|
||||
代码生成模板:
|
||||
- 主模块 CRUD:`server/resource/generate/default/curd/`
|
||||
- 插件脚手架:`server/resource/generate/default/addon/`
|
||||
|
||||
以上模板可参考逻辑,**禁止直接修改模板文件**。
|
||||
|
||||
---
|
||||
|
||||
## 错误处理规范
|
||||
|
||||
### 基本原则
|
||||
|
||||
- 业务错误直接用 `gerror.New("描述清晰的中文信息")` 返回,GoFrame 框架会统一处理响应码
|
||||
- 错误信息要面向用户,清晰说明**发生了什么**,而不是堆栈路径
|
||||
|
||||
### 错误创建方式
|
||||
|
||||
```go
|
||||
import "github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
// 普通业务错误(最常用)
|
||||
return gerror.New("用户名已存在")
|
||||
|
||||
// 包装底层错误,追加上下文(调用第三方/dao 层出错时使用)
|
||||
if err := dao.User.Ctx(ctx).Data(data).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "创建用户失败")
|
||||
}
|
||||
|
||||
// 带格式化参数
|
||||
return gerror.Newf("订单 %d 状态不合法,当前状态:%d", in.Id, order.Status)
|
||||
```
|
||||
|
||||
### 错误传递原则
|
||||
|
||||
- **logic 层**:遇到底层错误用 `gerror.Wrap` 追加上下文后返回;纯业务校验失败用 `gerror.New` 直接返回
|
||||
- **controller 层**:直接 `return` 透传 logic 的 error,**不再包装**,不打印日志(框架统一处理)
|
||||
- **禁止**在 controller 层 `fmt.Println(err)` 或 `g.Log().Error` 打印已经会被框架记录的错误
|
||||
|
||||
---
|
||||
|
||||
## 缓存使用规范
|
||||
|
||||
### 统一使用 `cache.Instance()`
|
||||
|
||||
**禁止直接调用 `g.Redis().Do()`**,项目统一通过 `cache.Instance()` 操作缓存,底层适配器(Memory / Redis / File)由配置决定,业务层无需关心。
|
||||
|
||||
```go
|
||||
import (
|
||||
"hotgo/internal/library/cache"
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 设置缓存(带过期时间)
|
||||
err := cache.Instance().Set(ctx, "user:info:1", userInfo, 30*time.Minute)
|
||||
|
||||
// 获取缓存
|
||||
val, err := cache.Instance().Get(ctx, "user:info:1")
|
||||
if err != nil || val.IsNil() {
|
||||
// 缓存未命中,查数据库
|
||||
}
|
||||
|
||||
// 删除缓存
|
||||
_, err = cache.Instance().Remove(ctx, "user:info:1")
|
||||
|
||||
// GetOrSet:缓存不存在时自动执行函数并写入(推荐用于只读类缓存)
|
||||
val, err := cache.Instance().GetOrSet(ctx, "user:info:1", func(ctx context.Context) (interface{}, error) {
|
||||
return dao.AdminMember.Ctx(ctx).Where(...).One()
|
||||
}, 30*time.Minute)
|
||||
```
|
||||
|
||||
### 缓存 Key 命名规范
|
||||
|
||||
- 格式:`模块:实体:唯一标识`,例如:`user:info:1`、`config:site:basic`、`dict:options:XxxStatus`
|
||||
- 同一模块的 Key 统一在 `internal/consts/cache.go`(或对应模块的 consts 文件)中定义为常量,禁止散落在 logic 里硬编码
|
||||
- 插件缓存 Key 加插件名前缀:`hgexample:order:1`
|
||||
|
||||
### 缓存使用原则
|
||||
|
||||
- 更新或删除数据后,**主动清除**相关缓存,不依赖过期自动失效
|
||||
- 缓存时间:频繁更新的数据(配置、状态)用较短时间(5~30 分钟);基本不变的数据(字典、枚举)可用更长时间
|
||||
- 缓存穿透场景:查询结果为空时也写入短时缓存(如 1 分钟)防止击穿
|
||||
|
||||
---
|
||||
|
||||
## 日志规范
|
||||
|
||||
### 日志级别选择
|
||||
|
||||
| 级别 | 方法 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| Info | `g.Log().Info` / `g.Log().Infof` | 正常业务流程的关键节点(登录成功、订单创建、支付回调等) |
|
||||
| Warning | `g.Log().Warning` / `g.Log().Warningf` | **遇到错误但不影响主流程**(重试、降级、外部服务异常、非致命校验失败) |
|
||||
| Panic | `g.Log().Panic` / `g.Log().Panicf` | 极其严重、必须立即终止的情况(配置缺失、核心依赖不可用) |
|
||||
|
||||
### 使用示例
|
||||
|
||||
```go
|
||||
// Info:记录正常流程关键事件
|
||||
g.Log().Infof(ctx, "用户登录成功 uid:%d ip:%s", member.Id, ip)
|
||||
|
||||
// Info:记录外部调用结果
|
||||
g.Log().Info(ctx, "支付回调处理完成", g.Map{"orderId": orderId, "status": status})
|
||||
|
||||
// Warning:外部接口失败但已降级处理
|
||||
g.Log().Warningf(ctx, "短信发送失败,已跳过 mobile:%s err:%+v", mobile, err)
|
||||
|
||||
// Warning:业务异常但不中断流程
|
||||
g.Log().Warning(ctx, "用户余额不足,跳过自动扣款", g.Map{"uid": uid, "balance": balance})
|
||||
|
||||
// Panic:启动时核心配置缺失
|
||||
g.Log().Panicf(ctx, "缓存未初始化,无法启动: %+v", err)
|
||||
```
|
||||
|
||||
### 日志上下文原则
|
||||
|
||||
- 日志**必须传入 `ctx`**(第一个参数),便于链路追踪
|
||||
- 关键日志需携带足够的业务字段(uid、orderId、requestId 等),用 `g.Map{}` 附加结构化信息
|
||||
- **禁止**用 `fmt.Println` 替代日志,禁止在 logic/controller 层裸打印调试信息
|
||||
|
||||
---
|
||||
|
||||
## 数据库建表约定
|
||||
|
||||
建表时遵循以下字段命名约定,代码生成器和 ORM hook 会自动识别并处理:
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| `id` | bigint(20) NOT NULL AUTO_INCREMENT | 主键,必须 |
|
||||
| `created_at` | datetime | 创建时间,**自动写入**,禁止手动赋值 |
|
||||
| `updated_at` | datetime | 更新时间,**自动维护**,禁止手动赋值 |
|
||||
| `deleted_at` | datetime | 软删除时间,存在时查询自动加 `IS NULL`,删除只更新此字段 |
|
||||
| `created_by` | bigint(20) | 创建者 ID,**自动写入** |
|
||||
| `updated_by` | bigint(20) | 更新者 ID,**自动维护** |
|
||||
| `deleted_by` | bigint(20) | 删除者 ID,**自动写入** |
|
||||
| `status` | tinyint | 状态字段,默认使用系统字典 `sys_normal_disable` |
|
||||
| `sort` | int | 排序字段,存在时编辑表单自动获取最大值+1填充 |
|
||||
| `pid` | bigint(20) | 树表上级ID(树表必须) |
|
||||
| `level` | int | 树等级(树表必须,自动维护) |
|
||||
| `tree` | varchar(512) | 关系树(树表必须,自动维护) |
|
||||
| `tenant_id` | bigint(20) | 租户ID(多租户场景按需加) |
|
||||
|
||||
**特殊字段查询方式(代码生成器默认生成):**
|
||||
- `string` 类型字段 → `LIKE` 模糊查询
|
||||
- `int/uint/int64` → `=` 精确查询
|
||||
- `created_by`/`updated_by`/`deleted_by` → 关键词查询(ID/用户名/手机号匹配),调用 `service.AdminMember().GetIdsByKeyword()`
|
||||
|
||||
**数据类型 → Go 类型映射(影响生成代码的字段类型):**
|
||||
- `tinyint/int/smallint` → `int`;加 `unsigned` → `uint`
|
||||
- `bigint` → `int64`;加 `unsigned` → `uint64`
|
||||
- `varchar/text/char` → `string`
|
||||
- `datetime/timestamp` → `*gtime.Time`
|
||||
- `json/jsonb` → `*gjson.Json`
|
||||
- `decimal/float/double` → `float64`
|
||||
|
||||
---
|
||||
|
||||
## 数据权限(权限过滤)
|
||||
|
||||
业务查询中如需过滤数据权限(仅自己/所属部门/下级等),在 ORM 链式调用中加入 handler:
|
||||
|
||||
```go
|
||||
import "hotgo/internal/library/hgorm/handler"
|
||||
|
||||
// 按登录用户的数据权限范围过滤(表需含 created_by 或 member_id 字段)
|
||||
dao.XxxTable.Ctx(ctx).Handler(handler.FilterAuth).Scan(&res)
|
||||
|
||||
// 表中无标准字段时,指定自定义字段
|
||||
dao.XxxTable.Ctx(ctx).Handler(handler.FilterAuthWithField("custom_field")).Scan(&res)
|
||||
|
||||
// 多租户数据权限过滤(表需含 tenant_id 字段)
|
||||
// 在 Model() 方法中通过 handler.Option 开启
|
||||
func (s *sSysXxx) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{FilterTenant: true})
|
||||
}
|
||||
return handler.Model(dao.XxxTable.Ctx(ctx), option...)
|
||||
}
|
||||
```
|
||||
|
||||
多租户增改时自动维护租户关系,在 ORM 操作后加 `.Hook(hook.SaveTenant)`:
|
||||
|
||||
```go
|
||||
import "hotgo/internal/library/hgorm/hook"
|
||||
|
||||
// 新增时自动写入 tenant_id/merchant_id/user_id
|
||||
dao.XxxTable.Ctx(ctx).Fields(sysin.XxxInsertFields{}).Hook(hook.SaveTenant).Data(in).Insert()
|
||||
|
||||
// 更新时自动维护
|
||||
s.Model(ctx).Fields(sysin.XxxUpdateFields{}).WherePri(in.Id).Data(in).Hook(hook.SaveTenant).Update()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 定时任务(Crons)
|
||||
|
||||
定时任务通过实现接口 + `init()` 注册,在后台「系统设置 → 定时任务」中配置执行策略:
|
||||
|
||||
**主模块**:新建 `server/internal/crons/<name>.go`
|
||||
**插件**:新建 `server/addons/<addonName>/crons/<name>.go`
|
||||
|
||||
```go
|
||||
package crons
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/library/cron"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cron.Register(MyTask)
|
||||
}
|
||||
|
||||
var MyTask = &cMyTask{name: "myTask"} // name 与后台配置的任务名称一致
|
||||
|
||||
type cMyTask struct{ name string }
|
||||
|
||||
func (c *cMyTask) GetName() string { return c.name }
|
||||
|
||||
func (c *cMyTask) Execute(ctx context.Context, parser *cron.Parser) (err error) {
|
||||
parser.Logger.Infof(ctx, "任务执行:%v", ...)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 消息队列(Queues)
|
||||
|
||||
队列采用接口 + `init()` 注册,topic 统一定义在 `internal/consts/queue.go`(或对应模块 consts 文件)。
|
||||
|
||||
**主模块**:新建 `server/internal/queues/<name>.go`
|
||||
**插件**:新建 `server/addons/<addonName>/queues/<name>.go`
|
||||
|
||||
```go
|
||||
package queues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/queue"
|
||||
)
|
||||
|
||||
func init() {
|
||||
queue.RegisterConsumer(MyConsumer)
|
||||
}
|
||||
|
||||
var MyConsumer = &qMyConsumer{}
|
||||
|
||||
type qMyConsumer struct{}
|
||||
|
||||
func (q *qMyConsumer) GetTopic() string { return consts.QueueMyTopic }
|
||||
|
||||
func (q *qMyConsumer) Handle(ctx context.Context, mqMsg queue.MqMsg) (err error) {
|
||||
var data MyData
|
||||
if err = json.Unmarshal(mqMsg.Body, &data); err != nil {
|
||||
return
|
||||
}
|
||||
// 处理消息...
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
**发送消息:**
|
||||
|
||||
```go
|
||||
// 即时消息
|
||||
queue.Push(consts.QueueMyTopic, data)
|
||||
|
||||
// 延迟消息(仅 redis/rocketmq 驱动支持)
|
||||
queue.SendDelayMsg(consts.QueueMyTopic, data, 10) // redis: 延迟秒数;rocketmq: 延迟级别
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具库使用规范
|
||||
|
||||
**后端(`server/utility/`)优先使用已有工具,禁止重复造轮子:**
|
||||
|
||||
| 包 | 用途 |
|
||||
|----|------|
|
||||
| `utility/convert` | 数据类型转换 |
|
||||
| `utility/encrypt` | 加密/解密 |
|
||||
| `utility/excel` | 电子表格导出/导入(导出接口用此包) |
|
||||
| `utility/validate` | 数据验证工具 |
|
||||
| `utility/tree` | 树形结构处理 |
|
||||
| `utility/simple` | 简捷函数集合 |
|
||||
| `utility/format` | 数据格式化 |
|
||||
|
||||
**前端(`web/src/utils/`)优先使用已有工具:**
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| `@/utils/http/axios`(`http.request`) | HTTP 请求,**唯一入口** |
|
||||
| `@/utils/dateUtil`(`defRangeShortcuts`) | 日期范围快捷选项 |
|
||||
| `@/utils`(`renderImage`、`renderFile`、`renderPopoverMemberSumma`) | 表格渲染工具函数 |
|
||||
| `@/store/modules/dict`(`useDictStore`) | 字典数据加载与渲染 |
|
||||
| `@/hooks/web/usePermission`(`hasPermission`) | 权限判断 |
|
||||
|
||||
---
|
||||
|
||||
## 树表(Tree CRUD)特殊约定
|
||||
|
||||
树表在普通 CRUD 基础上有以下差异:
|
||||
|
||||
- 表中必须含 `pid`、`level`、`tree` 字段(参见建表约定)
|
||||
- `pid`/`level`/`tree` 字段**由系统自动维护**,禁止在编辑表单中手动设置这三个字段
|
||||
- 前端列表页使用树形表格(`BasicTable` 的 `childrenKey` 或 `treeData` 模式),而非普通分页列表
|
||||
- 参考实现:`server/internal/logic/sys/tree_demo.go`、`web/src/views/develop/curd/treeDemo/`
|
||||
|
||||
---
|
||||
|
||||
## 响应格式约定
|
||||
|
||||
所有接口统一响应格式(由框架中间件自动处理,**无需在 controller 手动包装**):
|
||||
|
||||
```json
|
||||
{ "code": 0, "message": "操作成功", "timestamp": 1234567890, "traceID": "xxx", "data": {} }
|
||||
```
|
||||
|
||||
- 成功状态码:`0`;失败状态码:`-1`(GoFrame 内置,无需手动设置)
|
||||
- `gerror.Wrap(err, "用户友好的提示")` → 客户端只看到最外层提示,开发者日志可见完整堆栈
|
||||
- 禁止在 controller 层手动 `Write`/`WriteJSON` 等直接写响应(破坏统一格式)
|
||||
|
||||
---
|
||||
|
||||
## 常见错误禁止清单
|
||||
|
||||
**后端:**
|
||||
- 禁止在 controller 层直接调用 dao
|
||||
- 禁止硬编码字段名字符串(用 `dao.Table.Columns().Field`)
|
||||
- 禁止手改 `entity/`、`do/`、`dao/internal/` 目录下的文件
|
||||
- 禁止在 logic 层重复做 `Filter()` 已完成的校验
|
||||
- 禁止在 `internal/service/sys.go` 之外新建主模块 service 文件(同文件追加)
|
||||
- 禁止插件 import 另一个插件的包(插件间完全隔离,通过主模块 service 通信)
|
||||
- 禁止在插件路由文件里硬编码路由前缀(用 `addons.RouterPrefix()` 生成)
|
||||
- 禁止在 `internal/library/` 下修改基础库(如需扩展,提 issue 或在 utility/ 添加)
|
||||
|
||||
**前端:**
|
||||
- 禁止直接使用 axios,必须用 `http.request`
|
||||
- 禁止硬编码字典值文本,必须用 `dict.getLabel()` 渲染
|
||||
- 禁止引入 Naive UI 以外的 UI 框架
|
||||
- 禁止内联 style
|
||||
- 禁止在源码里硬编码菜单路由路径
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-present HotGo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,167 +1,2 @@
|
||||
# HotGo-V2
|
||||
<div align="center">
|
||||
<img width="140px" src="https://gmycos.facms.cn/hotgo/logo.sig.png">
|
||||
<p>
|
||||
<h1>HotGo V2</h1>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://goframe.org/pages/viewpage.action?pageId=1114119" target="_blank">
|
||||
<img src="https://img.shields.io/badge/goframe-2.10.0-green" alt="goframe">
|
||||
</a>
|
||||
<a href="https://v3.vuejs.org/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/vue.js-vue3.4-green" alt="vue">
|
||||
</a>
|
||||
<a href="https://www.naiveui.com" target="_blank">
|
||||
<img src="https://img.shields.io/badge/naiveui-%3E2.44.1-blue" alt="naiveui">
|
||||
</a>
|
||||
<a href="https://www.tslang.cn/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/typescript-%3E4.0.0-blue" alt="typescript">
|
||||
</a>
|
||||
<a href="https://vitejs.dev/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/vite-%3E5.4.2-yellow" alt="vite">
|
||||
</a>
|
||||
<a href="https://github.com/bufanyun/hotgo/blob/v2.0/LICENSE" target="_blank">
|
||||
<img src="https://img.shields.io/badge/license-MIT-success" alt="license">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
## 平台简介
|
||||
* 基于 GoFrame2 + Vue3 + NaiveUI + uniapp 的企业级全栈框架,为二次开发而生。
|
||||
* 前端采用 Naive-Ui-Admin、Vue、Naive UI、uniapp。
|
||||
* 企业级 AI 赋能基础开发平台,内置 AI 开发规范,适配主流 AI 开发工具,实现人机协同开发,大幅提升开发效率。
|
||||
|
||||
## 演示地址
|
||||
- [https://hotgo.facms.cn/admin](https://hotgo.facms.cn/admin)
|
||||
> 账号:admin 密码:123456
|
||||
|
||||
### 使用文档
|
||||
|
||||
[安装文档](docs/guide-zh-CN/start-installation.md) · [本地文档](docs/guide-zh-CN/README.md) · [更新历史](docs/guide-zh-CN/start-update-log.md) · [常见问题](docs/guide-zh-CN/start-issue.md)
|
||||
|
||||
|
||||
## 特征
|
||||
* 高生产率:极强的可扩展性,应用化、模块化、插件化机制敏捷开发,几分钟即可搭建一个应用开发骨架。
|
||||
* 多应用入口:多入口分为 Admin (后台)、Home (前台页面)、Api (对外通用接口)、WebSocket (即时通讯接口),不同的业务,进入不同的应用入口。
|
||||
* 极致的插件化: 微核架构,功能隔离,高可定制性,可以渐进式开发,亦可以多人协同开发。支持一键创建插件模板、一键安装、更新、卸载插件、可以非常方便的将插件迁移到新项目中。
|
||||
* 快速生成代码:无需编写代码,只需创建表进行简单配置就能生成一个完善的 CURD、树表等常用的开发代码,其中所需表单控件也是勾选即可直接生成。
|
||||
* 认证机制:采用 JWT 的用户状态认证及 casbin 的权限认证
|
||||
* 路由模式:得益于 GoFrame 提供了规范化的路由注册方式,无需注解自动生成api文档
|
||||
* 模块化设计,面向接口开发
|
||||
|
||||
|
||||
## 内置功能
|
||||
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
2. 部门管理:配置系统组织机构(公司、部门、岗位),树结构展现支持数据权限。
|
||||
3. 岗位管理:配置系统用户所属担任职务。
|
||||
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||
5. 角色管理:角色菜单权限分配、设置角色按机构或按上下级关系进行数据范围权限划分。
|
||||
6. 字典管理:对系统中经常使用的一些特定数据进行维护,支持枚举字典和自定义方法字典。
|
||||
7. 配置管理:对系统动态配置常用参数。
|
||||
8. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
9. 登录日志:系统登录日志记录查询包含登录异常。
|
||||
10. 服务日志:服务端运行所产生的警告、异常、崩溃日志的详细数据和堆栈信息。
|
||||
11. 支付网关:集成支付宝、微信支付、QQ支付等多种支付方式,只需简单配置即可使用。
|
||||
12. 资金管理:支持在线充值、订单申请/原路退款、资金提现、资金/积分变动明细等通用模块。
|
||||
13. 在线用户:当前系统中活跃用户状态监控。
|
||||
14. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
|
||||
15. 代码生成:支持自动化生成前后端代码。CURD关联表、树表、消息队列、定时任务一键生成等。
|
||||
16. 插件应用:支持一键生成插件模板,每个插件之间开发隔离,拥有独立多应用入口、独立配置。完美支持多人协同开发、插件插拔不会对原系统产生影响等。
|
||||
17. 服务监控:监视当前系统CPU、内存、磁盘、网络、堆栈等相关信息。
|
||||
18. 附件管理:文件图片上传,大文件分片上传、断点续传,支持本地、阿里云oss、腾讯云cos、ucloud对象存储、七牛云对象存储、minio等多种上传驱动,后台一键切换配置,并集成了文件选择器。
|
||||
19. TCP服务:基于gtcp的服务应用,支持长连接、断线重连、服务认证、路由分发、RPC消息、拦截器和数据绑定等。简化和规范了服务器开发流程。
|
||||
20. 消息队列:同时兼容 kafka、redis、rocketmq、磁盘队列,一键配置切换到场景适用的MQ。
|
||||
21. 通知公告:采用WebSocket实时推送在线用户最新通知、公告、私信消息。
|
||||
22. 地区编码:整合国内通用省市区编码,运用于项目于一身,支持动态省市区选项。
|
||||
23. 常用工具:集成常用的工具包和命令行工具,可以快速开发自定义命令行,多种启动入口。
|
||||
|
||||
|
||||
> HotGo开源以来得到了大家的很多支持,本项目初衷只为互相学习交流,没有任何盈利性目的!欢迎为HotGo贡献代码或提供建议!
|
||||
|
||||
|
||||
## 演示图
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/1.png"/></td>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/2.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/3.png"/></td>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/4.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/5.png"/></td>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/6.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/7.png"/></td>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/8.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/9.png"/></td>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/10.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/11.png"/></td>
|
||||
<td><img src="./docs/guide-zh-CN/images/demo/12.png"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## 感谢(排名不分先后)
|
||||
> gf框架 [https://github.com/gogf/gf](https://github.com/gogf/gf)
|
||||
>
|
||||
> naive-ui [https://www.naiveui.com](https://www.naiveui.com)
|
||||
>
|
||||
> naive-ui-admin [https://github.com/jekip/naive-ui-admin](https://github.com/jekip/naive-ui-admin)
|
||||
>
|
||||
> websocket [https://github.com/gorilla/websocket](github.com/gorilla/websocket)
|
||||
>
|
||||
> casbin [https://github.com/casbin/casbin](https://github.com/casbin/casbin)
|
||||
>
|
||||
> gopay [https://github.com/go-pay/gopay](https://github.com/go-pay/gopay)
|
||||
|
||||
|
||||
## 交流QQ群
|
||||
交流群①:190966648 <a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=mJafkvme3VNyiQlCFIFNRtY8Xlr7pj9U&jump_from=webapi&authKey=jL10vIESr+vO8wpxwyd6DlChzkrbHpzN9uhAsIHgAinL/Vvd+nvuRyilf2UqUlCy"><img border="0" src="https://gmycos.facms.cn/hotgo/group.png" alt="HotGo框架交流1群" title="HotGo框架交流1群"></a>
|
||||
> <img src="https://gmycos.facms.cn/hotgo/hotgo1qun.png" width="400px"/>
|
||||
|
||||
|
||||
## 商用说明
|
||||
|
||||
> HotGo 是开源免费的,遵循 MIT 开源协议,意味着您无需支付任何费用,也无需授权,即可将它应用到您的产品中。
|
||||
|
||||
* 使用本项目必须保留所有版权信息。
|
||||
|
||||
* 本项目包含的第三方源码和二进制文件之版权信息另行标注。
|
||||
|
||||
* 版权所有Copyright © 2020-2026 by Ms (https://github.com/bufanyun/hotgo)
|
||||
|
||||
* All rights reserved。
|
||||
|
||||
|
||||
## 免责声明:
|
||||
* HotGo为开源学习项目,一切商业行为与HotGo无关。
|
||||
|
||||
* 用户不得利用HotGo从事非法行为,用户应当合法合规的使用,发现用户在使用产品时有任何的非法行为,HotGo有权配合有关机关进行调查或向政府部门举报,HotGo不承担用户因非法行为造成的任何法律责任,一切法律责任由用户自行承担,如因用户使用造成第三方损害的,用户应当依法予以赔偿。
|
||||
|
||||
* 所有与使用HotGo相关的资源直接风险均由用户承担。
|
||||
|
||||
|
||||
#### 如果对您有帮助,您可以点右上角 💘Star💘支持
|
||||
|
||||
|
||||
|
||||
## [感谢JetBrains提供的免费GoLand](https://jb.gg/OpenSource)
|
||||
[](https://jb.gg/OpenSource)
|
||||
|
||||
|
||||
## License
|
||||
[MIT © HotGo-2026](./LICENSE)
|
||||
|
||||
|
||||
|
||||
|
||||
# im_manager
|
||||
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
# IM 后台未实现功能清单
|
||||
|
||||
> 编写日期:2026-08-16
|
||||
> 目的:记录当前 HotGo IM 后台已经完成的能力、尚未实现的后台功能,以及后续应优先通过 `im_server` 管理 API 完成的写操作。
|
||||
|
||||
## 1. 当前已实现
|
||||
|
||||
### 1.1 平台控制库
|
||||
|
||||
已实现:
|
||||
|
||||
| 模块 | 当前能力 |
|
||||
| --- | --- |
|
||||
| IM 租户管理 | 租户列表、详情、新增/编辑、状态切换 |
|
||||
| IM 租户配额配置 | `im_tenant_config` 随租户编辑维护 |
|
||||
| 租户业务库路由 | 后台按 `im_tenant.db_dsn` 或 `db_name` 动态连接租户库 |
|
||||
| 后台菜单 | 已导入 IM 管理根菜单及各模块权限点 |
|
||||
|
||||
未完成:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 租户 provisioning 流程 | 创建租户业务库、执行 schema、初始化系统账号和默认 Topic 尚未实现 |
|
||||
| 租户删除流程 | 删除前检查、异步归档、业务库删除/保留策略未实现 |
|
||||
| 平台级审计写入 | 租户创建、停用、恢复、删除等平台操作还未写平台审计 |
|
||||
| 租户用量统计 | 用户数、群数、文件容量、消息量统计接口未实现 |
|
||||
|
||||
### 1.2 租户业务库只读后台
|
||||
|
||||
已实现:
|
||||
|
||||
| 模块 | 当前能力 |
|
||||
| --- | --- |
|
||||
| IM 用户管理 | 按租户查看用户列表、用户详情 |
|
||||
| IM 群组管理 | 群列表、群详情、群成员列表 |
|
||||
| 群治理只读视图 | 查看 `im_group_settings`、入群申请、群邀请、群公告、禁言记录、群管理日志 |
|
||||
| 实名审核 | 实名提交列表、详情、通过、拒绝 |
|
||||
| 举报反馈工单 | 举报/反馈列表、详情、状态更新 |
|
||||
| 租户审计日志 | `im_audit_logs` 列表、详情,只读 |
|
||||
|
||||
注意:
|
||||
|
||||
1. 群成员列表优先读取 `im_group_members`,如果租户库尚未回填业务真源,会回退读取 `im_subscriptions`。
|
||||
2. 群治理写操作暂未在 HotGo 后台直接实现,避免绕过 `im_server` 的 Topic、Subscription、ACL、审计和 Outbox 状态机。
|
||||
3. 举报反馈当前只更新工单状态,尚未实现“警告、禁言、封禁、撤回消息、通知用户”等处置动作。
|
||||
|
||||
## 2. P0 未实现
|
||||
|
||||
### 2.1 后台认证、租户 Principal 与数据权限
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 租户级 Principal | 后台 Token 中可信 `tenant_id`、角色、权限版本尚未完整建模 |
|
||||
| 企业后台权限边界 | 普通租户管理员只能访问本租户数据的服务端强校验还未系统化 |
|
||||
| 跨租户操作二次验证 | 平台管理员切换租户、导出、代操作缺少二次确认 |
|
||||
| 数据范围权限 | 部门级、群级、安全审计员的数据范围过滤未实现 |
|
||||
|
||||
### 2.2 审计写入
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 平台级审计表 | 控制库中的平台审计表和查询页面尚未实现 |
|
||||
| 租户级审计写入 | 用户、群、实名、举报、反馈、文件等敏感操作尚未统一写 `im_audit_logs` |
|
||||
| 审计导出 | 审计导出、导出审计、脱敏策略未实现 |
|
||||
| 审计不可篡改策略 | 追加写、防删除、防修改、保留周期尚未实现 |
|
||||
|
||||
### 2.3 Outbox 与后台任务
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 租户级 Outbox 查询 | `im_outbox_events`、`im_group_events_outbox` 后台查看未实现 |
|
||||
| Outbox 重试/死信 | 人工重放、死信处理、幂等检查未实现 |
|
||||
| 任务状态监控 | 文件 GC、索引重建、Push、外部审核任务状态未实现 |
|
||||
|
||||
## 3. P1 未实现
|
||||
|
||||
### 3.1 群治理写操作
|
||||
|
||||
这些操作必须优先通过 `im_server` 管理 API 实现,HotGo 后台只调用 API,不直接改业务表:
|
||||
|
||||
| 功能 | 涉及表 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 修改群资料 | `im_group_settings`、`im_topics` | 群名、头像、简介,需同步通信投影 |
|
||||
| 修改治理配置 | `im_group_settings` | 入群审批、全员禁言、上传文件、成员私聊、成员邀请 |
|
||||
| 入群审批 | `im_group_join_requests`、`im_group_members`、`im_subscriptions` | 同意/拒绝,需版本号、幂等、配额校验 |
|
||||
| 邀请处理 | `im_group_invitations`、`im_group_join_requests` | 接受邀请后可能直接入群或进入审批 |
|
||||
| 群公告发布/更新/删除 | `im_group_announcements`、`im_group_announcement_reads` | 需要乐观锁、系统消息、Outbox |
|
||||
| 成员禁言/解除 | `im_group_mutes` | 需写管理日志、审计和群系统事件 |
|
||||
| 管理员管理 | `im_group_members`、`im_subscriptions` | 添加/移除管理员并同步 ACL |
|
||||
| 群主转让 | `im_group_settings`、`im_group_members`、`im_topics` | 二次确认,同步 Owner 和 ACL |
|
||||
| 解散群 | `im_group_settings`、`im_topics`、邀请、二维码、文件入口 | 需撤销入口、写审计、发系统事件 |
|
||||
| 群二维码 | `im_group_qrcodes` | 生成、撤销、过期、扫码解析管理 |
|
||||
|
||||
当前 HotGo 后台只读查看这些表,不做上述写操作。
|
||||
|
||||
### 3.2 文件与存储治理
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 文件列表 | 按租户、用途、状态、MIME、上传人查询 |
|
||||
| 文件详情 | 文件元数据、引用消息、引用群、ACL、扫描状态 |
|
||||
| 文件下载审计 | 后台下载必须写审计 |
|
||||
| 隔离/删除文件 | 必须通过 `im_server` API 处理引用、消息、收藏、举报证据 |
|
||||
| 文件策略 | 单文件大小、总容量、允许 MIME、保留期、病毒扫描策略 |
|
||||
| 群文件治理 | `im_group_files` 列表、删除、下载权限未实现 |
|
||||
|
||||
### 3.3 组织架构与企业通讯录
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 部门管理 | 部门树、移动部门、防环、删除校验 |
|
||||
| 成员归属 | 用户与部门、岗位、负责人关系 |
|
||||
| 组织数据权限 | 按部门/下级部门限制日报、会议、通讯录可见范围 |
|
||||
|
||||
### 3.4 用户与账号安全写操作
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 停用/恢复用户 | 需同步登录、Token、WS、审计 |
|
||||
| 重置密码/安全项 | 必须二次确认并写审计 |
|
||||
| 设备管理 | 查看、踢下线、撤销设备 Token |
|
||||
| 用户导出 | 需要导出审计和字段脱敏 |
|
||||
| 黑名单/好友关系后台 | 查询、处置、修复关系未实现 |
|
||||
|
||||
## 4. P2 未实现
|
||||
|
||||
### 4.1 反诈与内容审核
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 风控策略配置 | 高风险动作、提示文案、二次确认、阻断策略 |
|
||||
| 内容审核策略 | `sync_block`、`async_hold`、`async_post_audit` 配置 |
|
||||
| 审核插件配置 | 平台统一审核服务、租户专属审核服务 |
|
||||
| 风险事件列表 | 确认、取消、过期、阻断记录 |
|
||||
| 审核任务列表 | 待审核消息/文件/图片/视频任务 |
|
||||
| 审核通过/拒绝 | 必须通过 `im_server` 推进状态机 |
|
||||
| 审核撤回消息 | 已发布违规消息系统撤回、审计、Outbox 未实现 |
|
||||
|
||||
### 4.2 举报反馈完整处置
|
||||
|
||||
当前已实现列表、详情、状态更新。未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 举报原因配置 | 按用户、群、消息目标类型配置原因 |
|
||||
| 工单分派 | 分派给处理人或安全团队 |
|
||||
| 工单回复 | 处理结果回复举报人/反馈人 |
|
||||
| 标记重复 | 合并重复举报或反馈 |
|
||||
| 处置动作 | 警告、禁言、封禁用户、解散群、撤回消息 |
|
||||
| 结果通知 | 通过系统通知或客户端查询展示处理结果 |
|
||||
| 证据文件管理 | `im_evidence_files` 或附件引用的 ACL、保留期、下载审计 |
|
||||
|
||||
### 4.3 实名审核完善
|
||||
|
||||
当前已实现列表、详情、通过、拒绝。未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 实名策略配置 | 是否强制实名、允许跳过、提醒周期、重复身份策略 |
|
||||
| 要求补充材料 | 当前只有通过/拒绝,没有补充材料状态 |
|
||||
| 敏感原文查看流程 | 需要权限、二次验证、审计,普通管理员不可查看 |
|
||||
| 审核结果通知 | 审核后通过系统事件通知用户 |
|
||||
| 审核操作审计 | 当前未统一写租户审计 |
|
||||
|
||||
### 4.4 会议、日报、月报
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 会议管理 | 会议列表、详情、参与人响应、提醒 |
|
||||
| 日报/月报列表 | 按部门、成员、状态、时间查询 |
|
||||
| 统计 | 日报/月报提交率、阅读率、部门统计 |
|
||||
| 提醒 | 未提交、未读提醒,需限流和逐项结果 |
|
||||
|
||||
### 4.5 总经理信箱
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 信箱配置 | 是否启用、匿名、必填字段、附件策略、提示文案 |
|
||||
| 提交列表/详情 | 匿名模式下处理人不可见提交人 |
|
||||
| 分派/回复/关闭 | 需要审计和通知 |
|
||||
|
||||
### 4.6 工作台应用配置
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 应用模板 | 平台应用模板、默认权限、分类 |
|
||||
| 租户应用配置 | 启用/停用、排序、权限、外链安全 |
|
||||
| 外部链接审计 | 工作台外链访问和安全策略 |
|
||||
|
||||
### 4.7 Push、短信、TURN、对象存储配置
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| Push 配置 | 租户级 Push 通道和测试 |
|
||||
| 短信配置 | 租户短信模板、签名、发送日志 |
|
||||
| TURN 配置 | 音视频 TURN 服务配置和连通性检查 |
|
||||
| 对象存储配置 | 租户桶、路径、回源、签名、上传策略 |
|
||||
| 外部服务变更审计 | 修改配置必须二次确认和审计 |
|
||||
|
||||
## 5. 后续开发建议顺序
|
||||
|
||||
建议按以下顺序继续:
|
||||
|
||||
1. **审计写入基础设施**:先实现统一 `im_audit_logs` 写入工具和平台审计表。
|
||||
2. **im_server 管理 API 对接层**:HotGo 后台新增统一 client,所有群治理、文件治理、用户处置写操作都走它。
|
||||
3. **群治理写操作**:入群审批、治理配置、公告、禁言、管理员、群主转让、解散。
|
||||
4. **文件治理**:文件列表、群文件、下载审计、隔离/删除、文件策略。
|
||||
5. **Outbox 与任务后台**:事件查看、重试、死信、任务监控。
|
||||
6. **内容审核与风险处置**:审核任务、风险事件、违规消息撤回。
|
||||
7. **组织/会议/日报/月报/工作台**:企业协同类功能。
|
||||
|
||||
## 6. 开发边界提醒
|
||||
|
||||
1. 租户业务库是“一个租户一个库”,业务表不带 `tenant_id`。
|
||||
2. HotGo 后台直连租户库适合只读查询、诊断和报表。
|
||||
3. 会影响用户、群、消息、文件、`im_topics`、`im_subscriptions`、ACL、Outbox 的写操作,应优先通过 `im_server` 管理 API。
|
||||
4. 敏感操作必须具备:权限校验、二次确认、幂等键或版本号、审计、Outbox 通知。
|
||||
5. 后台前端隐藏按钮只是体验优化,后端接口仍必须做权限和租户边界校验。
|
||||
@@ -1,883 +0,0 @@
|
||||
# IM 后台需求文档
|
||||
|
||||
> 编写日期:2026-08-12
|
||||
> 依据文档:`IM-产品需求服务端改造分析报告.md`、`IM-独立库多租户改造方案.md`、`IM-多租户第5至12章代码改造与测试方案.md`、`IM-前后端对接文档.md`、`IM-系统设计文档.md`
|
||||
> 目标读者:后台产品、后端、管理端前端、测试
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文从现有 IM 产品与服务端设计文档中提取“后台管理端”需求,用于后续后台系统开发。本文只描述后台能力,不重复普通移动端聊天、通讯录、消息收发等客户端功能。
|
||||
|
||||
后台系统分为两类:
|
||||
|
||||
| 后台 | 使用对象 | 数据范围 |
|
||||
| --- | --- | --- |
|
||||
| 平台运营后台 | 平台管理员、运维、安全负责人 | 可管理全部租户,但跨租户操作必须二次验证和审计 |
|
||||
| 企业管理后台 | 租户负责人、企业管理员、安全审计员、组织管理员 | 只能管理当前租户内的数据 |
|
||||
|
||||
所有后台能力必须建立在多租户隔离之上:当前多租户采用“控制库 + 每租户独立业务库”方案。后台 Token 必须包含可信 `tenant_id`、`uid`、角色、权限版本;其中 `tenant_id` 只作为运行时租户上下文和选库依据,不要求租户业务库内业务表保存 `tenant_id` 字段。平台后台的跨租户操作必须走独立后台 API,禁止通过普通 WebSocket `AsUser` 任意代操作用户。
|
||||
|
||||
后台系统与 `im_server` 的职责边界如下:
|
||||
|
||||
1. HotGo 后台负责管理平台控制库、后台页面、菜单权限、平台运营流程和必要的数据展示。
|
||||
2. 租户内用户、群、文件、实名、举报、反馈、Outbox 等业务动作优先通过 `im_server` 管理 API 执行,避免绕过 `im_server` 的状态机、审计、Outbox、权限校验和租户选库逻辑。
|
||||
3. HotGo 后台如需直连租户业务库,只能用于只读报表、诊断或经过评审的维护任务;写操作必须明确说明为什么不能调用 `im_server` API。
|
||||
|
||||
## 2. 后台角色与权限
|
||||
|
||||
### 2.1 角色
|
||||
|
||||
| 角色 | 作用域 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `platform_admin` | 平台级 | 租户生命周期、平台配置、运维诊断、跨租户审计 |
|
||||
| `tenant_owner` | 租户级 | 企业最高管理员,拥有本租户全部管理权限 |
|
||||
| `tenant_admin` | 租户级 | 组织、用户、群、安全策略和工作台配置 |
|
||||
| `security_auditor` | 租户级 | 实名、举报、反馈、审计读取和安全处置 |
|
||||
| `org_admin` | 租户级或部门级 | 部门、成员、岗位、组织数据范围 |
|
||||
| `report_admin` | 部门级 | 日报、月报、会议统计与提醒 |
|
||||
| `group_admin` | 群级 | 群成员、公告、禁言、群文件等群治理 |
|
||||
|
||||
### 2.2 权限原则
|
||||
|
||||
1. 平台管理员可以选择租户上下文进行运维,但所有跨租户读取、导出、代操作必须记录审计。
|
||||
2. 企业管理员只能访问自己租户的数据,不能通过 URL、Header、Body 指定其他 `tenant_id`、`db_name` 或 `db_dsn`。
|
||||
3. 后台菜单和按钮由服务端返回权限裁剪结果,前端隐藏只是体验优化,服务端仍必须二次鉴权。
|
||||
4. 敏感操作必须支持二次认证、幂等键、版本号或 `If-Match`,并写入审计。
|
||||
5. 普通租户管理员不能修改或删除审计日志。
|
||||
|
||||
## 3. 后台功能总览
|
||||
|
||||
| 模块 | 平台后台 | 企业后台 | 优先级 |
|
||||
| --- | --- | --- | --- |
|
||||
| 租户管理 | 是 | 否 | P0 |
|
||||
| 租户配置与配额 | 是 | 部分只读或申请变更 | P0 |
|
||||
| 管理员与角色权限 | 是 | 是 | P0 |
|
||||
| 用户与账号安全 | 支持跨租户运维 | 是 | P0 |
|
||||
| 组织架构 | 否 | 是 | P1 |
|
||||
| 群治理 | 运维只读/处置 | 是 | P1 |
|
||||
| 工作台应用配置 | 平台模板 | 租户配置 | P2 |
|
||||
| 会议、日报、月报 | 否 | 是 | P2 |
|
||||
| 总经理信箱 | 否 | 是 | P2 |
|
||||
| 实名认证审核 | 平台可审计 | 是 | P2 |
|
||||
| 反诈与内容审核 | 平台策略/插件 | 租户策略/处置 | P2 |
|
||||
| 举报与反馈工单 | 平台可审计 | 是 | P2 |
|
||||
| 文件与存储治理 | 平台运维 | 租户范围 | P1 |
|
||||
| Push、短信、TURN、对象存储配置 | 是 | 租户专属配置 | P1 |
|
||||
| 审计日志 | 是 | 只读本租户 | P0 |
|
||||
| Outbox 与后台任务 | 是 | 租户任务可见 | P1 |
|
||||
|
||||
## 4. 平台后台需求
|
||||
|
||||
### 4.1 租户管理
|
||||
|
||||
后台需要支持租户的创建、查看、编辑、停用、恢复、删除前置检查和删除归档。
|
||||
|
||||
租户状态:
|
||||
|
||||
| 状态 | 登录 | 读消息 | 写业务 | 管理操作 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `provisioning` | 禁止 | 禁止 | 禁止 | 平台管理员可配置 |
|
||||
| `active` | 允许 | 允许 | 允许 | 按角色允许 |
|
||||
| `suspended` | 禁止新登录 | 可配置只读 | 禁止 | 平台管理员可恢复或导出 |
|
||||
| `deleting` | 禁止 | 禁止 | 禁止 | 只允许删除任务 |
|
||||
| `deleted` | 禁止 | 禁止 | 禁止 | 只保留审计 |
|
||||
|
||||
核心字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `id` | 内部租户 ID,普通客户端不返回 |
|
||||
| `code` | 企业码,全局唯一,最长 64 字符 |
|
||||
| `name` | 企业名称 |
|
||||
| `tenant_desc` | 企业说明 |
|
||||
| `db_name` | 租户业务库名称;适合同 MySQL 实例下按库隔离,如 `im_tenant_1001` |
|
||||
| `db_dsn` | 租户业务库完整 DSN;适合独立 MySQL 实例或特殊部署,优先级高于 `db_name` |
|
||||
| `state` | 租户状态 |
|
||||
| `created_by/updated_by` | 操作人 |
|
||||
| `created_at/updated_at` | 时间 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenants` | 租户列表,支持状态、企业码、名称过滤 |
|
||||
| `POST` | `/admin/v1/tenants` | 创建租户 |
|
||||
| `GET` | `/admin/v1/tenants/{tenant_id}` | 租户详情 |
|
||||
| `PATCH` | `/admin/v1/tenants/{tenant_id}` | 修改名称、说明等基础资料 |
|
||||
| `POST` | `/admin/v1/tenants/{tenant_id}/suspend` | 停用租户 |
|
||||
| `POST` | `/admin/v1/tenants/{tenant_id}/restore` | 恢复租户 |
|
||||
| `POST` | `/admin/v1/tenants/{tenant_id}/deletion-check` | 删除前检查 |
|
||||
| `DELETE` | `/admin/v1/tenants/{tenant_id}` | 进入删除流程 |
|
||||
|
||||
独立库模式下,创建租户必须按 provisioning 流程执行:
|
||||
|
||||
1. 在控制库创建 `im_tenant` 记录,初始状态为 `provisioning`。
|
||||
2. 生成或填写租户业务库路由信息,优先使用 `db_dsn`,否则使用主 DSN + `db_name`。
|
||||
3. 创建租户业务库并执行 `tenant_schema.sql`。
|
||||
4. 初始化默认系统数据,例如 `kvmeta.version`、系统账号、`sys` Topic 和必要的默认配置。
|
||||
5. 回写 `im_tenant.db_name/db_dsn`,状态改为 `active`。
|
||||
6. 任一步失败时保留 `provisioning` 或进入明确失败状态,后台提供错误详情和重试入口;禁止创建失败的租户进入登录流程。
|
||||
|
||||
验收点:
|
||||
|
||||
1. 租户 `code` 全局唯一。
|
||||
2. 停用后旧 Token、WS 登录、HTTP 产品接口均拒绝。
|
||||
3. 删除必须进入异步任务,不允许直接硬删租户业务库。
|
||||
4. 所有状态变更写平台审计。
|
||||
|
||||
### 4.2 租户配置与配额
|
||||
|
||||
首期配置真源是 `im_tenant_config`。
|
||||
|
||||
| 配置 | 说明 | 默认 |
|
||||
| --- | --- | --- |
|
||||
| `max_users` | 有效用户数上限 | `0` 表示不限 |
|
||||
| `max_groups` | 有效群数量上限 | `0` 表示不限 |
|
||||
| `max_group_members` | 单群有效成员上限 | `0` 表示不限 |
|
||||
|
||||
后续可扩展配置:
|
||||
|
||||
| 配置域 | 示例 |
|
||||
| --- | --- |
|
||||
| 功能开关 | 加好友、建群、用户搜索、音视频、群文件、日报月报 |
|
||||
| 安全策略 | 实名要求、青少年限制、风控模式、数据保留期 |
|
||||
| 消息策略 | 文本长度、媒体类型、消息频控、撤回时限、编辑时限 |
|
||||
| 文件策略 | 单文件大小、总存储配额、允许 MIME、病毒扫描 |
|
||||
| Push 配置 | APNS/FCM 凭证、模板、锁屏隐私 |
|
||||
| 外部服务 | 短信、REST 认证、TURN、对象存储、审核插件 |
|
||||
| 企业品牌 | Logo、主题色、企业描述、客户端下载地址 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenants/{tenant_id}/config` | 查询租户配置 |
|
||||
| `PATCH` | `/admin/v1/tenants/{tenant_id}/config` | 修改配额和策略 |
|
||||
| `GET` | `/admin/v1/tenants/{tenant_id}/usage` | 查询用户数、群数、文件容量、消息量 |
|
||||
| `GET` | `/admin/v1/tenants/{tenant_id}/features` | 查询功能开关 |
|
||||
| `PATCH` | `/admin/v1/tenants/{tenant_id}/features` | 修改功能开关 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 注册、创建群、入群审批必须在 `im_server` 对应租户业务库事务内校验配额。
|
||||
2. 超限返回稳定错误码,并记录当前用量、限制值、租户和操作。
|
||||
3. 修改配置只影响当前租户,不能污染全平台缓存。
|
||||
|
||||
### 4.3 平台管理员管理
|
||||
|
||||
需求:
|
||||
|
||||
1. 创建、停用、恢复平台管理员。
|
||||
2. 分配平台角色和权限。
|
||||
3. 平台管理员登录、二次认证、Token 撤销。
|
||||
4. 查看平台管理员操作审计。
|
||||
|
||||
敏感操作:
|
||||
|
||||
| 操作 | 要求 |
|
||||
| --- | --- |
|
||||
| 创建平台管理员 | 二次确认和审计 |
|
||||
| 授权 `platform_admin` | 二次认证 |
|
||||
| 跨租户代操作 | 必须填写原因,记录目标租户和目标对象 |
|
||||
| 导出数据 | 审批或二次认证,记录导出范围 |
|
||||
|
||||
### 4.4 平台运维与任务
|
||||
|
||||
需求:
|
||||
|
||||
1. 查看运行节点、数据库版本、Adapter 类型和多租户能力。
|
||||
2. 非 MySQL 或未完成独立库租户路由能力的 Adapter 在产品模式下必须拒绝启动。
|
||||
3. 查看 Outbox 事件积压、失败、死信和重试。
|
||||
4. 查看文件垃圾回收、搜索索引任务、Push 投递、外部审核调用状态。
|
||||
5. 支持按租户维度查看任务,但避免 Prometheus 高基数标签。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/system/health` | 系统健康 |
|
||||
| `GET` | `/admin/v1/system/nodes` | 节点列表 |
|
||||
| `GET` | `/admin/v1/system/outbox-events` | Outbox 查询 |
|
||||
| `POST` | `/admin/v1/system/outbox-events/{id}/retry` | 重试事件 |
|
||||
| `POST` | `/admin/v1/system/outbox-events/{id}/dead-letter` | 标记死信 |
|
||||
| `GET` | `/admin/v1/system/audit-logs` | 平台审计 |
|
||||
|
||||
## 5. 企业后台需求
|
||||
|
||||
### 5.1 企业管理员与角色权限
|
||||
|
||||
企业后台需要支持本租户内管理员管理。
|
||||
|
||||
角色:
|
||||
|
||||
| 角色 | 权限 |
|
||||
| --- | --- |
|
||||
| `tenant_owner` | 全部租户管理权限 |
|
||||
| `tenant_admin` | 用户、组织、群、安全、工作台配置 |
|
||||
| `security_auditor` | 实名、举报、反馈、审计读取 |
|
||||
| `org_admin` | 组织架构和成员维护 |
|
||||
| `report_admin` | 日报、月报、会议统计 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/admins` | 企业管理员列表 |
|
||||
| `POST` | `/admin/v1/tenant/admins` | 添加管理员 |
|
||||
| `PATCH` | `/admin/v1/tenant/admins/{user_id}` | 修改角色 |
|
||||
| `DELETE` | `/admin/v1/tenant/admins/{user_id}` | 移除管理员 |
|
||||
| `GET` | `/admin/v1/tenant/roles` | 可用角色和权限点 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. Tenant Admin 只能管理当前租户。
|
||||
2. 授权、移除、代操作必须审计。
|
||||
3. 不允许最后一个 `tenant_owner` 被删除或停用。
|
||||
|
||||
### 5.2 用户与账号安全管理
|
||||
|
||||
需求:
|
||||
|
||||
1. 用户列表、搜索、详情。
|
||||
2. 创建、启用、停用、离职、恢复用户。
|
||||
3. 重置密码、撤销 Token、踢下线。
|
||||
4. 查看登录设备、登录 Session、安全事件。
|
||||
5. 查看用户实名状态、青少年状态、隐私设置摘要。
|
||||
6. 支持同手机号在不同租户独立存在,本租户后台只看到本租户用户。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/users` | 用户列表 |
|
||||
| `POST` | `/admin/v1/tenant/users` | 创建用户 |
|
||||
| `GET` | `/admin/v1/tenant/users/{user_id}` | 用户详情 |
|
||||
| `PATCH` | `/admin/v1/tenant/users/{user_id}` | 修改资料或状态 |
|
||||
| `POST` | `/admin/v1/tenant/users/{user_id}/reset-password` | 重置密码 |
|
||||
| `POST` | `/admin/v1/tenant/users/{user_id}/revoke-sessions` | 撤销会话和 Token |
|
||||
| `GET` | `/admin/v1/tenant/users/{user_id}/devices` | 登录设备 |
|
||||
| `GET` | `/admin/v1/tenant/users/{user_id}/security-events` | 安全事件 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 停用用户不影响其他租户相同手机号或相同设备用户。
|
||||
2. 重置密码必须递增 `auth_version` 并撤销旧 Token。
|
||||
3. 查看敏感信息必须脱敏,证件号、手机号、Token 不写日志。
|
||||
4. 用户管理接口必须通过 `tenant_id` 选择对应租户业务库;不得在控制库中查找租户内用户主数据。
|
||||
|
||||
### 5.3 组织架构管理
|
||||
|
||||
需求:
|
||||
|
||||
1. 部门树管理:新增、编辑、移动、删除部门。
|
||||
2. 部门成员管理:添加、移除、调岗、设置负责人。
|
||||
3. 组织搜索:按部门、用户、岗位搜索。
|
||||
4. 离职用户处理:不可联系或从结果移除。
|
||||
5. 组织数据权限:管理员只能管理权限范围内部门。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_org_units` | 部门树 |
|
||||
| `im_org_members` | 部门成员、岗位、负责人 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/org/departments` | 部门树 |
|
||||
| `POST` | `/admin/v1/tenant/org/departments` | 创建部门 |
|
||||
| `PATCH` | `/admin/v1/tenant/org/departments/{id}` | 修改或移动部门 |
|
||||
| `DELETE` | `/admin/v1/tenant/org/departments/{id}` | 删除部门 |
|
||||
| `GET` | `/admin/v1/tenant/org/departments/{id}/members` | 部门成员 |
|
||||
| `POST` | `/admin/v1/tenant/org/departments/{id}/members` | 添加成员 |
|
||||
| `DELETE` | `/admin/v1/tenant/org/departments/{id}/members/{user_id}` | 移除成员 |
|
||||
| `GET` | `/admin/v1/tenant/org/search` | 组织搜索 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 部门 `parent_id`、负责人、成员必须来自当前租户业务库。
|
||||
2. 移动部门不能形成环。
|
||||
3. 删除部门需要校验子部门和成员处理策略。
|
||||
|
||||
### 5.4 群治理管理
|
||||
|
||||
企业后台和群主/群管理员都需要群治理能力。后台侧侧重全租户范围查询、策略配置、违规处置和审计。
|
||||
|
||||
需求:
|
||||
|
||||
1. 群列表、群详情、群成员列表。
|
||||
2. 群资料编辑:名称、头像、简介。
|
||||
3. 群治理配置:入群审批、全员禁言、成员上传文件、成员私聊、成员邀请。
|
||||
4. 群管理员管理:添加、删除管理员,管理员数量限制。
|
||||
5. 禁言管理:添加、解除、过期自动失效。
|
||||
6. 群主转让、解散群聊。
|
||||
7. 群二维码管理:生成、撤销、过期。
|
||||
8. 群文件管理:列表、搜索、删除、下载权限。
|
||||
9. 群操作日志:角色变化、禁言、公告、解散。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_group_settings` | 群治理配置 |
|
||||
| `im_group_members` | 群成员和角色真源 |
|
||||
| `im_group_join_requests` | 入群申请 |
|
||||
| `im_group_announcements` | 群公告 |
|
||||
| `im_group_mutes` | 禁言记录 |
|
||||
| `im_group_admin_logs` | 群管理日志 |
|
||||
| `im_group_files` | 群文件索引 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/groups` | 群列表 |
|
||||
| `GET` | `/admin/v1/tenant/groups/{topic}` | 群详情 |
|
||||
| `PATCH` | `/admin/v1/tenant/groups/{topic}` | 修改群资料 |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/groups/{topic}/governance` | 群治理配置 |
|
||||
| `GET` | `/admin/v1/tenant/groups/{topic}/members` | 成员列表 |
|
||||
| `POST` | `/admin/v1/tenant/groups/{topic}/admins/batch` | 添加管理员 |
|
||||
| `DELETE` | `/admin/v1/tenant/groups/{topic}/admins/{user_id}` | 删除管理员 |
|
||||
| `POST` | `/admin/v1/tenant/groups/{topic}/mutes/batch` | 添加禁言 |
|
||||
| `DELETE` | `/admin/v1/tenant/groups/{topic}/mutes/{user_id}` | 解除禁言 |
|
||||
| `POST` | `/admin/v1/tenant/groups/{topic}/owner-transfer` | 群主转让 |
|
||||
| `DELETE` | `/admin/v1/tenant/groups/{topic}` | 解散群 |
|
||||
| `GET` | `/admin/v1/tenant/groups/{topic}/files` | 群文件 |
|
||||
| `DELETE` | `/admin/v1/tenant/groups/{topic}/files/{group_file_id}` | 删除群文件 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 群业务真源与 Topic/Subscription/ACL 投影必须在同一事务内更新。
|
||||
2. 管理员不能越级操作群主,普通管理员之间操作规则由治理策略决定。
|
||||
3. 解散群必须撤销 Topic、二维码、邀请和文件新增权限。
|
||||
4. 群治理写操作必须由 `im_server` 在目标租户业务库内完成,HotGo 后台不能直接改 Topic、Subscription 或 ACL 绕过业务真源。
|
||||
|
||||
### 5.5 工作台应用配置
|
||||
|
||||
需求:
|
||||
|
||||
1. 工作台模块配置:启用、禁用、排序、图标、名称、路由。
|
||||
2. 根据组织角色、权限和功能开关裁剪用户可见模块。
|
||||
3. 维护会议、日报、月报、总经理信箱等入口。
|
||||
4. 工作台未读角标由系统事件更新。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_workbench_apps` | 租户工作台应用配置 |
|
||||
| `im_workbench_user_apps` | 用户或角色级工作台配置 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/workbench/apps` | 应用列表 |
|
||||
| `POST` | `/admin/v1/tenant/workbench/apps` | 新增应用 |
|
||||
| `PATCH` | `/admin/v1/tenant/workbench/apps/{code}` | 修改应用 |
|
||||
| `DELETE` | `/admin/v1/tenant/workbench/apps/{code}` | 删除或禁用应用 |
|
||||
| `POST` | `/admin/v1/tenant/workbench/apps/reorder` | 排序 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 客户端只接受白名单内部 `route`。
|
||||
2. 禁用模块后普通用户不可通过直接 URL 访问。
|
||||
|
||||
### 5.6 会议管理
|
||||
|
||||
需求:
|
||||
|
||||
1. 创建、编辑、取消会议。
|
||||
2. 管理参会人、提醒未响应者。
|
||||
3. 查看参会响应:`pending`、`accepted`、`declined`、`tentative`。
|
||||
4. 会议过期后只读展示。
|
||||
5. 生成系统通知和 Push。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_meetings` | 会议主表 |
|
||||
| `im_meeting_participants` | 参会人响应 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/meetings` | 会议列表 |
|
||||
| `POST` | `/admin/v1/tenant/meetings` | 创建会议 |
|
||||
| `GET` | `/admin/v1/tenant/meetings/{id}` | 会议详情 |
|
||||
| `PATCH` | `/admin/v1/tenant/meetings/{id}` | 更新会议 |
|
||||
| `POST` | `/admin/v1/tenant/meetings/{id}/cancel` | 取消会议 |
|
||||
| `POST` | `/admin/v1/tenant/meetings/{id}/reminders` | 提醒未响应者 |
|
||||
|
||||
### 5.7 日报与月报管理
|
||||
|
||||
需求:
|
||||
|
||||
1. 日报/月报模板和校验规则配置。
|
||||
2. 普通用户草稿、提交、修订。
|
||||
3. 管理员按部门查看统计和明细。
|
||||
4. 提醒未提交人员。
|
||||
5. 查看详情时记录已读状态。
|
||||
6. 附件使用文件 ACL,不能通过旧 URL 越权下载。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_reports` | 日报/月报统一表 |
|
||||
| `im_report_read_states` | 阅读状态 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/reports/daily/statistics` | 日报统计 |
|
||||
| `GET` | `/admin/v1/tenant/reports/daily` | 日报列表 |
|
||||
| `GET` | `/admin/v1/tenant/reports/daily/{id}` | 日报详情 |
|
||||
| `POST` | `/admin/v1/tenant/reports/daily/reminders` | 日报提醒 |
|
||||
| `GET` | `/admin/v1/tenant/reports/monthly/statistics` | 月报统计 |
|
||||
| `GET` | `/admin/v1/tenant/reports/monthly` | 月报列表 |
|
||||
| `GET` | `/admin/v1/tenant/reports/monthly/{id}` | 月报详情 |
|
||||
| `POST` | `/admin/v1/tenant/reports/monthly/reminders` | 月报提醒 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 管理员只能看组织权限范围内成员。
|
||||
2. 并发保存必须使用版本号,冲突返回当前最新版本。
|
||||
3. 提醒接口需要限流并返回逐项结果。
|
||||
|
||||
### 5.8 总经理信箱
|
||||
|
||||
需求:
|
||||
|
||||
1. 配置是否启用、是否允许匿名、必填字段、附件策略、提示文案。
|
||||
2. 用户提交意见或建议。
|
||||
3. 后台查看、分派、处理、回复和关闭。
|
||||
4. 匿名模式下业务接收方不可见提交人;安全审计是否保留内部追踪由企业政策决定。
|
||||
5. 文案变更后,客户端提交时可要求重新确认。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_manager_mailbox` | 信箱提交和处理记录 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/executive-mailbox/config` | 查询配置 |
|
||||
| `PATCH` | `/admin/v1/tenant/executive-mailbox/config` | 修改配置 |
|
||||
| `GET` | `/admin/v1/tenant/executive-mailbox/submissions` | 提交列表 |
|
||||
| `GET` | `/admin/v1/tenant/executive-mailbox/submissions/{id}` | 提交详情 |
|
||||
| `POST` | `/admin/v1/tenant/executive-mailbox/submissions/{id}/assign` | 分派 |
|
||||
| `POST` | `/admin/v1/tenant/executive-mailbox/submissions/{id}/reply` | 回复 |
|
||||
| `POST` | `/admin/v1/tenant/executive-mailbox/submissions/{id}/close` | 关闭 |
|
||||
|
||||
## 6. 安全、审核与工单后台
|
||||
|
||||
### 6.1 实名认证审核
|
||||
|
||||
需求:
|
||||
|
||||
1. 查看实名认证列表:未认证、待审核、已通过、已拒绝、申诉中。
|
||||
2. 查看脱敏信息,具备权限时按流程查看敏感原文。
|
||||
3. 审核通过、拒绝、要求补充材料。
|
||||
4. 实名策略配置:是否强制实名、允许跳过、提醒周期、重复身份策略。
|
||||
5. 审核结果通过系统事件通知用户。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_realname_verifications` | 实名信息、审核状态、脱敏展示 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/security/realname/submissions` | 实名审核列表 |
|
||||
| `GET` | `/admin/v1/tenant/security/realname/submissions/{id}` | 审核详情 |
|
||||
| `POST` | `/admin/v1/tenant/security/realname/submissions/{id}/approve` | 通过 |
|
||||
| `POST` | `/admin/v1/tenant/security/realname/submissions/{id}/reject` | 拒绝 |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/security/realname/policy` | 实名策略 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 姓名、证件号加密存储,普通日志不出现原文。
|
||||
2. 审核操作必须审计。
|
||||
3. 普通管理员无权读取敏感原文。
|
||||
|
||||
### 6.2 青少年模式策略
|
||||
|
||||
需求:
|
||||
|
||||
1. 配置青少年模式默认策略和限制范围。
|
||||
2. 查看用户青少年模式状态。
|
||||
3. 处理用户忘记独立密码后的重置流程。
|
||||
|
||||
限制范围首期建议包括:
|
||||
|
||||
1. 添加好友。
|
||||
2. 加群和创建群。
|
||||
3. 文件传输。
|
||||
4. 音视频通话。
|
||||
5. 工作台外部链接。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/security/youth-mode/policy` | 青少年策略 |
|
||||
| `GET` | `/admin/v1/tenant/security/youth-mode/users` | 用户状态列表 |
|
||||
| `POST` | `/admin/v1/tenant/security/youth-mode/users/{user_id}/reset-pin` | 重置独立密码 |
|
||||
|
||||
### 6.3 反诈与内容审核
|
||||
|
||||
需求:
|
||||
|
||||
1. 风控策略配置:高风险动作、提示文案、二次确认、阻断策略。
|
||||
2. 审核插件配置:平台统一审核服务或租户专属审核服务。
|
||||
3. 审核模式配置:`sync_block`、`async_hold`、`async_post_audit`。
|
||||
4. 查看风险事件、确认、取消、过期、阻断记录。
|
||||
5. 对已发布违规消息执行系统撤回。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/security/risk-policy` | 风控策略 |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/security/moderation-policy` | 内容审核策略 |
|
||||
| `GET` | `/admin/v1/tenant/security/risk-events` | 风险事件 |
|
||||
| `GET` | `/admin/v1/tenant/security/moderation-tasks` | 审核任务 |
|
||||
| `POST` | `/admin/v1/tenant/security/moderation-tasks/{id}/approve` | 审核通过 |
|
||||
| `POST` | `/admin/v1/tenant/security/moderation-tasks/{id}/reject` | 审核拒绝 |
|
||||
| `POST` | `/admin/v1/tenant/messages/{message_id}/recall-by-review` | 审核撤回 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 客户端提交的审核状态一律忽略。
|
||||
2. 插件返回 `REPLACE` 或 `RESPOND` 时不能修改 TenantID。
|
||||
3. 风控确认 Token 单次、短期、绑定原动作。
|
||||
|
||||
### 6.4 举报工单
|
||||
|
||||
需求:
|
||||
|
||||
1. 配置举报原因,按目标类型区分用户、群、消息。
|
||||
2. 举报列表、详情、证据、目标快照。
|
||||
3. 工单状态:`received`、`processing`、`resolved`、`rejected`、`closed`。
|
||||
4. 处置动作:警告、禁言、封禁用户、解散群、撤回消息、标记无效。
|
||||
5. 重复举报合并策略。
|
||||
6. 处理结果通知举报人,必要时通知被处置对象。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_abuse_reports` | 举报工单 |
|
||||
| `im_evidence_files` | 证据文件 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/reports-abuse/reasons` | 举报原因配置 |
|
||||
| `GET` | `/admin/v1/tenant/reports-abuse` | 举报列表 |
|
||||
| `GET` | `/admin/v1/tenant/reports-abuse/{id}` | 举报详情 |
|
||||
| `POST` | `/admin/v1/tenant/reports-abuse/{id}/assign` | 分派 |
|
||||
| `POST` | `/admin/v1/tenant/reports-abuse/{id}/resolve` | 处置 |
|
||||
| `POST` | `/admin/v1/tenant/reports-abuse/{id}/close` | 关闭 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 举报人必须能看到被举报目标,后台详情使用提交时固化快照。
|
||||
2. 证据文件使用独立 ACL 和保留期。
|
||||
3. 原消息删除或撤回后,举报证据仍可处理。
|
||||
|
||||
### 6.5 意见反馈工单
|
||||
|
||||
需求:
|
||||
|
||||
1. 配置反馈类别、最大正文、附件规则、是否允许联系方式、日志授权说明。
|
||||
2. 反馈列表、详情、附件、设备和版本信息。
|
||||
3. 分派、回复、关闭、标记重复。
|
||||
4. 处理结果通过系统通知或客户端查询展示。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_feedbacks` | 反馈工单 |
|
||||
| `im_evidence_files` | 反馈附件 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/feedback/config` | 反馈配置 |
|
||||
| `GET` | `/admin/v1/tenant/feedbacks` | 反馈列表 |
|
||||
| `GET` | `/admin/v1/tenant/feedbacks/{id}` | 反馈详情 |
|
||||
| `POST` | `/admin/v1/tenant/feedbacks/{id}/assign` | 分派 |
|
||||
| `POST` | `/admin/v1/tenant/feedbacks/{id}/reply` | 回复 |
|
||||
| `POST` | `/admin/v1/tenant/feedbacks/{id}/close` | 关闭 |
|
||||
|
||||
## 7. 文件、搜索与数据治理后台
|
||||
|
||||
### 7.1 文件治理
|
||||
|
||||
需求:
|
||||
|
||||
1. 按租户查看文件用量、文件列表、状态和业务用途。
|
||||
2. 支持群文件、收藏文件、举报证据、反馈附件、日报/月报附件分类管理。
|
||||
3. 文件下载审计。
|
||||
4. 清理未关联文件、过期文件和违规文件。
|
||||
5. 配置文件大小、MIME、总存储配额、保留期。
|
||||
|
||||
文件状态建议:
|
||||
|
||||
```text
|
||||
pending -> uploaded -> ready
|
||||
pending/uploaded -> failed
|
||||
uploaded(超时且无引用) -> orphaned -> deleted
|
||||
```
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/files` | 文件列表 |
|
||||
| `GET` | `/admin/v1/tenant/files/{file_id}` | 文件详情 |
|
||||
| `POST` | `/admin/v1/tenant/files/{file_id}/quarantine` | 隔离文件 |
|
||||
| `DELETE` | `/admin/v1/tenant/files/{file_id}` | 删除文件 |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/files/policy` | 文件策略 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 文件下载不能只靠 FileID,必须校验上传者或关联消息/Topic 读取权限。
|
||||
2. 后台删除文件必须审计,并处理消息、收藏、举报等引用。
|
||||
|
||||
### 7.2 搜索治理
|
||||
|
||||
需求:
|
||||
|
||||
1. 消息索引状态查看。
|
||||
2. 重建指定租户、Topic 或时间段索引。
|
||||
3. 搜索敏感内容审计。
|
||||
4. 查看搜索性能和失败任务。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/search/index-status` | 索引状态 |
|
||||
| `POST` | `/admin/v1/tenant/search/reindex` | 重建索引 |
|
||||
| `GET` | `/admin/v1/tenant/search/audit-logs` | 搜索审计 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 搜索索引不是授权来源,返回前仍需 Topic Read ACL 二次校验。
|
||||
2. 用户退群或权限变化后必须立即阻断搜索结果。
|
||||
|
||||
## 8. 通知、Push 与外部服务后台
|
||||
|
||||
### 8.1 系统消息模板
|
||||
|
||||
需求:
|
||||
|
||||
1. 管理系统事件类型和模板版本。
|
||||
2. 配置账号安全、好友、群、文件、通话、工作台、安全等通知模板。
|
||||
3. Outbox 事件驱动系统消息、Push、索引和定时任务。
|
||||
|
||||
首期事件:
|
||||
|
||||
| 业务 | 事件 |
|
||||
| --- | --- |
|
||||
| 账号安全 | 新设备登录、密码修改、实名提醒 |
|
||||
| 好友 | 新申请、通过、拒绝、删除 |
|
||||
| 群 | 邀请、申请、审批、公告、禁言、管理员变更、群主转让、解散 |
|
||||
| 文件 | 群文件上传、文件失效 |
|
||||
| 通话 | 未接、拒绝、取消、忙线、通话结束 |
|
||||
| 工作台 | 会议通知、会议变更、日报提醒、月报提醒、信箱处理 |
|
||||
| 安全 | 反诈提示、举报受理、反馈受理 |
|
||||
|
||||
### 8.2 Push、短信、TURN、对象存储
|
||||
|
||||
需求:
|
||||
|
||||
1. 租户级 Push 模板、App 包名、APNS/FCM 凭证引用。
|
||||
2. 短信服务配置和模板。
|
||||
3. TURN 配置。
|
||||
4. 对象存储 Bucket、Prefix、签名策略。
|
||||
5. 外部 REST 认证服务配置。
|
||||
6. 外部审核和风控插件配置。
|
||||
|
||||
验收点:
|
||||
|
||||
1. 租户没有配置时按明确策略失败,不得静默使用其他租户配置。
|
||||
2. 密钥只保存引用,不把明文 Secret 放入普通业务 JSON。
|
||||
3. 外部回调必须校验服务端签名的 TenantContext 和 nonce。
|
||||
|
||||
## 9. 审计与日志后台
|
||||
|
||||
### 9.1 审计范围
|
||||
|
||||
以下操作必须写审计:
|
||||
|
||||
1. 租户创建、停用、恢复、删除。
|
||||
2. 平台管理员和租户管理员授权、移除、代操作。
|
||||
3. 用户停用、离职、密码重置、数据导出。
|
||||
4. 群主转让、管理员变更、禁言、解散群。
|
||||
5. 实名审核、举报处置、反馈处理、匿名信箱读取。
|
||||
6. 文件下载、删除、批量导出、搜索敏感内容。
|
||||
7. 反诈二次确认、内容审核处置。
|
||||
8. 外部服务配置修改。
|
||||
|
||||
独立库模式下审计存储分为两类:
|
||||
|
||||
1. 平台级审计存控制库,覆盖租户生命周期、平台管理员、跨租户运维、租户业务库创建/删除/导出等操作。
|
||||
2. 租户级业务审计优先存对应租户业务库,覆盖用户、群、文件、实名、举报、反馈、办公协同等操作;后台查询时通过 `tenant_id` 选择租户业务库。
|
||||
|
||||
平台后台可汇总查询租户级审计,但跨租户聚合、导出和代操作必须记录平台级审计。
|
||||
|
||||
### 9.2 审计字段
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `tenant_id` | 租户 |
|
||||
| `actor_uid` | 操作人 |
|
||||
| `actor_role` | 操作角色 |
|
||||
| `action` | 操作类型 |
|
||||
| `target_type` | 目标类型 |
|
||||
| `target_id` | 目标 ID |
|
||||
| `before_snapshot` | 变更前摘要 |
|
||||
| `after_snapshot` | 变更后摘要 |
|
||||
| `reason` | 操作原因 |
|
||||
| `request_id` | 请求 ID |
|
||||
| `ip` | 操作 IP,必要时脱敏 |
|
||||
| `user_agent` | 操作客户端 |
|
||||
| `created_at` | 操作时间 |
|
||||
|
||||
审计表只允许追加,不允许普通管理员修改或删除。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/audit-logs` | 租户审计 |
|
||||
| `GET` | `/admin/v1/audit-logs` | 平台审计 |
|
||||
| `POST` | `/admin/v1/audit-logs/export` | 审计导出 |
|
||||
|
||||
## 10. 后台公共能力
|
||||
|
||||
### 10.1 认证与会话
|
||||
|
||||
1. 后台登录必须使用独立后台入口,不与普通 App 登录混用权限。
|
||||
2. 后台 Token 包含 `tenant_id`、`uid`、角色、权限版本、`auth_version`。
|
||||
3. 支持刷新、撤销、踢下线、登录设备管理。
|
||||
4. 高危操作需要二次认证或 `X-Action-Token`。
|
||||
|
||||
### 10.2 API 规范
|
||||
|
||||
后台 API 建议统一使用 `/admin/v1/*`。该路径是 `im_server` 管理 API 的目标契约;在 HotGo 中落地页面和接口时,可以按 HotGo 现有 `/admin/...` 路由规范做适配,但必须保持权限语义、请求字段、幂等、审计和错误码一致。
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {},
|
||||
"request_id": "01K...",
|
||||
"server_time": "2026-08-12T05:30:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
失败响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 30001,
|
||||
"reason": "FORBIDDEN",
|
||||
"message": "permission denied",
|
||||
"details": {},
|
||||
"request_id": "01K...",
|
||||
"server_time": "2026-08-12T05:30:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
公共要求:
|
||||
|
||||
1. 写操作必须带 `Idempotency-Key`。
|
||||
2. 更新操作必须带版本号或 `If-Match`。
|
||||
3. 列表统一使用 `limit + cursor`。
|
||||
4. 请求必须带 `X-Request-ID`。
|
||||
5. 不能从请求 Body、URL Query 或普通 Header 信任客户端自报 `tenant_id`。
|
||||
6. 手机号、证件号、验证码、Token、设备 Token、文件签名不写日志。
|
||||
|
||||
### 10.3 数据表公共规则
|
||||
|
||||
1. 新业务表统一使用 `im_` 前缀。
|
||||
2. 控制库只保存平台级租户元数据、平台级审计、平台任务和全局配置,例如 `im_tenant`、`im_tenant_config`。
|
||||
3. 租户业务库保存租户内业务表,例如用户安全、好友、群治理、收藏、实名、举报、反馈、组织、会议、日报月报等;这些表不包含 `tenant_id` 字段。
|
||||
4. 租户业务表唯一索引按租户库内业务键设计,例如 `business_key`、`user_id + peer_id`、`topic + seqid`,不使用 `(tenant_id, business_key)`。
|
||||
5. 子表外键按租户库内父子关系设计,不使用 `(tenant_id, parent_id)` 复合外键。
|
||||
6. 更新、删除、聚合、清理任务必须先从可信 Principal 或平台运维上下文取得 `tenant_id`,再路由到对应租户业务库执行。
|
||||
7. 禁止通过 URL、Header、Body 传入的 `db_name`、`db_dsn` 或 `tenant_id` 决定业务库;业务库路由只能来自控制库和服务端可信上下文。
|
||||
|
||||
### 10.4 Outbox 与后台任务存储
|
||||
|
||||
Outbox 和后台任务同样按作用域拆分:
|
||||
|
||||
1. 平台级 Outbox 存控制库,用于租户创建、租户停用、建库初始化、跨租户导出、平台通知等。
|
||||
2. 租户级 Outbox 存租户业务库,用于好友、群、文件、实名、举报、反馈、会议、日报月报等业务事件。
|
||||
3. 后台查询 Outbox 时,平台后台可以先按租户筛选,再路由到对应业务库;企业后台只能查询当前租户业务库内事件。
|
||||
4. 重试、死信和人工重放必须幂等,并写入对应作用域的审计。
|
||||
|
||||
## 11. 开发优先级建议
|
||||
|
||||
### P0:后台地基
|
||||
|
||||
1. `/admin/v1` 认证中间件、RBAC、租户 Principal。
|
||||
2. 平台租户管理:创建、停用、恢复、配置配额。
|
||||
3. 租户业务库 provisioning:建库、执行 schema、初始化系统数据、回写 `db_name/db_dsn`、失败重试。
|
||||
4. `im_server` 管理 API 对接和租户选库校验。
|
||||
5. 企业管理员与角色权限。
|
||||
6. 用户管理:停用、重置密码、撤销 Session。
|
||||
7. 审计日志基础表和查询。
|
||||
8. Outbox 查询与重试基础能力。
|
||||
|
||||
### P1:企业治理
|
||||
|
||||
1. 组织架构管理。
|
||||
2. 群治理后台:群列表、群成员、管理员、禁言、解散、群文件。
|
||||
3. 文件策略、文件列表、文件审计。
|
||||
4. 外部服务配置:Push、短信、对象存储、TURN。
|
||||
5. 功能开关和配额用量面板。
|
||||
|
||||
### P2:办公与安全业务
|
||||
|
||||
1. 工作台配置。
|
||||
2. 会议管理。
|
||||
3. 日报、月报统计和提醒。
|
||||
4. 总经理信箱。
|
||||
5. 实名认证审核。
|
||||
6. 青少年策略。
|
||||
7. 反诈、内容审核、举报、反馈工单。
|
||||
|
||||
## 12. 验收清单
|
||||
|
||||
- [ ] 平台后台与企业后台权限边界清晰,跨租户操作必须审计。
|
||||
- [ ] 所有后台列表、详情、更新、删除都按租户隔离。
|
||||
- [ ] 租户停用后普通用户登录和 HTTP 产品接口拒绝。
|
||||
- [ ] 用户停用、重置密码、撤销 Token 不影响其他租户同手机号用户。
|
||||
- [ ] 创建用户、建群、入群审批配额在事务内强制执行。
|
||||
- [ ] 群主转让、禁言、管理员变更、解散群写审计并同步系统消息。
|
||||
- [ ] 实名、举报、反馈、信箱等敏感数据脱敏展示并按权限读取。
|
||||
- [ ] 文件下载、删除、导出均有资源级授权和审计。
|
||||
- [ ] 外部服务密钥只保存引用,不写普通日志。
|
||||
- [ ] Outbox 事件可查询、重试、死信处理,重复执行不产生重复副作用。
|
||||
- [ ] 审计表只允许追加,普通管理员不可修改或删除。
|
||||
@@ -1,60 +0,0 @@
|
||||
## 目录
|
||||
|
||||
#### 介绍安装
|
||||
|
||||
- [系统介绍](../../README.md)
|
||||
- [环境搭建](start-environment.md)
|
||||
- [系统安装](start-installation.md)
|
||||
- [生产部署](start-deploy.md)
|
||||
- [如何提问](start-questions.md)
|
||||
- [常见问题](start-issue.md)
|
||||
- [更新历史](start-update-log.md)
|
||||
|
||||
|
||||
#### 系统开发
|
||||
|
||||
- [目录结构](sys-catalog.md)
|
||||
- [开发规范](sys-exploit.md)
|
||||
- [控制台](sys-console.md)
|
||||
- [中间件/拦截器](sys-middleware.md)
|
||||
- [WebHook](sys-webhook.md)
|
||||
- [权限控制](sys-auth.md)
|
||||
- [支付网关](sys-payment.md)
|
||||
- [定时任务](sys-cron.md)
|
||||
- [消息队列](sys-queue.md)
|
||||
- [功能扩展库](sys-library.md)
|
||||
- [工具方法](sys-utility.md)
|
||||
- [WebSocket服务器](sys-websocket-server.md)
|
||||
- [TCP服务器](sys-tcp-server.md)
|
||||
- [SaaS多租户](sys-tenant.md)
|
||||
- [国际化](sys-i18n.md)
|
||||
- [单元测试](sys-test.md)
|
||||
|
||||
|
||||
#### 插件模块开发
|
||||
|
||||
- [模块介绍及目录](addon-introduce-catalog.md)
|
||||
- [模块开发流程](addon-flow.md)
|
||||
- [模块辅助说明](addon-helper.md)
|
||||
|
||||
|
||||
#### 生成代码
|
||||
- [使用前提](code-start.md)
|
||||
- [数据库](sys-db.md)
|
||||
- [生成配置](code-config.md)
|
||||
- [生成CURD](code-curd.md)
|
||||
- [生成关联表CURD](code-curd-join.md)
|
||||
- [生成树型CURD](code-tree.md)
|
||||
- [生成业务模板](code-business.md)
|
||||
- [生成模板开发](code-template-dev.md)
|
||||
- [生成常见问题](code-help.md)
|
||||
|
||||
|
||||
### 前端开发
|
||||
- [表单组件](web-form.md)
|
||||
- [WebSocket客户端](sys-websocket-client.md)
|
||||
- [独立部署](web-deploy.md)
|
||||
|
||||
|
||||
#### 附录
|
||||
- [网址收录](append-website.md)
|
||||
@@ -1,216 +0,0 @@
|
||||
## 模块开发流程
|
||||
|
||||
目录
|
||||
|
||||
- 创建新插件
|
||||
- 开发
|
||||
- 调用主模块服务接口
|
||||
- 访问路径
|
||||
- 数据迁移
|
||||
|
||||
|
||||
|
||||
### 创建新插件
|
||||
|
||||
1、HotGo 后台进入 开发工具->插件管理->找到创建新插件,根据引导进行创建即可。
|
||||
|
||||
```shell
|
||||
创建成功后默认情况下会在以下目录中生成插件文件,假设新生成的插件名为:hgexample
|
||||
|
||||
1. /server/addons/hgexample/ # 插件模块目录
|
||||
2. /server/addons/modules/hgexample.go # 隐式注册插件文件
|
||||
3. /server/resource/addons/hgexample # 静态资源和页面模板目录,属于扩展功能选项,勾选对应选项后才会生成
|
||||
4. /web/src/api/addons/hgexample # webApi目录
|
||||
5. /web/src/views/addons/hgexample # web页面目录
|
||||
|
||||
# 默认情况下没有为web页面生成菜单权限,因为在实际场景中插件不一定需要用到web页面,所以如有需要请手动到后台 权限管理 -> 菜单权限->自行添加菜单和配置权限
|
||||
```
|
||||
|
||||
|
||||
2、创建插件完毕重启服务端后,插件管理中会出现你新创建的插件信息。操作栏有几个按钮,在此进行说明
|
||||
- 安装:会自动执行 server/hgexample/main.go 文件中的Install方法,方法中的具体逻辑默认为空,可以根据实际情况自行配置。如生成后台菜单、生成插件配置表初始化数据、迁移home页面、web项目文件等。
|
||||
```go
|
||||
// Install 安装模块
|
||||
func (m *module) Install(ctx context.Context) (err error) {
|
||||
// ...
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
- 更新:会自动执行 server/hgexample/main.go 文件中的Upgrade方法,方法中的具体逻辑默认为空,可以根据实际情况自行配置。
|
||||
```go
|
||||
// Upgrade 更新模块
|
||||
func (m *module) Upgrade(ctx context.Context) (err error) {
|
||||
// ...
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
- 卸载:会自动执行 server/hgexample/main.go 文件中的UnInstall方法,方法中的具体逻辑默认为空,可以根据实际情况自行配置。如会清除所有的数据表和已安装的信息等。
|
||||
```go
|
||||
// UnInstall 卸载模块
|
||||
func (m *module) UnInstall(ctx context.Context) (err error) {
|
||||
// ...
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 开发
|
||||
|
||||
完全可以根据HotGo正常的开发流程去开发对应的API、控制器、业务逻辑、插件内的应用
|
||||
|
||||
### 调用主模块服务接口
|
||||
|
||||
这里推荐的方式是在插件input层新建一个结构,继承主模块中的input结构。这样做的目的是为了服务与服务之间的输入/输出关系解耦,便于参数扩展和避免插件模块下使用`gf gen service`时出现`import cycle not allowed`。
|
||||
|
||||
一个简单的例子:
|
||||
> 假设hgexample插件模块要通过主模块的服务接口更新插件配置
|
||||
|
||||
插件模块input:\server\addons\hgexample\model\input\sysin\config.go
|
||||
```go
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"hotgo/internal/model/input/sysin"
|
||||
)
|
||||
|
||||
// UpdateConfigInp 更新指定配置
|
||||
type UpdateConfigInp struct {
|
||||
sysin.UpdateAddonsConfigInp
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
插件模块业务逻辑:\server\addons\hgexample\logic\sys\config.go
|
||||
```go
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/addons/hgexample/global"
|
||||
"hotgo/addons/hgexample/model/input/sysin"
|
||||
"hotgo/addons/hgexample/service"
|
||||
isc "hotgo/internal/service"
|
||||
)
|
||||
|
||||
type sSysConfig struct{}
|
||||
|
||||
func NewSysConfig() *sSysConfig {
|
||||
return &sSysConfig{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysConfig(NewSysConfig())
|
||||
}
|
||||
|
||||
// UpdateConfigByGroup 更新指定分组的配置
|
||||
func (s *sSysConfig) UpdateConfigByGroup(ctx context.Context, in sysin.UpdateConfigInp) error {
|
||||
in.UpdateAddonsConfigInp.AddonName = global.GetSkeleton().Name
|
||||
return isc.SysAddonsConfig().UpdateConfigByGroup(ctx, in.UpdateAddonsConfigInp)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
主模块input:\server\internal\model\input\sysin\addons_config.go
|
||||
```go
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// UpdateAddonsConfigInp 更新指定插件的配置
|
||||
type UpdateAddonsConfigInp struct {
|
||||
AddonName string `json:"addonName"`
|
||||
Group string `json:"group"`
|
||||
List g.Map `json:"list"`
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
主模块业务逻辑:\server\internal\logic\sys\addons_config.go
|
||||
```go
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
type sSysAddonsConfig struct{}
|
||||
|
||||
func NewSysAddonsConfig() *sSysAddonsConfig {
|
||||
return &sSysAddonsConfig{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysAddonsConfig(NewSysAddonsConfig())
|
||||
}
|
||||
|
||||
// UpdateConfigByGroup 更新指定分组的配置
|
||||
func (s *sSysAddonsConfig) UpdateConfigByGroup(ctx context.Context, in sysin.UpdateAddonsConfigInp) error {
|
||||
// ...
|
||||
return nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
### 访问路径
|
||||
|
||||
#### 后台插件访问路径
|
||||
|
||||
```
|
||||
// IP+端口或域名/admin/插件名称/API路径
|
||||
如:127.0.0.1:8000/admin/hgexample/index/test
|
||||
```
|
||||
|
||||
- 对应控制器路径:`server/addons/hgexample/controller/admin/sys/index.go`
|
||||
|
||||
#### 前端API插件访问路径
|
||||
|
||||
```
|
||||
// IP+端口或域名/api/插件名称/API路径
|
||||
如:127.0.0.1:8000/api/hgexample/index/test
|
||||
```
|
||||
|
||||
- 对应控制器路径:`server/addons/hgexample/controller/api/index.go`
|
||||
|
||||
#### 前台页面插件访问路径
|
||||
|
||||
```
|
||||
// IP+端口或域名/home/插件名称/API路径
|
||||
如:127.0.0.1:8000/home/hgexample/index/test
|
||||
```
|
||||
|
||||
- 对应控制器路径:`server/addons/hgexample/controller/home/index.go`
|
||||
- 对应模板路径:`server/addons/hgexample/resource/public/template`
|
||||
|
||||
#### 静态资源插件访问路径
|
||||
|
||||
```
|
||||
// IP+端口或域名/home/插件名称/API路径
|
||||
如:127.0.0.1:8000/addons/hgexample/default
|
||||
```
|
||||
|
||||
- 对应资源路径:`server/addons/hgexample/resource/public`
|
||||
|
||||
|
||||
#### Websocket插件访问路径
|
||||
|
||||
```
|
||||
// IP+端口或域名/socket/插件名称/API路径
|
||||
如:127.0.0.1:8000/socket/hgexample/index/test
|
||||
```
|
||||
|
||||
- 对应控制器路径:`server/addons/hgexample/controller/socket/index.go`
|
||||
|
||||
|
||||
### 数据迁移
|
||||
|
||||
- 可以将数据迁移逻辑写进server/xxx插件/main.go 文件中的Install方法中,并遵循系统规范进行数据安装
|
||||
@@ -1,86 +0,0 @@
|
||||
## 模块辅助说明
|
||||
|
||||
目录
|
||||
|
||||
- 模块结构
|
||||
- 获取模块信息
|
||||
- 插件路由规则
|
||||
|
||||
|
||||
#### 模块结构
|
||||
- 文件路径:server/internal/library/addons/module.go
|
||||
|
||||
```go
|
||||
// Skeleton 模块骨架
|
||||
type Skeleton struct {
|
||||
Label string `json:"label"` // 标识
|
||||
Name string `json:"name"` // 名称
|
||||
Group int `json:"group"` // 分组
|
||||
Logo string `json:"logo"` // logo
|
||||
Brief string `json:"brief"` // 简介
|
||||
Description string `json:"description"` // 详细描述
|
||||
Author string `json:"author"` // 作者
|
||||
Version string `json:"version"` // 版本号
|
||||
RootPath string `json:"rootPath"` // 根路径
|
||||
View *gview.View `json:"view"` // 模板引擎
|
||||
}
|
||||
|
||||
func (s *Skeleton) GetModule() Module {
|
||||
return GetModule(s.Name)
|
||||
}
|
||||
|
||||
// Module 插件模块
|
||||
type Module interface {
|
||||
Start(option *Option) (err error) // 启动模块
|
||||
Stop() (err error) // 停止模块
|
||||
Ctx() context.Context // 上下文
|
||||
GetSkeleton() *Skeleton // 获取模块
|
||||
Install(ctx context.Context) (err error) // 安装模块
|
||||
Upgrade(ctx context.Context) (err error) // 更新模块
|
||||
UnInstall(ctx context.Context) (err error) // 卸载模块
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取模块信息
|
||||
|
||||
- 在插件模块内
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hotgo/addons/hgexample/global"
|
||||
)
|
||||
|
||||
func test() {
|
||||
fmt.Printf("当前插件模块是:%+v", global.GetSkeleton())
|
||||
}
|
||||
```
|
||||
|
||||
- 在插件模块外
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hotgo/internal/library/addons"
|
||||
"hotgo/internal/library/contexts"
|
||||
)
|
||||
|
||||
func test(ctx context.Context) {
|
||||
fmt.Printf("当前是否为插件请求:%v", contexts.IsAddonRequest(ctx))
|
||||
if contexts.IsAddonRequest(ctx) {
|
||||
fmt.Printf("当前插件名称:%v", contexts.GetAddonName(ctx))
|
||||
fmt.Printf("当前插件信息:%v", addons.GetModule(contexts.GetAddonName(ctx)))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 更多辅助方法请参考插件功能库:server/internal/library/addons
|
||||
|
||||
#### 插件路由规则
|
||||
- 如果你不喜欢现在的路由风格,可以自行调整。修改位置在:\server\internal\library\addons\addons.go的RouterPrefix方法。
|
||||
- 注意调整后如web前端页面中如有之前的路由风格也需同步修改。
|
||||
@@ -1,40 +0,0 @@
|
||||
## 模块介绍及目录
|
||||
|
||||
目录
|
||||
|
||||
- 模块介绍
|
||||
- 启动流程
|
||||
- 目录结构
|
||||
|
||||
### 模块介绍
|
||||
|
||||
> 定位:开发独立、临时性、工具类型的功能时推荐使用插件化开发,例如:小游戏(大转盘/消消乐/抽奖/大屏互动/红包等)、小插件(广告管理/文章管理/友情链接等等)、小模块(报名/投票/签到)、小程序、大型插件微商城等等。
|
||||
|
||||
> 插件模块方便多项目复用,同时完美支持多人协同开发,每个插件模块都有独立的微架构目录结构,多插件之间完全隔离。
|
||||
|
||||
### 启动流程
|
||||
|
||||
HotGo 入口文件->隐式注入(hotgo/addons/modules)->注册所有插件->初始化已安装的插件->写入路由组->根据 HotGo 正常的开发和访问流程去开发访问插件
|
||||
|
||||
|
||||
### 目录结构
|
||||
- 详细介绍请参考:[目录结构](sys-catalog.md)
|
||||
```
|
||||
/server
|
||||
├── addons
|
||||
│ ├── modules
|
||||
│ ├── xxx插件
|
||||
│ | ├── api
|
||||
│ | ├── consts
|
||||
│ | ├── controller
|
||||
│ | ├── crons
|
||||
│ | ├── global
|
||||
│ | ├── logic
|
||||
│ | ├── model
|
||||
│ | ├── queues
|
||||
│ | ├── resource
|
||||
│ | ├── router
|
||||
│ | ├── service
|
||||
│ | ├── main.go
|
||||
│ | └── README.md
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
目录
|
||||
|
||||
- 框架文档
|
||||
- [goframe](https://goframe.org/pages/viewpage.action?pageId=1114119)
|
||||
- [naiveui](https://www.naiveui.com)
|
||||
- [naive-ui-admin](https://docs.naiveadmin.com/)
|
||||
|
||||
|
||||
- 常用组件
|
||||
- [websocket](https://github.com/gorilla/websocket)
|
||||
- [casbin](https://github.com/casbin/casbin)
|
||||
- [tailwind](https://tailwind.docs.73zls.com/docs/flex-direction)
|
||||
|
||||
|
||||
- 系统通用
|
||||
- [redis](https://redis.io/)
|
||||
|
||||
|
||||
- 其他
|
||||
- [awesome-go](https://github.com/avelino/awesome-go)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
## 生成业务模板
|
||||
|
||||
根据`api`接口文件一键生成业务模板:api、controller、logic、service
|
||||
|
||||
待写。
|
||||
@@ -1,172 +0,0 @@
|
||||
## 生成配置
|
||||
|
||||
目录
|
||||
|
||||
- 模板配置
|
||||
- CLI配置
|
||||
- 多数据库使用
|
||||
|
||||
### 模板配置
|
||||
|
||||
- 配置路径:server/manifest/config/config.yaml
|
||||
|
||||
```yaml
|
||||
# 生成代码
|
||||
hggen:
|
||||
allowedIPs: [ "127.0.0.1", "*" ] # 白名单,*代表所有,只有允许的IP后台才能使用生成代码功能
|
||||
selectDbs: [ "default" ] # 可选生成表的数据库配置名称,支持多库
|
||||
disableTables: [ "hg_sys_gen_codes","hg_admin_role_casbin" ] # 禁用的表,禁用以后将不会在选择表中看到
|
||||
delimiters: [ "@{", "}" ] # 模板引擎变量分隔符号
|
||||
# 生成应用模型,所有生成模板允许自定义,可以参考default模板进行改造
|
||||
application:
|
||||
# CRUD和关系树列表模板
|
||||
crud:
|
||||
templates:
|
||||
# 默认的主包模板
|
||||
- group: "default" # 分组名称
|
||||
isAddon: false # 是否为插件模板 false|true
|
||||
masterPackage: "sys" # 主包名称,需和controllerPath、logicPath、inputPath保持关联
|
||||
templatePath: "./resource/generate/default/curd" # 模板路径
|
||||
apiPath: "./api/admin" # goApi生成路径
|
||||
controllerPath: "./internal/controller/admin/sys" # 控制器生成路径
|
||||
logicPath: "./internal/logic/sys" # 主要业务生成路径
|
||||
inputPath: "./internal/model/input/sysin" # 表单过滤器生成路径
|
||||
routerPath: "./internal/router/genrouter" # 生成路由表路径
|
||||
sqlPath: "./storage/data/generate" # 生成sql语句路径
|
||||
webApiPath: "../web/src/api" # webApi生成路径
|
||||
webViewsPath: "../web/src/views" # web页面生成路径
|
||||
|
||||
# 默认的插件包模板,{$name}会自动替换成实际的插件名称
|
||||
- group: "addon" # 分组名称
|
||||
isAddon: true # 是否为插件模板 false|true
|
||||
masterPackage: "sys" # 主包名称,需和controllerPath、logicPath、inputPath保持关联
|
||||
templatePath: "./resource/generate/default/curd" # 模板路径
|
||||
apiPath: "./addons/{$name}/api/admin" # goApi生成路径
|
||||
controllerPath: "./addons/{$name}/controller/admin/sys" # 控制器生成路径
|
||||
logicPath: "./addons/{$name}/logic/sys" # 主要业务生成路径
|
||||
inputPath: "./addons/{$name}/model/input/sysin" # 表单过滤器生成路径
|
||||
routerPath: "./addons/{$name}/router/genrouter" # 生成路由表路径
|
||||
sqlPath: "./storage/data/generate/addons" # 生成sql语句路径
|
||||
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
|
||||
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
|
||||
|
||||
# 消息队列模板
|
||||
queue:
|
||||
templates:
|
||||
- group: "default"
|
||||
templatePath: "./resource/generate/default/queue"
|
||||
|
||||
# 定时任务模板
|
||||
cron:
|
||||
templates:
|
||||
- group: "default"
|
||||
templatePath: "./resource/generate/default/cron"
|
||||
|
||||
# 生成插件模块,通过后台创建新插件时使用的模板,允许自定义,可以参考default模板进行改造
|
||||
addon:
|
||||
srcPath: "./resource/generate/default/addon" # 生成模板路径
|
||||
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
|
||||
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
|
||||
```
|
||||
|
||||
### CLI配置
|
||||
|
||||
- hotgo在生成dao、service配置时,默认了和gf官方一致的配置方式和代码生成规则。所以无论你是通过hotgo亦或gf命令生成,最终代码格式完全一致,遵循一致的代码规范。
|
||||
|
||||
- 配置路径:[server/hack/config.yaml](../../server/hack/config.yaml)
|
||||
|
||||
```yaml
|
||||
gfcli:
|
||||
build:
|
||||
name: "hotgo" # 编译后的可执行文件名称
|
||||
# arch: "all" #不填默认当前系统架构,可选:386,amd64,arm,all
|
||||
# system: "all" #不填默认当前系统平台,可选:linux,darwin,windows,all
|
||||
mod: "none"
|
||||
cgo: 0
|
||||
packSrc: "resource" # 将resource目录打包进可执行文件,静态资源无需单独部署
|
||||
packDst: "internal/packed/packed.go" # 打包后生成的Go文件路径,一般使用相对路径指定到本项目目录中
|
||||
version: ""
|
||||
output: "./temp/hotgo" # 可执行文件生成路径
|
||||
extra: ""
|
||||
|
||||
gen:
|
||||
dao:
|
||||
- link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
|
||||
# - link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
|
||||
group: "default" # 分组 使用hotgo代码生成功能时必须填
|
||||
# tables: "" # 指定当前数据库中需要执行代码生成的数据表。如果为空,表示数据库的所有表都会生成。
|
||||
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
|
||||
removePrefix: "hg_"
|
||||
descriptionTag: true
|
||||
noModelComment: true
|
||||
jsonCase: "CamelLower"
|
||||
gJsonSupport: true
|
||||
clear: true
|
||||
|
||||
service: # 生成业务配置
|
||||
srcFolder: "internal/logic"
|
||||
dstFolder: "internal/service"
|
||||
dstFileNameCase: "CamelLower"
|
||||
clear: true
|
||||
```
|
||||
|
||||
### 多数据库使用
|
||||
|
||||
- 假设我们要增加一个库名为`hotgo2`、分组为`default2`的数据库,并要为其生成代码
|
||||
|
||||
1. 配置[server/hack/config.yaml](../../server/hack/config.yaml) 如下:
|
||||
```yaml
|
||||
gen:
|
||||
dao:
|
||||
- link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
|
||||
group: "default" # 分组 使用hotgo代码生成功能时必须填
|
||||
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
|
||||
removePrefix: "hg_"
|
||||
descriptionTag: true
|
||||
noModelComment: true
|
||||
jsonCase: "CamelLower"
|
||||
gJsonSupport: true
|
||||
clear: false
|
||||
- link: "mysql:hotgo2:hg123456.@tcp(127.0.0.1:3306)/hotgo2?loc=Local&parseTime=true"
|
||||
group: "default2" # 分组 使用hotgo代码生成功能时必须填
|
||||
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
|
||||
removePrefix: ""
|
||||
descriptionTag: true
|
||||
noModelComment: true
|
||||
jsonCase: "CamelLower"
|
||||
gJsonSupport: true
|
||||
clear: false
|
||||
```
|
||||
|
||||
2. 配置`server/manifest/config/config.yaml`,
|
||||
|
||||
`database`配置如下:
|
||||
```yaml
|
||||
database:
|
||||
logger:
|
||||
level: "all"
|
||||
stdout: true
|
||||
default:
|
||||
link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
|
||||
# link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
|
||||
debug: true
|
||||
Prefix: "hg_"
|
||||
default2:
|
||||
link: "mysql:hotgo2:hg123456.@tcp(127.0.0.1:3306)/hotgo2?loc=Local&parseTime=true"
|
||||
debug: true
|
||||
Prefix: ""
|
||||
```
|
||||
|
||||
`hggen`配置如下:
|
||||
```yaml
|
||||
hggen:
|
||||
allowedIPs: ["127.0.0.1", "*"] # 白名单,*代表所有,只有允许的IP后台才能使用生成代码功能
|
||||
selectDbs: [ "default", "default2" ] # 可选生成表的数据库配置名称,支持多库
|
||||
disableTables : ["hg_sys_gen_codes","hg_admin_role_casbin"] # 禁用的表,禁用以后将不会在选择表中看到
|
||||
delimiters: ["@{", "}"] # 模板引擎变量分隔符号
|
||||
```
|
||||
|
||||
3. 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,就会发现`数据库`选项增加了一个`default2`,后续生成步骤和生成例子完全一样
|
||||
|
||||
> 注意:上述的配置中所有的`default2`名称必须保持一致
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
## 生成关联表CURD
|
||||
|
||||
### 热编译启动
|
||||
- 推荐使用热编译方式启动HotGo,这样生成完成页面自动刷新即可看到新生成内容,无需手动重启
|
||||
|
||||
```shell
|
||||
# 服务端
|
||||
cd server
|
||||
gf run main.go
|
||||
|
||||
# web端
|
||||
cd web
|
||||
pnpm run dev 或 npm run dev
|
||||
```
|
||||
|
||||
以下是一个关联表的CURD生成流程
|
||||
|
||||
### 创建表结构
|
||||
- 以下表结构和数据为了方便功能演示已经内置无需再次创建
|
||||
|
||||
|
||||
- 创建主表:hg_sys_gen_curd_demo
|
||||
```sql
|
||||
CREATE TABLE `hg_sys_gen_curd_demo` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`category_id` bigint(20) DEFAULT '0' COMMENT '分类ID',
|
||||
`title` varchar(64) NOT NULL COMMENT '标题',
|
||||
`description` varchar(255) DEFAULT '' COMMENT '描述',
|
||||
`content` text COMMENT '内容',
|
||||
`image` varchar(255) DEFAULT NULL COMMENT '单图',
|
||||
`attachfile` varchar(255) DEFAULT NULL COMMENT '附件',
|
||||
`city_id` bigint(20) DEFAULT '0' COMMENT '所在城市',
|
||||
`switch` int(11) DEFAULT '1' COMMENT '显示开关',
|
||||
`sort` int(11) DEFAULT NULL COMMENT '排序',
|
||||
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
|
||||
`created_by` bigint(20) DEFAULT '0' COMMENT '创建者',
|
||||
`updated_by` bigint(20) DEFAULT '0' COMMENT '更新者',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COMMENT='系统_生成curd演示';
|
||||
```
|
||||
|
||||
创建关联表:hg_test_category
|
||||
```sql
|
||||
CREATE TABLE `hg_test_category` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
|
||||
`name` varchar(255) NOT NULL COMMENT '分类名称',
|
||||
`short_name` varchar(128) DEFAULT NULL COMMENT '简称',
|
||||
`description` varchar(255) DEFAULT NULL COMMENT '描述',
|
||||
`sort` int(11) NOT NULL COMMENT '排序',
|
||||
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
|
||||
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='测试分类';
|
||||
```
|
||||
|
||||
### 表字段要求
|
||||
- 使用生成时,表中必须具有以下字段和属性
|
||||
|
||||
| 字段名称 | 字段含义 | 字段类型 | 可为空 |
|
||||
|--------|----------------------|---------------------|-----|
|
||||
| id | 主键ID | bigint(20) | 否 |
|
||||
|
||||
|
||||
|
||||
### 创建生成配置
|
||||
- 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,选择和填写如下参数:
|
||||
|
||||

|
||||
|
||||
|
||||
### 基本设置
|
||||
- 确认无误后,点击生成配置会跳转到生成配置页面,如下:
|
||||
- 你可以在该页面调整生成表格表头/表列的接口功能、菜单权限、高级设置和关联表设置
|
||||
|
||||

|
||||
|
||||
### 主表字段设置
|
||||
|
||||
- 在该页面你可以调整生成表格字段名称、表单组件、表单编辑/验证项、列表展示/查询项、字段排序、重置和同步字段
|
||||
|
||||

|
||||
|
||||
|
||||
### 关联表字段设置
|
||||
- 在该页面你可以调整生成表格关联表字段名称、列表展示/查询项、字段排序、重置和同步字段
|
||||
- 如果存在多个关联表,也可以对多个关联表字段进行设置
|
||||
|
||||

|
||||
|
||||
### 预览并生成
|
||||
|
||||
点击`预览代码`查看生成的代码内容。如果无需继续调整直接点击`提交生成`即可,以下是预览代码效果:
|
||||
|
||||

|
||||
|
||||
- 如果你使用的热编译,那么页面会在生成成功后立即刷新,刷新完成你即可在后台菜单栏中看到`测试表格`菜单。如果不是使用热编译启动,请手动重启服务后刷新。
|
||||
|
||||
|
||||
### 生成完成
|
||||
|
||||
- 让我们看看生成的表格页面,效果如下:
|
||||
|
||||

|
||||
|
||||
### 常见问题
|
||||
|
||||
- [生成常见问题](code-help.md)
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
## 生成CURD
|
||||
|
||||
### 热编译启动
|
||||
- 推荐使用热编译方式启动HotGo,这样生成完成页面自动刷新即可看到新生成内容,无需手动重启
|
||||
|
||||
```shell
|
||||
# 服务端
|
||||
cd server
|
||||
gf run main.go
|
||||
|
||||
# web端
|
||||
cd web
|
||||
pnpm run dev 或 npm run dev
|
||||
```
|
||||
|
||||
以下是一个基本的CURD生成流程
|
||||
|
||||
### 创建表结构
|
||||
- 以下表结构和数据为了方便功能演示已经内置无需再次创建
|
||||
|
||||
```sql
|
||||
CREATE TABLE `hg_test_category` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
|
||||
`name` varchar(255) NOT NULL COMMENT '分类名称',
|
||||
`short_name` varchar(128) DEFAULT NULL COMMENT '简称',
|
||||
`description` varchar(255) DEFAULT NULL COMMENT '描述',
|
||||
`sort` int(11) NOT NULL COMMENT '排序',
|
||||
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
|
||||
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='测试分类';
|
||||
```
|
||||
|
||||
### 表字段要求
|
||||
- 使用生成时,表中必须具有以下字段和属性
|
||||
|
||||
| 字段名称 | 字段含义 | 字段类型 | 可为空 |
|
||||
|--------|----------------------|---------------------|-----|
|
||||
| id | 主键ID | bigint(20) | 否 |
|
||||
|
||||
|
||||
### 创建生成配置
|
||||
- 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,选择和填写如下参数:
|
||||
|
||||

|
||||
|
||||
|
||||
### 基本设置
|
||||
- 确认无误后,点击生成配置会跳转到生成配置页面,如下:
|
||||
- 你可以在该页面调整生成表格表头/表列的接口功能、菜单权限、高级设置和关联表设置
|
||||
|
||||

|
||||
|
||||
### 表字段设置
|
||||
|
||||
- 在该页面你可以调整生成表格字段名称、表单组件、表单编辑/验证项、列表展示/查询项、字段排序、重置和同步字段
|
||||
|
||||

|
||||
|
||||
### 预览并生成
|
||||
|
||||
点击`预览代码`查看生成的代码内容。如果无需继续调整直接点击`提交生成`即可,以下是预览代码效果:
|
||||
|
||||

|
||||
|
||||
- 如果你使用的热编译,那么页面会在生成成功后立即刷新,刷新完成你即可在后台菜单栏中看到`测试表格`菜单。如果不是使用热编译启动,请手动重启服务后刷新。
|
||||
|
||||
|
||||
### 生成完成
|
||||
|
||||
- 让我们看看生成的表格页面,效果如下:
|
||||
|
||||

|
||||
|
||||
|
||||
### 常见问题
|
||||
|
||||
- [生成常见问题](code-help.md)
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
## 生成常见问题
|
||||
|
||||
### 生成完后页面提示:服务器错误,请稍候重试!
|
||||
|
||||
- 热编译环境下,web端往往会快于服务端重启并加载完成,此时接口访问会出现`服务器错误,请稍候重试!`或`404`。这是服务端正在重启导致的,一般稍等几秒就好,如果不行就手动重启下服务端。
|
||||
|
||||
|
||||
### fetching tables failed: SHOW TABLES: Error 1045 (28000): Access denied for user * (using password: YES)
|
||||
|
||||
- 请去确认`server/manifest/config/config.yaml`和`server/hack/config.yaml`下的数据库配置一致并且权限正确
|
||||
- 参考:[生成配置](code-config.md)
|
||||
|
||||
|
||||
### 为什么后台找不到开发工具菜单
|
||||
|
||||
- 请去确认`server/manifest/config/config.yaml`中的`system.mode`不为`product`。product模式下后台【开发工具】菜单自动隐藏
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
## 使用前提
|
||||
|
||||
|
||||
在HotGo中可以通过后台开发工具快速的一键生成CRUD,自动生成Api、控制器、业务逻辑、Web页面、表单组件、菜单权限等。
|
||||
|
||||
|
||||
- hotgo 版本号 >= 2.13.1
|
||||
- 使用前必须配置 [生成配置](code-config.md)
|
||||
- 使用前必须了解 [数据库](sys-db.md)
|
||||
|
||||
|
||||
相关目录
|
||||
|
||||
- [生成配置](code-config.md)
|
||||
- [生成CURD](code-curd.md)
|
||||
- [生成关联表CURD](code-curd-join.md)
|
||||
- [生成树型CURD](code-tree.md)
|
||||
- [生成业务模板](code-business.md)
|
||||
- [生成模板开发](code-template-dev.md)
|
||||
- [生成常见问题](code-help.md)
|
||||
@@ -1,59 +0,0 @@
|
||||
## 生成模板开发
|
||||
|
||||
### 自定义生成模板
|
||||
|
||||
- HotGo允许你新建新的模板分组来满足你的需求,模板可根据现有模板基础拷贝一份出来做改造,默认模板目录:[server/resource/generate/default](../../server/resource/generate/default)
|
||||
|
||||
- 系统内置了两组CURD生成模板,请参考[生成模板配置](sys-code.md#生成模板配置)。default:是默认的生成到主模块下;addon:是默认生成到指定的插件下
|
||||
|
||||
|
||||
|
||||
### 内置gf-cli
|
||||
|
||||
- 为了确保生成代码的依赖稳定性,在面对`gf`版本更新可能导致向下不兼容情况时,HotGo将`gf-cli`工具内置到系统中并进行在线执行调整,从而提供更可靠和一致的生成代码功能。
|
||||
|
||||
- 后续我们也将开放在线运行`gf gen ...`功能。在做插件开发时也会支持到在线生成插件下的service接口,这将会使得插件开发更加方便
|
||||
|
||||
|
||||
|
||||
### 指定gf-cli版本
|
||||
|
||||
- HotGo多数情况下会和最新版本的gf-cli保持同步,如果更新不及时或你不想使用最新版本的gf-cli来生成代码,可以找到自己想要的版本进行替换即可。
|
||||
|
||||
- 下面大致做一些替换步骤说明:
|
||||
|
||||
1. 打开`github.com/gogf/gf` 找到你想要使用的版本`clone`下来
|
||||
2. 将`clone`代码中`gf/cmd/gf/internal/`目录覆盖到`server/internal/library/hggen/internal`
|
||||
3. 将覆盖过来的目录文件中引入包名`github.com/gogf/gf/cmd/gf/v2/`批量改为`hotgo/internal/library/hggen/`
|
||||
4. 运行`go mod tidy`
|
||||
5. 运行`go run main.go`,如果没有报错,那么恭喜你已经完成了。如果有报错一般都是版本差异带来的影响,需要根据情况自行调整
|
||||
|
||||
|
||||
|
||||
### 指定数据库驱动
|
||||
|
||||
> HotGo默认使用mysql驱动,如果你想用其他数据库驱动打开下方文件中注释即可,目前已支持mysql、pgsql
|
||||
|
||||
- 修改文件路径:[server/internal/library/hggen/internal/cmd/cmd_gen_dao.go](../../server/internal/library/hggen/internal/cmd/cmd_gen_dao.go)
|
||||
|
||||
```go
|
||||
package cmd
|
||||
|
||||
import (
|
||||
//_ "github.com/gogf/gf/contrib/drivers/clickhouse/v2"
|
||||
//_ "github.com/gogf/gf/contrib/drivers/mssql/v2"
|
||||
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
//_ "github.com/gogf/gf/contrib/drivers/oracle/v2"
|
||||
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
|
||||
//_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
|
||||
"hotgo/internal/library/hggen/internal/cmd/gendao"
|
||||
)
|
||||
|
||||
type (
|
||||
cGenDao = gendao.CGenDao
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
修改完成后运行`go mod tidy`
|
||||
@@ -1,93 +0,0 @@
|
||||
## 生成树型CURD
|
||||
|
||||
### 热编译启动
|
||||
- 推荐使用热编译方式启动HotGo,这样生成完成页面自动刷新即可看到新生成内容,无需手动重启
|
||||
|
||||
```shell
|
||||
# 服务端
|
||||
cd server
|
||||
gf run main.go
|
||||
|
||||
# web端
|
||||
cd web
|
||||
pnpm run dev 或 npm run dev
|
||||
```
|
||||
|
||||
以下是一个基本的树形CURD生成流程
|
||||
|
||||
### 创建表结构
|
||||
- 以下表结构和数据为了方便功能演示已经内置无需再次创建
|
||||
|
||||
```sql
|
||||
CREATE TABLE `hg_sys_gen_tree_demo` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`pid` bigint(20) DEFAULT NULL COMMENT '上级ID',
|
||||
`level` int(11) DEFAULT '1' COMMENT '关系树级别',
|
||||
`tree` varchar(512) COMMENT '关系树',
|
||||
`category_id` bigint(20) DEFAULT '0' COMMENT '分类ID',
|
||||
`title` varchar(64) NOT NULL COMMENT '标题',
|
||||
`description` varchar(255) DEFAULT '' COMMENT '描述',
|
||||
`sort` int(11) DEFAULT NULL COMMENT '排序',
|
||||
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
|
||||
`created_by` bigint(20) DEFAULT '0' COMMENT '创建者',
|
||||
`updated_by` bigint(20) DEFAULT '0' COMMENT '更新者',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COMMENT='系统_生成树表演示';
|
||||
```
|
||||
|
||||
### 表字段要求
|
||||
- 使用生成时,表中必须具有以下字段和属性
|
||||
|
||||
| 字段名称 | 字段含义 | 字段类型 | 可为空 |
|
||||
|--------|----------------------|---------------------|-----|
|
||||
| id | 主键ID | bigint(20) | 否 |
|
||||
| pid | 上级树ID | bigint(20) | 是 |
|
||||
| level | 关系树级别 | int(11) | 否 |
|
||||
| tree | 关系树 | varchar(512) | 是 |
|
||||
|
||||
### 创建生成配置
|
||||
- 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,选择和填写如下参数:
|
||||
|
||||

|
||||
|
||||
|
||||
### 基本设置
|
||||
- 确认无误后,点击生成配置会跳转到生成配置页面,如下:
|
||||
- 你可以在该页面调整生成表格表头/表列的接口功能、菜单权限、高级设置和关联表设置
|
||||
- 相比普通CURD,这里多了`树名称字段`和`树表格样式`选项
|
||||
|
||||

|
||||
|
||||
### 表字段设置
|
||||
|
||||
- 在该页面你可以调整生成表格字段名称、表单组件、表单编辑/验证项、列表展示/查询项、字段排序、重置和同步字段
|
||||
- 树表中的pid、level、tree字段由系统维护,单独设置编辑表单功能无效!
|
||||
|
||||

|
||||
|
||||
### 预览并生成
|
||||
|
||||
点击`预览代码`查看生成的代码内容。如果无需继续调整直接点击`提交生成`即可,以下是预览代码效果:
|
||||
|
||||

|
||||
|
||||
- 如果你使用的热编译,那么页面会在生成成功后立即刷新,刷新完成你即可在后台菜单栏中看到`测试表格`菜单。如果不是使用热编译启动,请手动重启服务后刷新。
|
||||
|
||||
|
||||
### 生成完成
|
||||
|
||||
- 让我们看看生成的表格页面,效果如下:
|
||||
|
||||
- 普通树表
|
||||

|
||||
|
||||
- 选项式树表
|
||||

|
||||
|
||||
### 常见问题
|
||||
|
||||
- [生成常见问题](code-help.md)
|
||||
|
||||
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 636 KiB |
|
Before Width: | Height: | Size: 697 KiB |
|
Before Width: | Height: | Size: 512 KiB |
|
Before Width: | Height: | Size: 589 KiB |
|
Before Width: | Height: | Size: 681 KiB |
|
Before Width: | Height: | Size: 722 KiB |
|
Before Width: | Height: | Size: 666 KiB |
|
Before Width: | Height: | Size: 603 KiB |
|
Before Width: | Height: | Size: 491 KiB |
|
Before Width: | Height: | Size: 450 KiB |
|
Before Width: | Height: | Size: 634 KiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 1012 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 57 KiB |
@@ -1,108 +0,0 @@
|
||||
## 生产部署
|
||||
|
||||
目录
|
||||
|
||||
- 编译配置
|
||||
- 编译
|
||||
- 修改生产配置文件
|
||||
- 启动服务
|
||||
- Nginx配置
|
||||
|
||||
### 编译配置
|
||||
|
||||
- 配置文件:server/hack/config.yaml,以下是默认配置
|
||||
```yaml
|
||||
gfcli:
|
||||
build:
|
||||
name: "hotgo" # 编译后的可执行文件名称
|
||||
# arch: "all" #不填默认当前系统架构,可选:386,amd64,arm,all
|
||||
# system: "all" #不填默认当前系统平台,可选:linux,darwin,windows,all
|
||||
mod: "none"
|
||||
cgo: 0
|
||||
packSrc: "resource" # 将resource目录打包进可执行文件,静态资源无需单独部署
|
||||
packDst: "internal/packed/packed.go" # 打包后生成的Go文件路径,一般使用相对路径指定到本项目目录中
|
||||
version: ""
|
||||
output: "./temp/hotgo" # 可执行文件生成路径
|
||||
extra: ""
|
||||
```
|
||||
|
||||
|
||||
### 编译
|
||||
|
||||
- 以下方式任选其一即可
|
||||
|
||||
1、 make一键编译 (确认已安装`make环境`)
|
||||
```shell
|
||||
cd server && make build
|
||||
```
|
||||
|
||||
2、 按步骤手动编译
|
||||
```shell
|
||||
cd server # 切换到服务端目录下
|
||||
rm -rf ./resource/public/admin/ # 删除之前的web资源
|
||||
mkdir ./resource/public/admin/ # 重新创建web资源存放目录,除首次编译后续可以跳过执行此步骤
|
||||
cd ../web && pnpm run build # 切换到web项目下,编译web项目
|
||||
\cp -rf ./dist/* ../server/resource/public/admin/ # 将编译好的web资源复制到server对应的资源存放路径下
|
||||
cd ../server # 切换回服务端目录下
|
||||
echo "y" | gf build # 编译hotgo服务端
|
||||
|
||||
# 不出意外你已经编译好了hotgo可执行文件!
|
||||
```
|
||||
|
||||
3、分端编译 (多人开发时推荐)
|
||||
```shell
|
||||
# 编译服务端
|
||||
cd server # 切换到服务端目录下
|
||||
rm -rf ./resource/public/admin/ # 删除之前的web资源,如果开发环境没有这个目录可以忽略此步骤
|
||||
echo "y" | gf build # 编译hotgo服务端
|
||||
|
||||
# 编译web端
|
||||
cd web
|
||||
pnpm run build 或 npm run build
|
||||
|
||||
# web端编译完成后,将web/dist/*中的文件上传到`server`端线上运行目录:/resource/public/admin即可
|
||||
# 至此,web端和server端都可以独立覆盖更新
|
||||
```
|
||||
|
||||
|
||||
### 修改生产配置文件
|
||||
- 配置文件:server/manifest/config/config.yaml
|
||||
> 如关闭debug、mode设为生产环境、修改数据库地址、缓存驱动、队列驱动、日志路径等
|
||||
|
||||
|
||||
|
||||
### 启动服务
|
||||
> 这里可以接使用gf官方推荐的启动方式,请参考:https://goframe.org/pages/viewpage.action?pageId=1114403
|
||||
|
||||
|
||||
### Nginx配置
|
||||
```
|
||||
# websocket
|
||||
location = /socket {
|
||||
proxy_pass http://127.0.0.1:8000/socket;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_connect_timeout 600s;
|
||||
proxy_read_timeout 600;
|
||||
proxy_send_timeout 600s;
|
||||
}
|
||||
|
||||
# http
|
||||
location ^~ / {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://127.0.0.1:8000/;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection upgrade;
|
||||
}
|
||||
```
|
||||
@@ -1,31 +0,0 @@
|
||||
## 环境搭建
|
||||
|
||||
目录
|
||||
|
||||
- 前端环境
|
||||
- 后端环境
|
||||
- 使用说明
|
||||
|
||||
### 前端环境
|
||||
|
||||
1. 前往https://nodejs.org/zh-cn/下载当前版本node
|
||||
2. 命令行运行 `node -v` 若控制台输出版本号则node安装成功
|
||||
3. node 版本需大于等于 `16.0`
|
||||
4. 安装pnpm:`npm install -g pnpm`
|
||||
5. 命令行运行 `pnpm -v` 若控制台输出版本号则前端环境搭建成功
|
||||
|
||||
### 后端环境
|
||||
1. 下载golang安装 版本号需>=1.23
|
||||
2. 国际: https://golang.org/dl/
|
||||
3. 国内: https://golang.google.cn/dl/
|
||||
4. 命令行运行 go 若控制台输出各类提示命令 则安装成功 输入 `go version` 确认版本大于1.23
|
||||
5. 开发工具推荐 [Goland](https://www.jetbrains.com/go/)
|
||||
|
||||
### 使用说明
|
||||
|
||||
> 需要本地具有 git node golang 环境
|
||||
|
||||
- node版本 >= 20.0.0
|
||||
- golang版本 >= 1.23
|
||||
- mysql版本 >= 5.7,引擎需要是 innoDB 或 postgresql版本 >=14
|
||||
- IDE推荐:Goland
|
||||
@@ -1,107 +0,0 @@
|
||||
## 系统安装
|
||||
|
||||
目录
|
||||
|
||||
- 环境要求
|
||||
- 安装
|
||||
|
||||
### 环境要求
|
||||
|
||||
- node版本 >= v20.0.0
|
||||
- golang版本 >= v1.23
|
||||
- goframe版本 >=v2.9.4
|
||||
- mysql版本 >=5.7 或 postgresql版本 >=14
|
||||
|
||||
> 必须先看[环境搭建文档](start-environment.md),如果安装遇到问题务必先查看[常见问题文档](start-issue.md)
|
||||
|
||||
### 安装
|
||||
|
||||
|
||||
一、克隆项目
|
||||
|
||||
```
|
||||
git clone https://github.com/bufanyun/hotgo.git && cd hotgo
|
||||
```
|
||||
|
||||
二、配置你的站点信息
|
||||
|
||||
1、服务端:
|
||||
- 项目数据库文件 `storage/data/hotgo.sql` 创建数据库并导入
|
||||
- 将配置文件 `manifest/config/config.example.yaml` 复制后改为`manifest/config/config.yaml`
|
||||
- 将`manifest/config/config.yaml`中的`database.default.link`数据库配置改为你自己的:
|
||||
```yaml
|
||||
# Database. 配置参考:https://goframe.org/pages/viewpage.action?pageId=1114245
|
||||
database:
|
||||
logger:
|
||||
path: "logs/database" # 日志文件路径。默认为空,表示关闭,仅输出到终端
|
||||
<<: *defaultLogger
|
||||
stdout: true
|
||||
default:
|
||||
# link: "mysql:账号:密码@tcp(127.0.0.1:3306)/数据库名?loc=Local&parseTime=true&charset=utf8mb4"
|
||||
link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4"
|
||||
debug: true
|
||||
Prefix: "hg_"
|
||||
```
|
||||
|
||||
- 将`hack/config.yaml`中的`gfcli.gen.dao[0].link`数据库配置改为你自己的:
|
||||
```yaml
|
||||
gfcli:
|
||||
gen:
|
||||
dao:
|
||||
- link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4"
|
||||
group: "default" # 分组 使用hotgo代码生成功能时必须填
|
||||
# tables: "" # 指定当前数据库中需要执行代码生成的数据表。如果为空,表示数据库的所有表都会生成。
|
||||
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
|
||||
removePrefix: "hg_"
|
||||
descriptionTag: true
|
||||
noModelComment: true
|
||||
jsonCase: "CamelLower"
|
||||
gJsonSupport: true
|
||||
clear: false
|
||||
```
|
||||
|
||||
2、web前端:
|
||||
- 配置服务端地址,包含在以下文件中:
|
||||
* /hotgo/web/.env.development
|
||||
* /hotgo/web/.env.production
|
||||
* /hotgo/web/.env
|
||||
|
||||
|
||||
三、 启动服务
|
||||
|
||||
1、服务端:
|
||||
```shell
|
||||
cd server
|
||||
|
||||
# 设置国内代理,如果已经设置好了代理可以跳过
|
||||
go env -w GOPROXY=https://goproxy.io,direct
|
||||
|
||||
# 更新包
|
||||
go mod tidy
|
||||
|
||||
# 查看命令行方法
|
||||
go run main.go help
|
||||
|
||||
# 启动所有服务
|
||||
go run main.go # 热编译启动: gf run main.go
|
||||
```
|
||||
|
||||
2、web前端:
|
||||
```shell
|
||||
cd web
|
||||
# 首先确定你以安装node16.0以上版本并安装了包[npm、pnpm],否则可能会出现一些未知报错
|
||||
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 启动web项目
|
||||
pnpm run dev
|
||||
|
||||
# 如果顺利,至此到浏览器打开:http://你的IP:8001/admin
|
||||
# 登录账号:admin, 密码:123456
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
## 常见问题
|
||||
|
||||
目录
|
||||
|
||||
- 一、后台相关
|
||||
- 二、数据库相关
|
||||
- 三、环境相关
|
||||
|
||||
|
||||
### 一、后台相关
|
||||
|
||||
#### 1、连接超时,请刷新重试。如仍未解决请检查websocket连接是否正确!
|
||||
|
||||
线上或非本地运行时,请到 系统设置 -> 配置管理 -> 基本设置 -> 找到网站域名和websocket地址,改成你自己实际的地址,保存刷新页面即可
|
||||
|
||||
#### 2、web页面菜单切换后页面出现白屏
|
||||
|
||||
请参考:https://github.com/jekip/naive-ui-admin/issues/183
|
||||
|
||||
|
||||
### 二、数据库相关
|
||||
|
||||
#### 1、安装数据库出现 json 报错不支持
|
||||
|
||||
请安装 mysql5.7 及以上版本的数据库。如果你使用的是mariadb请确认版本号,mariadb从 10.2 版本开始支持 json 数据类型。
|
||||
|
||||
|
||||
|
||||
### 三、环境相关
|
||||
|
||||
#### 1、not found in resource manager or following system searching paths
|
||||
|
||||
> 报错信息:panic: possible config files "config" or "config.toml/yaml/yml/json/ini/xml/properties" not found in resource manager or following system searching paths:
|
||||
|
||||
系统没有找到配置文件,将配置文件 `manifest/config/config.yaml.bak` 复制后改为`manifest/config/config.yaml`
|
||||
|
||||
|
||||
#### 2、net.DialTimeout failed with network
|
||||
|
||||
> 报错信息:connect to 127.0.0.1:8099 error: net.DialTimeout failed with network "tcp", address "127.0.0.1:8099", timeout "10s": dial tcp
|
||||
|
||||
- http服务没有启动或正在启动
|
||||
- 通过一键启动所有服务运行时属正常情况,多服务启动时存在先后顺序问题,`tcpClient`比`tcpServer`先启动完成导致的,等`tcpServer`启动完成后会自动重连
|
||||
|
||||
详细请参考 - [系统安装](start-installation.md)
|
||||
|
||||
|
||||
#### 3、cannot find "hack/config.yaml" in following paths:
|
||||
> 报错信息:get cli configuration file:hack/config.yaml, err:cannot find "hack/config.yaml" in following paths:
|
||||
|
||||
系统运行目录下配置hack/config.yaml文件。如果是生产环境运行,并且不需要开发工具相关功能,可以将`manifest/config/config.yaml`配置文件中的`system.mode`值改为`product`,这样启动时不会加载开发工具相关功能
|
||||
|
||||
#### 4、非超管角色提示:你没有访问权限!
|
||||

|
||||
|
||||
此问题因当前角色访问的页面包含未分配权限的接口所致,在多角色场景中较为常见,解决步骤如下:
|
||||
|
||||
1. 定位缺失权限的接口
|
||||
- 用超管账号进入系统应用 -> 日志管理 -> 全局日志,搜索状态码为`62 无访问权限`的记录
|
||||
- 示例:若请求接口为`/admin/hgexample/treeTable/list`,需处理的权限路径为`/hgexample/treeTable/list`(去除`/admin`前缀)
|
||||
|
||||

|
||||
|
||||
假如查到请求接口是:`/admin/hgexample/treeTable/list`,那需要分配的菜单权限就是:`/hgexample/treeTable/list`,要把`/admin`去掉
|
||||
|
||||
2. 配置菜单权限
|
||||
- 进入权限管理 -> 菜单权限:
|
||||
- 若目标菜单不存在,先新增菜单
|
||||
- 若菜单已存在但未配置接口权限,就把步骤1中的权限路径添加到【分配权限】中(已配置则跳过)
|
||||
|
||||

|
||||
|
||||
3. 分配权限给角色
|
||||
- 进入权限管理 -> 角色权限,找到对应角色并点击【菜单权限】
|
||||
- 勾选步骤2中配置的菜单权限,保存即可
|
||||
|
||||

|
||||
|
||||
### 四、前端相关
|
||||
|
||||
#### 1、Error: connect ECONNREFUSED ::1:8000
|
||||
|
||||
```text
|
||||
11:44:52 [vite] http proxy error at /member/info:
|
||||
Error: connect ECONNREFUSED ::1:8000
|
||||
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1246:16)
|
||||
```
|
||||
|
||||
- 服务端没有启动
|
||||
- `.\wen\.env.development`中的`VITE_PROXY`配置的服务器地址或端口与实际不一致
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
## 如何提问
|
||||
|
||||
#### 说明环境情况
|
||||
|
||||
- 平台: windows/linux
|
||||
- 软件与版本:golang 1.19, Mysql 5.7 ...
|
||||
- 系统版本:hotgo 2.6.2
|
||||
|
||||
#### 你做了什么?
|
||||
|
||||
- 我按文档(附链接)上写了一个控制器xxx,代码如下:xxxx
|
||||
- 然后我使用xxx访问
|
||||
- 这其中我还做了啥
|
||||
- ...
|
||||
|
||||
#### 出现什么错误?
|
||||
|
||||
- 当我试图xxx的时候,xxx报了如下错误:(截图)
|
||||
- 我确认过我的目录权限是正确的,xxx是正确的(必要时带上截图)
|
||||
|
||||
以上是举例提问时所应该描述的情况,当别人看到详细的情况时,也便于快速定位问题所在。
|
||||
@@ -1,333 +0,0 @@
|
||||
## 更新历史
|
||||
|
||||
升级说明:
|
||||
|
||||
> 创建一个Git版本库,并创建二个分支比如A、B,A分支默认为你的二开分支、B分支为你的版本库分支。
|
||||
> 等需要升级的时候,把版本库分支(B)升级为最新的版本,然后切换到A分支,把B分支合并过来,解决掉合并冲突即可。
|
||||
|
||||
注意:
|
||||
|
||||
> 各个小版本升级例如 2.1.x 升级到 2.2.x 以上不能完美升级最好重新安装
|
||||
|
||||
> 如果升级(覆盖)代码后打开会出现 sql 报错, 请检查更新的数据库格式或自行调整
|
||||
|
||||
|
||||
### v2.18.9
|
||||
updated 2026.5.9
|
||||
|
||||
- 新增:新增 `AGENTS.md` 项目开发规范,统一前后端开发约定,作为 AI 辅助开发的唯一事实来源
|
||||
- 优化:naive-ui版本升级到2.44.1
|
||||
- 优化:gf版本升级到v2.10.0
|
||||
|
||||
|
||||
### v2.18.6
|
||||
updated 2025.10.25
|
||||
|
||||
- 新增:国际化(i18n)支持,内置简体中文、繁体中文、英文三种语言包
|
||||
- 新增:附件管理支持外链图片转存至系统存储驱动
|
||||
- 优化:全局微信公众号实例初始化逻辑,`AccessToken` 获取方式调整为稳定版接口
|
||||
- 优化:菜单权限搜索功能,支持按菜单名称或路由地址进行检索
|
||||
- 优化:角色权限管理中的菜单权限分配交互体验
|
||||
- 优化:naive-ui版本升级到2.43.1
|
||||
- 优化:gf版本升级到v2.9.4
|
||||
- 修复:删除角色导致关联用户无法登录的问题,新增角色删除前置校验(检查是否存在关联用户)
|
||||
|
||||
|
||||
### v2.17.8
|
||||
updated 2025.7.13
|
||||
|
||||
- 优化:表格排序处理器兼容关联表别名
|
||||
- 优化:优化动态统计数字在0值时显示问题
|
||||
- 优化:优化首页快捷菜单点击事件范围
|
||||
- 优化:优化短信、邮件验证码相关验证类数据排序
|
||||
- 优化:优化Nginx配置,支持流式请求和兼容部分版本无法匹配到websocket规则问题
|
||||
- 优化:naive-ui版本升级到2.42.0
|
||||
- 优化:vue3-json-viewer版本升级到2.4.1
|
||||
- 修复:修复可选用户选项`Fields`生成错误问题
|
||||
|
||||
|
||||
### v2.16.10
|
||||
updated 2025.3.22
|
||||
|
||||
- 增加:增加组合下拉用户筛选组件
|
||||
- 优化:文件上传类型限制改为统一由后台配置控制
|
||||
- 优化:websocket重连时间调整为10秒,初始化数据库中的网站地址调整为`127.0.0.1`
|
||||
- 优化:优化访问日志字段展示方式
|
||||
- 优化:系统公告发送编辑模态框调整为抽屉
|
||||
- 优化:gf版本升级到v2.9.0
|
||||
- 优化:naive-ui版本升级到2.41.0
|
||||
- 修复:修复vue路由`Redirect`命名重复问题
|
||||
- 修复:优化代码生成在插件中`使用生成字典选项`时model包名重复问题
|
||||
|
||||
|
||||
### v2.15.11
|
||||
updated 2024.11.27
|
||||
|
||||
- 增加:增加配置管理子页面选项参数记忆
|
||||
- 修复:修复定时任务中日志配置的读取并发读问题
|
||||
- 优化:gf版本升级到v2.8.2
|
||||
- 优化:优化数据卡片展示
|
||||
|
||||
|
||||
### v2.15.7
|
||||
updated 2024.7.21
|
||||
|
||||
- 增加:访问日志、服务日志增加关键词搜索
|
||||
- 增加:web端增加字典状态管理,重构字典选项使用方式,大幅减少冗余代码
|
||||
- 修复:修复生成代码选项式树表已知的一些小bug
|
||||
- 优化:gf版本升级到v2.7.2
|
||||
- 优化:naive-ui版本升级到2.39.0
|
||||
- 优化:访问日志不再记录过大的请求头参数,减少日志大小
|
||||
|
||||
|
||||
### v2.15.1
|
||||
updated 2024.4.22
|
||||
|
||||
- 增加:生成代码增加树表生成、字段排序、字段重置、操作人维护,调整关联表生成方式
|
||||
- 增加:角色菜单权限增加一键导入支持
|
||||
- 增加:访问日志增加忽略请求方式
|
||||
- 增加:插件生成增加`consts`目录维护
|
||||
- 增加:IP归属地定位增加默认重试,增加新的缓存机制
|
||||
- 增加:登录日志增加UA信息、IP归属信息,解藕访问日志
|
||||
- 增加:消息队列`rocketmq`驱动更新版本问题,增加延迟队列,支持自动创建主题
|
||||
- 增加:部门增加类型支持,通过部门类型实现多租户业务支持
|
||||
- 增加:增加多租户业务开发演示
|
||||
- 修复:修复部门列表搜索算法在部分情况下无效问题
|
||||
- 修复:修复短信、邮件IP发送数量限制
|
||||
- 修复:修复分布式锁内存泄漏问题
|
||||
- 优化:gf版本升级到v2.7.0
|
||||
- 优化:naive-ui版本升级到2.38.1
|
||||
- 优化:优化`cmd`服务退出流程
|
||||
- 优化:优化生成代码格式化和移除未使用的包
|
||||
- 优化:服务端配置文件参数`hotgo`改为`system`
|
||||
- 优化:web端表格/表单增加自适应宽度计算
|
||||
- 优化:日期选择组件不再使用默认值
|
||||
- 优化:优化菜单添加/编辑表单,使其操作更加简单方便
|
||||
|
||||
|
||||
### v2.13.1
|
||||
updated 2024.3.7
|
||||
|
||||
- 增加:增加内置数据字典类型:`枚举字典`和`自定义方法字典`,支持代码生成时关联选项使用
|
||||
- 增加:增加大文件上传,支持分片上传、断点续传,存储驱动已适配`本地存储`
|
||||
- 增加:插件模块增加停止服务回调接口,调整静态资源默认存放位置,创建插件选项增加可选扩展功能
|
||||
- 增加:功能案例插件增加`30+`常用组件示例,增加`websocket`消息收发测试
|
||||
- 增加:文档增`加功能扩展库`、`websocket服务器`、`websocket客户端`使用说明,当前版本文档已完善
|
||||
- 修复:修复省市区无法添加地区问题
|
||||
- 优化:gf版本升级到v2.6.4
|
||||
- 优化:优化缓存组件依赖关系
|
||||
- 优化:调整部分前端表格自适应宽度
|
||||
- 优化:HTTP错误码接管统一改为由响应中间件处理
|
||||
|
||||
### v2.12.1
|
||||
updated 2023.12.29
|
||||
|
||||
- 修复:修复访问日志权限过滤
|
||||
- 优化:gf版本升级到v2.6.1
|
||||
- 优化:naive-ui版本升级到2.36.0,vue版本升级到3.4.0,vite版本升级到4.2.7
|
||||
- 优化:优化curd代码生成,编辑表单增加自适应,详情改为`抽屉`,修复菜单权限关系树,简化`State`
|
||||
- 优化:优化字典编辑和查询
|
||||
- 优化:优化角色列表数据加载缓慢问题
|
||||
|
||||
### v2.11.5
|
||||
updated 2023.11.25
|
||||
|
||||
- 增加`minio`上传驱动支持
|
||||
- 增加定时任务调度日志,增加同方法多任务支持
|
||||
- 增加gf运行模式控制,生产环境时隐藏开发相关菜单
|
||||
- 增加树形表格新增/修改自动维护上下级关系通用处理
|
||||
- 增加树形表格使用例子
|
||||
- 优化:gf版本升级到v2.5.7
|
||||
- 优化`websocket`默认路径配置
|
||||
- 优化预处理中间件错误码跟随
|
||||
- 优化插件静态目录为可选项
|
||||
- 修复升级gf版本后服务日志记录内容为空问题
|
||||
- 修复角色权限选择数据重复问题
|
||||
- 修复服务日志无日志内容问题
|
||||
- 修复用户批量删除可以删除超管问题
|
||||
- 修复代码生成时偶尔找不到主键索引问题
|
||||
- 修复树形选项组件在`BasicForm`无法使用问题
|
||||
- 修复表格批量删除重复传参问题
|
||||
- 修复文件缓存`GetOrSet`和`GetOrSetFunc`缓存过期不刷新问题
|
||||
|
||||
### v2.9.8
|
||||
updated 2023.10.21
|
||||
|
||||
- 优化:gf版本升级到v2.5.5
|
||||
- 优化:优化代码生成表单滑动
|
||||
- 优化:优化字典列表编辑生效时机
|
||||
- 修复:修复日期组件默认值无效问题
|
||||
### v2.9.3
|
||||
updated 2023.10.08
|
||||
|
||||
- 优化:gf版本升级到v2.5.4
|
||||
- 优化:优化上传文件资源复用策略
|
||||
- 修复:修复登录角色的部门状态检查
|
||||
- 修复:修复配置参数`exceptLogin`命名
|
||||
|
||||
### v2.8.9
|
||||
updated 2023.09.06
|
||||
|
||||
- 优化:gf版本升级到v2.5.2
|
||||
- 优化:优化代码生成,自定义模板时自动过滤前缀,菜单权限生成预检查
|
||||
- 优化:服务日志内容长度超出最大限制截取
|
||||
- 修复:修复tcp服务器定时任务执行异常
|
||||
- 修复:修复文件选择器错误传参导致的显示异常问题
|
||||
|
||||
### v2.8.4
|
||||
updated 2023.07.20
|
||||
|
||||
- 增加:增加输入预处理中间件
|
||||
- 增加:增加文件选择器
|
||||
- 增加:增加在线服务监控,服务许可证管理
|
||||
- 增加:TCP服务器增加RPC路由消息注册、路由分发、拦截器注册,更方便的进行应用开发
|
||||
- 优化:gf版本升级到v2.5.0
|
||||
- 优化:优化CURD代码生成,简化控制器代码逻辑,升级列表数据查询方式
|
||||
- 修复:修复角色菜单子权限取消导致父级权限失效问题
|
||||
- 修复:修复上传驱动路径错误
|
||||
- 修复:修复CURD代码生成时间类型字段默认值获取异常
|
||||
|
||||
### v2.7.6
|
||||
updated 2023.06.19
|
||||
|
||||
- 优化:gf版本升级到v2.4.3
|
||||
- 修复:部门管理查询空指针问题
|
||||
- 优化:附件md5生成方式调整(与更新前已上传的文件md5不兼容,如果业务有影响请注意调整)
|
||||
|
||||
### v2.7.3
|
||||
updated 2023.05.14
|
||||
|
||||
- 增加:增加注册、手机号登录、二维码邀请注册功能
|
||||
- 优化:优化管理员业务模块验证唯一属性接口,将登录相关功能拆分到管理员基础模块中
|
||||
- 移除:移除web端mock插件
|
||||
|
||||
### v2.6.10
|
||||
updated 2023.05.12
|
||||
|
||||
- 增加:增加jwt+缓存驱动实现的令牌认证,支持自动续约、可控过期失效策略
|
||||
- 移除:移除旧jwt功能库
|
||||
- 移除:移除`decimal`、`mahonia` package
|
||||
|
||||
### v2.6.7
|
||||
updated 2023.05.10
|
||||
|
||||
- 增加:增加支付网关:集成支付宝、微信支付、QQ支付,可通过后台直接配置参数
|
||||
- 增加:增加资金管理模块,包含在线充值、申请提现、资金变动明细功能
|
||||
- 增加:增加redis延迟队列
|
||||
- 增加:tcp服务增加rpc协议通讯支持
|
||||
- 增加:通过微信访问后台自动获取openid,后续微信相关业务使用
|
||||
- 优化:gf版本升级到v2.4.1
|
||||
- 优化:优化CURD代码生成单表列表
|
||||
- 优化:优化菜单权限路由,解决偶尔修改权限不能实时生效的情况
|
||||
- 优化:优化后台导出功能搜索参数无效问题
|
||||
- 优化:将定时任务拆分成可独立部署的程序,通过tcp长连接方式与后台交互
|
||||
- 修复:修复相同文件多次上传页面无响应问题
|
||||
- 修复:修复新用户不更新活跃时间问题
|
||||
- 修复:修复本地缓存当缓存不存在时的异常错误问题
|
||||
- 修复:修复访问日志偶尔出现无法记录问题
|
||||
|
||||
### v2.5.3
|
||||
updated 2023.04.10
|
||||
|
||||
- 优化:优化IP地理位置定位,增加地理位置缓存,避免相同IP并发操作
|
||||
- 优化:优化日志管理中相关列表字段过多在小屏设备列表不显示滚动条情况
|
||||
- 增加:增加短信驱动:腾讯云短信,可通过后台一键切换
|
||||
- 修复:修复角色权限选择自定义部门时,部门选项无法显示问题
|
||||
|
||||
### v2.4.9
|
||||
updated 2023.04.05
|
||||
|
||||
- 优化:优化全局日志数据加载和分页加载,大分页增加分批次加载
|
||||
- 优化:优化TCP服务认证,超出多端登录上限时直接踢掉所有在线让其重连
|
||||
- 增加:增加上传驱动:腾讯云cos、阿里云oss、七牛云对象存储
|
||||
- 优化:优化代码生成文档,数据库文档增加多数据库生成配置
|
||||
- 修复:修复系统监控在苹果m1芯片空指针以及显示问题
|
||||
|
||||
### v2.4.4
|
||||
updated 2023.03.16
|
||||
|
||||
- 优化:优化代码生成多库生成时的菜单sql默认指向默认数据库分组
|
||||
- 优化:优化TCP服务认证机制
|
||||
|
||||
### v2.4.2
|
||||
updated 2023.03.11
|
||||
|
||||
- 修复:修复字典管理列表无法添加/编辑问题
|
||||
- 优化:优化代码生成文档
|
||||
- 优化:优化部门树形选项排序问题
|
||||
- 优化:优化基础设置中的LOGO图片上传组件
|
||||
- 优化:优化黑名单IP过滤策略
|
||||
- 增加:增加基于gtcp实现的C/S服务器,为后续多服务通讯建立基础库
|
||||
- 优化:gf版本升级到v2.3.3
|
||||
|
||||
### v2.3.5
|
||||
updated 2023.02.26
|
||||
|
||||
- 优化: 调整消息队列消费初始化方式,支持在插件模块下注册消费者
|
||||
- 修复:后台用户添加模态框无法正常显示问题
|
||||
- 优化:创建新插件生成增加web页面的生成
|
||||
- 增加:增加生产部署、开发规范、生成代码、表单组件等相关文档
|
||||
- 修复:修复后台已知的一些小bug
|
||||
|
||||
### v2.2.10
|
||||
updated 2023.02.23
|
||||
|
||||
- 增加: 增加插件管理、设计新插件等插件模块
|
||||
- 增加: 增加插件应用:`功能案例`
|
||||
- 增加: 增加使用文档
|
||||
- 优化: 生成代码适配插件模块
|
||||
- 优化:消息队列增加磁盘队列,默认队列从`redis`改为`disk`
|
||||
- 优化:缓存驱动增加文件适配器,默认驱动从`redis`改为`file`,项目安装不再依赖`redis`
|
||||
- 优化:优化系统监控加载流程
|
||||
- 优化:gf版本升级到v2.3.2
|
||||
- 修复:生成代码刷新后偶尔出现加载失败问题
|
||||
|
||||
### v2.2.1
|
||||
updated 2023.01.12
|
||||
|
||||
- 优化: 缓存清理各个应用的缓存文件夹读写判断
|
||||
- 修复: Linux 环境下创建插件报找不到模板文件
|
||||
- 修复: Excel 导入找不到最后一列数据
|
||||
- 增加: 无权限菜单不显示
|
||||
- 优化: 省市区数据为最新的 2023.01.11 的国家统计局省市区数据
|
||||
- 优化: 前台关于图片上传的 js 和 css 引入,避免资源依赖找不到
|
||||
- 优化: 插件模块查询机制, 增加数据缓存依赖,依赖时间为 360 秒
|
||||
- 优化: 文件上传的处理
|
||||
- 修复: 网站配置的多图上传引入路径错误
|
||||
- 修复: 定时任务协程问题导致执行失败
|
||||
-
|
||||
### v2.1.1
|
||||
updated 2022.9.26
|
||||
|
||||
- 优化: 多图上传的显示样式及功能
|
||||
- 修复: 由于上传视频和语音开启了全域名返回导致的上传到服务器错误
|
||||
- 增加: 由增加home页面入口,前台页面
|
||||
|
||||
### v2.0.8
|
||||
updated 2022.9.21
|
||||
|
||||
- 优化: 优化菜单和角色数据权限,支持数据权限和按钮细分权限
|
||||
- 优化: 优化附件上传,增加文件上传选项
|
||||
|
||||
### v2.0.6
|
||||
updated 2022.9.18
|
||||
|
||||
- 增加: 增加阿里云短信配置和发送短信API
|
||||
- 增加: 增加高德地图配置
|
||||
- 优化: 优化数据字典,增加数据类型和标签支持
|
||||
-
|
||||
### v2.0.3
|
||||
updated 2022.9.10
|
||||
|
||||
- 2.0 全新上线
|
||||
|
||||
### v2.0.2
|
||||
updated 2022.6.11
|
||||
|
||||
- 增加: 增加生成代码功能,目前已支持一键生成CURD和关联表
|
||||
- 增加: 增加云存储UCloud配置,后台一键切换驱动
|
||||
|
||||
### v2.0.0
|
||||
updated 2022.5.20
|
||||
|
||||
- 初始化: 2.0 基础框架
|
||||
@@ -1,155 +0,0 @@
|
||||
## 权限控制
|
||||
|
||||
目录
|
||||
|
||||
- 一、菜单权限
|
||||
- 二、数据权限
|
||||
|
||||
|
||||
### 一、菜单权限
|
||||
|
||||
> 菜单权限可以控制web后台菜单功能,每个菜单可以绑定一个或多个API权限,API权限决定了用户是否可以调用服务端接口。
|
||||
|
||||
#### 为角色添加菜单权限
|
||||
|
||||
1、HotGo后台 -> 权限管理 -> 菜单权限 -> 找到添加菜单,按照表单提示添加你的菜单信息。
|
||||
|
||||
2、添加菜单完成后,到 权限管理 -> 角色权限 -> 找到一位已有角色或者添加新角色 -> 在列表右侧操作栏找到菜单权限打开 -> 将第1步中添加的菜单分配给该角色即可。
|
||||
|
||||
#### 细粒度权限
|
||||
|
||||
> 一般用于web后台页面上的功能按钮,对不同的角色展示不同的按钮时会用到。
|
||||
|
||||
1、参考【为角色添加菜单权限】添加一个按钮类型的菜单。并为菜单分配API权限,假设你分配权限是:/test/index
|
||||
|
||||
2、在web页面中创建一个按钮,如下:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<n-button v-if="hasPermission(['/test/index'])">拥有[/test/index]权限可见</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
</script>
|
||||
|
||||
```
|
||||
|
||||
#### 菜单表主要字段解释
|
||||
|
||||
- 代码片段:[server/internal/model/input/adminin/menu.go](../../server/internal/model/input/adminin/menu.go)
|
||||
|
||||
```go
|
||||
type MenuRouteMeta struct {
|
||||
// 解释参考:https://naive-ui-admin-docs.vercel.app/guide/router.html#%E5%A4%9A%E7%BA%A7%E8%B7%AF%E7%94%B1
|
||||
Title string `json:"title"` // 菜单名称 一般必填
|
||||
// Disabled bool `json:"disabled,omitempty"` // 禁用菜单
|
||||
Icon string `json:"icon,omitempty"` // 菜单图标
|
||||
KeepAlive bool `json:"keepAlive,omitempty"` // 缓存该路由
|
||||
Hidden bool `json:"hidden,omitempty"` // 隐藏菜单
|
||||
Sort int `json:"sort,omitempty"` // 排序越小越排前
|
||||
AlwaysShow bool `json:"alwaysShow,omitempty"` // 取消自动计算根路由模式
|
||||
ActiveMenu string `json:"activeMenu,omitempty"` // 当路由设置了该属性,则会高亮相对应的侧边栏。
|
||||
// 这在某些场景非常有用,比如:一个列表页路由为:/list/basic-list
|
||||
// 点击进入详情页,这时候路由为/list/basic-info/1,但你想在侧边栏高亮列表的路由,就可以进行如下设置
|
||||
// 注意是配置高亮路由 `name`,不是path
|
||||
IsRoot bool `json:"isRoot,omitempty"` // 是否跟路由 顶部混合菜单,必须传 true,否则左侧会显示异常(场景就是,分割菜单之后,当一级菜单没有子菜单)
|
||||
FrameSrc string `json:"frameSrc,omitempty" ` // 内联外部地址
|
||||
Permissions string `json:"permissions,omitempty"` // 菜单包含权限集合,满足其中一个就会显示
|
||||
Affix bool `json:"affix,omitempty"` // 是否固定 设置为 true 之后 多页签不可删除
|
||||
Type int `json:"type"` // 菜单类型
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### API权限验证流程
|
||||
|
||||
```mermaid
|
||||
|
||||
graph TD
|
||||
A(用户请求API) --> B(验证中间件<br> server/internal/logic/middleware/admin_auth.go)
|
||||
B -->|没有登录,但API需要登录| H(提示登录)
|
||||
B -->|没有登录,但API不需要登录| G(验证通过,进入业务)
|
||||
B -->|已登录| C(检查用户角色是否拥有API权限<br> server/internal/logic/admin/role.go,方法:Verify) -->|有权限| G(验证通过,进入业务)
|
||||
C -->|API无需验证权限| G(验证通过,进入业务流程)
|
||||
C -->|无权限| D(提示无权限)
|
||||
```
|
||||
|
||||
#### 菜单权限添加或修改后多久生效?
|
||||
|
||||
- API权限实时生效,web后台菜单刷新页面后生效,无需重启服务
|
||||
|
||||
|
||||
### 二、数据权限
|
||||
|
||||
> 数据权限是某人只能看到某些数据,可以为某人设置一定的数据范围,让其只能看到允许他看到的数据。
|
||||
|
||||
> 例如:公司存在多个销售部门,不同的部门仅能查看到自己所属部门下的数据;再如:用户有多级代理商,只能查看自己和自己下级的数据。
|
||||
|
||||
#### 目前已经支持的数据权限如下
|
||||
|
||||
| 数据范围 | 描述 |
|
||||
|---------|--------------------------------------------|
|
||||
| 全部权限 | 不做过滤,可以查看所有数据 |
|
||||
| 当前部门 | 仅可以看到所属部门下的数据 |
|
||||
| 当前以及下部门 | 仅可以看到所属以及下级部门的数据 |
|
||||
| 自定义部门 | 可以看到设置的指定部门,支持多个 |
|
||||
| 仅自己 | 仅可以看到自己的数据 |
|
||||
| 自己和直属下级 | 仅可以看到自己和直属下级的数据,直属下级是指当前用户往下1级的用户 |
|
||||
| 自己和全部下级 | 仅可以看到自己和全部下级的数据,全部下级是指当前用户所有的下级用户,包含其下级的下级 |
|
||||
|
||||
#### 如何区分部门和下级用户?
|
||||
|
||||
- 在实际使用时,部门更多的是在公司或机构中使用,可以通过在 组织管理 -> 后台用户 ->为用户绑定部门
|
||||
- 下级用户在代理商或分销系统中比较常见,后台用户由谁添加的,那么被添加的用户就是其下级用户
|
||||
|
||||
#### 如何判断数据是谁的?
|
||||
|
||||
- 在数据库建表时,只要表中包含字段:`created_by`或`member_id`即可过滤 [仅自己、自己和直属下级、自己和全部下级] 类型的数据权限
|
||||
|
||||
#### 如何在具体业务中使用数据权限过滤?
|
||||
|
||||
- 只需在查询、更新或删除等ORM链式操作中加入相应`Handler`方法即可
|
||||
|
||||
- 如果你想对Handler有更多详细了解,请查看:https://goframe.org/pages/viewpage.action?pageId=20087340
|
||||
|
||||
- 下面提供一个简单使用例子:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/hgorm/handler"
|
||||
)
|
||||
|
||||
func test(ctx context.Context) {
|
||||
dao.AdminPost.Ctx(ctx).Where("id", 1).Handler(handler.FilterAuth).Scan(&res)
|
||||
}
|
||||
```
|
||||
|
||||
- 如果表中没有字段:`created_by`或`member_id`,也可以使用自定义字段方法来过滤数据权限,如下:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/hgorm/handler"
|
||||
)
|
||||
|
||||
func test(ctx context.Context) {
|
||||
dao.AdminPost.Ctx(ctx).Where("id", 1).Handler(handler.FilterAuthWithField("test_field")).Scan(&res)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
- 过滤方法实现请参考:[server/internal/library/hgorm/handler/filter_auth.go](../../server/internal/library/hgorm/handler/filter_auth.go)
|
||||
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
## 目录结构
|
||||
|
||||
目录
|
||||
|
||||
- 服务端
|
||||
- web前端
|
||||
- uniapp端(待开放)
|
||||
|
||||
|
||||
#### 服务端
|
||||
```
|
||||
/server
|
||||
├── addons
|
||||
│ ├── modules
|
||||
│ ├── xxx插件
|
||||
│ | ├── api
|
||||
│ | ├── controller
|
||||
│ | ├── crons
|
||||
│ | ├── global
|
||||
│ | ├── logic
|
||||
│ | ├── model
|
||||
│ | ├── queues
|
||||
│ | ├── resource
|
||||
│ | ├── router
|
||||
│ | ├── service
|
||||
│ | ├── main.go
|
||||
│ | └── README.md
|
||||
├── api
|
||||
│ ├── admin
|
||||
│ ├── api
|
||||
│ ├── home
|
||||
│ ├── websocket
|
||||
├── hack
|
||||
├── internal
|
||||
│ ├── cmd
|
||||
│ ├── consts
|
||||
│ ├── controller
|
||||
│ ├── crons
|
||||
│ ├── dao
|
||||
│ ├── global
|
||||
│ ├── library
|
||||
│ ├── logic
|
||||
│ ├── model
|
||||
│ | ├── do
|
||||
│ │ ├── entity
|
||||
│ │ └── input
|
||||
│ ├── packed
|
||||
│ ├── queues
|
||||
│ ├── router
|
||||
│ ├── service
|
||||
│ └── websocket
|
||||
├── manifest
|
||||
├── resource
|
||||
├── storage
|
||||
├── utility
|
||||
├── go.mod
|
||||
├── main.go
|
||||
├── Makefile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
| 目录 | 描述 |
|
||||
|--------------------------|-----------------------------------------------------------------|
|
||||
| 基于gf的工程目录结构做了部分调整 | 参考地址: https://goframe.org/pages/viewpage.action?pageId=30740166 |
|
||||
| **addons** | 所有的插件模块都放在这里 |
|
||||
| --- modules | 为插件模块提供隐式初始化 |
|
||||
| --- xxx插件 | 插件模块名称 |
|
||||
| --- --- api | 对外接口。提供服务的输入/输出数据结构定义 |
|
||||
| --- --- --- admin | 后台接口 |
|
||||
| --- --- --- api | 前台通用接口,包含PC端、移动端接口等 |
|
||||
| --- --- --- home | 前台PC端、H5端页面 |
|
||||
| --- --- --- websocket | 可同时为多应用提供websocket接口 |
|
||||
| --- --- consts | 插件内主要的常量定义 |
|
||||
| --- --- controller | 接收/解析用户输入参数的入口/接口层,也可以理解为控制器 |
|
||||
| --- --- crons | 项目中由系统统一接管的定时任务处理 |
|
||||
| --- --- global | 项目内主要的全局变量和系统的一些初始化操作 |
|
||||
| --- --- logic | 业务逻辑封装管理,特定的业务逻辑实现和封装往往是项目中最复杂的部分 |
|
||||
| --- --- model | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义 |
|
||||
| --- --- --- input | 对内接口。用于controller调用service或service之间调用时的输入/输出结构定义,以及输入过滤和预处理 |
|
||||
| --- queues | 为项目内所有的消息队列的消费者提供统一的初始化和处理 |
|
||||
| --- resource | 静态资源文件。这些文件往往可以通过 资源打包/镜像编译 的形式注入到发布文件中 |
|
||||
| --- --- router | 注册对外接口和分组中间件 |
|
||||
| --- --- service | 用于业务模块解耦的接口定义层具体的接口实现在logic中进行注入 |
|
||||
| --- main.go | 插件始化文件和模块插拔接口 |
|
||||
| **api** | 对外接口。提供服务的输入/输出数据结构定义 |
|
||||
| --- admin | 后台接口 |
|
||||
| --- api | 前台通用接口,包含PC端、移动端接口等 |
|
||||
| --- home | 前台PC端、H5端页面 |
|
||||
| --- websocket | 可同时为多应用提供websocket接口 |
|
||||
| **hack** | 存放项目开发工具、脚本等内容例如,CLI工具的配置,各种shell/bat脚本等文件 |
|
||||
| **internal** | 业务逻辑存放目录通过Golang internal特性对外部隐藏可见性 |
|
||||
| --- cmd | 命令行管理目录可以管理维护多个命令行 |
|
||||
| --- consts | 项目内主要的常量定义 |
|
||||
| --- controller | 接收/解析用户输入参数的入口/接口层,也可以理解为控制器 |
|
||||
| --- crons | 项目中由系统统一接管的定时任务处理 |
|
||||
| --- dao | 数据访问对象,这是一层抽象对象,用于和底层数据库交互,仅包含最基础的 CURD 方法 |
|
||||
| --- global | 项目内主要的全局变量和系统的一些初始化操作 |
|
||||
| --- library | 项目内常用功能的扩展库 |
|
||||
| --- logic | 业务逻辑封装管理,特定的业务逻辑实现和封装往往是项目中最复杂的部分 |
|
||||
| --- model | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义 |
|
||||
| --- --- do | 用于dao数据操作中业务模型与实例模型转换,由工具维护,用户不能修改 |
|
||||
| --- --- entity | 与数据集合绑定的程序数据结构定义,通常和数据表一一对应 |
|
||||
| --- --- input | 对内接口。用于controller调用service或service之间调用时的输入/输出结构定义,以及输入过滤和预处理 |
|
||||
| --- packed | 将静态资源打包进可执行文件,无需单独部署 |
|
||||
| --- queues | 为项目内所有的消息队列的消费者提供统一的初始化和处理 |
|
||||
| --- router | 注册对外接口和分组中间件 |
|
||||
| --- service | 用于业务模块解耦的接口定义层具体的接口实现在logic中进行注入 |
|
||||
| **manifest** | 包含程序编译、部署、运行、配置的文件常见内容如下: |
|
||||
| --- config | 配置文件存放目录 |
|
||||
| --- docker | Docker镜像相关依赖文件,脚本文件等等 |
|
||||
| --- deploy | 部署相关的文件默认提供了Kubernetes集群化部署的Yaml模板,通过kustomize管理 |
|
||||
| **resource** | 静态资源文件。这些文件往往可以通过 资源打包/镜像编译 的形式注入到发布文件中 |
|
||||
| **storage** | 本地数据存储目录,例如文件缓存、磁盘队列数据、sql数据文件、SSL证书等 |
|
||||
| **utility** | 一些常用的工具方法 |
|
||||
| go.mod | 使用Go Module包管理的依赖描述文件 |
|
||||
| main.go | 程序入口文件 |
|
||||
| Makefile | 程序构建发布和开发快捷指令 |
|
||||
| README.md | 项目介绍文件 |
|
||||
|
||||
|
||||
|
||||
|
||||
#### web前端
|
||||
```
|
||||
/web
|
||||
├── build # 打包脚本相关
|
||||
│ ├── config # 配置文件
|
||||
│ ├── generate # 生成器
|
||||
│ ├── script # 脚本
|
||||
│ └── vite # vite配置
|
||||
├── mock # mock文件夹
|
||||
├── public # 公共静态资源目录
|
||||
├── src # 主目录
|
||||
│ ├── api # 接口文件
|
||||
│ ├── assets # 资源文件
|
||||
│ │ ├── icons # icon sprite 图标文件夹
|
||||
│ │ ├── images # 项目存放图片的文件夹
|
||||
│ │ └── svg # 项目存放svg图片的文件夹
|
||||
│ ├── components # 公共组件
|
||||
│ ├── design # 样式文件
|
||||
│ ├── directives # 指令
|
||||
│ ├── enums # 枚举/常量
|
||||
│ ├── hooks # hook
|
||||
│ │ ├── component # 组件相关hook
|
||||
│ │ ├── core # 基础hook
|
||||
│ │ ├── event # 事件相关hook
|
||||
│ │ ├── setting # 配置相关hook
|
||||
│ │ └── web # web相关hook
|
||||
│ ├── layouts # 布局文件
|
||||
│ │ ├── default # 默认布局
|
||||
│ │ ├── iframe # iframe布局
|
||||
│ │ └── page # 页面布局
|
||||
│ ├── locales # 多语言
|
||||
│ ├── logics # 逻辑
|
||||
│ ├── main.ts # 主入口
|
||||
│ ├── router # 路由配置
|
||||
│ ├── settings # 项目配置
|
||||
│ │ ├── componentSetting.ts # 组件配置
|
||||
│ │ ├── designSetting.ts # 样式配置
|
||||
│ │ ├── encryptionSetting.ts # 加密配置
|
||||
│ │ ├── localeSetting.ts # 多语言配置
|
||||
│ │ ├── projectSetting.ts # 项目配置
|
||||
│ │ └── siteSetting.ts # 站点配置
|
||||
│ ├── store # 数据仓库
|
||||
│ ├── utils # 工具类
|
||||
│ └── views # 页面
|
||||
├── types # 类型文件
|
||||
├── vite.config.ts # vite配置文件
|
||||
└── windi.config.ts # windcss配置文件
|
||||
```
|
||||
|
||||
|
||||
#### uniapp端
|
||||
```
|
||||
// 待开放
|
||||
```
|
||||
@@ -1,379 +0,0 @@
|
||||
## 代码生成
|
||||
|
||||
目录
|
||||
|
||||
- 使用条件
|
||||
- 模板配置
|
||||
- CLI配置
|
||||
- 生成CRUD表格
|
||||
- 生成树形表格
|
||||
- 多数据库使用
|
||||
- 自定义生成模板
|
||||
- 内置gf-cli
|
||||
- 指定gf-cli版本
|
||||
- 指定数据库驱动
|
||||
|
||||
|
||||
> 在HotGo中可以通过后台开发工具快速的一键生成CRUD,自动生成Api、控制器、业务逻辑、Web页面、表单组件、菜单权限等。
|
||||
|
||||
|
||||
### 使用条件
|
||||
|
||||
- hotgo 版本号 >= 2.13.1
|
||||
- 使用前必须先看 [数据库](sys-db.md)
|
||||
|
||||
|
||||
### 生成模板配置
|
||||
|
||||
- 配置路径:server/manifest/config/config.yaml
|
||||
```yaml
|
||||
# 生成代码
|
||||
hggen:
|
||||
allowedIPs: [ "127.0.0.1", "*" ] # 白名单,*代表所有,只有允许的IP后台才能使用生成代码功能
|
||||
selectDbs: [ "default" ] # 可选生成表的数据库配置名称,支持多库
|
||||
disableTables: [ "hg_sys_gen_codes","hg_admin_role_casbin" ] # 禁用的表,禁用以后将不会在选择表中看到
|
||||
delimiters: [ "@{", "}" ] # 模板引擎变量分隔符号
|
||||
# 生成应用模型,所有生成模板允许自定义,可以参考default模板进行改造
|
||||
application:
|
||||
# CRUD和关系树列表模板
|
||||
crud:
|
||||
templates:
|
||||
# 默认的主包模板
|
||||
- group: "default" # 分组名称
|
||||
isAddon: false # 是否为插件模板 false|true
|
||||
masterPackage: "sys" # 主包名称,需和controllerPath、logicPath、inputPath保持关联
|
||||
templatePath: "./resource/generate/default/curd" # 模板路径
|
||||
apiPath: "./api/admin" # goApi生成路径
|
||||
controllerPath: "./internal/controller/admin/sys" # 控制器生成路径
|
||||
logicPath: "./internal/logic/sys" # 主要业务生成路径
|
||||
inputPath: "./internal/model/input/sysin" # 表单过滤器生成路径
|
||||
routerPath: "./internal/router/genrouter" # 生成路由表路径
|
||||
sqlPath: "./storage/data/generate" # 生成sql语句路径
|
||||
webApiPath: "../web/src/api" # webApi生成路径
|
||||
webViewsPath: "../web/src/views" # web页面生成路径
|
||||
|
||||
# 默认的插件包模板,{$name}会自动替换成实际的插件名称
|
||||
- group: "addon" # 分组名称
|
||||
isAddon: true # 是否为插件模板 false|true
|
||||
masterPackage: "sys" # 主包名称,需和controllerPath、logicPath、inputPath保持关联
|
||||
templatePath: "./resource/generate/default/curd" # 模板路径
|
||||
apiPath: "./addons/{$name}/api/admin" # goApi生成路径
|
||||
controllerPath: "./addons/{$name}/controller/admin/sys" # 控制器生成路径
|
||||
logicPath: "./addons/{$name}/logic/sys" # 主要业务生成路径
|
||||
inputPath: "./addons/{$name}/model/input/sysin" # 表单过滤器生成路径
|
||||
routerPath: "./addons/{$name}/router/genrouter" # 生成路由表路径
|
||||
sqlPath: "./storage/data/generate/addons" # 生成sql语句路径
|
||||
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
|
||||
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
|
||||
|
||||
# 消息队列模板
|
||||
queue:
|
||||
templates:
|
||||
- group: "default"
|
||||
templatePath: "./resource/generate/default/queue"
|
||||
|
||||
# 定时任务模板
|
||||
cron:
|
||||
templates:
|
||||
- group: "default"
|
||||
templatePath: "./resource/generate/default/cron"
|
||||
|
||||
# 生成插件模块,通过后台创建新插件时使用的模板,允许自定义,可以参考default模板进行改造
|
||||
addon:
|
||||
srcPath: "./resource/generate/default/addon" # 生成模板路径
|
||||
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
|
||||
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
|
||||
```
|
||||
|
||||
### 生成CLI配置
|
||||
|
||||
- hotgo在生成dao、service配置时,默认了和gf官方一致的配置方式和代码生成规则。所以无论你是通过hotgo亦或gf命令生成,最终代码格式完全一致,遵循一致的代码规范。
|
||||
|
||||
- 配置路径:[server/hack/config.yaml](../../server/hack/config.yaml)
|
||||
|
||||
```yaml
|
||||
gfcli:
|
||||
build:
|
||||
name: "hotgo" # 编译后的可执行文件名称
|
||||
# arch: "all" #不填默认当前系统架构,可选:386,amd64,arm,all
|
||||
# system: "all" #不填默认当前系统平台,可选:linux,darwin,windows,all
|
||||
mod: "none"
|
||||
cgo: 0
|
||||
packSrc: "resource" # 将resource目录打包进可执行文件,静态资源无需单独部署
|
||||
packDst: "internal/packed/packed.go" # 打包后生成的Go文件路径,一般使用相对路径指定到本项目目录中
|
||||
version: ""
|
||||
output: "./temp/hotgo" # 可执行文件生成路径
|
||||
extra: ""
|
||||
|
||||
gen:
|
||||
dao:
|
||||
- link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
|
||||
# - link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
|
||||
group: "default" # 分组 使用hotgo代码生成功能时必须填
|
||||
# tables: "" # 指定当前数据库中需要执行代码生成的数据表。如果为空,表示数据库的所有表都会生成。
|
||||
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
|
||||
removePrefix: "hg_"
|
||||
descriptionTag: true
|
||||
noModelComment: true
|
||||
jsonCase: "CamelLower"
|
||||
gJsonSupport: true
|
||||
clear: true
|
||||
|
||||
service: # 生成业务配置
|
||||
srcFolder: "internal/logic"
|
||||
dstFolder: "internal/service"
|
||||
dstFileNameCase: "CamelLower"
|
||||
clear: true
|
||||
```
|
||||
|
||||
### 生成CRUD表格
|
||||
|
||||
- 推荐使用热编译方式启动HotGo,这样生成完成页面自动刷新即可看到新生成内容,无需手动重启
|
||||
- 服务端热编译启动:`gf run main.go`, web前端启动:`pnpm run dev` 或 `npm run dev`
|
||||
|
||||
1、创建数据表
|
||||
|
||||
1.1 创建测试表格表`hg_test_table`
|
||||
```mysql
|
||||
CREATE TABLE `hg_test_table` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`category_id` bigint(20) NOT NULL COMMENT '分类ID',
|
||||
`title` varchar(64) NOT NULL COMMENT '标题',
|
||||
`description` varchar(255) NOT NULL COMMENT '描述',
|
||||
`content` text NOT NULL COMMENT '内容',
|
||||
`image` varchar(255) DEFAULT NULL COMMENT '单图',
|
||||
`attachfile` varchar(255) DEFAULT NULL COMMENT '附件',
|
||||
`city_id` bigint(20) DEFAULT '0' COMMENT '所在城市',
|
||||
`switch` int(11) DEFAULT '1' COMMENT '显示开关',
|
||||
`sort` int(11) NOT NULL COMMENT '排序',
|
||||
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
|
||||
`created_by` bigint(20) DEFAULT '0' COMMENT '创建者',
|
||||
`updated_by` bigint(20) DEFAULT '0' COMMENT '更新者',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='测试表格';
|
||||
|
||||
```
|
||||
|
||||
1.2 测试分类表`hg_test_category`:
|
||||
- 注意:该表为了方便功能演示已经内置无需再次创建,此处提示表结构只是为了方便大家梳理关联表关系
|
||||
```mysql
|
||||
CREATE TABLE `hg_test_category` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
|
||||
`name` varchar(255) NOT NULL COMMENT '分类名称',
|
||||
`description` varchar(255) DEFAULT NULL COMMENT '描述',
|
||||
`sort` int(11) NOT NULL COMMENT '排序',
|
||||
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
|
||||
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
|
||||
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='测试分类';
|
||||
|
||||
```
|
||||
|
||||
1.3 插入测试数据
|
||||
```mysql
|
||||
INSERT INTO `hg_test_table` (`id`, `category_id`, `title`, `description`, `content`, `image`, `attachfile`, `city_id`, `switch`, `sort`, `status`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 1, '测试标题', '描述', '<h2><strong>不知道写点啥!</strong></h2><p><br></p><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"https://media.w3.org/2010/05/sintel/trailer.mp4\"></iframe><p><br></p><p><img src=\"https://gmycos.facms.cn/hotgo/attachment/2023-02-09/cqdq9iuv0phsg8patk.png\"></p>', 'https://gmycos.facms.cn/hotgo/logo.sig.png', 'https://gmycos.facms.cn/hotgo/attachment/2022-12-30/cpf1x44idoycrtajf2.xlsx', 110102, 1, 10, 1, 0, 1, '2022-12-15 19:30:14', '2023-02-23 13:55:32', NULL);
|
||||
|
||||
```
|
||||
|
||||
|
||||
2、创建生成配置
|
||||
- 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,选择和填写如下参数:
|
||||
|
||||

|
||||
|
||||
|
||||
3、自定义配置
|
||||
- 确认配置无误后,点击生成配置会自动跳转到生成配置页面,如下:
|
||||
|
||||

|
||||
|
||||
- 你可以在该页面点击`预览代码`查看生成的内容,如果无需调整亦可以直接点击`提交生成`,以下是预览代码效果:
|
||||
|
||||

|
||||
|
||||
- 如果你对默认的生成配置不满意,可以根据页面表单提示,自定义表格属性和字段属性
|
||||
|
||||
- 这里假设我们要一个关联表,让表`hg_test_table`字段`category_id`去关联 表`hg_test_category`字段`id`,我们可以这样做:
|
||||
|
||||

|
||||
|
||||
|
||||
- 如果你想调整主表每列字段的显示方式,可以点击上方导航栏中的 [主表字段] 进行调整
|
||||
- 这里我们不做任何调整直接进入下一步。目的是为了后续演示对最终生成结果不满意的再次调整方案
|
||||
|
||||

|
||||
|
||||
4、以上内容都已配置无误后,点击提交生成即可。
|
||||
|
||||
- 如果你使用的热编译,那么页面会在生成成功后立即刷新,刷新完成你即可在后台菜单栏中看到`测试表格`菜单。如果不是使用热编译启动,请手动重启服务后刷新。
|
||||
|
||||
- 注意:热编译环境下,web端往往会快于服务端重启并加载完成,此时访问新生成的菜单页面可能会存在某接口请求超时或404问题,这是服务端正在重启导致的,属于正常现象,一般稍等几秒再试即可。
|
||||
|
||||
- 接下来让我们看看生成的表格页面,效果如下:
|
||||
|
||||

|
||||
|
||||
|
||||
5、假设我们对生成结果不满意,有新的优化需求如下:
|
||||
1. 把`单图`、`附件`改换成上传组件
|
||||
2. 把`创建者`、`更新者`隐藏
|
||||
3. 把`分类ID`隐藏,然后把关联表中的`分类名称`显示出来
|
||||
|
||||
> 那么我们可以回到 开发工具 -> 代码生成 -> 找到刚刚生成的记录 -> 在右侧操作栏中点击生成配置,再次进入配置页面做如下操作即可:
|
||||
|
||||
1. 点击上方导航栏[主表字段],调整字段选项:
|
||||
|
||||

|
||||
|
||||
|
||||
2. 点击上方导航栏[关联表],调整字段选项:
|
||||
|
||||

|
||||
|
||||
|
||||
3. 返回[基本信息],勾选强制覆盖,然后点击`预览代码`,确认生成代码无误后点击`提交生成`
|
||||
|
||||

|
||||
|
||||
|
||||
- 生成完成刷新页面后,再次来到`测试表格`,发现表格已经调整到了我们预期效果
|
||||
|
||||
1. 列表效果:
|
||||
|
||||

|
||||
|
||||
2. 编辑表单效果
|
||||
|
||||

|
||||
|
||||
|
||||
- 至此生成增删改查列表示例结束!
|
||||
|
||||
### 生成树形表格
|
||||
|
||||
待写。
|
||||
|
||||
### 多数据库使用
|
||||
|
||||
- 假设我们要增加一个库名为`hotgo2`、分组为`default2`的数据库,并要为其生成代码
|
||||
|
||||
1. 配置[server/hack/config.yaml](../../server/hack/config.yaml) 如下:
|
||||
```yaml
|
||||
gen:
|
||||
dao:
|
||||
- link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
|
||||
# - link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
|
||||
group: "default" # 分组 使用hotgo代码生成功能时必须填
|
||||
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
|
||||
removePrefix: "hg_"
|
||||
descriptionTag: true
|
||||
noModelComment: true
|
||||
jsonCase: "CamelLower"
|
||||
gJsonSupport: true
|
||||
clear: false
|
||||
- link: "mysql:hotgo2:hg123456.@tcp(127.0.0.1:3306)/hotgo2?loc=Local&parseTime=true"
|
||||
group: "default2" # 分组 使用hotgo代码生成功能时必须填
|
||||
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
|
||||
removePrefix: ""
|
||||
descriptionTag: true
|
||||
noModelComment: true
|
||||
jsonCase: "CamelLower"
|
||||
gJsonSupport: true
|
||||
clear: false
|
||||
```
|
||||
|
||||
2. 配置`server/manifest/config/config.yaml`,
|
||||
|
||||
`database`配置如下:
|
||||
```yaml
|
||||
database:
|
||||
logger:
|
||||
level: "all"
|
||||
stdout: true
|
||||
default:
|
||||
link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
|
||||
# - link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
|
||||
debug: true
|
||||
Prefix: "hg_"
|
||||
default2:
|
||||
link: "mysql:hotgo2:hg123456.@tcp(127.0.0.1:3306)/hotgo2?loc=Local&parseTime=true"
|
||||
debug: true
|
||||
Prefix: ""
|
||||
```
|
||||
|
||||
`hggen`配置如下:
|
||||
```yaml
|
||||
hggen:
|
||||
allowedIPs: ["127.0.0.1", "*"] # 白名单,*代表所有,只有允许的IP后台才能使用生成代码功能
|
||||
selectDbs: [ "default", "default2" ] # 可选生成表的数据库配置名称,支持多库
|
||||
disableTables : ["hg_sys_gen_codes","hg_admin_role_casbin"] # 禁用的表,禁用以后将不会在选择表中看到
|
||||
delimiters: ["@{", "}"] # 模板引擎变量分隔符号
|
||||
```
|
||||
|
||||
3. 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,就会发现`数据库`选项增加了一个`default2`,后续生成步骤和生成例子完全一样
|
||||
|
||||
> 注意:上述的配置中所有的`default2`名称必须保持一致
|
||||
|
||||
|
||||
### 自定义生成模板
|
||||
|
||||
> 系统内置了两组CURD生成模板,请参考[生成模板配置](sys-code.md#生成模板配置)。default:是默认的生成到主模块下;addon:是默认生成到指定的插件下
|
||||
|
||||
- HotGo允许你新建新的模板分组来满足你的需求,模板可根据现有模板基础拷贝一份出来做改造,默认模板目录:[server/resource/generate/default](../../server/resource/generate/default)
|
||||
|
||||
|
||||
|
||||
### 内置gf-cli
|
||||
|
||||
> 为了确保生成代码的依赖稳定性,在面对`gf`版本更新可能导致向下不兼容情况时,HotGo将`gf-cli`工具内置到系统中并进行在线执行调整,从而提供更可靠和一致的生成代码功能。
|
||||
|
||||
- 后续我们也将开放在线运行`gf gen ...`功能。在做插件开发时也会支持到在线生成插件下的service接口,这将会使得插件开发更加方便
|
||||
|
||||
|
||||
|
||||
### 指定gf-cli版本
|
||||
|
||||
> HotGo多数情况下会和最新版本的gf-cli保持同步,如果更新不及时或你不想使用最新版本的gf-cli来生成代码,可以找到自己想要的版本进行替换即可。
|
||||
|
||||
- 下面大致做一些替换步骤说明:
|
||||
|
||||
1. 打开https://github.com/gogf/gf,找到你想要使用的版本`clone`下来
|
||||
2. 将`clone`代码中`gf/cmd/gf/internal/`目录覆盖到`server/internal/library/hggen/internal`
|
||||
3. 将覆盖过来的目录文件中引入包名`github.com/gogf/gf/cmd/gf/v2/`批量改为`hotgo/internal/library/hggen/`
|
||||
4. 运行`go mod tidy`
|
||||
5. 运行`go run main.go`,如果没有报错,那么恭喜你已经完成了。如果有报错一般都是版本差异带来的影响,需要根据情况自行调整
|
||||
|
||||
|
||||
|
||||
### 指定数据库驱动
|
||||
|
||||
> HotGo默认使用mysql驱动,如果你想用其他数据库驱动打开下方文件中注释即可,目前已支持mysql、pgsql
|
||||
|
||||
修改文件路径:server/internal/library/hggen/internal/cmd/cmd_gen_dao.go
|
||||
```go
|
||||
package cmd
|
||||
|
||||
import (
|
||||
//_ "github.com/gogf/gf/contrib/drivers/clickhouse/v2"
|
||||
//_ "github.com/gogf/gf/contrib/drivers/mssql/v2"
|
||||
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
//_ "github.com/gogf/gf/contrib/drivers/oracle/v2"
|
||||
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
|
||||
//_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
|
||||
"hotgo/internal/library/hggen/internal/cmd/gendao"
|
||||
)
|
||||
|
||||
type (
|
||||
cGenDao = gendao.CGenDao
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
修改完成后运行`go mod tidy`
|
||||
@@ -1,75 +0,0 @@
|
||||
## 控制台
|
||||
|
||||
目录
|
||||
|
||||
- 启动所有服务
|
||||
- HTTP服务
|
||||
- 消息队列
|
||||
- 定时任务
|
||||
- 常用工具
|
||||
- Makefile
|
||||
|
||||
### 启动所有服务
|
||||
- 仅推荐在开发期间快速调试使用,线上实际部署时建议将各个服务分开部署,这样重新部署某个服务时无需全部重启。
|
||||
|
||||
```shell
|
||||
|
||||
# 默认
|
||||
go run main.go
|
||||
|
||||
# 通过热编译启动
|
||||
gf run main.go
|
||||
```
|
||||
|
||||
### HTTP服务
|
||||
- 启动HTTP服务,包含websocket。
|
||||
|
||||
```shell
|
||||
# 默认
|
||||
go run main.go http
|
||||
|
||||
# 通过热编译启动
|
||||
gf run main.go --args "http"
|
||||
```
|
||||
|
||||
### 消息队列
|
||||
- 启动消息队列的消费者。
|
||||
|
||||
```shell
|
||||
# 默认
|
||||
go run main.go queue
|
||||
|
||||
# 通过热编译启动
|
||||
gf run main.go --args "queue"
|
||||
```
|
||||
|
||||
### 定时任务
|
||||
- 启动系统中统一注册的定时任务。
|
||||
|
||||
```shell
|
||||
# 默认
|
||||
go run main.go cron
|
||||
|
||||
# 通过热编译启动
|
||||
gf run main.go --args "cron"
|
||||
```
|
||||
|
||||
|
||||
### 常用工具
|
||||
- 释放casbin权限,用于清理无效的权限设置。
|
||||
|
||||
|
||||
```shell
|
||||
go run main.go tools -m=casbin -a1=refresh
|
||||
```
|
||||
|
||||
|
||||
### Makefile
|
||||
- 通过make提供一些快捷命令
|
||||
|
||||
```shell
|
||||
# 一键编译,打包前后端代码到可执行文件
|
||||
make build
|
||||
|
||||
# 更多请查看 /server/Makefile文件
|
||||
```
|
||||
@@ -1,70 +0,0 @@
|
||||
## 定时任务
|
||||
|
||||
目录
|
||||
|
||||
- 实现接口
|
||||
- 一个例子
|
||||
- 更多
|
||||
|
||||
> 在实际的项目开发中,定时任务几乎成为不可或缺的一部分。HotGo为定时任务提供一个方便的后台操作界面,让您能够轻松地进行在线启停、修改和立即执行等操作。这样的设计可以极大地改善您在使用定时任务过程中的体验,让整个过程更加顺畅、高效。
|
||||
|
||||
|
||||
### 实现接口
|
||||
- 为了提供高度的扩展性,定时任务在设计上采用了接口化的思路。只需要实现以下接口,您就可以在任何地方注册和使用定时任务功能,从而实现更大的灵活性和可扩展性。
|
||||
|
||||
```go
|
||||
// Cron 定时任务接口
|
||||
type Cron interface {
|
||||
// GetName 获取任务名称
|
||||
GetName() string
|
||||
// Execute 执行任务
|
||||
Execute(ctx context.Context, parser *Parser) (err error)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 一个例子
|
||||
|
||||
定时任务的文件结构可以根据具体需要进行调整,以下是一个常见的参考结构:
|
||||
|
||||
- 文件路径:server/internal/crons/test.go
|
||||
|
||||
```go
|
||||
package crons
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/library/cron"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cron.Register(Test)
|
||||
}
|
||||
|
||||
// Test 测试任务(无参数)
|
||||
var Test = &cTest{name: "test"}
|
||||
|
||||
type cTest struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (c *cTest) GetName() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
// Execute 执行任务
|
||||
func (c *cTest) Execute(ctx context.Context, parser *cron.Parser) (err error) {
|
||||
parser.Logger.Infof(ctx, "cron test Execute:%v", time.Now()) // 记录任务调度日志
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
继续在后台系统设置-定时任务-添加任务,填写的任务名称需要和上面的名称保持一致,再进行简单的策略配置以后,一个后台可控的定时任务就添加好了!
|
||||
|
||||
|
||||
### 更多
|
||||
|
||||
定时任务源码路径:server/internal/library/cron/cron.go
|
||||
|
||||
更多文档请参考:https://goframe.org/pages/viewpage.action?pageId=1114187
|
||||
@@ -1,150 +0,0 @@
|
||||
## 数据库
|
||||
|
||||
目录
|
||||
|
||||
- 字段类型
|
||||
- 特殊字段默认表单组件
|
||||
- 特殊字段默认表单验证器
|
||||
- SQL默认查询方式
|
||||
- 其他
|
||||
|
||||
### 字段类型
|
||||
|
||||
- 创建数据库表当按如下的规则进行字段命名、类型、属性设置和备注后,再生成CRUD代码时会自动生成对应的Api、控制器、业务逻辑、Web页面、[表单组件](web-form.md)等的一些默认属性
|
||||
- 当你了解这些默认技巧后,会有效提高你在实际开发中的生产效率
|
||||
|
||||
| 数据库类型 | 额外属性 | 转换Go类型 | 转换Ts类型 | 表单组件 |
|
||||
|---------------------------------------------------------------|--------------|--------------|---------|-----------------------|
|
||||
| int, tinyint,small_int,smallint,medium_int,mediumint,serial | / | int | number | InputNumber(数字输入框) |
|
||||
| int, tinyint,small_int,smallint,medium_int,mediumint,serial | unsigned | uint | number | InputNumber(数字输入框) |
|
||||
| big_int,bigint,bigserial | / | int64 | number | InputNumber(数字输入框) |
|
||||
| big_int,bigint,bigserial | unsigned | uint64 | number | InputNumber(数字输入框) |
|
||||
| real | / | float32 | number | InputNumber(数字输入框) |
|
||||
| float,double,decimal,money,numeric,smallmoney | / | float64 | number | InputNumber(数字输入框) |
|
||||
| bit(1) 、bit(true)、bit(false) | / | bool | boolean | Input(文本输入框,默认) |
|
||||
| bit | / | int64-bytes | array | InputDynamic(动态KV表单) |
|
||||
| bit | unsigned | uint64-bytes | array | InputDynamic (动态KV表单) |
|
||||
| bool | / | bool | boolean | Input(文本输入框,默认) |
|
||||
| date | / | *gtime.Time | string | Date(日期选择器) |
|
||||
| datetime,timestamp,timestamptz | / | *gtime.Time | string | Time(时间选择器) |
|
||||
| json | / | *gjson.Json | string | Input(文本输入框) |
|
||||
| jsonb | / | *gjson.Json | string | Input(文本输入框) |
|
||||
| 以下为物理类型中包含字段部分时的转换方式,默认情况 | / | / | / | / |
|
||||
| text,char,character | / | string | string | Input(文本输入框) |
|
||||
| float,double,numeric | / | string | string | Input(文本输入框) |
|
||||
| bool | / | bool | boolean | Input(文本输入框,默认) |
|
||||
| binary,blob | / | []byte | string | Input(文本输入框,默认) |
|
||||
| int | / | int | number | InputNumber(数字输入框) |
|
||||
| int | unsigned | int | number | InputNumber(数字输入框) |
|
||||
| time | / | *gtime.Time | string | Time(时间选择器) |
|
||||
| date | / | *gtime.Time | string | Date(日期选择器) |
|
||||
| 没有满足以上任何条件的 | / | string | string | Input(文本输入框) |
|
||||
|
||||
|
||||
### 特殊字段默认表单组件
|
||||
- 以下字段在不设置表单组件时会默认使用的表单组件
|
||||
|
||||
| 数据库字段 | 字段名称 | 表单组件 |
|
||||
|--------------|----------------------|----------------------|
|
||||
| status | 状态字段(任意int类型) | Select (单选下拉框) |
|
||||
| created_at | 创建时间字段 | TimeRange (时间范围选择) |
|
||||
| province_id | 省份ID字段(任意int类型) | CitySelector (省市区选择) |
|
||||
| city_id | 城市ID字段(任意int类型) | CitySelector (省市区选择) |
|
||||
| 任意字串符字段 | 长度>= 200 and <= 500 | InputTextarea (文本域) |
|
||||
| 任意字串符字段 | 长度> 500 | InputEditor (富文本) |
|
||||
|
||||
|
||||
### 特殊字段默认表单验证器
|
||||
- 以下字段在不设置表单组件时会默认使用的表单验证器
|
||||
|
||||
| 数据库字段/Go类型 | 字段名称 | 表单验证规则 |
|
||||
|-------------------|--------|-----------------------|
|
||||
| mobile | 手机号 | 不为空时必须是手机号码(国内) |
|
||||
| qq | QQ | 不为空时必须是QQ号码 |
|
||||
| email | 邮箱地址 | 不为空时必须是邮箱格式 |
|
||||
| id_card | 身份证号码 | 不为空时必须是15或18位身份证号码 |
|
||||
| bank_card | 银行卡号码 | 银行卡号码 |
|
||||
| password | 密码 | 密码验证,必须包含6-18为字母和数字 |
|
||||
| price | 价格 | 金额验证,最多允许输入10位整数及2位小数 |
|
||||
| Go类型为uint、uint64 | 正整数 | 非零正整数验证 |
|
||||
|
||||
### SQL默认查询方式
|
||||
- Go类型取决于数据库物理类型,请参考 [字段类型] 部分
|
||||
|
||||
| Go类型 | 查询方式 |
|
||||
|-------------------------|--------------------------------------|
|
||||
| string | LIKE |
|
||||
| date,datetime | = |
|
||||
| int,uint,int64,uint64 | = |
|
||||
| []int,[]int64,[]uint64 | IN (...) |
|
||||
| float32,float64 | = |
|
||||
| []byte4 | =(默认) |
|
||||
| time.Time,*gtime.Time | = |
|
||||
| *gjson.Json | JSON_CONTAINS(json_doc, val[, path]) |
|
||||
|
||||
|
||||
|
||||
### 其他
|
||||
|
||||
#### 默认字典选项
|
||||
|
||||
- 数据库字段为 `status`且类型为任意数字类型的会使用系统默认的状态字典
|
||||
|
||||
#### 默认属性
|
||||
|
||||
- 默认必填,当数据库字段存在非空`IS_NULLABLE`属性时,默认勾选必填验证
|
||||
- 默认唯一,当数据库字段索引存在`UNI`时,默认勾选唯一值验证
|
||||
- 默认主键,当数据库字段索引存在`PRI`时,默认为主键,不允许编辑
|
||||
- 默认排序,当数据库字段存在`sort`时,默认开启排序,添加表单自动获取最大排序增量值并填充表单
|
||||
- 默认列名,默认使用字段注释作为表格的列名。当数据库字段未设置注释时,默认使用字段名称作为列名
|
||||
|
||||
#### 自动更新/插入
|
||||
|
||||
- 自动更新,当数据库字段为`updated_at`(更新时间),`updated_by`(更新者)
|
||||
- 自动插入,当数据库字段为`created_at`(创建时间),`created_by`(创建者)
|
||||
- 软删除,表存在字段`deleted_at`时,使用表的Orm模型查询条件将会自动加入[ `deleted_at` IS NULL ],删除时只更新删除时间而不会真的删除数据
|
||||
- 树表:不论更新插入,都会根据表中字段`pid`(上级ID)自动维护`level`(树等级)和`tree`(关系树)
|
||||
|
||||
|
||||
#### 操作人字段维护
|
||||
|
||||
- 生成列表中存在并且勾选展示字段`created_by`(创建者)、`updated_by`(修改者)、`deleted_by`(删除者)时,会自动到表`hg_admin_member`中获取操作人的基本信息摘要,并渲染到列表中,效果如下:
|
||||
|
||||

|
||||
|
||||
- 生成列表中存在并且勾选查询字段`created_by`(创建者)、`updated_by`(修改者)、`deleted_by`(删除者)时,会强制将查询表单改为关键词查询,从`hg_admin_member`查询操作人。效果如下:
|
||||
|
||||

|
||||
|
||||
- 查询代码片段,参考路径:[server/internal/logic/admin/member.go](../../server/internal/logic/admin/member.go)
|
||||
|
||||
```go
|
||||
|
||||
// 查询创建者
|
||||
if in.CreatedBy != "" {
|
||||
ids, err := service.AdminMember().GetIdsByKeyword(ctx, in.CreatedBy)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
mod = mod.WhereIn(dao.SysGenCurdDemo.Columns().CreatedBy, ids)
|
||||
}
|
||||
|
||||
// GetIdsByKeyword 根据关键词查找符合条件的用户ID
|
||||
func (s *sAdminMember) GetIdsByKeyword(ctx context.Context, ks string) (res []int64, err error) {
|
||||
ks = gstr.Trim(ks)
|
||||
if len(ks) == 0 {
|
||||
return
|
||||
}
|
||||
array, err := dao.AdminMember.Ctx(ctx).Fields("id").
|
||||
Where("`id` = ? or `real_name` = ? or `username` = ? or `mobile` = ?", ks, ks, ks, ks).
|
||||
Array()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, "根据关键词获取用户ID失败,请稍后重试!")
|
||||
}
|
||||
res = gvar.New(array).Int64s()
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
> 这里只列举了较为常用的默认规则,其他更多默认规则请参考:[server/internal/library/hggen/views/column_default.go](../../server/internal/library/hggen/views/column_default.go)
|
||||
@@ -1,201 +0,0 @@
|
||||
## 开发规范
|
||||
|
||||
目录
|
||||
|
||||
- 规范说明
|
||||
- 规范建议
|
||||
- 命名规范
|
||||
- 编码规范
|
||||
- 框架规范
|
||||
- 插件规范
|
||||
- 数据库规范
|
||||
- 包功能规范
|
||||
|
||||
### 规范说明
|
||||
|
||||
- 开发规范意义是是让大家尽可能写出风格统一的代码,让团队代码有更好的可读性与可维护性。
|
||||
- 在实际业务开发中,除了要提高业务开发效率,保证线上业务高性能外,好的编程习惯也是一个开发人员基本素养之一。
|
||||
|
||||
|
||||
### 规范建议
|
||||
|
||||
- 在我们实际开发中,有很多开发人可能是由某一语言转到另外一个语言领域,在转到另外一门语言后, 我们都会保留着对旧语言的编程习惯,在这里,我建议的是,虽然不同语言之前的某些规范可能是相通的, 但是我们最好能够按照官方的一些demo来熟悉是渐渐适应当前语言的编程规范,而不是直接将原来语言的编程规范也随之迁移过来。
|
||||
|
||||
|
||||
### 命名规范
|
||||
|
||||
|
||||
#### 命名准则
|
||||
|
||||
* 当变量名称在定义和最后一次使用之间的距离很短时,简短的名称看起来会更好
|
||||
* 变量命名应尽量描述其内容,而不是类型
|
||||
* 常量命名应尽量描述其值,而不是如何使用这个值
|
||||
* 在遇到for,if等循环或分支时,推荐单个字母命名来标识参数和返回值
|
||||
* package名称也是命名的一部分,推荐全部使用小写,请尽量将其利用起来
|
||||
* 使用一致的命名风格
|
||||
|
||||
#### 文件命名规范
|
||||
* 全部小写
|
||||
* 除unit test外避免下划线(_)
|
||||
* 文件名称不宜过长,但不必为了简短而忽略含义
|
||||
|
||||
#### 变量命名规范参考
|
||||
* 驼峰命名
|
||||
* 见名知义,避免拼音替代英文
|
||||
* 不建议包含下划线(_)
|
||||
* 不建议包含数字
|
||||
|
||||
|
||||
#### 函数、常量命名规范
|
||||
* 驼峰式命名
|
||||
* 可导出的必须首字母大写
|
||||
* 不可导出的必须首字母小写
|
||||
* 避免全部大写与下划线(_)组合
|
||||
|
||||
|
||||
#### 参考文档
|
||||
|
||||
* [Practical Go: Real world advice for writing maintainable Go programs](https://dave.cheney.net/practical-go/presentations/gophercon-singapore-2019.html#_simplicity)
|
||||
|
||||
|
||||
### 编码规范
|
||||
|
||||
#### import
|
||||
* 单行import不建议用圆括号包裹
|
||||
* 按照`官方包`,NEW LINE,`当前工程包`,NEW LINE,`第三方依赖包`顺序引入
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"string"
|
||||
|
||||
"greet/user/internal/config"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
```
|
||||
|
||||
#### 函数返回
|
||||
* 对象避免非指针返回
|
||||
* 遵循有正常值返回则一定无error,有error则一定无正常值返回的原则
|
||||
|
||||
#### 错误处理
|
||||
* 有error必须处理,如果不能处理就必须抛出。
|
||||
* 避免下划线(_)接收error
|
||||
|
||||
#### 函数体编码
|
||||
* 建议一个block结束空一行,如if、for等
|
||||
|
||||
```go
|
||||
func main (){
|
||||
if x==1{
|
||||
// do something
|
||||
}
|
||||
|
||||
fmt.println("xxx")
|
||||
}
|
||||
```
|
||||
* return前尽可能空一行
|
||||
|
||||
```go
|
||||
func getUser(id string)(string,error){
|
||||
....
|
||||
|
||||
return "xx",nil
|
||||
}
|
||||
```
|
||||
|
||||
### 框架规范
|
||||
|
||||
- 项目启动初始化方法都放在`server/internal/global/init.go`,插件放在:`server/addons/xxx插件/global/init.go`
|
||||
- Api 对外接口层 都放在`server/api`,根据实际应用和应用具体功能划分子目录,插件放在:`server/addons/xxx插件/api`
|
||||
- Router 路由注册方式应尽可能按gf2.0版本风格定义,参考:https://goframe.org/pages/viewpage.action?pageId=30736904
|
||||
- Controller 控制器层 都放在`server/internal/controller`,根据应用和具体功能拆分子目录,插件放在:`server/addons/xxx插件/controller`
|
||||
- Input 对内接口层 都放在`server/internal/model/input`,根据应用和具体功能拆分子目录并以`in`结尾,插件放在:`server/addons/xxx插件/model/input`
|
||||
- Logic 业务逻辑层 统一放在`server/internal/logic` 根据实际业务需求按logic/xxx/*.go拆分,插件放在:`server/addons/xxx插件/logic`
|
||||
- Orm 模型实体层 统一放在`server/internal/model/entity`
|
||||
- Dao 数据访问层 统一放在`server/internal/dao`
|
||||
- 自定义工具函数都放在 `server/utility`,且应根据不同类型的方法做分包,相同类型的方法放在同一个包内
|
||||
- 状态枚举统一调用`server/internal/consts`中的常量和属性
|
||||
|
||||
### 插件规范
|
||||
|
||||
- 插件命名统一全部小写
|
||||
- 资源文件在 `addons/xxx插件/resource`或`server/resource`
|
||||
- 每个插件之前应该是完全独立的,尽可能避免插件于插件之间的调用依赖
|
||||
- 插件和主模块之前的调用应尽可能通过 Service 接口来调用
|
||||
- 插件业务初始化顺序下应在主模块之后,应尽可能避免使用init方法隐式初始化
|
||||
|
||||
### 数据库规范
|
||||
|
||||
#### 命名规范
|
||||
|
||||
- 数据表名小写,多关键字使用下划线分割(关键字尽量全称)
|
||||
- 字段名小写,多关键字使用下划线分割(关键字尽量全称)
|
||||
- 禁止使用保留字并且尽量少用含有关键词来命名
|
||||
- 临时表必须以tmp_开头、以日期结尾,备份表必须以bak_开头、以日期结尾
|
||||
- 同数据库表名设置统一前缀,默认是`hg_`
|
||||
- 插件模块表名建议以`hg_addon_`开头,如:`hg_addon_hgexample_table`,含义:`插件_案例_表格`。在生成代码时可自动识别实体命名为:`Table`
|
||||
|
||||
#### 基础规范
|
||||
|
||||
- 所有的字段必须添加注释
|
||||
- 尽可能地使用InnoDB作为表的存储引擎
|
||||
- 所有的表和字段必须添加注释
|
||||
- 尽量控制表行数在500万以内
|
||||
- 尽可能采用冷热数据分离策略
|
||||
- 禁止以图片、文件等二进制数据
|
||||
|
||||
#### 表设计规范
|
||||
|
||||
- 尽可能每张表的索引数量控制在5个以内
|
||||
- 每一张InnoDB表都必须含有一个主键,自增主键必须是`bigint`类型
|
||||
- 尽可能避免使用外键约束
|
||||
- 设置数据表架构应考虑后期扩展型
|
||||
- 遵循范式与冗余平衡原则
|
||||
|
||||
#### 字段设计规范
|
||||
|
||||
- 尽可能将所有的数据列定义为 `NOT NULL` 类型
|
||||
- 避免 ENUM 数据类型
|
||||
- json 存储的数据用 `json`字段代替 `text`
|
||||
- 表与表关联的键名保持一致或以关联表名的缩写为前缀
|
||||
- 固定长度的字符串字段务必使用 `char`
|
||||
- 使用 `UNSIGNEG` 存储非负整数
|
||||
- 禁止敏感数据以明文形式存储
|
||||
- 金额相关的数据必须使用 `DECIMAL` 数据类型
|
||||
- 尽量所有表有 `status` 字段来标注数据状态(1:启用,2:禁用,3:已删除),业务状态请使用其他字段;`status`字段类型 为带符号的 `tinyint(1)`。如果还需要其他的数据状态 请先判断该状态的数据是有用的数据还是无意义的数据,有用的数据状态 > 0,无意义的数据状态 < -1
|
||||
- 所有的删除(除开清空回收站操作) 请标记 `status` 为 3
|
||||
- 创建时间字段为`created_at`,修改时间字段为`updated_at`,删除时间字段为`deleted_at`,类型`datetime`
|
||||
- 用户关联 id 字段为 `member_id` 或 `created_by`
|
||||
- 排序字段为 `sort`
|
||||
- 区分应用字段为 `app_id`
|
||||
- 区分插件来源需要增加字段为 `addon_name` 和 `is_addon`
|
||||
- 关系树表中必须包含字段:`id`(主键)、`pid`(上级ID)、`level`(树等级)、`tree`(关系树)
|
||||
- 多选、多图、多文件字段类型应尽量使用类型`json`
|
||||
|
||||
### 包功能规范
|
||||
|
||||
##### Input(对内接口 - 过滤验证)
|
||||
|
||||
- 接收Api 对外接口数据和参数过滤
|
||||
- 对提交的复杂数据参数进行验证处理返回
|
||||
|
||||
##### View(视图)
|
||||
|
||||
- 数据处理显示
|
||||
- 页面渲染
|
||||
|
||||
##### Controller(控制器)
|
||||
|
||||
- 数据接收
|
||||
- 服务调用
|
||||
- 简单CURD入库
|
||||
- 简单业务逻辑
|
||||
|
||||
|
||||
##### Logic(业务逻辑 - 服务接口功能)
|
||||
|
||||
- 数据逻辑处理
|
||||
- 数据入库
|
||||
- 数据查询
|
||||
@@ -1,73 +0,0 @@
|
||||
## 国际化
|
||||
|
||||
|
||||
目录
|
||||
|
||||
- 介绍
|
||||
- 配置文件
|
||||
- 服务端使用
|
||||
- Web端使用
|
||||
- 更多文档
|
||||
|
||||
### 介绍
|
||||
|
||||
从`v2.18.6`版本开始,HotGo 提供了完善的国际化(i18n)支持,内置简体中文、繁体中文和英文三种语言包,默认语言为简体中文。目前已对后台管理首页进行了多语言适配,开发者可基于该机制扩展实现业务模块的多语言功能。
|
||||
|
||||
### 配置文件
|
||||
|
||||
国际化配置位于 `server/manifest/config/config.yaml` 文件中:
|
||||
|
||||
```yaml
|
||||
# hotgo系统配置
|
||||
system:
|
||||
# ...
|
||||
# 国际化
|
||||
i18n:
|
||||
switch: true # 国际化功能开关,可选值:false|true,默认:true
|
||||
defaultLanguage: "zh-CN" # 默认语言,可选值:zh-CN|zh-TW|en,默认:zh-CN
|
||||
```
|
||||
|
||||
### 服务端使用
|
||||
|
||||
多语言配置文件存放目录:`server/manifest/i18n`,您可以在该目录下添加自定义的语言映射文件。
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```go
|
||||
// 设置当前上下文的语言为英文
|
||||
ctx := gctx.New()
|
||||
gi18n.WithLanguage(ctx, "en")
|
||||
|
||||
// 基础翻译
|
||||
gi18n.T(ctx, "你好,美丽世界")
|
||||
// 输出:Hello, Beautiful World
|
||||
|
||||
// 格式化翻译(支持参数替换)
|
||||
gi18n.Tf(ctx, "剩余%v余额", 100)
|
||||
// 输出:Remaining 100 Balance
|
||||
```
|
||||
|
||||
|
||||
### Web端使用
|
||||
|
||||
多语言配置文件存放目录:`web/src/locale`,您可以在该目录下添加自定义的语言映射文件。
|
||||
|
||||
> 提示:启用国际化功能后,后台管理界面右上角将显示语言切换选项。
|
||||
|
||||
**使用示例(假设当前语言为英文):**
|
||||
|
||||
```vue
|
||||
<!-- 基础翻译 -->
|
||||
<div>{{ t('你好,美丽世界') }}</div>
|
||||
<!-- 输出:Hello, Beautiful World -->
|
||||
|
||||
<!-- 格式化翻译(支持参数替换) -->
|
||||
<div>{{ t('剩余{num}余额', {num:100}) }}</div>
|
||||
<!-- 输出:Remaining 100 Balance -->
|
||||
```
|
||||
|
||||
|
||||
### 更多文档
|
||||
|
||||
- [gi18n](https://goframe.org/docs/core/gi18n)
|
||||
- [vie-i18n](https://vue-i18n.intlify.dev)
|
||||
@@ -1,318 +0,0 @@
|
||||
## 功能扩展库
|
||||
|
||||
目录
|
||||
|
||||
- 缓存驱动
|
||||
- 请求上下文
|
||||
- JWT
|
||||
- 数据字典
|
||||
- 地理定位(待写)
|
||||
- 通知(待写)
|
||||
|
||||
|
||||
### 缓存驱动
|
||||
|
||||
> 系统默认的缓存驱动为file,目前已支持:memory|redis|file等多种驱动。请自行选择适合你的驱动使用。
|
||||
|
||||
- 配置文件:server/manifest/config/config.yaml
|
||||
|
||||
```yaml
|
||||
#缓存
|
||||
cache:
|
||||
adapter: "file" # 缓存驱动方式,支持:memory|redis|file,不填默认memory
|
||||
fileDir: "./storage/cache" # 文件缓存路径,adapter=file时必填
|
||||
```
|
||||
|
||||
#### 使用方式
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"hotgo/internal/library/cache"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
func test() {
|
||||
ctx := gctx.New()
|
||||
|
||||
// 添加/修改
|
||||
cache.Instance().Set(ctx, "qwe", 123, 0)
|
||||
|
||||
// 查询
|
||||
cache.Instance().Get(ctx, "qwe")
|
||||
|
||||
// 删除
|
||||
cache.Instance().Remove(ctx, "qwe")
|
||||
|
||||
// 更多方法请参考:https://goframe.org/pages/viewpage.action?pageId=27755640
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 请求上下文
|
||||
|
||||
- 主要用于在处理HTTP和websocket请求时通过中间件将用户、应用、插件等信息绑定到上下文中,方便在做业务处理时用到这些信息
|
||||
|
||||
```go
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/addons"
|
||||
)
|
||||
|
||||
|
||||
func test(ctx context.Context) {
|
||||
// 获取当前请求的所有上下文变量
|
||||
var ctxModel = contexts.Get(ctx)
|
||||
fmt.Printf("当前请求的所有上下文变量:%+v\n", ctxModel)
|
||||
|
||||
// 获取当前请求的应用模块
|
||||
var module = contexts.GetModule(ctx)
|
||||
fmt.Printf("当前请求的应用:%+v\n", module)
|
||||
|
||||
// 获取当前请求的用户信息
|
||||
var member = contexts.GetUser(ctx)
|
||||
fmt.Printf("当前访问用户信息:%+v\n", member)
|
||||
|
||||
// 获取当前请求的插件模块
|
||||
fmt.Printf("当前是否为插件请求:%v", contexts.IsAddonRequest(ctx))
|
||||
if contexts.IsAddonRequest(ctx) {
|
||||
fmt.Printf("当前插件名称:%v", contexts.GetAddonName(ctx))
|
||||
fmt.Printf("当前插件信息:%v", addons.GetModule(contexts.GetAddonName(ctx)))
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### JWT
|
||||
|
||||
- 基于jwt+缓存驱动实现的用户登录令牌功能,支持自动续约,解决了jwt服务端无法退出问题和jwt令牌无法主动失效问题
|
||||
|
||||
#### 配置示例
|
||||
```yaml
|
||||
# 登录令牌
|
||||
token:
|
||||
secretKey: "hotgo123" # 令牌加密秘钥,考虑安全问题生产环境中请修改默认值
|
||||
expires: 604800 # 令牌有效期,单位:秒。默认7天
|
||||
autoRefresh: true # 是否开启自动刷新过期时间, false|true 默认为true
|
||||
refreshInterval: 86400 # 刷新间隔,单位:秒。必须小于expires,否则无法触发。默认1天内只允许刷新一次
|
||||
maxRefreshTimes: 30 # 最大允许刷新次数,-1不限制。默认30次
|
||||
multiLogin: true # 是否允许多端登录, false|true 默认为true
|
||||
|
||||
```
|
||||
|
||||
```go
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"hotgo/internal/library/token"
|
||||
"hotgo/internal/model"
|
||||
)
|
||||
|
||||
|
||||
func test(ctx context.Context) {
|
||||
// 登录
|
||||
user := &model.Identity{
|
||||
Id: mb.Id,
|
||||
Pid: mb.Pid,
|
||||
DeptId: mb.DeptId,
|
||||
RoleId: ro.Id,
|
||||
RoleKey: ro.Key,
|
||||
Username: mb.Username,
|
||||
RealName: mb.RealName,
|
||||
Avatar: mb.Avatar,
|
||||
Email: mb.Email,
|
||||
Mobile: mb.Mobile,
|
||||
App: consts.AppAdmin,
|
||||
LoginAt: gtime.Now(),
|
||||
}
|
||||
|
||||
loginToken, expires, err := token.Login(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// gf请求对象
|
||||
r := *ghttp.Request
|
||||
|
||||
// 获取登录用户信息
|
||||
user, err := token.ParseLoginUser(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 注销登录
|
||||
err = token.Logout(r)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 数据字典
|
||||
|
||||
- hotgo增加了对枚举字典和自定义方法字典的内置支持,从而在系统中经常使用的一些特定数据上做出了增强。
|
||||
|
||||
#### 字典数据选项
|
||||
- 文件路径:server/internal/model/dict.go
|
||||
|
||||
```go
|
||||
package model
|
||||
|
||||
// Option 字典数据选项
|
||||
type Option struct {
|
||||
Key interface{} `json:"key"`
|
||||
Label string `json:"label" description:"字典标签"`
|
||||
Value interface{} `json:"value" description:"字典键值"`
|
||||
ValueType string `json:"valueType" description:"键值数据类型"`
|
||||
Type string `json:"type" description:"字典类型"`
|
||||
ListClass string `json:"listClass" description:"表格回显样式"`
|
||||
}
|
||||
```
|
||||
|
||||
#### 枚举字典
|
||||
- 适用于系统开发期间内置的枚举数据,这样即维护了枚举值,又关联了数据字典
|
||||
|
||||
##### 一个例子
|
||||
- 定义枚举值和字典数据选项,并注册字典类型
|
||||
- 文件路径:server/internal/consts/credit_log.go
|
||||
|
||||
```go
|
||||
package consts
|
||||
|
||||
import (
|
||||
"hotgo/internal/library/dict"
|
||||
"hotgo/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
dict.RegisterEnums("creditType", "资金变动类型", CreditTypeOptions)
|
||||
dict.RegisterEnums("creditGroup", "资金变动分组", CreditGroupOptions)
|
||||
}
|
||||
|
||||
const (
|
||||
CreditTypeBalance = "balance" // 余额
|
||||
CreditTypeIntegral = "integral" // 积分
|
||||
)
|
||||
|
||||
const (
|
||||
CreditGroupDecr = "decr" // 扣款
|
||||
CreditGroupIncr = "incr" // 加款
|
||||
CreditGroupOpDecr = "op_decr" // 操作扣款
|
||||
CreditGroupOpIncr = "op_incr" // 操作加款
|
||||
CreditGroupBalanceRecharge = "balance_recharge" // 余额充值
|
||||
CreditGroupBalanceRefund = "balance_refund" // 余额退款
|
||||
CreditGroupApplyCash = "apply_cash" // 申请提现
|
||||
)
|
||||
|
||||
// CreditTypeOptions 变动类型
|
||||
var CreditTypeOptions = []*model.Option{
|
||||
dict.GenSuccessOption(CreditTypeBalance, "余额"),
|
||||
dict.GenInfoOption(CreditTypeIntegral, "积分"),
|
||||
}
|
||||
|
||||
// CreditGroupOptions 变动分组
|
||||
var CreditGroupOptions = []*model.Option{
|
||||
dict.GenWarningOption(CreditGroupDecr, "扣款"),
|
||||
dict.GenSuccessOption(CreditGroupIncr, "加款"),
|
||||
dict.GenWarningOption(CreditGroupOpDecr, "操作扣款"),
|
||||
dict.GenSuccessOption(CreditGroupOpIncr, "操作加款"),
|
||||
dict.GenWarningOption(CreditGroupBalanceRefund, "余额退款"),
|
||||
dict.GenSuccessOption(CreditGroupBalanceRecharge, "余额充值"),
|
||||
dict.GenInfoOption(CreditGroupApplyCash, "申请提现"),
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
#### 自定义方法字典
|
||||
- 适用于非固定选项,如数据是从某个表/文件读取或从第三方读取,数据需要进行转换时使用
|
||||
|
||||
##### 方法字典接口
|
||||
- 文件路径:server/internal/consts/credit_log.go
|
||||
|
||||
```go
|
||||
package dict
|
||||
|
||||
// FuncDict 方法字典,实现本接口即可使用内置方法字典
|
||||
type FuncDict func(ctx context.Context) (res []*model.Option, err error)
|
||||
```
|
||||
|
||||
##### 一个例子
|
||||
- 定义获取字典数据方法,并注册字典类型
|
||||
- 文件路径:server/internal/logic/admin/post.go
|
||||
|
||||
```go
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/dict"
|
||||
"hotgo/internal/model"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
type sAdminPost struct{}
|
||||
|
||||
func NewAdminPost() *sAdminPost {
|
||||
return &sAdminPost{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAdminPost(NewAdminPost())
|
||||
dict.RegisterFunc("adminPostOption", "岗位选项", service.AdminPost().Option)
|
||||
}
|
||||
|
||||
// Option 岗位选项
|
||||
func (s *sAdminPost) Option(ctx context.Context) (opts []*model.Option, err error) {
|
||||
var list []*entity.AdminPost
|
||||
if err = dao.AdminPost.Ctx(ctx).OrderAsc(dao.AdminPost.Columns().Sort).Scan(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
opts = make([]*model.Option, 0)
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range list {
|
||||
opts = append(opts, dict.GenHashOption(v.Id, v.Name))
|
||||
}
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
#### 代码生成支持
|
||||
- 内置的枚举字典和自定义方法字典在生成代码时可以直接进行选择,生成代码格式和系统字典管理写法一致
|
||||
|
||||

|
||||
|
||||
|
||||
#### 内置字典和系统字典的区分
|
||||
|
||||
##### 主要区别
|
||||
- 系统字典由表:`hg_sys_dict_type`和`hg_sys_dict_data`共同进行维护,使用时需通过后台到字典管理中进行添加
|
||||
- 内置字典是系统开发期间在代码层面事先定义和注册好的数据选项
|
||||
|
||||
|
||||
##### 数据格式区别
|
||||
- 系统字典所有ID都是大于0的int64类型
|
||||
- 内置字典ID都是小于0的int64类型。枚举字典以20000开头,如:-200001381053496;方法字典以30000开头,如:-30000892528327;开头以外数字是根据数据选项的`key`值进行哈希算法得出
|
||||
|
||||
### 地理定位
|
||||
```go
|
||||
// 待写
|
||||
```
|
||||
|
||||
### 通知
|
||||
```go
|
||||
// 待写
|
||||
```
|
||||
@@ -1,250 +0,0 @@
|
||||
## 中间件/拦截器
|
||||
|
||||
目录
|
||||
|
||||
- 介绍
|
||||
- 全局中间件
|
||||
- 鉴权中间件
|
||||
- 响应中间件
|
||||
- 更多
|
||||
|
||||
### 介绍
|
||||
- 在hotgo中,中间件/拦截器主要作用于web请求的上下文预设、跨域请求处理、鉴权处理、请求拦截和请求结束后统一响应处理等。
|
||||
|
||||
|
||||
### 全局中间件
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// 初始化请求上下文,一般需要第一个进行加载,后续中间件存在依赖关系
|
||||
service.Middleware().Ctx()
|
||||
|
||||
// 跨域中间件,自动处理跨域问题
|
||||
service.Middleware().CORS()
|
||||
|
||||
// IP黑名单中间件,如果请求IP被后台拉黑,所有请求将被拒绝
|
||||
service.Middleware().Blacklist()
|
||||
|
||||
// 演示系統操作限制,当开启演示模式时,所有POST请求将被拒绝
|
||||
service.Middleware().DemoLimit()
|
||||
|
||||
// 请求输入预处理,api使用gf规范路由并且XxxReq结构体实现了validate.Filter接口即可隐式预处理
|
||||
service.Middleware().PreFilter()
|
||||
|
||||
// HTTP响应预处理,在业务处理完成后,对响应结果进行格式化和错误过滤,将处理后的数据发送给请求方
|
||||
service.Middleware().ResponseHandler()
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
### 鉴权中间件
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// 在鉴权中间件下的路由如果没有通过权限验证,后续请求将被拒绝
|
||||
// 在hotgo中,鉴权中间件一般是配合一个业务模块下的路由组进行使用
|
||||
// 目前admin、api、home、websocket模块都已接入
|
||||
// 如果你需要创建一个新的模块也需要用到鉴权中间件,可以参考:server/internal/logic/middleware/admin_auth.go
|
||||
|
||||
|
||||
// 一个简单例子
|
||||
s := g.Server()
|
||||
s.Group("/api", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(service.Middleware().ApiAuth)
|
||||
group.Bind(
|
||||
member.Member, // 管理员
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 响应中间件
|
||||
- 文件路径:server/internal/logic/middleware/response.go
|
||||
|
||||
|
||||
#### 常用响应类型
|
||||
|
||||
- hotgo为一些常用的响应类型做了统一格式封装,例如:`application/json`、`text/xml`、`text/html`、`text/event-stream`等,默认使用`application/json`。
|
||||
- 下面我们以`text/xml`为例简单演示几种使用方法:
|
||||
|
||||
1. 当你使用规范化路由时,可直接在XxxRes结构体的`g.Meta`中声明响应类型:
|
||||
|
||||
```go
|
||||
type HelloReq struct {
|
||||
g.Meta `path:"/hello" tags:"Hello" method:"get" summary:"You first hello api"`
|
||||
Name string `json:"name" d:"hotgo" dc:"名字"`
|
||||
}
|
||||
|
||||
type HelloRes struct {
|
||||
g.Meta `mime:"text/xml" type:"string"`
|
||||
Tips string `json:"tips"`
|
||||
}
|
||||
```
|
||||
|
||||
2. 在响应前设置响应头:
|
||||
|
||||
```go
|
||||
var (
|
||||
Hello = cHello{}
|
||||
)
|
||||
|
||||
type cHello struct{}
|
||||
|
||||
func (c *cHello) Hello(ctx context.Context, req *user.HelloReq) (res *user.HelloRes, err error) {
|
||||
r := ghttp.RequestFromCtx(ctx)
|
||||
r.Response.Header().Set("Content-Type", "text/xml")
|
||||
|
||||
res = &user.HelloRes{
|
||||
Tips: fmt.Sprintf("hello %v, this is the api for %v applications.", req.Name, simple.AppName(ctx)),
|
||||
}
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
- 浏览器中访问响应内容如下:
|
||||
|
||||

|
||||
|
||||
|
||||
#### 自定义响应
|
||||
- 在实际开发中,可能需要使用自定义的响应类型,由于响应中间件是全局的,因此您需要对其进行单独处理。
|
||||
- 推荐以下几种处理方案,可做参考:
|
||||
1. 使用`ghttp.ExitAll()`,需要注意的是此方法会终止后续所有的http处理
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := new(ghttp.Request) // 当前请求对象
|
||||
|
||||
// 清空响应
|
||||
r.Response.ClearBuffer()
|
||||
|
||||
// 写入响应
|
||||
r.Response.Write("自定义响应内容")
|
||||
|
||||
// 终止后续http处理
|
||||
r.ExitAll()
|
||||
}
|
||||
```
|
||||
|
||||
2. 在`server/internal/logic/middleware/response.go`中根据请求的独有特征进行单独的处理,兼容后续http处理。
|
||||
|
||||
#### 重写响应错误提示
|
||||
- 在实际开发中,我们可能想要隐藏一些敏感错误,返回给客户端友好的错误提示,但开发者同时又想需要看到真实的敏感错误。对此hotgo已经进行了过滤处理,下面是一个简单的例子:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
func test() error {
|
||||
err = gerror.New("这是一个sql执行错误")
|
||||
err = gerror.Wrap(err, "用户创建失败,请稍后重试!~")
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
- 开启debug时的客户端响应:
|
||||
```json
|
||||
{
|
||||
"code": -1,
|
||||
"message": "用户创建失败,请稍后重试!~",
|
||||
"error": [
|
||||
"1. 用户创建失败,请稍后重试!~",
|
||||
" 1). hotgo/internal/logic/admin.(*sAdminMember).List",
|
||||
" E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/logic/admin/member.go:526",
|
||||
"2. 这是一个sql执行错误", " 1). hotgo/internal/logic/admin.(*sAdminMember).List",
|
||||
" E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/logic/admin/member.go:525",
|
||||
" 2). hotgo/internal/controller/admin/admin.(*cMember).List",
|
||||
" E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/controller/admin/admin/member.go:157", ""
|
||||
],
|
||||
"timestamp": 1684145107,
|
||||
"traceID": "084022730d495f17f19e550140f3e1a8"
|
||||
}
|
||||
```
|
||||
|
||||
- 关闭debug时的客户端响应:
|
||||
```json
|
||||
{
|
||||
"code": -1,
|
||||
"message": "用户创建失败,请稍后重试!~",
|
||||
"timestamp": 1684145107,
|
||||
"traceID": "084022730d495f17f19e550140f3e1a8"
|
||||
}
|
||||
```
|
||||
|
||||
- 控制台的输出日志:
|
||||
|
||||
```shell
|
||||
2023-05-15 18:05:07.776 {084022730d495f17f19e550140f3e1a8} 200 "GET http localhost:8000 /admin/member/list?page=1&pageSize=10&roleId=-1 HTTP/1.1" 0.002, 127.0.0.1, "http://192.168.0.207:8001/login", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Co
|
||||
re/1.94.197.400 QQBrowser/11.7.5287.400", -1, "", ""
|
||||
Stack:
|
||||
1. 用户创建失败,请稍后重试!~
|
||||
1). hotgo/internal/logic/admin.(*sAdminMember).List
|
||||
E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/logic/admin/member.go:526
|
||||
2. 这是一个sql执行错误
|
||||
1). hotgo/internal/logic/admin.(*sAdminMember).List
|
||||
E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/logic/middleware/response.go:24
|
||||
13). hotgo/internal/logic/middleware.(*sMiddleware).DemoLimit
|
||||
E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/logic/middleware/init.go:90
|
||||
|
||||
```
|
||||
|
||||
- 如果你开启了访问日志,那么日志记录中会详细记录本次请求的相关信息,内容如下:
|
||||

|
||||
|
||||
|
||||
#### 重写错误码
|
||||
- hotgo默认使用了gf内置的错误码进行业务处理,通常情况下成功状态码为`0`,失败状态码为`-1`
|
||||
- 查看gf内置错误码:https://goframe.org/pages/viewpage.action?pageId=30739587
|
||||
- 以下是自定义错误码的简单例子:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
func test() error {
|
||||
// 使用自定义状态码30001响应客户端
|
||||
err = gerror.NewCode(gcode.New(30001, "用户创建失败,请稍后重试!~", nil))
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
- 客户端响应如下:
|
||||
```json
|
||||
{
|
||||
"code": 30001,
|
||||
"message": "用户创建失败,请稍后重试!~",
|
||||
"timestamp": 1684146313,
|
||||
"traceID": "b4f90e16264a5f17cd3fc27141aba448"
|
||||
}
|
||||
```
|
||||
|
||||
### 更多
|
||||
- 更多关于中间件/拦截器的介绍请参考:https://goframe.org/pages/viewpage.action?pageId=55289881
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
## 支付网关
|
||||
|
||||
目录
|
||||
|
||||
- 介绍
|
||||
- 一个简单的支付流程
|
||||
- 支付配置
|
||||
- 创建支付订单
|
||||
- 注册支付回调
|
||||
- 订单退款
|
||||
- 单笔转账(待更新)
|
||||
- 其他
|
||||
|
||||
|
||||
### 介绍
|
||||
> 在web应用开发中,支付功能可以做为不可或缺的一部分,HotGo提供了相对通用的支付网关,目前已支持支付宝、微信支付、QQ支付,包含创建支付订单、支付回调、订单退款等功能,开发者几乎只需关注订单业务处理即可。
|
||||
|
||||
#### 一个简单的支付流程
|
||||
|
||||
```mermaid
|
||||
|
||||
graph TD
|
||||
A(用户下单) --> B(创建订单<br> 创建成功后,将订单支付信息提交到支付网关处理)
|
||||
B -->|网关返回失败| H(提示错误)
|
||||
B -->|网关返回成功| G(拿到支付链接,用户侧跳转支付页面)
|
||||
G -->|支付完成| C(支付平台通知支付网关<br> 支付网关进行验签和效验支付处理状态) -->|验证通过,回调订单业务| E(订单更新状态,发放业务产品)
|
||||
G -->|支付取消| D(订单支付超时关闭)
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 支付配置
|
||||
> 请到 系统设置 -> 配置管理 -> 支付配置 -> 找到你需要的支付方式,进行配置即可
|
||||
|
||||
|
||||
|
||||
### 创建支付订单
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
func main() {
|
||||
// 创建支付网关订单
|
||||
create, err := service.Pay().Create(ctx, payin.PayCreateInp{
|
||||
Subject: "充值100元",
|
||||
OrderSn: "唯一业务订单编号",
|
||||
OrderGroup: "admin_order", // 订单分组,用于订单分类和绑定支付成功的回调方法
|
||||
PayType: "wxpay", // 微信支付
|
||||
TradeType: "scan", // 二维码扫码
|
||||
PayAmount: "100", // 100元
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 注册支付回调
|
||||
- 在文件`server/internal/logic/pay/notify.go` 加入你的业务订单分组回调方法,当订单支付成功验签通过后会自动进行回调,参考以下:
|
||||
|
||||
```go
|
||||
package global
|
||||
|
||||
import (
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/payment"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
// RegisterNotifyCall 注册支付成功回调方法
|
||||
func (s *sPay) RegisterNotifyCall() {
|
||||
payment.RegisterNotifyCallMap(map[string]payment.NotifyCallFunc{
|
||||
consts.OrderGroupAdminOrder: service.AdminOrder().PayNotify, // 后台充值订单
|
||||
// ...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 订单退款
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
func main() {
|
||||
refund, err := service.PayRefund().Refund(ctx, payin.PayRefundInp{
|
||||
OrderSn: "唯一业务订单编号",
|
||||
RefundMoney: "退款金额",
|
||||
Reason: "买家申请退款原因",
|
||||
Remark: "商家同意退款备注",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 单笔转账(待更新)
|
||||
|
||||
|
||||
|
||||
### 其他
|
||||
- 由于各个支付平台中交易方式较多,如果目前已有的交易方式不满足你的支付场景需求,请自行参考gopay文档对文件`server/internal/library/payment/payment.go`中的`PayClient`接口实现进行扩展
|
||||
- gopay文档地址:https://github.com/go-pay/gopay
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
## 消息队列
|
||||
|
||||
目录
|
||||
|
||||
- 配置文件
|
||||
- 实现接口
|
||||
- 一个例子
|
||||
- 控制台
|
||||
- 自定义队列驱动
|
||||
|
||||
> 系统默认的队列驱动为disk(磁盘队列),目前已支持:disk、redis、rocketmq、kafka等多种驱动。请自行选择适合你的驱动使用。
|
||||
|
||||
### 配置文件
|
||||
- 配置文件:server/manifest/config/config.yaml
|
||||
|
||||
```yaml
|
||||
# 消息队列
|
||||
queue:
|
||||
switch: true # 队列开关,可选:true|false,默认为true
|
||||
driver: "disk" # 队列驱动,可选:disk|redis|rocketmq|kafka,默认为disk
|
||||
groupName: "hotgo" # mq群组名称
|
||||
# 磁盘队列
|
||||
disk:
|
||||
path: "./storage/diskqueue" # 数据存放路径
|
||||
batchSize: 100 # 每100条消息同步一次,batchSize和batchTime满足其一就会同步一次
|
||||
batchTime: 1 # 每1秒消息同步一次
|
||||
segmentSize: 10485760 # 每个topic分片数据文件最大字节,默认10M
|
||||
segmentLimit: 3000 # 每个topic最大分片数据文件数量,超出部分将会丢弃
|
||||
# redis,默认使用全局redis运行队列
|
||||
redis:
|
||||
timeout: 0 # 队列超时时间以秒为单位,0表示永不超时。如果队列在设定的超时时间内没有被消费,则会被销毁
|
||||
rocketmq:
|
||||
nameSrvAdders: ["127.0.0.1:9876"] # nameSrvAdder+端口,支持多个
|
||||
accessKey: "" # 选填。如果开启了acl控制就必填
|
||||
secretKey: "" # 选填。如果开启了acl控制就必填
|
||||
brokerAddr: "127.0.0.1:10911" # brokerAddr+端口,选填。用于消费者订阅主题前会检查主题是否存在,不存在会自动创建。你也可以在rocketmq控制台手动创建主题
|
||||
retry: 0 # 重试次数
|
||||
logLevel: "info" # 系统日志级别,可选:all|close|debug|info|warn|error|fatal
|
||||
kafka:
|
||||
address: "127.0.0.1:9092" # kafka地址+端口
|
||||
version: "2.0.0.0" # kafka专属配置,默认2.0.0.0
|
||||
randClient: true # 开启随机生成clientID,可以实现启动多实例同时一起消费相同topic,加速消费能力的特性,默认为true
|
||||
multiConsumer: true # 是否支持创建多个消费者
|
||||
```
|
||||
|
||||
### 实现接口
|
||||
- 为了提供高度的扩展性,消费队列在设计上采用了接口化的思路。只需要实现以下接口,您就可以在任何地方注册和使用消费队列消费功能,从而实现更大的灵活性和可扩展性。
|
||||
|
||||
```go
|
||||
// Consumer 消费者接口,实现该接口即可加入到消费队列中
|
||||
type Consumer interface {
|
||||
GetTopic() string // 获取消费主题
|
||||
Handle(ctx context.Context, mqMsg MqMsg) (err error) // 处理消息的方法
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 一个例子
|
||||
|
||||
每个被发送到队列的消息应该被定义为一个单独的文件结构。
|
||||
|
||||
例如,如果您需要异步记录系统日志,内容大致如下:
|
||||
|
||||
- 文件路径:server/internal/queues/sys_log.go
|
||||
|
||||
```go
|
||||
package queues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/queue"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
func init() {
|
||||
queue.RegisterConsumer(SysLog)
|
||||
}
|
||||
|
||||
// SysLog 系统日志
|
||||
var SysLog = &qSysLog{}
|
||||
|
||||
type qSysLog struct{}
|
||||
|
||||
// GetTopic 主题
|
||||
func (q *qSysLog) GetTopic() string {
|
||||
return consts.QueueLogTopic
|
||||
}
|
||||
|
||||
// Handle 处理消息
|
||||
func (q *qSysLog) Handle(ctx context.Context, mqMsg queue.MqMsg) (err error) {
|
||||
var data entity.SysLog
|
||||
if err = json.Unmarshal(mqMsg.Body, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.SysLog().RealWrite(ctx, data)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
下面是将消息添加到队列的方式,大概内容如下:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/queue"
|
||||
"hotgo/internal/model/entity"
|
||||
)
|
||||
|
||||
func test() {
|
||||
data := &entity.SysLog{
|
||||
//...
|
||||
}
|
||||
if err := queue.Push(consts.QueueLogTopic, data); err != nil {
|
||||
fmt.Printf("queue.Push err:%+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
延迟队列,目前只有redis、rocketmq驱动支持:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/queue"
|
||||
"hotgo/internal/model/entity"
|
||||
)
|
||||
|
||||
func test() {
|
||||
data := &entity.SysLog{
|
||||
//...
|
||||
}
|
||||
|
||||
// redis 延迟10秒
|
||||
if err := queue.SendDelayMsg(consts.QueueLogTopic, data, 10); err != nil {
|
||||
fmt.Printf("queue.Push err:%+v", err)
|
||||
}
|
||||
|
||||
// rocketmq 延迟5秒
|
||||
// 注意rocketmq这里传入的是延迟级别,而不是秒
|
||||
// 消息的延时级别level一共有18级,分别为:
|
||||
// 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
|
||||
// 参考:https://github.com/apache/rocketmq-client-go/blob/0e19ee654819bda396a08d950c883f9008b8222b/primitive/message.go#L174
|
||||
if err := queue.SendDelayMsg(consts.QueueLogTopic, data, 2); err != nil {
|
||||
fmt.Printf("queue.Push err:%+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 控制台
|
||||
|
||||
控制台用于处理队列消息,即消费者。
|
||||
|
||||
相关命令请参考: [控制台](sys-console.md)
|
||||
|
||||
|
||||
### 自定义队列驱动
|
||||
|
||||
只需实现消息队列的生成者和消费者接口,并加入到初始化中进行相应调用即可。
|
||||
|
||||
- 接口片段:server/internal/library/queue/init.go
|
||||
|
||||
```go
|
||||
package queue
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type MqMsg struct {
|
||||
RunType int `json:"run_type"`
|
||||
Topic string `json:"topic"`
|
||||
MsgId string `json:"msg_id"`
|
||||
Offset int64 `json:"offset"`
|
||||
Partition int32 `json:"partition"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Body []byte `json:"body"`
|
||||
}
|
||||
|
||||
type MqProducer interface {
|
||||
SendMsg(topic string, body string) (mqMsg MqMsg, err error)
|
||||
SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error)
|
||||
SendDelayMsg(topic string, body string, delay int64) (mqMsg MqMsg, err error)
|
||||
}
|
||||
|
||||
type MqConsumer interface {
|
||||
ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
将实现过接口(MqProducer、MqConsumer)的实例方法分别加入到NewProducer、NewConsumer中进行相应调用即可。
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
## TCP服务器
|
||||
|
||||
目录
|
||||
|
||||
- 配置文件
|
||||
- 一个基本的消息收发例子
|
||||
- 注册路由
|
||||
- 拦截器
|
||||
- 服务认证
|
||||
- 更多
|
||||
|
||||
> HotGo基于GoFrame的TCP服务器组件,提供了一个简单而灵活的方式快速搭建基于TCP的服务应用。集成了许多常用功能,如长连接、服务认证、路由分发、RPC消息、拦截器和数据绑定等,大大简化和规范了服务器开发流程。
|
||||
|
||||
### 配置文件
|
||||
- 配置文件:server/manifest/config/config.yaml
|
||||
|
||||
```yaml
|
||||
tcp:
|
||||
# 服务器
|
||||
server:
|
||||
address: ":8099"
|
||||
# 客户端
|
||||
client:
|
||||
# 定时任务
|
||||
cron:
|
||||
group: "cron" # 分组名称
|
||||
name: "cron1" # 客户端名称
|
||||
address: "127.0.0.1:8099" # 服务器地址
|
||||
appId: "1002" # 应用名称
|
||||
secretKey: "hotgo" # 密钥
|
||||
# 系统授权
|
||||
auth:
|
||||
group: "auth" # 分组名称
|
||||
name: "auth1" # 客户端名称
|
||||
address: "127.0.0.1:8099" # 服务器地址
|
||||
appId: "mengshuai" # 应用名称
|
||||
secretKey: "123456" # 密钥
|
||||
|
||||
```
|
||||
- 可以看到,除了服务器配置外,还有两个客户端配置`cron` 和`auth`
|
||||
- `cron`是HotGo内置的定时任务服务,和http服务通过RPC通讯以实现和后台交互,使其可以独立、集群部署。
|
||||
- `auth`可以为第三方平台提供授权服务。如果你需要他,可以将它部署在第三方程序中,在重要的位置进行授权验证。
|
||||
|
||||
### 一个基本的消息收发测试用例
|
||||
|
||||
- 文件路径:server/internal/library/network/tcp/tcp_example_test.go
|
||||
|
||||
```go
|
||||
package tcp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
"hotgo/internal/library/network/tcp"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var T *testing.T // 声明一个全局的 *testing.T 变量
|
||||
|
||||
type TestMsgReq struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type TestMsgRes struct {
|
||||
tcp.ServerRes
|
||||
}
|
||||
|
||||
type TestRPCMsgReq struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type TestRPCMsgRes struct {
|
||||
tcp.ServerRes
|
||||
}
|
||||
|
||||
func onTestMsg(ctx context.Context, req *TestMsgReq) {
|
||||
fmt.Printf("服务器收到消息 ==> onTestMsg:%+v\n", req)
|
||||
conn := tcp.ConnFromCtx(ctx)
|
||||
gtest.C(T, func(t *gtest.T) {
|
||||
t.AssertNE(conn, nil)
|
||||
})
|
||||
|
||||
res := new(TestMsgRes)
|
||||
res.Message = fmt.Sprintf("你的名字:%v", req.Name)
|
||||
conn.Send(ctx, res)
|
||||
}
|
||||
|
||||
func onResponseTestMsg(ctx context.Context, req *TestMsgRes) {
|
||||
fmt.Printf("客户端收到响应消息 ==> TestMsgRes:%+v\n", req)
|
||||
err := req.GetError()
|
||||
gtest.C(T, func(t *gtest.T) {
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func onTestRPCMsg(ctx context.Context, req *TestRPCMsgReq) (res *TestRPCMsgRes, err error) {
|
||||
fmt.Printf("服务器收到消息 ==> onTestRPCMsg:%+v\n", req)
|
||||
res = new(TestRPCMsgRes)
|
||||
res.Message = fmt.Sprintf("你的名字:%v", req.Name)
|
||||
return
|
||||
}
|
||||
|
||||
func startTCPServer() {
|
||||
serv := tcp.NewServer(&tcp.ServerConfig{
|
||||
Name: "hotgo",
|
||||
Addr: ":8002",
|
||||
})
|
||||
|
||||
// 注册路由
|
||||
serv.RegisterRouter(
|
||||
onTestMsg,
|
||||
)
|
||||
|
||||
// 注册RPC路由
|
||||
serv.RegisterRPCRouter(
|
||||
onTestRPCMsg,
|
||||
)
|
||||
|
||||
// 服务监听
|
||||
err := serv.Listen()
|
||||
gtest.C(T, func(t *gtest.T) {
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
// 一个基本的消息收发
|
||||
func TestSendMsg(t *testing.T) {
|
||||
T = t
|
||||
go startTCPServer()
|
||||
|
||||
ctx := gctx.New()
|
||||
client := tcp.NewClient(&tcp.ClientConfig{
|
||||
Addr: "127.0.0.1:8002",
|
||||
})
|
||||
|
||||
// 注册路由
|
||||
client.RegisterRouter(
|
||||
onResponseTestMsg,
|
||||
)
|
||||
|
||||
go func() {
|
||||
err := client.Start()
|
||||
gtest.C(T, func(t *gtest.T) {
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}()
|
||||
|
||||
// 确保服务都启动完成
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
// 拿到客户端的连接
|
||||
conn := client.Conn()
|
||||
gtest.C(T, func(t *gtest.T) {
|
||||
t.AssertNE(conn, nil)
|
||||
})
|
||||
|
||||
// 向服务器发送tcp消息,不会阻塞程序执行
|
||||
err := conn.Send(ctx, &TestMsgReq{Name: "Tom"})
|
||||
gtest.C(T, func(t *gtest.T) {
|
||||
t.AssertNil(err)
|
||||
})
|
||||
|
||||
// 向服务器发送rpc消息,会等待服务器响应结果,直到拿到结果或响应超时才会继续
|
||||
var res TestRPCMsgRes
|
||||
if err = conn.RequestScan(ctx, &TestRPCMsgReq{Name: "Tony"}, &res); err != nil {
|
||||
gtest.C(T, func(t *gtest.T) {
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Printf("客户端收到RPC消息响应 ==> TestRPCMsgRes:%+v\n", res)
|
||||
time.Sleep(time.Second * 1)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 注册路由
|
||||
|
||||
- 从上面的例子可以看到,不管是普通TCP消息和RPC消息的请求/响应结构体都采用类似GF框架的规范路由的结构,请求`XxxRes`/响应`XxxRes`的格式,是不是很亲切?
|
||||
|
||||
|
||||
### 拦截器
|
||||
|
||||
- 不管是服务端还是客户端,在初始化时都可以注册多个拦截器来满足更多场景的服务开发,下面是一个使用例子:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"hotgo/internal/library/network/tcp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
serv = tcp.NewServer(&tcp.ServerConfig{
|
||||
Name: "hotgo",
|
||||
Addr: ":8002",
|
||||
})
|
||||
|
||||
// 注册拦截器
|
||||
// 执行顺序是从前到后,即Interceptor -> Interceptor2 -> Interceptor3。如果中间有任意一个抛出错误,则会中断后续处理
|
||||
serv.RegisterInterceptor(Interceptor, Interceptor2, Interceptor3)
|
||||
|
||||
// 服务监听
|
||||
if err := serv.Listen(); err != nil {
|
||||
if !serv.IsClose() {
|
||||
g.Log().Warningf(ctx, "TCPServer Listen err:%v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Interceptor(ctx context.Context, msg *tcp.Message) (err error) {
|
||||
// 可以在拦截器中通过上下文拿到连接
|
||||
conn := tcp.ConnFromCtx(ctx)
|
||||
|
||||
// 拿到原始请求消息
|
||||
g.Dump(msg)
|
||||
|
||||
// 如果想要中断后续处理只需返回一个错误即可,但注意两种情况
|
||||
// tcp消息:如果你还想对该消息进行回复应在拦截器中进行处理,例如:conn.Send(ctx, 回复消息内容)
|
||||
// rpc消息:返回一个错误后系统会将错误自动回复到rpc响应中,无需单独处理
|
||||
return
|
||||
}
|
||||
|
||||
func Interceptor2(ctx context.Context, msg *tcp.Message) (err error) {
|
||||
// ...
|
||||
return
|
||||
}
|
||||
|
||||
func Interceptor3(ctx context.Context, msg *tcp.Message) (err error) {
|
||||
// ...
|
||||
return
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 服务认证
|
||||
|
||||
- 一般情况下,建议客户端连接到服务器时都通过`授权许可证`的方式进行登录认证,当初始化客户端配置认证数据时,连接成功后会自动进行登录认证。
|
||||
|
||||
```go
|
||||
// 创建客户端配置
|
||||
clientConfig := &tcp.ClientConfig{
|
||||
Addr: "127.0.0.1:8002",
|
||||
AutoReconnect: true,
|
||||
// 认证数据
|
||||
// 认证数据可以在后台-系统监控-在线服务-许可证列表中添加,同一个授权支持多个服务使用,但多个服务不能使用相同的名称进行连接
|
||||
Auth: &tcp.AuthMeta{
|
||||
Name: "服务名称",
|
||||
Group: "服务分组",
|
||||
AppId: "APPID",
|
||||
SecretKey: "SecretKey",
|
||||
},
|
||||
}
|
||||
|
||||
// 初始化客户端
|
||||
client = tcp.NewClient(clientConfig)
|
||||
```
|
||||
|
||||
|
||||
### 更多
|
||||
|
||||
TCP服务器源码路径:server/internal/library/network/tcp
|
||||
|
||||
更多文档请参考:https://goframe.org/pages/viewpage.action?pageId=1114625
|
||||
@@ -1,175 +0,0 @@
|
||||
## SaaS多租户
|
||||
|
||||
|
||||
目录
|
||||
|
||||
- 介绍
|
||||
- 职责划分
|
||||
- 如何在HotGo开发多租户业务
|
||||
- 多租户数据库设计
|
||||
- 多租户功能演示
|
||||
|
||||
### 介绍
|
||||
|
||||
SaaS系统多租户多应用设计,已成为互联网企业的重要发展建设方向。核心在于多租户SAAS系统独立前台、共享后台、共享数据库的平台应用架构。
|
||||
|
||||
> 目前HotGo的部分基础功能并不完全支持多租户设计,但这并不妨碍开发者基于HotGo构建自己的多租户产品
|
||||
|
||||
|
||||
### 职责划分
|
||||
|
||||
在SaaS系统多租户中,不同身份用户的职责功能划分假设可以是这样:
|
||||
|
||||
> 这只是一个粗略的概念,实际开发时应根据业务来进行调整,每个身份也都不是必须的。
|
||||
|
||||
| 身份 | 标识 | 职责和功能划分 |
|
||||
|-----------------|----------|-------------------------------------------------------|
|
||||
| 公司| company | 管理整个平台,包括商户和用户账户、系统设置以及其他全局性业务流程。 |
|
||||
| 租户 | tenant | 多租户系统中顶层实体客户、组织或实体。有自己的多个商户、用户、产品、订单等。拥有独立的数据隔离和安全边界。 |
|
||||
| 商户 | merchant | 受租户的监管和管理,可独立经营的实体。提供产品或服务,管理自己的业务,包括库存管理、订单处理、结算等。 |
|
||||
| 用户 | user | 真正购买产品或享受服务的人,与商户互动,管理个人信息等个性化功能。 |
|
||||
|
||||
|
||||
### 如何在HotGo开发多租户业务
|
||||
#### 一、应用功能
|
||||
|
||||
根据角色来划分用户的后台功能,在创建用户时为其绑定角色,然后为不同角色分配不同的功能菜单
|
||||
|
||||
请参考: [权限控制](sys-auth.md)
|
||||
|
||||
|
||||
#### 二、 数据隔离
|
||||
|
||||
根据部门来划定用户的数据权限范围,在创建用户时为其绑定部门
|
||||
|
||||
- 在用户登录成功后,server端可通过上下文来获取用户部门类型来确定用户身份
|
||||
- 文件路径:server/internal/library/contexts/context.go
|
||||
|
||||
```go
|
||||
package contexts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/model"
|
||||
)
|
||||
|
||||
// GetDeptType 获取用户部门类型
|
||||
func GetDeptType(ctx context.Context) string {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
return user.DeptType
|
||||
}
|
||||
|
||||
// IsCompanyDept 是否为公司部门
|
||||
func IsCompanyDept(ctx context.Context) bool {
|
||||
return GetDeptType(ctx) == consts.DeptTypeCompany
|
||||
}
|
||||
|
||||
// IsTenantDept 是否为租户部门
|
||||
func IsTenantDept(ctx context.Context) bool {
|
||||
return GetDeptType(ctx) == consts.DeptTypeTenant
|
||||
}
|
||||
|
||||
// IsMerchantDept 是否为商户部门
|
||||
func IsMerchantDept(ctx context.Context) bool {
|
||||
return GetDeptType(ctx) == consts.DeptTypeMerchant
|
||||
}
|
||||
|
||||
// IsUserDept 是否为普通用户部门
|
||||
func IsUserDept(ctx context.Context) bool {
|
||||
return GetDeptType(ctx) == consts.DeptTypeUser
|
||||
}
|
||||
```
|
||||
|
||||
- 在用户登录成功后,web端可通`useUserStore`来获取用户部门类型来确定用户身份
|
||||
- 文件路径:web/src/store/modules/user.ts
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
console.log('用户部门类型:' + userStore.info?.deptType);
|
||||
console.log('是否为公司:' + userStore.isCompanyDept);
|
||||
console.log('是否为租户:' + userStore.isTenantDept);
|
||||
console.log('是否为商户:' + userStore.isMerchantDept);
|
||||
console.log('是否为用户:' + userStore.isUserDept);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 多租户数据库设计
|
||||
|
||||
HotGo定位是中小型应用开发,推荐采用一套数据库不同Schema。就是在多租户业务表中加入用户标识字段,来区分不同用户的数据,如:`tenant_id`
|
||||
|
||||
- 参考文章:https://blog.csdn.net/haponchang/article/details/104246317
|
||||
|
||||
|
||||
### 多租户功能演示
|
||||
|
||||
请登录后台【插件应用】-【功能案例】-【多租户功能演示】查看
|
||||
|
||||
#### 自动维护租户关系
|
||||
|
||||
- 只需在表设计时包含以下字段,即可使用handler和hook实现租户权限过滤和租户关系维护
|
||||
|
||||
| 字段名称 | 数据类型 | 字段注释 | 必选 |
|
||||
|------|----------|------|-----------------------------------------------------|
|
||||
| tenant_id | bigint(20) | 租户ID | 否 |
|
||||
| merchant_id | bigint(20) | 商户ID | 否 |
|
||||
| user_id | bigint(20) | 用户ID | 否 |
|
||||
|
||||
下面是多租户功能演示例子代码中的使用片段
|
||||
|
||||
- 封装查询Model
|
||||
|
||||
```go
|
||||
// Model 多租户功能演示ORM模型
|
||||
func (s *sSysTenantOrder) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
// 过滤多租户数据权限
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
//FilterAuth: true, // 如果还需要维护created_by、member_id等部门数据权限范围可放开注释
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.AddonHgexampleTenantOrder.Ctx(ctx), option...)
|
||||
}
|
||||
```
|
||||
|
||||
- 增改数据自动维护租户关系
|
||||
```go
|
||||
// Edit 修改/新增多租户功能演示
|
||||
func (s *sSysTenantOrder) Edit(ctx context.Context, in *sysin.TenantOrderEditInp) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(sysin.TenantOrderUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).
|
||||
Hook(hook.SaveTenant). // 自动维护租户关系更新
|
||||
Update(); err != nil {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
if _, err = dao.AddonHgexampleTenantOrder.Ctx(ctx).
|
||||
Fields(sysin.TenantOrderInsertFields{}).
|
||||
Hook(hook.SaveTenant). // 自动维护租户关系更新
|
||||
Data(in).
|
||||
Insert(); err != nil {
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
相关代码文件:/server/addons/hgexample/logic/sys/tenant_order.go
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
## 单元测试
|
||||
|
||||
请参考:https://goframe.org/pages/viewpage.action?pageId=1114153
|
||||
@@ -1,20 +0,0 @@
|
||||
## 工具方法
|
||||
|
||||
HotGo还提供一些系统中常用的工具库方法,在这里简单说明:
|
||||
|
||||
```
|
||||
/server
|
||||
├── utility
|
||||
│ ├── charset # 字符串处理
|
||||
│ ├── convert # 数据类型转换
|
||||
│ ├── encrypt # 数据加密/解密
|
||||
│ ├── excel # 电子表格导出/导入
|
||||
│ ├── file # 文件/目录处理
|
||||
│ ├── format # 数据格式化
|
||||
│ ├── simple # 一些简捷函数
|
||||
│ ├── tree # 树形结构
|
||||
│ ├── url # URL处理
|
||||
│ ├── useragent # 请求头代理处理
|
||||
└── └── validate # 数据验证
|
||||
```
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
## WebHook
|
||||
|
||||
请参考:https://goframe.org/pages/viewpage.action?pageId=1114387
|
||||
@@ -1,213 +0,0 @@
|
||||
## WebSocket客户端
|
||||
|
||||
目录
|
||||
|
||||
- 全局消息监听
|
||||
- 单页面消息监听
|
||||
- 发送消息
|
||||
|
||||
> 基于WebSocket服务器,hotgo还对客户端的上做了一些封装,使其使用起来更加方便
|
||||
- [WebSocket服务器](sys-websocket-server.md)
|
||||
|
||||
### 全局消息监听
|
||||
- 所有全局的消息监听都在这里
|
||||
- 文件路径:web/src/utils/websocket/registerMessage.ts
|
||||
|
||||
```ts
|
||||
import { TABS_ROUTES } from '@/store/mutation-types';
|
||||
import { SocketEnum } from '@/enums/socketEnum';
|
||||
import { useUserStoreWidthOut } from '@/store/modules/user';
|
||||
import { notificationStoreWidthOut } from '@/store/modules/notification';
|
||||
import { addOnMessage, WebSocketMessage } from '@/utils/websocket/index';
|
||||
|
||||
// 注册全局消息监听
|
||||
export function registerGlobalMessage() {
|
||||
// 心跳
|
||||
addOnMessage(SocketEnum.EventPing, function (_message: WebSocketMessage) {
|
||||
// console.log('ping..');
|
||||
});
|
||||
|
||||
// 强制退出
|
||||
addOnMessage(SocketEnum.EventKick, function (_message: WebSocketMessage) {
|
||||
const useUserStore = useUserStoreWidthOut();
|
||||
useUserStore.logout().then(() => {
|
||||
// 移除标签页
|
||||
localStorage.removeItem(TABS_ROUTES);
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
// 消息通知
|
||||
addOnMessage(SocketEnum.EventNotice, function (message: WebSocketMessage) {
|
||||
const notificationStore = notificationStoreWidthOut();
|
||||
notificationStore.triggerNewMessages(message.data);
|
||||
});
|
||||
|
||||
// 更多全局消息处理都可以在这里注册
|
||||
// ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### 单页面消息监听
|
||||
- 当你只需要某个页面使用WebSocket,这将是一个不错的选择,下面是一个简单的演示例子
|
||||
- 文件路径:web/src/views/addons/hgexample/portal/websocketTest.vue
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<div class="n-layout-page-header">
|
||||
<n-card :bordered="false" title="测试websocket">
|
||||
尝试在下方输入框中输入任意文字消息内容,发送后websocket服务器收到会原样返回
|
||||
</n-card>
|
||||
</div>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<n-space vertical>
|
||||
<n-input-group style="width: 520px">
|
||||
<n-input
|
||||
@keyup.enter="sendMessage"
|
||||
:style="{ width: '78%' }"
|
||||
placeholder="请输入消息内容"
|
||||
:on-focus="onFocus"
|
||||
:on-blur="onBlur"
|
||||
v-model:value="inputMessage"
|
||||
/>
|
||||
<n-button type="primary" @click="sendMessage"> 发送消息</n-button>
|
||||
</n-input-group>
|
||||
|
||||
<div class="mt-5"></div>
|
||||
|
||||
<n-timeline :icon-size="20">
|
||||
<n-timeline-item color="grey" content="输入中.." v-if="isInput">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<MessageOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-timeline-item>
|
||||
|
||||
<n-timeline-item
|
||||
v-for="item in messages"
|
||||
:key="item"
|
||||
:type="item.type == Enum.SendType ? 'success' : 'info'"
|
||||
:title="item.type == Enum.SendType ? '发送消息' : '收到消息'"
|
||||
:content="item.content"
|
||||
:time="item.time"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SendOutlined v-if="item.type == Enum.SendType" />
|
||||
<SoundOutlined v-if="item.type == Enum.ReceiveType" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-timeline-item>
|
||||
</n-timeline>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { MessageOutlined, SendOutlined, SoundOutlined } from '@vicons/antd';
|
||||
import { format } from 'date-fns';
|
||||
import { addOnMessage, removeOnMessage, sendMsg, WebSocketMessage } from '@/utils/websocket';
|
||||
import { useMessage } from 'naive-ui';
|
||||
|
||||
interface Message {
|
||||
type: Enum;
|
||||
content: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const message = useMessage();
|
||||
const messages = ref<Message[]>([]);
|
||||
const inputMessage = ref('你好,HotGo');
|
||||
const isInput = ref(false);
|
||||
const testMessageEvent = 'admin/addons/hgexample/testMessage';
|
||||
|
||||
enum Enum {
|
||||
SendType = 1, // 发送类型
|
||||
ReceiveType = 2, // 接受类型
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
isInput.value = true;
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
isInput.value = false;
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (inputMessage.value == '') {
|
||||
message.error('消息内容不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
sendMsg(testMessageEvent, {
|
||||
message: inputMessage.value,
|
||||
});
|
||||
|
||||
const msg: Message = {
|
||||
type: Enum.SendType,
|
||||
content: inputMessage.value,
|
||||
time: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
};
|
||||
insertMessage(msg);
|
||||
inputMessage.value = '';
|
||||
}
|
||||
|
||||
// 存入本地记录
|
||||
function insertMessage(msg: Message): void {
|
||||
messages.value.unshift(msg); // 在头部插入消息
|
||||
if (messages.value.length > 10) {
|
||||
messages.value = messages.value.slice(0, 10); // 如果超过10个,则只保留最前面10个
|
||||
}
|
||||
}
|
||||
|
||||
const onMessage = (res: WebSocketMessage) => {
|
||||
const msg: Message = {
|
||||
type: Enum.ReceiveType,
|
||||
content: res.data.message,
|
||||
time: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
};
|
||||
insertMessage(msg);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 在当前页面注册消息监听
|
||||
addOnMessage(testMessageEvent, onMessage);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 移除消息监听
|
||||
removeOnMessage(testMessageEvent);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
```
|
||||
|
||||
#### 发送消息
|
||||
- 向服务器发送一条消息
|
||||
|
||||
```ts
|
||||
import { sendMsg } from '@/utils/websocket';
|
||||
|
||||
const event = 'admin/addons/hgexample/testMessage'; // 消息路由
|
||||
const data: object | null = { // 消息内容
|
||||
message: 'message content...',
|
||||
};
|
||||
const isRetry = false; // 发送失败是否重试,不传默认为true
|
||||
|
||||
// 基本使用
|
||||
sendMsg(event, data);
|
||||
|
||||
// 无消息内容
|
||||
sendMsg(event);
|
||||
|
||||
// 发送失败不重试
|
||||
sendMsg(event, data, isRetry);
|
||||
```
|
||||
@@ -1,148 +0,0 @@
|
||||
## WebSocket服务器
|
||||
|
||||
目录
|
||||
|
||||
- 一个基本的消息收发例子
|
||||
- 常用方法
|
||||
- HTTP接口
|
||||
- 其他
|
||||
|
||||
> hotgo提供了一个WebSocket服务器,随`HTTP服务`启停。集成了许多常用功能,如JWT身份认证、路由消息处理器、一对一消息/群组消息/广播消息、在线用户管理、心跳保持等,大大简化和规范了WebSocket服务器的开发流程。
|
||||
- [Websocket客户端](sys-websocket-client.md)
|
||||
|
||||
### 一个基本的消息收发例子
|
||||
- 这是一个基本的消息接收并进行处理的简单例子
|
||||
|
||||
#### 1.消息处理接口
|
||||
- 消息处理在设计上采用了接口化的思路。只需要实现以下接口,即可进行WebSocket消息注册
|
||||
- 文件路径:server/internal/websocket/model.go
|
||||
|
||||
```go
|
||||
package websocket
|
||||
|
||||
// EventHandler 消息处理器
|
||||
type EventHandler func(client *Client, req *WRequest)
|
||||
```
|
||||
|
||||
#### 2.定义消息处理方法
|
||||
- 以下是功能案例中的一个简单演示,实现了消息处理接口,并将收到的消息原样发送给客户端
|
||||
- 文件路径:server/addons/hgexample/controller/websocket/handler/index.go
|
||||
|
||||
```go
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"hotgo/internal/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
Index = cIndex{}
|
||||
)
|
||||
|
||||
type cIndex struct{}
|
||||
|
||||
// TestMessage 测试消息
|
||||
func (c *cIndex) TestMessage(client *websocket.Client, req *websocket.WRequest) {
|
||||
g.Log().Infof(client.Context(), "收到客户端测试消息:%v", gjson.New(req).String())
|
||||
// 将收到的消息原样发送给客户端
|
||||
websocket.SendSuccess(client, req.Event, req.Data)
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.注册消息
|
||||
- 定义消息处理方法后,需要将其注册到WebSocket消息处理器,一般放在对应应用模块的`router/websocket.go`下即可
|
||||
- 文件路径:server/addons/hgexample/router/websocket.go
|
||||
|
||||
```go
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"hotgo/addons/hgexample/controller/websocket"
|
||||
"hotgo/addons/hgexample/controller/websocket/handler"
|
||||
ws "hotgo/internal/websocket"
|
||||
)
|
||||
|
||||
// WebSocket ws路由配置
|
||||
func WebSocket(ctx context.Context, group *ghttp.RouterGroup) {
|
||||
// 注册消息路由
|
||||
ws.RegisterMsg(ws.EventHandlers{
|
||||
"admin/addons/hgexample/testMessage": handler.Index.TestMessage, // 测试消息
|
||||
})
|
||||
|
||||
// 这里"admin/addons/hgexample/testMessage"代表的是一个消息处理ID,可以自定义。建议的格式是和HTTP接口格式保持一致,这样还可以便于对用户请求的消息进行权限验证
|
||||
// 客户端连接后,向WebSocket服务器发送event为"admin/addons/hgexample/testMessage"的消息时,会调用TestMessage方法
|
||||
}
|
||||
```
|
||||
|
||||
- 到此,你已了解了WebSocket消息接收并进行处理的基本流程
|
||||
|
||||
|
||||
### 常用方法
|
||||
- websocket服务器还提供了一些常用的方法,下面只对部分进行说明
|
||||
|
||||
```go
|
||||
func test() {
|
||||
websocket.SendToAll() // 发送全部客户端
|
||||
websocket.SendToClientID() // 发送单个客户端
|
||||
websocket.SendToUser() // 发送单个用户
|
||||
websocket.SendToTag() // 发送某个标签、群组
|
||||
|
||||
client := websocket.Manager().GetClient(id) // 通过连接ID获取客户端连接
|
||||
client := websocket.Manager().GetUserClient(userId) // 通过用户ID获取客户端连接,因为用户是可多端登录的,这里返回的是一个切片
|
||||
|
||||
websocket.SendSuccess(client, "admin/addons/hgexample/testMessage", "消息内容") // 向指定客户端发送一条成功的消息
|
||||
websocket.SendError(client, "admin/addons/hgexample/testMessage", gerror.New("错误内容")) // 向指定客户端发送一条失败的消息
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### HTTP接口
|
||||
- 你还可以通过http接口方式调用WebSocket发送消息
|
||||
- 参考文件:server/internal/controller/websocket/send.go
|
||||
|
||||
|
||||
### 其他
|
||||
- WebSocket被连接时需验证用户认证中间件,所以用户必须登录成功后才能连接成功
|
||||
- 参考文件:server/internal/logic/middleware/weboscket_auth.go
|
||||
|
||||
```go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/response"
|
||||
"hotgo/utility/simple"
|
||||
)
|
||||
|
||||
// WebSocketAuth websocket鉴权中间件
|
||||
func (s *sMiddleware) WebSocketAuth(r *ghttp.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
path = gstr.Replace(r.URL.Path, simple.RouterPrefix(ctx, consts.AppWebSocket), "", 1)
|
||||
)
|
||||
|
||||
// 不需要验证登录的路由地址
|
||||
if s.IsExceptLogin(ctx, consts.AppWebSocket, path) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息传递到上下文中
|
||||
if err := s.DeliverUserContext(r); err != nil {
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
r.Middleware.Next()
|
||||
}
|
||||
```
|
||||
|
||||
- 如果您不要求用户进行登录即可使用 WebSocket,那么需要对身份验证中间件进行修改。然而,这样做会降低连接的安全性,并且无法应用于需要确定用户身份的情景,因此并不建议采取这种策略
|
||||
@@ -1,261 +0,0 @@
|
||||
## 独立部署
|
||||
|
||||
目录
|
||||
|
||||
- 构建
|
||||
- 旧版浏览器兼容
|
||||
- 预览
|
||||
- 分析构建文件体积
|
||||
- 压缩
|
||||
- 部署
|
||||
|
||||
> 在实际开发中,web前端也可能需要独立部署,所以在此提供一下部署方案。
|
||||
|
||||
### 构建
|
||||
|
||||
项目开发完成之后,执行以下命令进行构建
|
||||
```shell
|
||||
pnpm run build 或 npm run build
|
||||
```
|
||||
构建打包成功之后,会在根目录生成 dist 文件夹,里面就是构建打包好的文件
|
||||
|
||||
|
||||
### 旧版浏览器兼容
|
||||
|
||||
在 .env.production 内
|
||||
|
||||
设置 `VITE_LEGACY=true` 即可打包出兼容旧版浏览器的代码
|
||||
```shell
|
||||
VITE_LEGACY = true
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 预览
|
||||
|
||||
发布之前可以在本地进行预览,有多种方式,这里介绍两种
|
||||
|
||||
##### 不能直接打开构建后的 html 文件
|
||||
|
||||
使用项目自定的命令进行预览(推荐)
|
||||
```shell
|
||||
# 先打包在进行预览
|
||||
pnpm run preview 或 npm run preview
|
||||
# 直接预览本地 dist 文件目录
|
||||
pnpm run preview:dist 或 npm run preview:dist
|
||||
```
|
||||
|
||||
- 本地服务器预览(通过 live-server)
|
||||
```shell
|
||||
# 1.全局安装live-server
|
||||
npm -g install live-server
|
||||
# 2. 进入打包的后目录
|
||||
cd ./dist
|
||||
# 本地预览,默认端口8080
|
||||
live-server
|
||||
# 指定端口
|
||||
live-server --port 9000
|
||||
```
|
||||
|
||||
### 分析构建文件体积
|
||||
如果你的构建文件很大,可以通过项目内置 [rollup-plugin-analyzer](https://github.com/doesdev/rollup-plugin-analyzer) 插件进行代码体积分析,从而优化你的代码。
|
||||
```shell
|
||||
pnpm run report 或 npm run report
|
||||
```
|
||||
运行之后,在自动打开的页面可以看到具体的体积分布,以分析哪些依赖有问题。
|
||||
|
||||
- 左上角可以切换 显示 gzip 或者 brotli
|
||||
|
||||

|
||||
|
||||
|
||||
### 压缩
|
||||
|
||||
#### 开启 gzip 压缩
|
||||
|
||||
开启 gzip,并配合 nginx 的 `gzip_static` 功能可以大大加快页面访问速度
|
||||
|
||||
- 只需开启 `VITE_BUILD_COMPRESS='gzip'` 即可在打包的同时生成 .gz 文件
|
||||
|
||||
```shell
|
||||
# 根据自己路径来配置更改
|
||||
# 例如部署在nginx /next/路径下 则VITE_PUBLIC_PATH=/next/
|
||||
VITE_PUBLIC_PATH=/
|
||||
```
|
||||
|
||||
#### 开启 brotli 压缩
|
||||
|
||||
brotli 是比 gzip 压缩率更高的算法,可以与 gzip 共存不会冲突,需要 nginx 安装指定模块并开启即可。
|
||||
|
||||
- 只需开启 VITE_BUILD_COMPRESS='brotli' 即可在打包的同时生成 .br 文件
|
||||
|
||||
```shell
|
||||
# 根据自己路径来配置更改
|
||||
# 例如部署在nginx /next/路径下 则VITE_PUBLIC_PATH=/next/
|
||||
VITE_PUBLIC_PATH=/
|
||||
```
|
||||
|
||||
|
||||
#### 同时开启 gzip 与 brotli
|
||||
|
||||
只需开启 VITE_BUILD_COMPRESS='brotli,gzip' 即可在打包的同时生成 .gz 和 .br 文件。
|
||||
|
||||
|
||||
#### gzip 与 brotli 在 nginx 内的配置
|
||||
```
|
||||
http {
|
||||
# 开启gzip
|
||||
gzip on;
|
||||
# 开启gzip_static
|
||||
# gzip_static 开启后可能会报错,需要安装相应的模块, 具体安装方式可以自行查询
|
||||
# 只有这个开启,vue文件打包的.gz文件才会有效果,否则不需要开启gzip进行打包
|
||||
gzip_static on;
|
||||
gzip_proxied any;
|
||||
gzip_min_length 1k;
|
||||
gzip_buffers 4 16k;
|
||||
#如果nginx中使用了多层代理 必须设置这个才可以开启gzip。
|
||||
gzip_http_version 1.0;
|
||||
gzip_comp_level 2;
|
||||
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
|
||||
gzip_vary off;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
# 开启 brotli压缩
|
||||
# 需要安装对应的nginx模块,具体安装方式可以自行查询
|
||||
# 可以与gzip共存不会冲突
|
||||
brotli on;
|
||||
brotli_comp_level 6;
|
||||
brotli_buffers 16 8k;
|
||||
brotli_min_length 20;
|
||||
brotli_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 部署
|
||||
|
||||
- 注意:项目默认是在生产环境开启 Mock,这样做非常不好,只是为了演示环境有数据,不建议在生产环境使用 Mock,而应该使用真实的后台接口,并将 Mock 关闭。
|
||||
|
||||
#### 发布
|
||||
|
||||
简单的部署只需要将最终生成的静态文件,dist 文件夹的静态文件发布到你的 cdn 或者静态服务器即可,需要注意的是其中的 index.html 通常会是你后台服务的入口页面,在确定了 js 和 css 的静态之后可能需要改变页面的引入路径。
|
||||
|
||||
例如上传到 nginx `/srv/www/project/index.html`
|
||||
|
||||
```
|
||||
# nginx配置
|
||||
location / {
|
||||
# 不缓存html,防止程序更新后缓存继续生效
|
||||
if ($request_filename ~* .*\.(?:htm|html)$) {
|
||||
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
|
||||
access_log on;
|
||||
}
|
||||
# 这里是vue打包文件dist内的文件的存放路径
|
||||
root /srv/www/project/;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
部署时可能会发现资源路径不对,只需要修改`.env.production`文件即可。
|
||||
|
||||
```shell
|
||||
# 根据自己路径来配置更改
|
||||
# 注意需要以 / 开头和结尾
|
||||
VITE_PUBLIC_PATH=/
|
||||
VITE_PUBLIC_PATH=/xxx/
|
||||
|
||||
```
|
||||
|
||||
#### 前端路由与服务端的结合
|
||||
|
||||
项目前端路由使用的是 vue-router,所以你可以选择两种方式:history 和 hash。
|
||||
|
||||
- hash 默认会在 url 后面拼接#
|
||||
- history 则不会,不过 history 需要服务器配合
|
||||
|
||||
可在 src/router/index.ts 内进行 mode 修改
|
||||
|
||||
```vue
|
||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
|
||||
|
||||
createRouter({
|
||||
history: createWebHashHistory(),
|
||||
// or
|
||||
history: createWebHistory(),
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
#### history 路由模式下服务端配置
|
||||
|
||||
开启 history 模式需要服务器配置,更多的服务器配置详情可以看 [history-mode](https://router.vuejs.org/guide/essentials/history-mode.html#html5-mode)
|
||||
|
||||
这里以 nginx 配置为例
|
||||
|
||||
|
||||
##### 部署到根目录
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
# 用于配合 History 使用
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 部署到非根目录
|
||||
|
||||
1. 首先需要在打包的时候更改配置
|
||||
|
||||
```shell
|
||||
# 在.env.production内,配置子目录路径
|
||||
VITE_PUBLIC_PATH = /sub/
|
||||
```
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
location /sub/ {
|
||||
# 这里是vue打包文件dist内的文件的存放路径
|
||||
alias /srv/www/project/;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /sub/index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用 nginx 处理跨域
|
||||
|
||||
使用 nginx 处理项目部署后的跨域问题
|
||||
|
||||
1. 配置前端项目接口地址
|
||||
|
||||
```
|
||||
# 在.env.production内,配置接口地址
|
||||
VITE_GLOB_API_URL=/api
|
||||
```
|
||||
|
||||
2. 在 nginx 配置请求转发到后台
|
||||
|
||||
```
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
# 接口代理,用于解决跨域问题
|
||||
location /api {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# 后台接口地址
|
||||
proxy_pass http://110.110.1.1:8080/api;
|
||||
proxy_redirect default;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Headers X-Requested-With;
|
||||
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,879 +0,0 @@
|
||||
## 表单组件
|
||||
|
||||
目录
|
||||
|
||||
- 文本输入 Input
|
||||
- 数字输入 Input Number
|
||||
- 文本域 InputTextarea
|
||||
- 富文本 InputEditor
|
||||
- 动态键值对 InputDynamic
|
||||
- 日期选择 Date(Y-M-D)
|
||||
- 日期范围选择 DateRange
|
||||
- 时间选择 Time(Y-M-D H:i:s)
|
||||
- 时间范围选择 TimeRange
|
||||
- 单选按钮 Radio
|
||||
- 复选框 Checkbox
|
||||
- 单选下拉框 Select
|
||||
- 多选下拉框 SelectMultiple
|
||||
- 树型选择 Tree Select
|
||||
- 单图上传 UploadImage
|
||||
- 多图上传 UploadImage
|
||||
- 单文件上传 UploadFile
|
||||
- 多文件上传 UploadFile
|
||||
- 文件选择器 FileChooser
|
||||
- 大文件上传 MultipartUpload
|
||||
- 开关 Switch
|
||||
- 评分 Rate
|
||||
- 省市区选择器 CitySelector
|
||||
- 图标选择器 IconSelector
|
||||
|
||||
### 文本输入 Input
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<n-input v-model:value="value" type="text" placeholder="基本的 Input" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 数字输入 Input Number
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<n-input-number v-model:value="value" clearable />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 文本域 InputTextarea
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<n-input
|
||||
v-model:value="value"
|
||||
type="textarea"
|
||||
placeholder="基本的 Textarea"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 富文本 InputEditor
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Editor style="height: 450px" v-model:value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import Editor from '@/components/Editor/editor.vue';
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 动态键值对 InputDynamic
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<n-dynamic-input
|
||||
v-model:value="value"
|
||||
preset="pair"
|
||||
key-placeholder="键名"
|
||||
value-placeholder="键值"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 日期选择 Date(Y-M-D)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<DatePicker v-model:formValue="value" type="date" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import DatePicker from '@/components/DatePicker/datePicker.vue';
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 日期范围选择 DateRange
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<DatePicker
|
||||
v-model:startValue="startValue"
|
||||
v-model:endValue="endValue"
|
||||
type="datetimerange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import DatePicker from '@/components/DatePicker/datePicker.vue';
|
||||
const startValue = ref(null);
|
||||
const endValue = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 时间选择 Time(Y-M-D H:i:s)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<n-time-picker :default-formatted-value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 时间范围选择 TimeRange
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<template>
|
||||
<n-space>
|
||||
<n-time-picker :default-value="startValue" />
|
||||
<n-time-picker :default-value="endValue" />
|
||||
</n-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
const startValue = ref(null);
|
||||
const endValue = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 单选按钮 Radio
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<n-space vertical>
|
||||
<n-radio-group v-model:value="value" name="radiobuttongroup1">
|
||||
<n-radio-button
|
||||
v-for="song in songs"
|
||||
:key="song.value"
|
||||
:value="song.value"
|
||||
:disabled="
|
||||
(song.label === 'Live Forever' && disabled1) ||
|
||||
(song.label === 'Shakermaker' && disabled2)
|
||||
"
|
||||
:label="song.label"
|
||||
/>
|
||||
</n-radio-group>
|
||||
<n-space>
|
||||
<n-checkbox v-model:checked="disabled2" style="margin-right: 12px">
|
||||
禁用 Shakemaker
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="disabled1">
|
||||
禁用 Live Forever
|
||||
</n-checkbox>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
return {
|
||||
value: ref(null),
|
||||
disabled2: ref(false),
|
||||
disabled1: ref(false),
|
||||
songs: [
|
||||
{
|
||||
value: "Rock'n'Roll Star",
|
||||
label: "Rock'n'Roll Star"
|
||||
},
|
||||
{
|
||||
value: 'Shakermaker',
|
||||
label: 'Shakermaker'
|
||||
},
|
||||
{
|
||||
value: 'Live Forever',
|
||||
label: 'Live Forever'
|
||||
},
|
||||
{
|
||||
value: 'Up in the Sky',
|
||||
label: 'Up in the Sky'
|
||||
},
|
||||
{
|
||||
value: '...',
|
||||
label: '...'
|
||||
}
|
||||
].map((s) => {
|
||||
s.value = s.value.toLowerCase()
|
||||
return s
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 复选框 Checkbox
|
||||
```vue
|
||||
<template>
|
||||
<n-space item-style="display: flex;" align="center">
|
||||
<n-checkbox v-model:checked="value">
|
||||
复选框
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="value" />
|
||||
<n-checkbox v-model:checked="value" :disabled="disabled">
|
||||
复选框
|
||||
</n-checkbox>
|
||||
<n-button size="small" @click="disabled = !disabled">
|
||||
禁用
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
return {
|
||||
value: ref(false),
|
||||
disabled: ref(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
### 单选下拉框 Select
|
||||
```vue
|
||||
<template>
|
||||
<n-space vertical>
|
||||
<n-select v-model:value="value" :options="options" />
|
||||
<n-select v-model:value="value" disabled :options="options" />
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
return {
|
||||
value: ref(null),
|
||||
options: [
|
||||
{
|
||||
label: "Everybody's Got Something to Hide Except Me and My Monkey",
|
||||
value: 'song0',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'Drive My Car',
|
||||
value: 'song1'
|
||||
},
|
||||
{
|
||||
label: 'Norwegian Wood',
|
||||
value: 'song2'
|
||||
},
|
||||
{
|
||||
label: "You Won't See",
|
||||
value: 'song3',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'Nowhere Man',
|
||||
value: 'song4'
|
||||
},
|
||||
{
|
||||
label: 'Think For Yourself',
|
||||
value: 'song5'
|
||||
},
|
||||
{
|
||||
label: 'The Word',
|
||||
value: 'song6'
|
||||
},
|
||||
{
|
||||
label: 'Michelle',
|
||||
value: 'song7',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'What goes on',
|
||||
value: 'song8'
|
||||
},
|
||||
{
|
||||
label: 'Girl',
|
||||
value: 'song9'
|
||||
},
|
||||
{
|
||||
label: "I'm looking through you",
|
||||
value: 'song10'
|
||||
},
|
||||
{
|
||||
label: 'In My Life',
|
||||
value: 'song11'
|
||||
},
|
||||
{
|
||||
label: 'Wait',
|
||||
value: 'song12'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 多选下拉框 SelectMultiple
|
||||
```vue
|
||||
<template>
|
||||
<n-space vertical>
|
||||
<n-select v-model:value="value" multiple :options="options" />
|
||||
<n-select v-model:value="value" multiple disabled :options="options" />
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
return {
|
||||
value: ref(['song3']),
|
||||
options: [
|
||||
{
|
||||
label: "Everybody's Got Something to Hide Except Me and My Monkey",
|
||||
value: 'song0',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'Drive My Car',
|
||||
value: 'song1'
|
||||
},
|
||||
{
|
||||
label: 'Norwegian Wood',
|
||||
value: 'song2'
|
||||
},
|
||||
{
|
||||
label: "You Won't See",
|
||||
value: 'song3',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'Nowhere Man',
|
||||
value: 'song4'
|
||||
},
|
||||
{
|
||||
label: 'Think For Yourself',
|
||||
value: 'song5'
|
||||
},
|
||||
{
|
||||
label: 'The Word',
|
||||
value: 'song6'
|
||||
},
|
||||
{
|
||||
label: 'Michelle',
|
||||
value: 'song7',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'What goes on',
|
||||
value: 'song8'
|
||||
},
|
||||
{
|
||||
label: 'Girl',
|
||||
value: 'song9'
|
||||
},
|
||||
{
|
||||
label: "I'm looking through you",
|
||||
value: 'song10'
|
||||
},
|
||||
{
|
||||
label: 'In My Life',
|
||||
value: 'song11'
|
||||
},
|
||||
{
|
||||
label: 'Wait',
|
||||
value: 'song12'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 树型选择 Tree Select
|
||||
```vue
|
||||
<template>
|
||||
<n-tree-select
|
||||
:options="options"
|
||||
default-value="Drive My Car"
|
||||
@update:value="handleUpdateValue"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { TreeSelectOption } from 'naive-ui'
|
||||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
return {
|
||||
handleUpdateValue (
|
||||
value: string | number | Array<string | number> | null,
|
||||
option: TreeSelectOption | null | Array<TreeSelectOption | null>
|
||||
) {
|
||||
console.log(value, option)
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Rubber Soul',
|
||||
key: 'Rubber Soul',
|
||||
children: [
|
||||
{
|
||||
label:
|
||||
"Everybody's Got Something to Hide Except Me and My Monkey",
|
||||
key: "Everybody's Got Something to Hide Except Me and My Monkey"
|
||||
},
|
||||
{
|
||||
label: 'Drive My Car',
|
||||
key: 'Drive My Car',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'Norwegian Wood',
|
||||
key: 'Norwegian Wood'
|
||||
},
|
||||
{
|
||||
label: "You Won't See",
|
||||
key: "You Won't See",
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'Nowhere Man',
|
||||
key: 'Nowhere Man'
|
||||
},
|
||||
{
|
||||
label: 'Think For Yourself',
|
||||
key: 'Think For Yourself'
|
||||
},
|
||||
{
|
||||
label: 'The Word',
|
||||
key: 'The Word'
|
||||
},
|
||||
{
|
||||
label: 'Michelle',
|
||||
key: 'Michelle',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'What goes on',
|
||||
key: 'What goes on'
|
||||
},
|
||||
{
|
||||
label: 'Girl',
|
||||
key: 'Girl'
|
||||
},
|
||||
{
|
||||
label: "I'm looking through you",
|
||||
key: "I'm looking through you"
|
||||
},
|
||||
{
|
||||
label: 'In My Life',
|
||||
key: 'In My Life'
|
||||
},
|
||||
{
|
||||
label: 'Wait',
|
||||
key: 'Wait'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Let It Be',
|
||||
key: 'Let It Be Album',
|
||||
children: [
|
||||
{
|
||||
label: 'Two Of Us',
|
||||
key: 'Two Of Us'
|
||||
},
|
||||
{
|
||||
label: 'Dig A Pony',
|
||||
key: 'Dig A Pony'
|
||||
},
|
||||
{
|
||||
label: 'Across The Universe',
|
||||
key: 'Across The Universe'
|
||||
},
|
||||
{
|
||||
label: 'I Me Mine',
|
||||
key: 'I Me Mine'
|
||||
},
|
||||
{
|
||||
label: 'Dig It',
|
||||
key: 'Dig It'
|
||||
},
|
||||
{
|
||||
label: 'Let It Be',
|
||||
key: 'Let It Be'
|
||||
},
|
||||
{
|
||||
label: 'Maggie Mae',
|
||||
key: 'Maggie Mae'
|
||||
},
|
||||
{
|
||||
label: "I've Got A Feeling",
|
||||
key: "I've Got A Feeling"
|
||||
},
|
||||
{
|
||||
label: 'One After 909',
|
||||
key: 'One After 909'
|
||||
},
|
||||
{
|
||||
label: 'The Long And Winding Road',
|
||||
key: 'The Long And Winding Road'
|
||||
},
|
||||
{
|
||||
label: 'For You Blue',
|
||||
key: 'For You Blue'
|
||||
},
|
||||
{
|
||||
label: 'Get Back',
|
||||
key: 'Get Back'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
</script><template>
|
||||
<n-tree-select
|
||||
:options="options"
|
||||
default-value="Drive My Car"
|
||||
@update:value="handleUpdateValue"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { TreeSelectOption } from 'naive-ui'
|
||||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
return {
|
||||
handleUpdateValue (
|
||||
value: string | number | Array<string | number> | null,
|
||||
option: TreeSelectOption | null | Array<TreeSelectOption | null>
|
||||
) {
|
||||
console.log(value, option)
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Rubber Soul',
|
||||
key: 'Rubber Soul',
|
||||
children: [
|
||||
{
|
||||
label:
|
||||
"Everybody's Got Something to Hide Except Me and My Monkey",
|
||||
key: "Everybody's Got Something to Hide Except Me and My Monkey"
|
||||
},
|
||||
{
|
||||
label: 'Drive My Car',
|
||||
key: 'Drive My Car',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'Norwegian Wood',
|
||||
key: 'Norwegian Wood'
|
||||
},
|
||||
{
|
||||
label: "You Won't See",
|
||||
key: "You Won't See",
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'Nowhere Man',
|
||||
key: 'Nowhere Man'
|
||||
},
|
||||
{
|
||||
label: 'Think For Yourself',
|
||||
key: 'Think For Yourself'
|
||||
},
|
||||
{
|
||||
label: 'The Word',
|
||||
key: 'The Word'
|
||||
},
|
||||
{
|
||||
label: 'Michelle',
|
||||
key: 'Michelle',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'What goes on',
|
||||
key: 'What goes on'
|
||||
},
|
||||
{
|
||||
label: 'Girl',
|
||||
key: 'Girl'
|
||||
},
|
||||
{
|
||||
label: "I'm looking through you",
|
||||
key: "I'm looking through you"
|
||||
},
|
||||
{
|
||||
label: 'In My Life',
|
||||
key: 'In My Life'
|
||||
},
|
||||
{
|
||||
label: 'Wait',
|
||||
key: 'Wait'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Let It Be',
|
||||
key: 'Let It Be Album',
|
||||
children: [
|
||||
{
|
||||
label: 'Two Of Us',
|
||||
key: 'Two Of Us'
|
||||
},
|
||||
{
|
||||
label: 'Dig A Pony',
|
||||
key: 'Dig A Pony'
|
||||
},
|
||||
{
|
||||
label: 'Across The Universe',
|
||||
key: 'Across The Universe'
|
||||
},
|
||||
{
|
||||
label: 'I Me Mine',
|
||||
key: 'I Me Mine'
|
||||
},
|
||||
{
|
||||
label: 'Dig It',
|
||||
key: 'Dig It'
|
||||
},
|
||||
{
|
||||
label: 'Let It Be',
|
||||
key: 'Let It Be'
|
||||
},
|
||||
{
|
||||
label: 'Maggie Mae',
|
||||
key: 'Maggie Mae'
|
||||
},
|
||||
{
|
||||
label: "I've Got A Feeling",
|
||||
key: "I've Got A Feeling"
|
||||
},
|
||||
{
|
||||
label: 'One After 909',
|
||||
key: 'One After 909'
|
||||
},
|
||||
{
|
||||
label: 'The Long And Winding Road',
|
||||
key: 'The Long And Winding Road'
|
||||
},
|
||||
{
|
||||
label: 'For You Blue',
|
||||
key: 'For You Blue'
|
||||
},
|
||||
{
|
||||
label: 'Get Back',
|
||||
key: 'Get Back'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
### 单图上传 UploadImage
|
||||
```vue
|
||||
<template>
|
||||
<UploadImage :maxNumber="1" v-model:value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import UploadImage from '@/components/Upload/uploadImage.vue';
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 多图上传 UploadImage
|
||||
```vue
|
||||
<template>
|
||||
<UploadImage :maxNumber="10" v-model:value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import UploadImage from '@/components/Upload/uploadImage.vue';
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 单文件上传 UploadFile
|
||||
```vue
|
||||
<template>
|
||||
<UploadFile :maxNumber="1" v-model:value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import UploadFile from '@/components/Upload/uploadFile.vue';
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 多文件上传 UploadFile
|
||||
```vue
|
||||
<template>
|
||||
<UploadFile :maxNumber="10" v-model:value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import UploadFile from '@/components/Upload/uploadFile.vue';
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 文件选择器 FileChooser
|
||||
- 基础用法
|
||||
```vue
|
||||
<template>
|
||||
<FileChooser v-model:value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import FileChooser from '@/components/FileChooser/index.vue';
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
- 指定fileType,支持多种选择器类型,默认情况是全部都可以选择
|
||||
```ts
|
||||
type FileType = 'image' | 'doc' | 'audio' | 'video' | 'zip' | 'other' | 'default';
|
||||
```
|
||||
|
||||
- 图片选择器
|
||||
```vue
|
||||
<FileChooser v-model:value="value" fileType="image" />
|
||||
```
|
||||
|
||||
- 多选支持,指定`maxNumber`多选数量
|
||||
```vue
|
||||
<FileChooser v-model:value="value" :maxNumber="10" fileType="image" />
|
||||
```
|
||||
|
||||
### 大文件上传 MultipartUpload
|
||||
- 基础用法
|
||||
```vue
|
||||
<template>
|
||||
<MultipartUpload ref="multipartUploadRef" @onFinish="handleFinishCall" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import MultipartUpload from '@/components/Upload/multipartUpload.vue';
|
||||
import { Attachment } from '@/components/FileChooser/src/model';
|
||||
const multipartUploadRef = ref();
|
||||
|
||||
// 打开上传Modal
|
||||
function handleMultipartUpload() {
|
||||
multipartUploadRef.value.openModal();
|
||||
}
|
||||
|
||||
// 上传成功回调附件内容
|
||||
function handleFinishCall(result: Attachment, success: boolean) {
|
||||
if (success) {
|
||||
reloadTable();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 开关 Switch
|
||||
```vue
|
||||
<template>
|
||||
<n-switch v-model:value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 评分 Rate
|
||||
```vue
|
||||
<template>
|
||||
<n-rate allow-half :default-value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 省市区选择器 CitySelector
|
||||
```vue
|
||||
<template>
|
||||
<CitySelector v-model:value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import CitySelector from '@/components/CitySelector/citySelector.vue';
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
### 图标选择器 IconSelector
|
||||
```vue
|
||||
<template>
|
||||
<IconSelector style="width: 100%" v-model:value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import IconSelector from '@/components/IconSelector/index.vue';
|
||||
const value = ref(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
更多组件请参考:https://www.naiveui.com/zh-CN/os-theme/components/button
|
||||
|
||||
|
||||
@@ -1,852 +0,0 @@
|
||||
# IM 产品需求服务端改造分析报告
|
||||
|
||||
> 分析日期:2026-08-09
|
||||
> 分析对象:`chat-0.25.3` 当前工作区源码
|
||||
> 需求依据:[IM-产品原型需求文档](IM-产品原型需求文档.md)
|
||||
> 报告范围:服务端能力、数据模型、协议/API、安全、实施计划和验收建议
|
||||
|
||||
## 1. 报告结论
|
||||
|
||||
当前项目是一个以 Tinode 为核心的 Go 即时通讯服务端,已经具备稳定的实时通信底座:账号登录、单聊、群聊、订阅与访问控制、消息收发、已读/送达回执、输入状态、消息删除、文件上传、推送、点对点 WebRTC 信令,以及可扩展的 gRPC 插件机制。
|
||||
|
||||
原型中的基础聊天能力不需要推倒重做,建议继续复用现有 Topic、Subscription、Message 和 Session 模型。在此基础上增加产品业务层,负责好友关系、群治理、内容检索、企业组织、会议/日报/月报、实名认证、青少年模式、举报与安全审计等业务。
|
||||
|
||||
本次分析得到以下关键结论:
|
||||
|
||||
1. **现有服务端可以承担实时通信内核,但不能直接满足完整产品需求。** 当前代码主要解决消息路由、会话和 ACL,不包含完整的社交关系、企业协同和安全治理领域模型。
|
||||
2. **应将“实时消息能力”和“产品业务能力”分层。** 聊天消息继续走 WebSocket;需要事务、查询、审批和管理的业务走 HTTP API;状态变化通过结构化系统消息回推 WebSocket。
|
||||
3. **必须先确定企业租户模式。** 当前 User、Topic、Subscription、Message 和 FileDef 均没有 `tenant_id`。如果多个企业共用一套数据库,租户隔离必须作为所有功能之前的基础改造。
|
||||
4. **消息幂等和文件授权是上线前必须修复的基础问题。** 发布消息处仍有“重复消息 ID 检查”的 TODO;文件下载只校验登录身份,没有验证用户是否有权访问对应消息或话题。
|
||||
5. **仓库部分旧文档与当前源码存在漂移。** 旧文档提到的 `x-im-*` 扩展、`im_*` 业务表、数据库版本 119 和 `tools/wsmock`,在当前源码中均未发现。本报告分析时各数据库适配器版本为 116;多租户第一阶段已把 MySQL 提升到 118,其他适配器仍为 116。本报告以可执行源码为准,不把文档设想视为已实现能力。
|
||||
|
||||
建议按“基础安全与协议 → 好友和群治理 → 搜索与文件 → 通话与设置 → 企业工作台”的顺序实施。若团队和测试资源稳定,完整服务端改造建议拆成 4 个产品阶段、6 至 9 个迭代交付。
|
||||
|
||||
## 2. 分析范围与判定口径
|
||||
|
||||
### 2.1 需求范围
|
||||
|
||||
本报告覆盖原型文档的 11 个功能域:
|
||||
|
||||
1. 账号注册、登录与身份安全;
|
||||
2. 消息首页、全局导航与系统通知;
|
||||
3. 单聊会话、会话建立与消息状态;
|
||||
4. 消息输入、媒体内容与消息操作;
|
||||
5. 通讯录、好友关系与黑名单;
|
||||
6. 群聊创建、资料维护与成员治理;
|
||||
7. 文件下载、收藏管理与聊天记录检索;
|
||||
8. 实时语音与视频通话;
|
||||
9. 企业工作台与办公协同;
|
||||
10. 个人资料、通用设置、隐私与青少年模式;
|
||||
11. 反诈安全、举报与意见反馈。
|
||||
|
||||
### 2.2 能力分类
|
||||
|
||||
文中的需求按以下四类判定:
|
||||
|
||||
| 分类 | 含义 | 处理方式 |
|
||||
| --- | --- | --- |
|
||||
| 已支持 | 当前代码已有可用服务端能力 | 直接复用并补验收测试 |
|
||||
| 扩展支持 | 现有核心模型可承载,但缺少业务字段、校验或接口 | 在现有链路中增加明确的领域逻辑 |
|
||||
| 全新建设 | 当前没有对应模型和服务 | 新增业务模块、表、API 和通知桥接 |
|
||||
| 客户端为主 | 服务端只需提供数据或开关,交互由 App 完成 | 避免在服务端重复实现 UI 状态 |
|
||||
|
||||
### 2.3 源码核查范围
|
||||
|
||||
重点核查了以下目录与文件:
|
||||
|
||||
- `server/session.go`:客户端消息分发、发布、订阅和元数据处理;
|
||||
- `server/topic.go`:Topic 生命周期、ACL、消息广播、回执和元数据读写;
|
||||
- `server/datamodel.go`:WebSocket 客户端/服务端协议模型;
|
||||
- `server/calls.go`:WebRTC 呼叫信令;
|
||||
- `server/hdl_files.go`:文件上传与下载;
|
||||
- `server/plugins.go`:gRPC 插件和消息拦截;
|
||||
- `server/store/store.go`、`server/store/types/types.go`:存储接口和核心实体;
|
||||
- `server/db/mysql/adapter.go`、`server/db/mysql/schema.sql`:当前 MySQL 实现;
|
||||
- `server/auth`、`server/validate`、`server/push`:认证、验证码和推送;
|
||||
- 根目录与 `docs` 目录的 Markdown 文档。
|
||||
|
||||
## 3. 当前服务端真实能力
|
||||
|
||||
### 3.1 核心架构
|
||||
|
||||
当前服务端采用“Session 接收协议包 → Hub/Topic 路由 → Store 持久化 → Topic 广播/Push”的模型。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
C["移动端 / Web 客户端"] -->|"WebSocket / Long Poll / gRPC"| S["Session"]
|
||||
S --> P["Plugin Firehose"]
|
||||
P --> H["Hub"]
|
||||
H --> T["Topic Actor"]
|
||||
T --> ST["Store Adapter"]
|
||||
ST --> DB["MySQL / PostgreSQL / MongoDB / RethinkDB"]
|
||||
T --> PU["Push Handlers"]
|
||||
T --> C
|
||||
C -->|"HTTP"| F["File Handler"]
|
||||
F --> ST
|
||||
```
|
||||
|
||||
Topic 以串行事件循环处理同一话题内的状态和消息,适合维持顺序号、订阅状态和广播一致性。扩展时不应在 Topic 事件循环中执行耗时的远程 RPC、全文搜索或复杂审批事务,否则会阻塞该话题的全部消息。
|
||||
|
||||
### 3.2 已有能力清单
|
||||
|
||||
| 能力 | 当前实现 | 主要源码位置 | 复用判断 |
|
||||
| --- | --- | --- | --- |
|
||||
| 账号创建与登录 | `acc`、`login` 协议;basic、token、REST 等认证方式 | `server/session.go`、`server/auth` | 已支持基础能力 |
|
||||
| 手机验证码 | 电话号码校验、验证码、重试控制,可配置 Twilio | `server/validate/tel/validate.go` | 可用于注册/找回密码,需补产品流程 |
|
||||
| 单聊与群聊 | P2P、group、channel Topic | `server/topic.go` | 已支持 |
|
||||
| 会话成员与 ACL | `ModeWant`、`ModeGiven`,包含 Join、Read、Write、Admin、Owner 等权限 | `server/topic.go`、`server/store/types` | 已支持底层权限 |
|
||||
| 消息收发 | 消息持久化、Topic 内递增序号、广播、离线推送 | `server/topic.go:997`、`server/store/store.go` | 已支持 |
|
||||
| 已读/送达/输入中 | `note` 的 `read`、`recv`、`kp` 等事件 | `server/datamodel.go:309`、`server/topic.go:1137` | 已支持 |
|
||||
| 消息扩展内容 | `head` 和 `content` 为通用 JSON;支持 Drafty | `server/datamodel.go:265`、`server/store/types/types.go:1213` | 能承载,缺少产品级规范与校验 |
|
||||
| 用户/群资料扩展 | Public、Trusted、Private、Aux、Tags | `server/datamodel.go`、`server/topic.go:2202` | 能承载少量扩展信息 |
|
||||
| 收藏话题 | 存在 `slf` Topic 类别 | `server/topic.go`、`server/store/types` | 可复用,但缺少完整收藏模型与检索 |
|
||||
| 文件 | HTTP 上传、元数据保存、附件与消息关联 | `server/hdl_files.go`、`server/store/store.go` | 基础可用,下载授权需修复 |
|
||||
| 音视频信令 | 邀请、接听、Offer、Answer、ICE、挂断;单聊中维护一个当前呼叫 | `server/calls.go` | 可复用,需产品化扩展 |
|
||||
| 推送 | 可插拔 Push Handler,包含消息和通话字段 | `server/push.go`、`server/push` | 已支持基础能力 |
|
||||
| 外部风控/审核 | gRPC 插件可继续、丢弃、替换或直接响应协议包 | `server/plugins.go:353` | 适合接外部审核,不替代内部授权 |
|
||||
|
||||
### 3.3 当前核心数据模型
|
||||
|
||||
当前模型聚焦通信内核:
|
||||
|
||||
| 实体 | 关键字段 | 缺少的产品概念 |
|
||||
| --- | --- | --- |
|
||||
| User | 状态、访问权限、Public、Trusted、Tags、设备 | 企业、部门、好友关系、实名认证状态、青少年策略 |
|
||||
| Topic | Owner、默认 ACL、Public、Trusted、Tags、Aux | 入群策略、群公告、禁言规则、群二维码、企业归属 |
|
||||
| Subscription | Topic、User、ModeWant、ModeGiven、Private | 备注、置顶、通知策略、单独禁言、好友状态 |
|
||||
| Message | Topic、SeqId、From、Head、Content | 客户端幂等 ID、消息类型索引、全文检索字段、审核状态 |
|
||||
| FileDef | 文件位置、状态、上传用户等 | 资源级访问策略、业务用途、病毒扫描状态 |
|
||||
|
||||
MySQL 当前消息表只对 `(topic, seqid)` 建唯一索引,没有消息类型索引、客户端消息 ID 唯一索引或全文检索索引。分析基线中各适配器均为 schema 116;当前 MySQL 已因租户主表升级到 118,PostgreSQL、MongoDB、RethinkDB 仍为 116。
|
||||
|
||||
### 3.4 当前主要缺口
|
||||
|
||||
1. `MsgGetQuery` 只支持 desc、sub、data、del,消息查询只有序号范围和数量,没有关键词、媒体类型、发送人、时间区间等检索条件。
|
||||
2. 发布消息时没有客户端消息幂等校验,`server/session.go:691` 仍保留重复消息 ID 检查 TODO。
|
||||
3. 当前群成员模型能表达 ACL,但不能完整表达申请加入、审批记录、过期状态和审批理由。直接用 `ModeGiven` 表示待审批会与当前订阅逻辑冲突。
|
||||
4. 当前群“静音”更接近通知/Presence 控制,不等同于禁止指定成员发言;服务端缺少群禁言策略。
|
||||
5. 文件下载只校验 API Key 和登录用户,没有根据文件关联消息、Topic ACL、发送者或接收者判断访问权限。
|
||||
6. WebRTC 只维护 P2P Topic 的一个当前呼叫,缺少稳定的业务 `call_id`、跨设备抢答、通话详单、失败原因规范和群通话能力。
|
||||
7. 没有企业、组织、部门、会议、日报、月报、信箱等领域模型。
|
||||
8. 没有实名认证审批、反诈策略、举报工单、反馈工单、青少年模式的服务端强制规则。
|
||||
9. 没有租户字段;共享数据库的企业 SaaS 部署会有数据越界风险。
|
||||
|
||||
## 4. 总体改造原则
|
||||
|
||||
### 4.1 保留通信内核,新增产品业务层
|
||||
|
||||
不建议将会议、日报、实名认证等业务硬塞进 Topic 的 Public/Aux JSON。Aux 适合兼容性元数据,不适合作为需要索引、唯一约束、状态机、审批记录和审计日志的主数据。
|
||||
|
||||
推荐分为四层:
|
||||
|
||||
1. **Tinode 实时内核**:Session、Hub、Topic、ACL、Message、Push、WebRTC 信令;
|
||||
2. **IM 产品策略层**:结构化消息解析、发消息前鉴权、好友/黑名单/群禁言/青少年模式校验;
|
||||
3. **业务服务层**:好友申请、群审批、组织、会议、报告、实名认证、举报等 HTTP API;
|
||||
4. **业务存储与事件层**:业务表、事务、Outbox、审计日志、搜索索引。
|
||||
|
||||
### 4.2 目标架构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
APP["iOS / Android / Web"]
|
||||
|
||||
subgraph Gateway["同一 Go 服务进程或同域网关"]
|
||||
WS["现有实时协议入口"]
|
||||
API["新增 /v1/im/* 业务 API"]
|
||||
FILE["文件上传/下载"]
|
||||
end
|
||||
|
||||
subgraph Core["通信与产品策略"]
|
||||
SESSION["Session / Hub / Topic"]
|
||||
POLICY["IM Policy Engine"]
|
||||
CALL["Call Signaling"]
|
||||
NOTIFY["Notification Bridge"]
|
||||
end
|
||||
|
||||
subgraph Business["业务模块"]
|
||||
REL["好友与黑名单"]
|
||||
GROUP["群治理"]
|
||||
ORG["组织与工作台"]
|
||||
SAFETY["实名 / 青少年 / 举报 / 风控"]
|
||||
SEARCH["消息与文件索引"]
|
||||
end
|
||||
|
||||
subgraph Infra["存储与外部系统"]
|
||||
DB["核心表 + im_* 业务表"]
|
||||
OUTBOX["Outbox / 异步任务"]
|
||||
MOD["外部审核与反诈服务"]
|
||||
PUSH["Push / SMS / 邮件"]
|
||||
MEDIA["对象存储 / 转码 / 病毒扫描"]
|
||||
end
|
||||
|
||||
APP --> WS
|
||||
APP --> API
|
||||
APP --> FILE
|
||||
WS --> SESSION
|
||||
SESSION --> POLICY
|
||||
POLICY --> DB
|
||||
API --> Business
|
||||
Business --> DB
|
||||
Business --> OUTBOX
|
||||
OUTBOX --> NOTIFY
|
||||
NOTIFY --> SESSION
|
||||
SESSION --> CALL
|
||||
POLICY --> MOD
|
||||
FILE --> MEDIA
|
||||
SESSION --> PUSH
|
||||
```
|
||||
|
||||
### 4.3 WebSocket 与 HTTP 的职责边界
|
||||
|
||||
| 场景 | 推荐通道 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 发消息、回执、输入中、在线事件、通话信令 | 现有 WebSocket | 低延迟、已有 Topic 路由和广播 |
|
||||
| 好友申请/审批、群申请/审批 | HTTP 写操作 + WS 通知 | 需要幂等、状态机、审计和列表查询 |
|
||||
| 组织架构、日报/月报、会议管理 | HTTP API | 强查询和事务型业务,不应阻塞 Topic |
|
||||
| 消息关键词/类型搜索 | HTTP 或扩展后的 `get` | 需要独立索引、分页游标和查询权限 |
|
||||
| 实名认证、举报、反馈、版本配置 | HTTP API | 涉及附件、敏感字段、工单和后台管理 |
|
||||
| 文件上传/下载 | HTTP | 沿用当前链路,但补资源级授权 |
|
||||
| 业务状态变化提醒 | 结构化系统消息/系统 Topic | 复用实时和推送能力 |
|
||||
|
||||
## 5. 11 类原型需求的服务端适配方案
|
||||
|
||||
### 5.1 账号注册、登录与身份安全
|
||||
|
||||
| 原型能力 | 现状 | 改造方案 | 优先级 |
|
||||
| --- | --- | --- | --- |
|
||||
| 企业码登录 | 无企业模型 | 新增企业码解析接口;企业码映射到企业、部署地址和登录策略 | P0 |
|
||||
| 手机号/密码登录 | 认证框架已有 | 固化账号标识规范、失败次数、锁定时长、设备记录和安全审计 | P0 |
|
||||
| 注册和验证码倒计时 | 电话验证器已有 | 新增注册编排接口;倒计时由客户端显示,服务端返回 `retry_after`、过期时间和重试上限 | P0 |
|
||||
| 忘记密码 | 有认证与验证器,无完整流程 | 增加发送验证码、校验票据、重置密码三个阶段;票据一次性、短时有效 | P0 |
|
||||
| 设置独立密码 | 可更新账号,但无产品流程 | 增加二次验证和密码策略;写审计日志并撤销旧 Token | P1 |
|
||||
| 实名认证 | 无 | 新建实名申请、状态查询、审核回调;敏感字段加密,客户端仅返回脱敏结果 | P1 |
|
||||
|
||||
企业码有两种实现方式:
|
||||
|
||||
- **推荐第一阶段:一企一实例/一企一库。** 企业码只负责发现该企业的服务地址和认证方式。现有核心表无需立即加入 `tenant_id`,隔离边界清晰。
|
||||
- **共享 SaaS:多企业共用服务和数据库。** 必须先完成第 6 节的全链路租户隔离,再开放企业码登录。
|
||||
|
||||
建议新增接口:
|
||||
|
||||
```text
|
||||
POST /v1/im/auth/enterprise/resolve
|
||||
POST /v1/im/auth/sms/send
|
||||
POST /v1/im/auth/password/reset-ticket
|
||||
POST /v1/im/auth/password/reset
|
||||
POST /v1/im/identity/verifications
|
||||
GET /v1/im/identity/verifications/current
|
||||
```
|
||||
|
||||
不要把身份证号、证件照片地址写入 User.Public、User.Trusted 或消息内容。实名数据应单独加密存储,并限制后台角色、访问理由和留痕。
|
||||
|
||||
### 5.2 消息首页、全局导航与系统通知
|
||||
|
||||
| 原型能力 | 现状 | 改造方案 | 优先级 |
|
||||
| --- | --- | --- | --- |
|
||||
| 会话列表 | Subscription 已有未读、时间等信息 | 扩展每用户会话设置:置顶、免打扰、草稿、隐藏;草稿可优先本地保存 | P1 |
|
||||
| 离线/重连 | Session 支持连接和队列 | 客户端指数退避;服务端增加连接状态指标、消息同步游标和限流 | P0 |
|
||||
| 长按置顶/删除/免打扰 | 部分可由 Private/订阅状态承载 | 明确 `im_conversation_settings`,避免 Private JSON 无法查询和约束 | P1 |
|
||||
| 系统消息 | 可创建系统 Topic/系统账号 | 建立系统消息类型字典、模板版本、跳转参数和去重键 | P1 |
|
||||
|
||||
系统消息应使用正常的 Message 存储和 Push 链路,而不是只发临时 `info` 包。这样用户离线后仍可拉取、已读和跨设备同步。
|
||||
|
||||
### 5.3 单聊会话、会话建立与消息状态
|
||||
|
||||
| 原型能力 | 现状 | 改造方案 | 优先级 |
|
||||
| --- | --- | --- | --- |
|
||||
| 单聊建立 | P2P Topic 已支持 | 在创建/订阅前检查好友、黑名单和隐私策略 | P0 |
|
||||
| 在线、输入中 | Presence、`kp` 已支持 | 统一客户端节流和过期时间;按隐私设置决定是否透出 | P1 |
|
||||
| 已发送、已送达、已读 | Topic 序号和 `recv/read` 已支持 | 定义产品状态映射;群聊展示规则单独定义 | P0 |
|
||||
| 拉黑后限制沟通 | 无完整关系表 | 关系表作为真源;发布、建会话、呼叫、文件访问均执行同一策略 | P0 |
|
||||
| 删除消息/撤回 | 有消息删除能力 | 区分“仅自己删除”和“全员撤回”;增加撤回时限、操作者和审计事件 | P1 |
|
||||
| 名片消息 | 通用 JSON 可承载 | 定义 `contact_card` 类型,只保存稳定用户 ID 和展示快照,打开时重新鉴权 | P1 |
|
||||
|
||||
“拉黑”不能只修改客户端按钮或 Topic 私有字段。策略层至少要拦截:新建单聊、发送消息、发送好友请求、音视频呼叫、读取对方敏感资料。
|
||||
|
||||
### 5.4 消息输入、媒体内容与消息操作
|
||||
|
||||
当前 `head`、`content` 接受通用 JSON,技术上可以传文本、图片、语音、视频、文件、名片、合并转发等内容,但服务端没有类型白名单、字段校验、幂等、审核和索引。建议增加明确的消息信封:
|
||||
|
||||
```json
|
||||
{
|
||||
"pub": {
|
||||
"id": "client-request-id",
|
||||
"topic": "grp...",
|
||||
"head": {
|
||||
"x-im-schema": 1,
|
||||
"x-im-type": "image",
|
||||
"x-im-client-mid": "01J...",
|
||||
"x-im-reply-to": 128
|
||||
},
|
||||
"content": {
|
||||
"file_id": "f...",
|
||||
"width": 1080,
|
||||
"height": 1440,
|
||||
"thumbnail_file_id": "f..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
服务端改造要求:
|
||||
|
||||
1. 在 `server/datamodel.go` 定义扩展字段常量和允许类型,不再由各客户端自由命名。
|
||||
2. 在 `server/session.go` 发布入口解析 `x-im-client-mid`,按“租户 + 发送者 + client_mid”去重;重复请求返回第一次成功的 Topic/SeqId。
|
||||
3. 在 `server/topic.go` 持久化前执行类型、大小、引用消息、附件所有权、好友/黑名单、禁言、青少年和风控策略。
|
||||
4. 文本继续使用 Drafty;图片、视频、语音、文件只在消息里保存 `file_id` 和必要元数据,不信任客户端提交的任意下载 URL。
|
||||
5. `@成员` 保存稳定用户 ID 列表;服务端校验被提醒者确实是当前群成员,并生成定向推送。
|
||||
6. 合并转发生成不可变快照,保留原作者展示名、时间和内容类型,但不得泄露原会话 ID 或无权访问的附件。
|
||||
7. 多选、键盘、表情面板、录音进度、上传进度属于客户端交互;服务端只负责资源、消息和失败状态。
|
||||
|
||||
推荐消息类型首批固定为:
|
||||
|
||||
```text
|
||||
text, image, voice, video, file, contact_card,
|
||||
location, merged_forward, system_notice, call_record
|
||||
```
|
||||
|
||||
转发应区分两种语义:
|
||||
|
||||
- **逐条转发**:为每条内容创建新消息,原附件通过授权后重新建立引用;
|
||||
- **合并转发**:创建一个 `merged_forward` 快照,子项限制数量和总大小,服务端清理敏感字段。
|
||||
|
||||
### 5.5 通讯录、好友关系与黑名单
|
||||
|
||||
当前 Subscription 表示“用户订阅了哪个 Topic”,不能替代双边好友关系和申请状态机。建议新增:
|
||||
|
||||
- `im_user_relations`:双方关系、备注、黑名单、免打扰等当前状态;
|
||||
- `im_friend_requests`:申请、处理人、验证信息、状态、过期时间和幂等键;
|
||||
- `im_privacy_settings`:允许通过手机号/ID/二维码查找、加好友验证方式等。
|
||||
|
||||
好友流程:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Pending: 发起申请
|
||||
Pending --> Accepted: 接受
|
||||
Pending --> Rejected: 拒绝
|
||||
Pending --> Cancelled: 发起人撤回
|
||||
Pending --> Expired: 超时
|
||||
Accepted --> Deleted: 任一方删除
|
||||
Accepted --> Blocked: 任一方拉黑
|
||||
Blocked --> Accepted: 取消拉黑且关系仍保留
|
||||
```
|
||||
|
||||
实现要点:
|
||||
|
||||
1. 精准用户 ID 搜索必须受隐私设置、租户和账号状态限制;手机号搜索应使用规范化哈希进行匹配,禁止模糊枚举。
|
||||
2. 接受好友申请和建立双方关系必须在同一数据库事务内完成,并通过 Outbox 异步创建/激活 P2P Topic、发送系统通知。
|
||||
3. 删除好友是否删除 P2P 历史需产品确认。建议只解除关系,不物理删除历史;双方分别控制会话是否可见。
|
||||
4. 拉黑属于单方策略,服务端立即生效。是否同时解除好友关系应作为明确产品规则,不由客户端猜测。
|
||||
5. 关系变化必须使 ACL/策略缓存失效,避免短时间内继续发消息。
|
||||
|
||||
建议接口:
|
||||
|
||||
```text
|
||||
GET /v1/im/contacts
|
||||
GET /v1/im/users/exact-search?id=...
|
||||
POST /v1/im/friend-requests
|
||||
GET /v1/im/friend-requests
|
||||
POST /v1/im/friend-requests/{id}/accept
|
||||
POST /v1/im/friend-requests/{id}/reject
|
||||
DELETE /v1/im/contacts/{user_id}
|
||||
PUT /v1/im/contacts/{user_id}/block
|
||||
DELETE /v1/im/contacts/{user_id}/block
|
||||
GET /v1/im/blocks
|
||||
```
|
||||
|
||||
### 5.6 群聊创建、资料维护与成员治理
|
||||
|
||||
现有群 Topic、Owner/Admin ACL 和所有者转移可以复用。需要新增群业务规则和审批数据。
|
||||
|
||||
| 原型能力 | 可复用基础 | 新增服务端能力 |
|
||||
| --- | --- | --- |
|
||||
| 创建群、群资料 | Group Topic、Public/Trusted/Aux | 资料字段规范、头像资源校验、敏感词审核 |
|
||||
| 加入群聊 | Subscription/Join ACL | 入群方式、申请表、审批状态机、人数和频率限制 |
|
||||
| 邀请进群 | Admin/Share ACL | 邀请记录、邀请权限、被邀请人确认、批量幂等 |
|
||||
| 群公告 | 消息可承载 | 公告主表、版本、置顶、已读回执、发布权限 |
|
||||
| 管理员 | Admin ACL | 管理员人数上限、操作审计、角色显示 |
|
||||
| 禁言成员 | 无对应强制策略 | 单人/全员禁言表和发布前校验,管理员豁免规则 |
|
||||
| 转让群主 | 已有 Owner 转移逻辑 | 二次确认、风险校验、审计和通知 |
|
||||
| 群二维码 | 无 | 签名短 Token、有效期、次数限制、撤销和入群策略 |
|
||||
| 群文件 | 文件与消息已有关联 | 群文件索引、上传者/类型/时间筛选、删除权限 |
|
||||
|
||||
当前订阅代码在新订阅者没有 Join 权限时会拒绝加入,因此不能仅用“`ModeGiven` 不含 Join”来保存待审批申请。正确做法是先写 `im_group_join_requests`,审批通过后再创建或更新 Subscription。
|
||||
|
||||
群禁言必须在消息持久化前检查,建议规则优先级为:
|
||||
|
||||
```text
|
||||
账号停用 > 黑名单/关系限制 > 全员禁言 > 单人禁言 > Topic Write ACL > 允许发送
|
||||
```
|
||||
|
||||
群二维码只应包含服务端生成的短 Token,不应直接暴露可永久加入的 Topic ID。Token 至少绑定群、创建人、过期时间、最大使用次数和当前入群策略,并使用服务端密钥签名或只保存其哈希。
|
||||
|
||||
### 5.7 文件下载、收藏管理与聊天记录检索
|
||||
|
||||
#### 文件访问安全
|
||||
|
||||
这是当前代码中最明确的安全缺口。`server/hdl_files.go` 的下载流程验证 API Key 和已登录用户后即可读取 URL 对应文件,没有验证该用户是否是上传者、是否属于关联消息的 Topic、该消息是否已撤回,或文件是否为公开资源。
|
||||
|
||||
建议新增统一的 `FileCanAccess(uid, fileID, action)`:
|
||||
|
||||
1. 查询 FileDef 和 file-message links;
|
||||
2. 上传未完成的文件只允许上传者访问;
|
||||
3. 已关联消息的文件要求用户对至少一个关联 Topic 具有 Read 权限;
|
||||
4. 群文件删除要求上传者、群管理员或群主权限;
|
||||
5. 转发附件时重新建立授权关联,不暴露底层存储永久地址;
|
||||
6. 使用短时签名下载 URL,记录下载审计;
|
||||
7. 对上传文件执行 MIME 嗅探、扩展名一致性、病毒扫描、图片/视频解码安全检查。
|
||||
|
||||
#### 收藏
|
||||
|
||||
`slf` Topic 可以继续承担“收藏内容跨设备同步”的消息容器,但建议增加 `im_favorites` 索引表,保存源消息、收藏消息、类型、创建时间和已清理的展示摘要。源消息删除后,收藏快照是否保留需按合规策略处理。
|
||||
|
||||
#### 聊天记录检索
|
||||
|
||||
当前 `MessageGetAll` 仅支持 Topic、序号范围和数量,不支持关键词或类型。建议新增 `im_message_index`:
|
||||
|
||||
| 字段 | 用途 |
|
||||
| --- | --- |
|
||||
| tenant_id / topic / seq_id | 权限过滤和定位原消息 |
|
||||
| sender_id / message_type | 发送人和类型筛选 |
|
||||
| sent_at | 时间范围和排序 |
|
||||
| searchable_text | 文本、文件名、链接标题等可搜索文本 |
|
||||
| file_id / mime_group | 文件、图片、视频分类 |
|
||||
| deleted / moderation_state | 排除已删除或不可见内容 |
|
||||
|
||||
若仅支持 MySQL,可使用 InnoDB FULLTEXT 配合 `ngram` 分词;若需要更强中文检索、跨话题检索和高数据量,建议使用 OpenSearch/Elasticsearch。无论使用哪种索引,查询结果都必须再次以当前 Topic Read ACL、消息删除范围和租户进行过滤,不能把搜索索引当作授权来源。
|
||||
|
||||
需要提供全量回填任务和增量 Outbox 消费者,避免上线后只能搜索新消息。
|
||||
|
||||
### 5.8 实时语音与视频通话
|
||||
|
||||
现有 `server/calls.go` 已支持 P2P 呼叫的邀请、接听、Offer、Answer、ICE Candidate 和挂断,可作为首期基础。改造建议:
|
||||
|
||||
1. 为每次呼叫生成不可复用的 `call_id`,不要只用 Topic 和消息 SeqId 表示一次通话;
|
||||
2. 增加 `voice/video` 类型、主叫/被叫设备、创建/响铃/接听/结束时间、结束方、标准结束原因;
|
||||
3. 增加跨设备抢答:一台设备接听后,其他设备收到 `answered_elsewhere`;
|
||||
4. 呼叫前执行好友、黑名单、隐私、账号状态和青少年策略;
|
||||
5. 呼叫超时、拒接、忙线、取消分别生成一致的通话记录消息和推送;
|
||||
6. 配置可用的 STUN/TURN,监控 ICE 成功率和呼叫建立耗时;
|
||||
7. 麦克风、扬声器、摄像头开关和悬浮窗主要由客户端管理,服务端只同步必要的媒体状态事件;
|
||||
8. 群音视频不应扩展当前单个 `videoCall` 结构硬做,建议另建会议房间并接入 SFU。
|
||||
|
||||
### 5.9 企业工作台与办公协同
|
||||
|
||||
该部分属于全新业务域,当前服务端没有组织、部门、会议、日报、月报或信箱代码。建议它们作为独立业务模块,通过 Tinode 系统消息发送提醒,而不是修改聊天核心协议承载所有 CRUD。
|
||||
|
||||
#### 组织架构
|
||||
|
||||
- `im_org_units`:企业、部门、层级、排序、负责人、状态;
|
||||
- `im_org_members`:用户、部门、职位、主部门、在职状态和可见范围;
|
||||
- 支持树形查询、按姓名/部门搜索、部门成员分页;
|
||||
- 组织同步应有版本号,客户端可增量刷新;
|
||||
- 离职用户需在一个事务编排中禁用账号、退出企业群、撤销 Token 和处理数据归属。
|
||||
|
||||
#### 会议通知
|
||||
|
||||
- `im_meetings`:主题、时间、地点/会议链接、发起人、提醒策略、状态;
|
||||
- `im_meeting_participants`:参与人、回复状态、回复时间;
|
||||
- 创建/变更/取消写 Outbox,由通知桥接器向参与者系统 Topic 发结构化消息;
|
||||
- 定时提醒由任务调度器执行,任务必须使用幂等键避免重复推送;
|
||||
- 若未来增加在线视频会议,会议业务 ID 与通话房间 ID 分离。
|
||||
|
||||
#### 日报与月报
|
||||
|
||||
- 统一使用 `im_work_reports`,字段包含类型、周期、作者、部门、正文、附件、状态、提交时间;
|
||||
- 管理员列表以权限范围过滤,不以客户端传入部门为准;
|
||||
- 提交后是否可编辑、补交和撤回需形成明确状态机;
|
||||
- 汇总统计异步生成,不在请求中扫描所有报告。
|
||||
|
||||
#### 总经理信箱
|
||||
|
||||
- 使用 `im_mailbox_messages`,支持实名/匿名展示策略、正文、附件、处理状态和回复;
|
||||
- “匿名”只能对业务查看者隐藏,安全审计角色仍可在合法授权下追溯;
|
||||
- 附件走统一私有文件授权;
|
||||
- 每次查看和导出敏感信件都写审计日志。
|
||||
|
||||
### 5.10 个人资料、通用设置、隐私与青少年模式
|
||||
|
||||
| 能力 | 服务端责任 | 客户端责任 |
|
||||
| --- | --- | --- |
|
||||
| 昵称、生日、签名、头像 | 校验、敏感词、可见范围、版本和审计 | 编辑和展示 |
|
||||
| 个人二维码 | 生成短时签名 Token、隐私校验 | 展示、扫码 |
|
||||
| 清空缓存 | 无需删除服务端消息 | 清理本地缓存和下载文件 |
|
||||
| 版本更新 | 返回平台、渠道、最低版本、推荐版本、下载地址和更新文案 | 下载、安装、商店跳转 |
|
||||
| 隐私设置 | 作为服务端授权条件强制执行 | 提供开关界面 |
|
||||
| 青少年模式 | 服务端保存状态、PIN 哈希、策略版本并强制限制 | UI 切换和本地内容隐藏 |
|
||||
| 推送/相机/麦克风权限 | 保存推送 Token 和提醒偏好 | 调用系统权限 API |
|
||||
|
||||
青少年模式不能只是客户端开关,否则卸载重装或换设备即可绕过。建议 `im_teen_mode` 保存启用状态、监护 PIN 的强哈希、失败锁定、允许时段、功能策略版本和更新时间。服务端在登录、搜索、加好友、发消息、通话和工作台入口统一检查策略。
|
||||
|
||||
版本接口示例:
|
||||
|
||||
```text
|
||||
GET /v1/im/config/app-version?platform=android&channel=official&version=1.4.0
|
||||
```
|
||||
|
||||
响应需区分 `latest`、`recommended`、`minimum_supported` 和 `force_update`。客户端系统权限文案和弹窗不需要服务端逐页参与,但可以通过配置中心提供版本化文案。
|
||||
|
||||
### 5.11 反诈安全、举报与意见反馈
|
||||
|
||||
建议形成三层控制:
|
||||
|
||||
1. **同步硬规则**:账号状态、黑名单、限频、群禁言、文件大小/类型等,必须在服务端本地快速判断;
|
||||
2. **内容与风险审核**:通过现有 gRPC Plugin Firehose 或独立审核服务检查文本、图片和链接;
|
||||
3. **事后处置**:举报工单、证据固化、审核、处罚、申诉和审计。
|
||||
|
||||
现有插件在协议分发之前同步执行,严格审核会直接增加消息延迟。外部审核调用必须设置较短超时、熔断、指标和明确的 fail-open/fail-closed 策略。高风险场景可以先将消息标为 `pending_review`,审核通过后再广播;低风险场景可先发后审并支持撤回。
|
||||
|
||||
新增数据模型:
|
||||
|
||||
- `im_reports`:举报对象、原因、描述、提交人、状态、处理人、结论;
|
||||
- `im_report_evidence`:消息快照、文件引用、校验哈希;
|
||||
- `im_feedback`:意见类型、正文、联系方式、附件、处理状态;
|
||||
- `im_risk_events`:命中规则、风险等级、处置、模型/规则版本;
|
||||
- `im_audit_logs`:操作者、动作、对象、结果、IP、设备和关联请求 ID。
|
||||
|
||||
举报消息时不要只保存可变的 Topic/SeqId,应在用户仍有读取权限时由服务端固化必要证据快照和内容哈希。后台读取证据需要单独权限和完整审计。
|
||||
|
||||
## 6. 企业租户隔离决策
|
||||
|
||||
### 6.1 当前事实
|
||||
|
||||
当前 User、Topic、Subscription、Message 和 FileDef 没有 `tenant_id`。用户标签和 Topic 标签也没有企业隔离维度。仅在登录入口识别企业码,不能保证后续搜索、消息、文件和后台查询不会跨企业。
|
||||
|
||||
### 6.2 推荐方案:第一阶段一企一实例/一企一库
|
||||
|
||||
企业码网关返回企业专属服务地址。每个实例使用独立数据库、对象存储前缀、推送配置和审核配置。这种方案最贴合当前架构,改造量和误配风险最低,也便于企业级数据迁移与删除。
|
||||
|
||||
需要新增:
|
||||
|
||||
- 企业码目录服务;
|
||||
- 企业到服务地址/认证策略的映射;
|
||||
- 配置和密钥隔离;
|
||||
- 统一运维、监控和版本发布能力。
|
||||
|
||||
### 6.3 共享 SaaS 的必要改造
|
||||
|
||||
如果明确要求多个企业共用一套服务和数据库,则以下工作必须先于产品功能:
|
||||
|
||||
1. 为核心表和所有业务表增加 `tenant_id`;
|
||||
2. 所有唯一索引包含租户维度,例如用户登录标识、Topic、客户端消息 ID;
|
||||
3. Session 完成认证后固定 tenant,禁止客户端包覆盖;
|
||||
4. Topic 查找、用户查找、订阅、消息读写、文件访问和搜索全部强制带 tenant;
|
||||
5. 内存缓存、Hub 路由键、集群路由和 Push 任务均包含 tenant;
|
||||
6. 系统账号和系统 Topic 按企业创建;
|
||||
7. 后台管理账号采用企业范围和数据范围授权;
|
||||
8. 增加跨租户攻击自动化测试,覆盖枚举 ID、文件 URL、搜索和转发。
|
||||
|
||||
这不是加一个字段的局部修改,而是横跨认证、存储接口、全部数据库适配器、缓存、集群路由和测试的基础工程。
|
||||
|
||||
## 7. 协议与 API 改造
|
||||
|
||||
### 7.1 保持现有协议兼容
|
||||
|
||||
继续支持现有 `hi`、`acc`、`login`、`sub`、`leave`、`pub`、`get`、`set`、`del`、`note`。新客户端通过 `x-im-schema` 声明产品扩展版本;旧客户端没有扩展字段时仍按现有路径工作。
|
||||
|
||||
推荐新增固定消息头:
|
||||
|
||||
| 字段 | 必填条件 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `x-im-schema` | 产品消息必填 | 扩展协议版本 |
|
||||
| `x-im-type` | 产品消息必填 | 消息类型枚举 |
|
||||
| `x-im-client-mid` | 普通消息必填 | 客户端生成的幂等 ID |
|
||||
| `x-im-reply-to` | 回复消息时 | 被回复消息 SeqId |
|
||||
| `x-im-forward-mode` | 转发时 | `single` 或 `merged` |
|
||||
| `x-im-risk-state` | 仅服务端写 | 审核状态,客户端提交值必须忽略 |
|
||||
|
||||
### 7.2 消息幂等
|
||||
|
||||
新增唯一约束:
|
||||
|
||||
```text
|
||||
(tenant_id, sender_id, client_mid) UNIQUE
|
||||
```
|
||||
|
||||
一企一库时可省略物理 `tenant_id`,但代码接口仍建议预留租户上下文。幂等记录需要保存最终 Topic、SeqId 和响应摘要。消息保存、Topic 最新序号、订阅更新目前通过多次 Store 调用完成;新增幂等时应明确事务边界,避免数据库写成功但回复丢失后重试产生两条消息。
|
||||
|
||||
### 7.3 查询扩展
|
||||
|
||||
不建议继续把搜索条件塞进未定义的任意 JSON。可在 `MsgGetQuery` 中增加明确字段:
|
||||
|
||||
```go
|
||||
type MsgSearchQuery struct {
|
||||
Query string `json:"query,omitempty"`
|
||||
Types []string `json:"types,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
SinceTime string `json:"since_time,omitempty"`
|
||||
BeforeTime string `json:"before_time,omitempty"`
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
如果搜索由独立服务承担,则提供 `/v1/im/search/messages`,返回 Topic、SeqId 和安全摘要,客户端再通过原生 `get data` 拉取上下文。任何方案都必须在服务端重新做 Topic ACL 检查。
|
||||
|
||||
### 7.4 业务 API 约定
|
||||
|
||||
新增 `/v1/im/*` 路由,复用 Tinode Token 的认证结果。应把 `hdl_files.go` 中现有的请求认证逻辑抽取成通用 HTTP 中间件,而不是让每个 Handler 自行解析 Token。
|
||||
|
||||
所有写接口统一支持:
|
||||
|
||||
- `X-Request-ID`:链路追踪;
|
||||
- `Idempotency-Key`:创建和审批的幂等控制;
|
||||
- 统一错误结构:`code`、`message`、`details`、`request_id`;
|
||||
- 游标分页,不使用数据增长时不稳定的大 Offset;
|
||||
- 服务端从认证上下文取用户和租户,不接受正文伪造;
|
||||
- 状态变更写审计日志和 Outbox。
|
||||
|
||||
## 8. 建议新增的数据模型
|
||||
|
||||
以下是逻辑模型,正式建表前需补充字段长度、外键策略、冷热分层和数据保留周期。
|
||||
|
||||
| 表 | 核心字段 | 关键约束/索引 | 作用 |
|
||||
| --- | --- | --- | --- |
|
||||
| `im_user_relations` | user_id、peer_id、state、remark、blocked、muted | 双向记录唯一;peer/state 索引 | 好友、备注、拉黑和通知关系 |
|
||||
| `im_friend_requests` | id、from、to、message、state、expires_at | 待处理申请去重;to/state/time | 好友申请状态机 |
|
||||
| `im_conversation_settings` | user_id、topic、pinned、muted、hidden | user/topic 唯一 | 会话个性化设置 |
|
||||
| `im_privacy_settings` | user_id、search flags、friend policy | user 唯一 | 精准搜索和加好友授权 |
|
||||
| `im_group_settings` | topic、join_policy、invite_policy、mute_all | topic 唯一 | 群业务规则 |
|
||||
| `im_group_join_requests` | id、topic、applicant、inviter、state | topic/applicant/pending 去重 | 入群申请与审批 |
|
||||
| `im_group_mutes` | topic、user、expires_at、operator | topic/user 唯一;expires 索引 | 单人禁言 |
|
||||
| `im_group_announcements` | id、topic、version、content、publisher | topic/status/time | 群公告版本 |
|
||||
| `im_group_announcement_reads` | announcement_id、user、read_at | announcement/user 唯一 | 公告已读 |
|
||||
| `im_group_invite_tokens` | token_hash、topic、expires、max_uses | token_hash 唯一;expires | 群二维码/邀请链接 |
|
||||
| `im_message_idempotency` | sender、client_mid、topic、seq_id | sender/client_mid 唯一 | 消息去重 |
|
||||
| `im_message_index` | topic、seq_id、type、text、file_id、sent_at | topic/seq 唯一;搜索索引 | 消息、文件和媒体检索 |
|
||||
| `im_favorites` | user、favorite_seq、source topic/seq、type | user/time;源消息索引 | 收藏筛选与分享 |
|
||||
| `im_identity_verifications` | user、encrypted PII、state、review | user/current-state;request id | 实名认证和审计 |
|
||||
| `im_org_units` | id、parent_id、name、path、state | parent/sort;path | 企业组织树 |
|
||||
| `im_org_members` | user、org_unit、title、primary | unit/state;user | 部门成员关系 |
|
||||
| `im_meetings` | id、organizer、title、start/end、state | organizer/time;state/time | 会议通知 |
|
||||
| `im_meeting_participants` | meeting、user、response | meeting/user 唯一 | 参会状态 |
|
||||
| `im_work_reports` | type、period、author、department、content、state | author/type/period;dept/time | 日报/月报 |
|
||||
| `im_mailbox_messages` | id、sender、content、anonymous、state | state/time;sender/time | 总经理信箱 |
|
||||
| `im_user_settings` | user、setting key/value、version | user/key 唯一 | 跨设备设置 |
|
||||
| `im_teen_mode` | user、enabled、pin_hash、policy_version | user 唯一 | 青少年策略 |
|
||||
| `im_reports` | reporter、target_type/id、reason、state | state/time;target | 举报工单 |
|
||||
| `im_report_evidence` | report、snapshot、file、hash | report | 证据固化 |
|
||||
| `im_feedback` | user、category、content、state | state/time;user/time | 意见反馈 |
|
||||
| `im_risk_events` | actor、object、rule、level、action | actor/time;object | 风控记录 |
|
||||
| `im_audit_logs` | actor、action、object、result、request_id | actor/time;object/time | 管理和敏感操作审计 |
|
||||
| `im_outbox` | aggregate、event_type、payload、state | state/next_retry;dedupe key | 事务后可靠通知 |
|
||||
|
||||
所有表在共享 SaaS 模式下都必须包含 `tenant_id`,并将它放入唯一约束和查询前缀。软删除字段不能代替状态机和审计记录。
|
||||
|
||||
## 9. 代码级改造清单
|
||||
|
||||
### 9.1 现有文件修改
|
||||
|
||||
| 文件 | 建议修改 |
|
||||
| --- | --- |
|
||||
| `server/datamodel.go` | 定义产品消息头、消息类型、查询过滤和错误码;保持旧协议兼容 |
|
||||
| `server/session.go` | 发布前幂等校验;调用产品策略层;统一映射业务错误;认证后绑定租户上下文 |
|
||||
| `server/topic.go` | 发布前关系/禁言/青少年策略;结构化系统消息;群管理状态变化后的缓存失效 |
|
||||
| `server/calls.go` | 引入 call_id、设备维度、结束原因、抢答与通话详单;群通话走独立模块 |
|
||||
| `server/push.go` | 按消息类型生成 Push;支持 @提醒、审批、会议和审核状态;避免敏感内容出现在锁屏 |
|
||||
| `server/hdl_files.go` | 抽取通用 HTTP 认证;下载增加 FileCanAccess;签名 URL;扫描状态检查 |
|
||||
| `server/plugins.go` | 为外部审核增加超时、指标、熔断和场景化失败策略 |
|
||||
| `server/main.go` | 注册 `/v1/im/*` 路由、业务模块生命周期、健康检查和后台任务 |
|
||||
| `server/store/store.go` | 增加幂等、文件授权和搜索所需的核心接口,明确事务边界 |
|
||||
| `server/store/types/types.go` | 增加明确的扩展类型;若共享 SaaS,核心实体加入 TenantId |
|
||||
| `server/db/mysql/adapter.go` | schema 升级、索引、幂等和核心查询实现 |
|
||||
| `server/db/mysql/schema.sql` | 与 adapter 中建表 SQL 同步,禁止只改其中一份 |
|
||||
| `tinode.conf` | 产品模块、租户、搜索、审核、TURN、文件扫描和限流配置 |
|
||||
|
||||
### 9.2 建议新增包
|
||||
|
||||
```text
|
||||
server/imapi/ HTTP 路由、请求校验、响应和认证中间件
|
||||
server/impolicy/ 好友、黑名单、群禁言、隐私、青少年等同步策略
|
||||
server/immessage/ 消息类型注册、结构校验、摘要与转发净化
|
||||
server/imbiz/ 好友、群治理、组织、会议、报告、实名、举报服务
|
||||
server/imstore/ 产品业务存储接口和 MySQL 实现
|
||||
server/imnotify/ Outbox 消费、系统消息和 Push 桥接
|
||||
server/imsearch/ 索引写入、回填、检索和 ACL 二次过滤
|
||||
server/imjobs/ 过期申请、会议提醒、清理、索引回填等后台任务
|
||||
```
|
||||
|
||||
### 9.3 数据库适配策略
|
||||
|
||||
当前项目同时包含 MySQL、PostgreSQL、MongoDB 和 RethinkDB 适配器。新增所有产品能力到公共 Store 接口会要求四套实现同步完成,改造面很大。
|
||||
|
||||
建议先明确产品正式支持的数据库:
|
||||
|
||||
- 如果生产只使用 MySQL,则产品业务 Store 首期实现 MySQL,并在启用 `im_product` 配置且底层不支持时拒绝启动,不能静默降级;
|
||||
- 核心消息幂等、文件授权等通信安全能力应进入公共接口,或明确产品版只支持 MySQL;
|
||||
- schema 版本从 116 升级时,四个适配器必须分别维护自己的版本,不能把 MySQL 已升级误写成所有数据库都支持;
|
||||
- `adapter.go` 中的建表 SQL 与 `schema.sql` 必须通过测试保持一致。
|
||||
|
||||
## 10. 事务、一致性与异步通知
|
||||
|
||||
好友审批、入群审批、会议创建等操作通常同时涉及业务表、Topic/Subscription 和通知。若直接依次调用,任何一步失败都会出现“状态已接受但没有会话”或“会议创建了但没人收到通知”。
|
||||
|
||||
建议采用本地事务 + Outbox:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as App
|
||||
participant API as IM Business API
|
||||
participant DB as Database
|
||||
participant W as Outbox Worker
|
||||
participant T as Tinode Topic
|
||||
|
||||
A->>API: 接受好友/入群申请(Idempotency-Key)
|
||||
API->>DB: 开启事务
|
||||
API->>DB: 更新申请与关系/成员状态
|
||||
API->>DB: 写入 Outbox 事件
|
||||
API->>DB: 提交事务
|
||||
API-->>A: 返回确定的业务状态
|
||||
W->>DB: 锁定未处理事件
|
||||
W->>T: 创建/更新订阅并发送系统通知
|
||||
W->>DB: 标记完成或计划重试
|
||||
```
|
||||
|
||||
Outbox 消费需要事件去重;状态变更 API 自身也需要 Idempotency-Key。对于必须与核心 Topic 数据原子提交的操作,应把它们放到同一数据库事务接口,而不是依赖最终一致性掩盖授权窗口。
|
||||
|
||||
## 11. 安全与合规基线
|
||||
|
||||
服务端改造至少应包含:
|
||||
|
||||
1. **认证安全**:密码强哈希、验证码限频、设备和 IP 风险、Token 撤销、失败锁定;
|
||||
2. **授权安全**:所有对象查询从服务端认证上下文获得租户和用户;文件、搜索、转发必须二次校验 ACL;
|
||||
3. **数据安全**:身份证等 PII 字段加密、密钥轮换、响应脱敏、日志禁止明文;
|
||||
4. **内容安全**:文本、图片、链接和文件审核;病毒扫描;风险策略版本化;
|
||||
5. **审计安全**:管理员、群主、实名审核、举报处置、匿名信箱访问全部留痕;
|
||||
6. **接口安全**:限流、请求大小、游标签名、幂等、防重放、统一输入校验;
|
||||
7. **隐私删除**:明确账号注销、聊天记录、收藏、举报证据和办公数据的不同保留周期;
|
||||
8. **推送隐私**:锁屏 Push 默认不携带敏感正文,由用户设置控制预览;
|
||||
9. **可观测性**:请求 ID、用户/租户匿名化标识、消息处理延迟、拒绝原因和插件超时指标。
|
||||
|
||||
## 12. 分阶段实施建议
|
||||
|
||||
### 阶段 P0:基线和关键决策
|
||||
|
||||
- 确定一企一实例还是共享 SaaS;
|
||||
- 确定产品版支持的数据库;
|
||||
- 清理旧设计文档与源码的版本漂移,建立 schema/协议单一事实来源;
|
||||
- 建立数据库迁移、回滚、CI 和环境配置基线;
|
||||
- 修复文件下载授权;
|
||||
- 实现消息客户端 ID 幂等;
|
||||
- 定义消息类型、错误码和兼容策略。
|
||||
|
||||
完成标准:安全缺口关闭;重复重试不会产生重复消息;旧客户端仍可通信;开发和测试环境可重复升级/回滚。
|
||||
|
||||
### 阶段 P1:账号、关系和基础聊天产品化
|
||||
|
||||
- 企业码发现、注册、验证码和忘记密码流程;
|
||||
- 实名认证基础流程;
|
||||
- 好友申请、联系人、黑名单和隐私设置;
|
||||
- 结构化文本/图片/语音/视频/文件/名片消息;
|
||||
- 会话置顶、免打扰、系统消息;
|
||||
- @提醒、回复、逐条转发、合并转发;
|
||||
- 基础风控和审计。
|
||||
|
||||
完成标准:原型第 1 至 5 类主流程可由真实客户端端到端完成。
|
||||
|
||||
### 阶段 P2:群治理、收藏、搜索和文件
|
||||
|
||||
- 入群申请、邀请审批、群公告;
|
||||
- 管理员、群主转让、全员/成员禁言;
|
||||
- 群二维码和群文件;
|
||||
- 收藏索引;
|
||||
- 消息关键词、文件、链接、图片和视频检索;
|
||||
- 历史数据回填和搜索 ACL 验证。
|
||||
|
||||
完成标准:原型第 6、7 类全部服务端流程可验收,搜索不会返回无权消息或附件。
|
||||
|
||||
### 阶段 P3:通话、设置和安全治理
|
||||
|
||||
- call_id、通话记录、跨设备抢答、TURN 指标;
|
||||
- 个人资料和二维码;
|
||||
- 版本配置、隐私设置、青少年模式强制策略;
|
||||
- 反诈提示策略、举报、证据、反馈和后台处置;
|
||||
- 内容审核超时/降级策略演练。
|
||||
|
||||
完成标准:原型第 8、10、11 类服务端能力可验收,关键安全策略无法通过换设备绕过。
|
||||
|
||||
### 阶段 P4:企业工作台
|
||||
|
||||
- 组织与部门;
|
||||
- 会议通知和提醒;
|
||||
- 日报、月报和管理视图;
|
||||
- 总经理信箱;
|
||||
- 离职编排、数据权限和敏感操作审计。
|
||||
|
||||
完成标准:原型第 9 类完成业务闭环,组织数据权限和通知可靠性通过测试。
|
||||
|
||||
## 13. 测试与验收方案
|
||||
|
||||
### 13.1 自动化测试层次
|
||||
|
||||
| 测试层 | 重点 |
|
||||
| --- | --- |
|
||||
| 单元测试 | 消息类型校验、状态机、权限矩阵、Token/二维码、错误映射 |
|
||||
| Store 集成测试 | schema 升降级、唯一约束、事务、并发审批、Outbox 重试 |
|
||||
| WebSocket 端到端 | 登录、订阅、消息、重复发送、回执、禁言、拉黑、通话信令 |
|
||||
| HTTP API 测试 | 好友/群审批、组织、报告、实名、举报、文件授权 |
|
||||
| 搜索测试 | 中文关键词、类型筛选、删除同步、权限变化、历史回填 |
|
||||
| 安全测试 | 跨用户/跨租户、文件 URL 猜测、ID 枚举、重放、越权审批 |
|
||||
| 故障测试 | 审核超时、Push 失败、Outbox 重试、数据库瞬断、服务重启 |
|
||||
| 真实设备测试 | Push、相机/麦克风、弱网重连、WebRTC NAT/TURN、跨设备抢答 |
|
||||
|
||||
### 13.2 必须覆盖的关键用例
|
||||
|
||||
1. 同一个 `x-im-client-mid` 并发重试 10 次,只产生一条消息并返回同一个 SeqId;
|
||||
2. 拉黑后已有长连接不能继续发消息或呼叫;
|
||||
3. 被禁言成员通过旧客户端或直接构造协议包仍被服务端拒绝;
|
||||
4. 非群成员持有文件 URL 时无法下载,退出群后访问策略符合产品约定;
|
||||
5. 搜索结果不包含已撤回、仅对本人删除、无 Read ACL 或其他租户的消息;
|
||||
6. 入群审批重复点击不会重复创建 Subscription;
|
||||
7. 群主转让、管理员变更和禁言都有审计记录且权限立即生效;
|
||||
8. 实名信息不会出现在日志、Push、Public/Trusted 和普通后台响应;
|
||||
9. 青少年模式换设备、重装或使用旧客户端仍受服务端限制;
|
||||
10. 外部审核服务超时后,系统严格按配置的降级策略处理并产生告警;
|
||||
11. Outbox 消费进程崩溃重启后,通知不丢失且业务副作用不重复;
|
||||
12. 多租户模式下所有 API、消息、文件、搜索和后台查询均完成负向越权测试。
|
||||
|
||||
### 13.3 当前基线验证
|
||||
|
||||
本次分析执行了以下不依赖外部数据库的测试:
|
||||
|
||||
```bash
|
||||
go test -count=1 ./server ./server/drafty ./server/media ./server/store/types ./server/db/common
|
||||
```
|
||||
|
||||
上述包当前全部通过。MySQL/PostgreSQL/MongoDB/RethinkDB 的真实数据库集成测试未在本次报告中执行,改造前应在 CI 中配置正式支持的数据库并固定运行。
|
||||
|
||||
## 14. 风险与待确认事项
|
||||
|
||||
| 决策 | 不确认的风险 | 建议 |
|
||||
| --- | --- | --- |
|
||||
| 企业隔离模式 | 后期补 tenant 会重写大量查询和数据 | P0 首先决策,优先一企一实例 |
|
||||
| 正式支持的数据库 | 四种适配器同步开发显著拖慢交付 | 产品业务首期明确 MySQL |
|
||||
| 删除好友后的历史 | 客户端与服务端理解不同导致数据争议 | 默认保留历史、禁止新消息 |
|
||||
| 收藏是否为独立副本 | 源消息删除后的合规行为不明确 | 明确快照和保留策略 |
|
||||
| 全员撤回时限 | 无规则会导致历史任意篡改 | 配置时限、角色和审计 |
|
||||
| 匿名信箱的匿名边界 | 绝对匿名与合规追溯冲突 | 业务匿名、受控审计可追溯 |
|
||||
| 审核失败策略 | 失败开放有风险,失败关闭影响可用性 | 按场景分级并可观测 |
|
||||
| 群音视频范围 | 当前 P2P 模型无法自然扩展 | 若需要多人通话,独立评估 SFU |
|
||||
| 搜索技术选型 | MySQL 与独立搜索引擎成本差异大 | 依据消息量、中文检索和运维能力压测决定 |
|
||||
|
||||
## 15. 建议的第一批工程任务
|
||||
|
||||
建议立刻拆分以下可执行任务:
|
||||
|
||||
1. 建立“源码真实基线”文档,删除或标记未实现的 `x-im-*`、schema 119 和测试工具说明;
|
||||
2. 编写消息扩展协议 v1,包括类型、字段、大小、错误码和兼容矩阵;
|
||||
3. 设计并实现消息幂等,补并发和断线重试测试;
|
||||
4. 修复文件下载授权,补上传者、群成员、退群用户和泄露 URL 测试;
|
||||
5. 决定租户和数据库支持范围,冻结第一版 schema;
|
||||
6. 新建 `imapi`、`impolicy`、`imbiz`、`imstore`、`imnotify` 的最小骨架;
|
||||
7. 以“好友申请 → 接受 → 创建会话 → 发消息 → 拉黑”为第一条端到端业务链;
|
||||
8. 以“群申请 → 审批 → 禁言 → 公告”为第二条端到端业务链;
|
||||
9. 引入 Outbox 和审计日志,所有后续业务复用;
|
||||
10. 在 CI 中加入服务端单元测试、MySQL 集成测试和 WebSocket 协议回归测试。
|
||||
|
||||
## 16. 最终建议
|
||||
|
||||
本项目具备可继续演进的实时通信核心,最合理的路线不是重写聊天服务,而是把 Tinode 明确定位为通信内核,在它外部增加可事务、可检索、可审计的产品业务层,并只在发布、订阅、文件和通话等关键入口加入同步策略校验。
|
||||
|
||||
实施时应优先解决租户、幂等和文件授权,再做好友与群业务。企业工作台与通信内核保持模块边界,通过 HTTP API 管理业务状态、通过 Outbox 和系统 Topic 可靠通知。这样既能复用现有 Topic/ACL/Push/WebRTC 能力,也能避免把大量办公和审批逻辑耦合进消息路由主链路。
|
||||
@@ -1,659 +0,0 @@
|
||||
# IM 服务端多租户第 5 至 12 章代码改造与测试方案
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文是《IM-服务端多租户改造方案》第 5 章至第 12 章的代码实施方案,覆盖租户识别与认证、Session 与 Topic、Store 与 Adapter、MySQL 数据访问、缓存与集群、文件、搜索,以及 Push、插件和外部服务。
|
||||
|
||||
本文只规划代码改造和测试,不包含数据库建表、数据迁移、历史数据回填、双写、灰度迁移或旧 Token 兼容。实施前提是本地 MySQL 已经更新为最终多租户表结构,所有业务表的 `tenant_id`、复合索引和复合外键均已就绪。
|
||||
|
||||
## 2. 实施范围与原则
|
||||
|
||||
### 2.1 本期范围
|
||||
|
||||
- WebSocket 握手解析企业码并固定 Session TenantID;
|
||||
- 注册、登录、Token、验证码、REST 认证和密码找回租户化;
|
||||
- User、Topic、Subscription、Message、Device、File、Cache 的 Store 与 MySQL 查询租户化;
|
||||
- Hub、本地缓存、Session Store 和集群路由键租户化;
|
||||
- 文件上传、下载、关联、删除和对象存储路径租户化;
|
||||
- 用户、Topic、Tags、消息搜索和组织数据租户化;
|
||||
- Push、Plugin Firehose 及外部服务调用携带可信 TenantID;
|
||||
- 单元测试、MySQL 集成测试、双租户隔离测试和集群测试。
|
||||
|
||||
### 2.2 明确不做
|
||||
|
||||
- 不实现 `tenant_ticket`,首次 `hi` 继续直接提交公开企业码;
|
||||
- 不兼容无 TenantID 的旧 Token;
|
||||
- 不设置默认租户;
|
||||
- 不提供无租户业务 Store;
|
||||
- 不处理历史数据;
|
||||
- 本期产品运行时只支持 MySQL。MongoDB、PostgreSQL 和 RethinkDB 不得以未隔离模式启动。
|
||||
|
||||
### 2.3 核心安全不变量
|
||||
|
||||
以下规则应通过类型、统一入口和测试共同保证:
|
||||
|
||||
```text
|
||||
tenant_id 只能由服务端根据 hi.tenant、已签名 Token 或已认证 Session 得到
|
||||
已认证 Session 的 tenant_id != 0、uid != 0
|
||||
认证结果 tenant_id == Session tenant_id
|
||||
业务对象 tenant_id == Session tenant_id
|
||||
内部消息、缓存键、集群路由和外部调用均携带同一个 tenant_id
|
||||
任何跨租户访问统一返回 NotFound/Denied,不泄露目标对象是否存在
|
||||
```
|
||||
|
||||
禁止从 Topic Tags、请求 Body、URL Query 或普通 Header 中读取客户端自报的内部 `tenant_id` 作为授权依据。
|
||||
|
||||
## 3. 当前实现基线
|
||||
|
||||
| 能力 | 当前状态 | 本方案处理 |
|
||||
| --- | --- | --- |
|
||||
| `TenantID`、租户实体和状态 | 已实现 | 保留并补充通用 TenantKey 类型 |
|
||||
| `hi.tenant` 企业码 | 已实现 | 保留;补充后续链路测试 |
|
||||
| Session 固定 TenantID | 已实现 | 补充认证后用户归属校验 |
|
||||
| MySQL `TenantGetByCode` | 已实现 | 保留 |
|
||||
| `ClientComMessage.TenantID` | 已实现 | 扩展到服务端消息、集群和后台任务 |
|
||||
| protobuf `ClientHi.tenant` | 已实现 | 补充内部 TenantID 字段或 RPC 元数据 |
|
||||
| `AuthContext`、`auth.Rec.TenantID` | 未实现 | 第 5 章实施 |
|
||||
| Token 携带 TenantID | 未实现 | 升级为 Token v2,拒绝旧格式 |
|
||||
| Store/Adapter 业务接口租户化 | 未实现 | 第 7 章实施 |
|
||||
| MySQL 业务 SQL 带租户条件 | 未实现 | 第 8 章实施 |
|
||||
| Hub/Cluster/Cache 路由键租户化 | 未实现 | 第 9 章实施 |
|
||||
| 文件、搜索、Push、插件租户化 | 未实现 | 第 10 至 12 章实施 |
|
||||
|
||||
## 4. 总体代码结构
|
||||
|
||||
### 4.1 租户作用域类型
|
||||
|
||||
在 `server/store/types` 增加统一键类型,所有内部路由和缓存只通过这些类型生成 Key:
|
||||
|
||||
```go
|
||||
type TopicKey struct {
|
||||
TenantID TenantID
|
||||
Topic string
|
||||
}
|
||||
|
||||
func (k TopicKey) RoutingKey() (string, error)
|
||||
|
||||
type UserKey struct {
|
||||
TenantID TenantID
|
||||
UID Uid
|
||||
}
|
||||
|
||||
func (k UserKey) CacheKey() (string, error)
|
||||
```
|
||||
|
||||
`TenantID == ZeroTenantID`、Topic 为空或 UID 为零时生成 Key 必须报错。业务代码不得自行拼接 `tenantID + ":" + topic`。
|
||||
|
||||
### 4.2 租户作用域 Store
|
||||
|
||||
推荐在 `server/store/store.go` 增加:
|
||||
|
||||
```go
|
||||
type TenantStore struct {
|
||||
TenantID types.TenantID
|
||||
Users TenantUsersStore
|
||||
Topics TenantTopicsStore
|
||||
Subs TenantSubsStore
|
||||
Messages TenantMessagesStore
|
||||
Devices TenantDevicesStore
|
||||
Files TenantFilesStore
|
||||
PCache TenantCacheStore
|
||||
}
|
||||
|
||||
func ForTenant(tenantID types.TenantID) (*TenantStore, error)
|
||||
```
|
||||
|
||||
Session、Topic、HTTP Handler 和后台任务先取得 `TenantStore`,再访问业务数据。`ForTenant(0)` 必须返回错误。租户注册表和平台管理操作继续使用独立的 `Tenants`/`PlatformStore`,不能混入业务 Store。
|
||||
|
||||
底层 MySQL Adapter 的业务方法显式接收 `tenantID`。为了避免未完成的数据库 Adapter 继续工作,启动时检查配置的 Adapter 是否完整实现租户能力;非 MySQL Adapter 返回 `ErrUnsupportedTenant` 并终止启动。
|
||||
|
||||
### 4.3 TenantID 的传播方向
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
HI["hi.tenant 企业码"] --> S["Session.tenantID"]
|
||||
S --> A["AuthContext"]
|
||||
A --> R["auth.Rec.TenantID"]
|
||||
S --> C["Client/Server 内部消息"]
|
||||
C --> T["TopicKey / UserKey"]
|
||||
T --> DB["TenantStore / MySQL"]
|
||||
T --> CL["Cluster RPC"]
|
||||
C --> F["File / Search / Push / Plugin"]
|
||||
```
|
||||
|
||||
传播只允许从左向右。认证器、插件、数据库结果和外部回调只能验证 TenantID,不能替换 Session 已绑定的 TenantID。
|
||||
|
||||
## 5. 第 5 章:租户识别与认证流程
|
||||
|
||||
### 5.1 修改目标
|
||||
|
||||
已完成的 `hi.tenant` 解析继续作为连接租户入口。本章补齐认证上下文、认证记录、Token v2、凭证查询和管理员边界。
|
||||
|
||||
### 5.2 代码修改
|
||||
|
||||
#### 5.2.1 认证接口
|
||||
|
||||
修改 `server/auth/auth.go`:
|
||||
|
||||
```go
|
||||
type AuthContext struct {
|
||||
TenantID types.TenantID
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
type Rec struct {
|
||||
TenantID types.TenantID `json:"tenant_id,omitempty"`
|
||||
// 原有字段保持不变
|
||||
}
|
||||
```
|
||||
|
||||
将 `AuthHandler` 的以下方法改为接收 `AuthContext` 或 TenantID:
|
||||
|
||||
```go
|
||||
AddRecord(ctx AuthContext, rec *Rec, secret []byte)
|
||||
UpdateRecord(ctx AuthContext, rec *Rec, secret []byte)
|
||||
Authenticate(ctx AuthContext, secret []byte)
|
||||
IsUnique(ctx AuthContext, secret []byte)
|
||||
DelRecords(tenantID types.TenantID, uid types.Uid)
|
||||
GetResetParams(tenantID types.TenantID, uid types.Uid)
|
||||
```
|
||||
|
||||
同步修改:
|
||||
|
||||
- `server/auth/basic/auth_basic.go`;
|
||||
- `server/auth/code/auth_code.go`;
|
||||
- `server/auth/rest/auth_rest.go`;
|
||||
- `server/auth/token/auth_token.go`;
|
||||
- `server/auth/anon/auth_anon.go`;
|
||||
- `server/auth/mock_auth`;
|
||||
- `server/session.go` 中注册、登录、临时认证和凭证校验调用点;
|
||||
- HTTP 文件认证调用点。
|
||||
|
||||
所有认证器入口先校验 `ctx.TenantID != 0`。认证结果没有 TenantID,或 `rec.TenantID != ctx.TenantID`,统一返回认证失败并记录安全日志。
|
||||
|
||||
#### 5.2.2 Basic、Code、REST 认证
|
||||
|
||||
- Basic 登录名按 `(tenant_id, scheme, unique)` 查询;
|
||||
- 注册时唯一性检查只在当前租户执行;
|
||||
- Code 认证的验证码记录、次数和过期时间按租户隔离;
|
||||
- REST 认证请求携带服务端签名的 TenantID,响应必须原样返回 TenantID;
|
||||
- 密码找回先从 Session/签名上下文得到 TenantID,再查询 Credential,禁止全库按手机号或邮箱查找。
|
||||
|
||||
#### 5.2.3 Token v2
|
||||
|
||||
修改 `server/auth/token/auth_token.go`。当前 Token 是 50 字节且没有版本和 TenantID,替换为明确版本的 v2 布局:
|
||||
|
||||
```text
|
||||
[1:version][8:tenant_id][8:uid][4:expires][2:auth_level]
|
||||
[2:serial][2:features][32:HMAC-SHA256]
|
||||
```
|
||||
|
||||
实施规则:
|
||||
|
||||
- `version` 固定为 2;
|
||||
- `tenant_id` 和其余 Token 字段一起参与 HMAC;
|
||||
- `GenSecret` 要求 `rec.TenantID != 0`;
|
||||
- `Authenticate` 校验版本、精确长度、签名、租户、过期时间和 serial;
|
||||
- v1/当前 50 字节 Token 直接返回 `ErrMalformed` 或 `ErrFailed`;
|
||||
- Token TenantID 与 `AuthContext.TenantID` 不一致时返回认证失败;
|
||||
- 认证成功返回包含 TenantID 的 `auth.Rec`。
|
||||
|
||||
本项目没有历史数据,因此不增加双版本解析和兼容开关。
|
||||
|
||||
#### 5.2.4 用户归属和管理员
|
||||
|
||||
登录取得 UID 后,使用当前 `TenantStore.Users.Get(uid)` 再确认 User 属于相同租户,然后才写入 `Session.uid`。Tenant Admin 角色通过租户内角色/权限数据判断,不能用全局 `AuthLevelRoot` 代替。普通 WebSocket 请求中的 `AsUser` 必须同时验证角色和目标 User 的 TenantID。
|
||||
|
||||
### 5.3 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T5-HI-001 | 单元 | 使用有效且 active 的 `acme` 执行首次 `hi` | Session 固定 Tenant A,响应只含 code/name |
|
||||
| T5-HI-002 | 单元 | `hi` 不传 tenant、传空白或超过 64 字符 | 握手失败,Session 仍未绑定 |
|
||||
| T5-HI-003 | 单元 | 使用不存在或 suspended 租户 | 分别返回 NotFound/Denied,不绑定 Session |
|
||||
| T5-HI-004 | 单元 | 已绑定 A 的 Session 再传 B | 拒绝且 Session 仍为 A |
|
||||
| T5-AUTH-001 | MySQL 集成 | A、B 分别创建相同登录名 `alice` | 两条记录均成功,分别登录到各自用户 |
|
||||
| T5-AUTH-002 | MySQL 集成 | A 使用 B 的 `alice` 密码登录 | 认证失败,不泄露 B 用户存在性 |
|
||||
| T5-AUTH-003 | 单元 | Authenticator 返回 B 的 `auth.Rec` 给 A Session | 登录失败并产生安全日志 |
|
||||
| T5-AUTH-004 | 单元 | 为 A 用户生成 Token v2,再在 A Session 登录 | 成功,TenantID/UID 完整恢复 |
|
||||
| T5-AUTH-005 | 单元 | A Token 在 B Session 使用 | 认证失败,B Session 不写入 UID |
|
||||
| T5-AUTH-006 | 单元 | 修改 Token 中 TenantID 或版本字节 | HMAC/版本校验失败 |
|
||||
| T5-AUTH-007 | 单元 | 使用当前 50 字节旧 Token | 明确拒绝,不进入兼容分支 |
|
||||
| T5-AUTH-008 | MySQL 集成 | A、B 使用相同手机号发起密码找回 | 只命中当前租户用户和验证码 |
|
||||
| T5-AUTH-009 | 权限 | A 的 tenant_admin 代操作 B 用户 | 拒绝;平台管理 API 不受普通 WS 入口影响 |
|
||||
|
||||
## 6. 第 6 章:Session、消息和 Topic 改造
|
||||
|
||||
### 6.1 修改目标
|
||||
|
||||
TenantID 从 Session 贯穿到内部双向消息、Topic 生命周期和所有 P2P/群聊权限检查。客户端 Topic 名和对外协议保持不变。
|
||||
|
||||
### 6.2 代码修改
|
||||
|
||||
#### 6.2.1 内部消息和 Session
|
||||
|
||||
- 保留 `Session.tenantID` 和 `ClientComMessage.TenantID`;
|
||||
- 给 `ServerComMessage` 增加不序列化给普通客户端的 `TenantID`;
|
||||
- Session 收到每条客户端消息时覆盖内部 TenantID,忽略反序列化对象中可能存在的值;
|
||||
- 所有 Topic、Hub、Cluster 和后台生成消息必须显式填写 TenantID;
|
||||
- 登录成功前验证 `User.tenant_id == Session.tenantID`;
|
||||
- PROXY、MULTIPLEX Session 同样必须有 TenantID。
|
||||
|
||||
#### 6.2.2 Topic 对象和路由键
|
||||
|
||||
修改 `server/topic.go`、`server/hub.go`、`server/session.go` 及相关辅助类型:
|
||||
|
||||
- `Topic` 增加初始化后不可变的 `tenantID`;
|
||||
- Hub `topicGet/topicPut/topicDel` 参数从裸字符串改为 `types.TopicKey`;
|
||||
- Session 订阅表、Topic Proxy、Multiplex Session 使用 `RoutingKey()`;
|
||||
- 调试输出可显示 TenantID 和外部 Topic 名,但不能把内部前缀回传客户端;
|
||||
- 数据库中的 Topic 名仍保存原始名称,不保存 `tenantID:` 前缀。
|
||||
|
||||
#### 6.2.3 特殊 Topic、P2P 和群聊
|
||||
|
||||
- `sys` 按 `(tenant_id, "sys")` 懒加载,不再创建全局唯一实例;
|
||||
- `me`、`fnd`、`slf` 使用 Session TenantStore 加载;
|
||||
- 创建 P2P 前加载双方 User,并验证双方与 Session 同租户;
|
||||
- 创建群、邀请、审批、踢人、转让群主、发送系统通知时验证 Topic、操作者和目标成员同租户;
|
||||
- Subscription、Outbox、系统消息和附件关联全部沿用 Topic TenantID。
|
||||
|
||||
### 6.3 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T6-MSG-001 | 单元 | A Session 发送普通消息 | `ClientComMessage.TenantID` 被服务端写为 A |
|
||||
| T6-MSG-002 | 单元 | 构造内部消息自带 B TenantID 后交给 A Session | 值被覆盖或请求被拒绝,不能进入 B |
|
||||
| T6-MSG-003 | 单元 | Topic 产生服务端响应/通知 | `ServerComMessage.TenantID` 等于 Topic TenantID |
|
||||
| T6-TOPIC-001 | 单元 | A、B 同时加载裸名称 `sys` | Hub 中存在两个不同 Topic 实例 |
|
||||
| T6-TOPIC-002 | 单元 | A、B 使用相同 Group Topic 名 | 内部 RoutingKey 不同,对外名称不变 |
|
||||
| T6-P2P-001 | 集成 | A 用户邀请 A 用户创建 P2P | 创建成功,Topic/Subs 均为 A |
|
||||
| T6-P2P-002 | 集成 | A 用户邀请 B 用户创建 P2P | 返回通用 NotFound/Denied,不产生 Topic/Subs |
|
||||
| T6-GRP-001 | 集成 | A 管理员邀请 B 用户加入 A 群 | 拒绝且没有部分写入 |
|
||||
| T6-GRP-002 | 集成 | A 用户用已知 B Topic 名订阅、读、写、删 | 所有操作均失败,B 数据不变化 |
|
||||
| T6-SYS-001 | 集成 | A 发布系统通知 | 只有 A 的 `sys` 订阅者收到 |
|
||||
|
||||
## 7. 第 7 章:Store 与 Adapter 改造
|
||||
|
||||
### 7.1 修改目标
|
||||
|
||||
消除业务代码中的无租户持久化入口,使漏传 TenantID 在编译期或统一 Store 入口立即失败,而不是依赖开发者记得追加 SQL 条件。
|
||||
|
||||
### 7.2 代码修改
|
||||
|
||||
#### 7.2.1 Store 接口
|
||||
|
||||
修改 `server/store/store.go`,为以下域建立 TenantStore Mapper:
|
||||
|
||||
- Users:Create/Get/GetAll/Update/Delete/State/Tags/Topics/Subs/Unread;
|
||||
- Auth:Add/Get/Update/Delete/Unique lookup;
|
||||
- Credentials:Upsert/Get/Confirm/Fail/Delete/Reset lookup;
|
||||
- Topics:Create/Get/Share/Delete/Update/Owner/Find;
|
||||
- Subscriptions:Create/Get/Update/Delete/按 User 或 Topic 查询;
|
||||
- Messages:Save/Get/Delete/GetDeleted/Topic SeqId;
|
||||
- Devices:Upsert/Get/Delete/Push Channel;
|
||||
- Files:Start/Finish/Get/Link/DeleteUnused/Access;
|
||||
- Persistent Cache:Get/Upsert/Delete/Expire。
|
||||
|
||||
现有 `store.Users`、`store.Topics` 等全局 Mapper 不再供 Session/Topic/Handler 使用。完成调用点替换后删除或限制为测试期私有入口,避免新代码绕过 TenantStore。
|
||||
|
||||
#### 7.2.2 Adapter 接口
|
||||
|
||||
修改 `server/db/adapter.go`,所有租户业务方法显式接收 `types.TenantID`,例如:
|
||||
|
||||
```go
|
||||
UserGet(tenantID types.TenantID, uid types.Uid)
|
||||
AuthGetUniqueRecord(tenantID types.TenantID, unique string)
|
||||
TopicGet(tenantID types.TenantID, topic string)
|
||||
SubscriptionGet(tenantID types.TenantID, topic string, uid types.Uid, keepDeleted bool)
|
||||
MessageGetAll(tenantID types.TenantID, topic string, uid types.Uid, opts *types.QueryOpt)
|
||||
FileGet(tenantID types.TenantID, fileID string)
|
||||
```
|
||||
|
||||
数据对象 `User`、`Topic`、`Subscription`、`Message`、`DelMessage`、`Credential`、`DeviceDef` 和 `FileDef` 增加内部 TenantID。Create/Save 时同时校验“方法参数 TenantID、父对象 TenantID、待写对象 TenantID”一致。
|
||||
|
||||
#### 7.2.3 非 MySQL Adapter
|
||||
|
||||
公共接口变化会影响 `server/db/postgres`、`server/db/mongodb`、`server/db/rethinkdb` 和 Mock。处理方式:
|
||||
|
||||
- Mock 完整实现租户参数,用于单元测试;
|
||||
- MySQL 完整实现业务逻辑;
|
||||
- 其他 Adapter 实现相同签名但返回 `ErrUnsupportedTenant`;
|
||||
- 服务启动阶段检测并拒绝使用不支持租户业务接口的 Adapter;
|
||||
- 不允许退回旧的无租户查询。
|
||||
|
||||
### 7.3 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T7-STORE-001 | 单元 | 调用 `store.ForTenant(0)` | 返回错误,不创建 Mapper |
|
||||
| T7-STORE-002 | 单元 | A TenantStore 查询 B UID | 返回 NotFound/Denied |
|
||||
| T7-STORE-003 | 单元 | Create 参数为 A、对象 TenantID 为 B | 写入前失败,Adapter 不被调用 |
|
||||
| T7-STORE-004 | 单元 | Mock 断言 Session 所有 Store 调用 | 每次调用都收到 Session TenantID |
|
||||
| T7-STORE-005 | 构建 | 搜索生产代码中的全局 `store.Users/Topics/...` | Session/Topic/Handler 路径不存在绕过入口 |
|
||||
| T7-ADP-001 | 启动 | 配置 PostgreSQL/MongoDB/RethinkDB | 启动失败并明确提示不支持多租户 |
|
||||
| T7-ADP-002 | 单元 | Adapter 收到零 TenantID | 统一返回 `ErrInvalidTenant`,不执行 SQL |
|
||||
|
||||
## 8. 第 8 章:MySQL 代码改造
|
||||
|
||||
### 8.1 前提
|
||||
|
||||
本地数据库已经更新,本章不执行任何 schema 修改。代码直接以最终多租户表结构为准,测试负责验证 SQL 使用 `tenant_id`、唯一约束和复合外键符合预期。
|
||||
|
||||
### 8.2 代码修改
|
||||
|
||||
修改 `server/db/mysql/adapter.go` 中所有业务 SQL:
|
||||
|
||||
- `SELECT` 必须包含 `WHERE tenant_id = ?`;
|
||||
- `UPDATE`、`DELETE` 必须同时包含 TenantID 和业务主键;
|
||||
- `INSERT` 明确写入 `tenant_id`,不能依赖数据库默认值;
|
||||
- JOIN 同时关联父子表 TenantID,例如 `child.tenant_id = parent.tenant_id`;
|
||||
- 唯一查询按复合业务键执行,如 `(tenant_id, uname)`;
|
||||
- 聚合、未读数、序号、清理任务和批处理同样按租户过滤;
|
||||
- 受影响行数为 0 时直接返回 NotFound/Denied,不再发起无租户探测查询;
|
||||
- 租户级垃圾清理采用 `tenantID + limit`,由平台调度器遍历 active 租户。
|
||||
|
||||
建议按域逐段改造并提交,每段同时更新对应 MySQL 测试:
|
||||
|
||||
1. User、Auth、Credential;
|
||||
2. Topic、Subscription、Tags;
|
||||
3. Message、DelLog、未读数;
|
||||
4. Device;
|
||||
5. FileUpload、FileMessageLink;
|
||||
6. Persistent Cache。
|
||||
|
||||
### 8.3 SQL 审计要求
|
||||
|
||||
新增测试辅助函数或代码审查清单,对 `adapter.go` 中业务 SQL 做静态检查。以下情况必须阻止合并:
|
||||
|
||||
- 业务表查询完全没有 `tenant_id`;
|
||||
- JOIN 只按 ID 关联,没有 TenantID;
|
||||
- Update/Delete 只按全局 ID;
|
||||
- `IN (...)` 批量查询没有 TenantID 前置条件;
|
||||
- 后台清理一次扫描全部租户业务数据;
|
||||
- 为判断“其他租户是否存在该对象”而执行无租户查询。
|
||||
|
||||
### 8.4 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T8-DB-001 | MySQL 集成 | A、B 插入相同 `auth.uname` | 均成功;同租户重复失败 |
|
||||
| T8-DB-002 | MySQL 集成 | A、B 插入相同 Topic name/tag | 均成功;查询只返回当前租户 |
|
||||
| T8-DB-003 | MySQL 集成 | A 查询 B 的 UID、Topic、Message、File | 均返回空/NotFound |
|
||||
| T8-DB-004 | MySQL 集成 | A 更新/删除 B 对象的业务 ID | 影响行数为 0,B 数据不变 |
|
||||
| T8-DB-005 | MySQL 集成 | 尝试写入 A Subscription 关联 B Topic/User | 代码校验或复合外键拒绝,事务回滚 |
|
||||
| T8-DB-006 | MySQL 集成 | A 消息关联 B 文件 | 代码校验或复合外键拒绝 |
|
||||
| T8-DB-007 | MySQL 集成 | A、B 分别统计未读数和设备数 | 结果只包含本租户数据 |
|
||||
| T8-DB-008 | MySQL 集成 | A 的批量删除、过期和垃圾清理 | B 的 Message/File/Cache 不变化 |
|
||||
| T8-DB-009 | 静态审计 | 扫描全部业务 SQL | 无缺失 TenantID 的读写和 JOIN |
|
||||
|
||||
## 9. 第 9 章:缓存与集群改造
|
||||
|
||||
### 9.1 代码修改
|
||||
|
||||
#### 9.1.1 Hub 与 User Cache
|
||||
|
||||
修改 `server/hub.go`:
|
||||
|
||||
- `topicGet/topicPut/topicDel` 使用 `TopicKey.RoutingKey()`;
|
||||
- 放入 Hub 前验证请求、Topic 对象和数据库行 TenantID 一致;
|
||||
- `topicUnreg`、rehash、stats 和 debug 结构增加 TenantID。
|
||||
|
||||
修改 User Cache、未读数、Pending Push 和 EvictUser 相关代码:
|
||||
|
||||
- 使用 `UserKey{TenantID, UID}`;
|
||||
- `EvictUser(tenantID, uid, skipSID)`;
|
||||
- 用户状态、设备和未读数更新都携带 TenantID。
|
||||
|
||||
Session ID 保持全局随机,但通过 SID 取 Session 后,使用方必须读取并验证 Session TenantID,不能再只取 UID。
|
||||
|
||||
#### 9.1.2 集群
|
||||
|
||||
修改 `server/cluster.go`:
|
||||
|
||||
- `ClusterSess`、`ClusterSessUpdate`、`ClusterReq`、`ClusterRoute`、`ClusterResp` 增加 TenantID;
|
||||
- `nodeForTopic`、`isRemoteTopic` 接收 `TopicKey` 并按 RoutingKey 做一致性哈希;
|
||||
- Proxy/Master 两端重算 RoutingKey,并验证 RPC、Session、Client/Server Message TenantID 一致;
|
||||
- User Cache RPC 和 Topic rehash 携带 TenantID;
|
||||
- Cluster 协议版本增加多租户能力位,旧节点拒绝加入。
|
||||
|
||||
#### 9.1.3 持久化缓存
|
||||
|
||||
增加统一 `TenantCacheKey(tenantID, namespace, key)`。租户配置、验证码、未读数和业务临时状态均使用该函数。平台级 schema/version 缓存使用独立 Platform Cache 接口。
|
||||
|
||||
### 9.2 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T9-HUB-001 | 单元 | A、B Put/Get 相同 Topic 名 | 分别命中各自对象,无覆盖 |
|
||||
| T9-HUB-002 | 单元 | 将 B Topic 放入 A TopicKey | 拒绝并记录安全告警 |
|
||||
| T9-USER-001 | 单元 | Evict A 的同值 UID | 只断开 A Session,B Session 保持 |
|
||||
| T9-CACHE-001 | MySQL 集成 | A、B 写同 namespace/key | 值独立;过期 A 不删除 B |
|
||||
| T9-CL-001 | 单元 | A、B 相同 Topic 计算节点 | 使用不同 RoutingKey;结果按各自 Key 稳定 |
|
||||
| T9-CL-002 | 集群集成 | RPC TenantID 与消息 TenantID 不一致 | 接收端拒绝,不创建 Proxy Topic |
|
||||
| T9-CL-003 | 集群集成 | A Session 伪造 B RcptTo | Master 重算路由后拒绝 |
|
||||
| T9-CL-004 | 集群集成 | 节点重哈希 | A、B 同名 Topic 均完整迁移且不合并 |
|
||||
| T9-CL-005 | 启动 | 多租户节点与旧协议节点组网 | 握手失败,不能形成混合版本集群 |
|
||||
|
||||
## 10. 第 10 章:文件与对象存储改造
|
||||
|
||||
### 10.1 代码修改
|
||||
|
||||
在 `server/hdl_files.go` 增加:
|
||||
|
||||
```go
|
||||
type RequestPrincipal struct {
|
||||
TenantID types.TenantID
|
||||
UID types.Uid
|
||||
Roles []string
|
||||
}
|
||||
```
|
||||
|
||||
`authFileRequest` 改为返回 `RequestPrincipal`:
|
||||
|
||||
- Token 路径从 Token v2 得到 TenantID;
|
||||
- SID 路径从 Session 得到 TenantID;
|
||||
- 两条路径都要求 TenantID 和 UID 非零;
|
||||
- API Key 只做应用准入,不推导 TenantID。
|
||||
|
||||
Store/Adapter 增加 `FileCanAccess(tenantID, uid, fileID, action)`,并改造上传、下载、断点续传、关联、删除和垃圾回收:
|
||||
|
||||
- 先使用 TenantID 加载 File;
|
||||
- 未完成上传只允许同租户上传者访问;
|
||||
- 已关联消息时校验同租户 Subscription 和 Read 权限;
|
||||
- 删除验证上传者、群主或租户管理员角色;
|
||||
- 转发时验证源文件、源消息和目标 Topic 同租户;
|
||||
- 签名 URL 覆盖 TenantID、FileID、Action 和 Expires;
|
||||
- 新对象路径使用 `tenants/{tenant_id}/yyyy/mm/{file_id}`;
|
||||
- Location 仅供服务端使用,不直接返回客户端。
|
||||
|
||||
### 10.2 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T10-AUTH-001 | 单元 | Token 和 SID 分别认证同一用户 | 得到相同 TenantID/UID Principal |
|
||||
| T10-AUTH-002 | 单元 | 无租户旧 Token、无效 SID 或零 UID | 认证失败,不进入 File Store |
|
||||
| T10-UP-001 | 集成 | A 上传文件 | DB TenantID 为 A,路径以 `tenants/A/` 开头 |
|
||||
| T10-DL-001 | 集成 | A 用户下载 A 群内有权限文件 | 成功 |
|
||||
| T10-DL-002 | 集成 | A 使用已知 B FileID 下载 | 通用 NotFound/Denied,不返回元数据/Location |
|
||||
| T10-LINK-001 | 集成 | A 消息关联 B FileID | 失败,文件 use_count 不变化 |
|
||||
| T10-FWD-001 | 集成 | A 转发 B 文件到 A Topic | 失败且不创建消息 |
|
||||
| T10-SIGN-001 | 单元 | 修改签名 URL 中 TenantID/FileID/Expires | 验签失败 |
|
||||
| T10-GC-001 | MySQL 集成 | 清理 A 未使用文件 | B 文件记录和对象不被删除 |
|
||||
|
||||
## 11. 第 11 章:搜索、Tags 与组织数据
|
||||
|
||||
### 11.1 代码修改
|
||||
|
||||
修改 `server/store/store.go`、`server/db/adapter.go` 和 `server/db/mysql/adapter.go`:
|
||||
|
||||
- `Find`、`FindOne`、User/Topic Tag 增删改查显式接收 TenantID;
|
||||
- 精确账号、手机号、邮箱查询只在当前租户执行;
|
||||
- 结果对象再次验证 TenantID 后才返回;
|
||||
- 无结果和命中其他租户统一表现为空/NotFound。
|
||||
|
||||
消息搜索索引文档必须包含 TenantID,文档键或路由键使用 `tenant_id + topic + seq_id`。查询时先强制加 TenantID,再按当前用户在同租户 Topic 的 Read ACL 二次过滤。TenantID 来自认证上下文,不接受 Query 参数覆盖。
|
||||
|
||||
组织表 Repository 均以 TenantID 为首参数。新增/移动部门、设置负责人、添加成员时加载父部门和 User,验证它们属于同一租户。涉及多表写入时使用事务,任何归属不一致都整体回滚。
|
||||
|
||||
### 11.2 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T11-FIND-001 | MySQL 集成 | A、B 用户具有相同 `email:`/`tel:` Tag | 各租户只返回自己的用户 |
|
||||
| T11-FIND-002 | MySQL 集成 | A 搜索只有 B 存在的 Tag | 返回空结果,不提示跨租户命中 |
|
||||
| T11-FIND-003 | MySQL 集成 | A、B 使用相同 Topic Tag | 排名和结果集合互不影响 |
|
||||
| T11-MSG-001 | 搜索集成 | A、B 消息正文相同 | A 查询结果中不出现 B 文档 |
|
||||
| T11-MSG-002 | 搜索集成 | A 用户搜索 A 内无 Read 权限 Topic | ACL 二次过滤移除结果 |
|
||||
| T11-MSG-003 | 安全 | Query 参数提交 B TenantID | 参数被忽略或请求被拒绝,仍使用 Token TenantID |
|
||||
| T11-ORG-001 | MySQL 集成 | A 部门添加 A 用户 | 成功,组织关系为 A |
|
||||
| T11-ORG-002 | MySQL 集成 | A 部门添加 B 用户/负责人 | 失败,事务无部分写入 |
|
||||
| T11-ORG-003 | MySQL 集成 | A 部门设置 B 部门为 parent | 失败,部门树不变化 |
|
||||
|
||||
## 12. 第 12 章:Push、插件和外部服务
|
||||
|
||||
### 12.1 Push
|
||||
|
||||
修改 `server/push/push.go`:
|
||||
|
||||
```go
|
||||
type Receipt struct { TenantID types.TenantID /* ... */ }
|
||||
type ChannelReq struct { TenantID types.TenantID /* ... */ }
|
||||
type Payload struct { TenantID types.TenantID `json:"-"` /* ... */ }
|
||||
```
|
||||
|
||||
- Push 构造入口要求 TenantID 非零;
|
||||
- 设备按 `(tenant_id, uid)` 查询;
|
||||
- Channel 使用统一函数生成租户前缀,不能直接拼接客户端值;
|
||||
- Push Handler 根据 TenantID 选择模板、App 包和密钥引用;
|
||||
- 未读数使用 UserKey;
|
||||
- 日志和指标带 TenantID,但不得输出完整设备 Token。
|
||||
|
||||
### 12.2 Plugin Firehose
|
||||
|
||||
修改 `server/plugins.go` 和 protobuf:
|
||||
|
||||
- `pluginGenerateClientReq` 写入 Session TenantID;
|
||||
- Plugin 请求包含 TenantID 和可选租户策略版本;
|
||||
- `REPLACE` 响应反序列化后,服务端强制恢复原 TenantID;
|
||||
- 替换后的目标 Topic 重新执行同租户校验;
|
||||
- `RESPOND` 生成的服务端消息也强制使用原 TenantID;
|
||||
- 插件超时、失败开放/关闭、审计和指标按租户配置执行。
|
||||
|
||||
### 12.3 REST、短信、TURN 和对象存储
|
||||
|
||||
- 外部请求使用服务端签名的 TenantContext,至少覆盖 TenantID、请求用途、过期时间和 nonce;
|
||||
- 回调响应必须返回 TenantID/nonce,服务端验证与原请求一致;
|
||||
- 秘钥管理只保存租户配置中的 Secret 引用,业务 JSON 不保存明文 Secret;
|
||||
- 短信模板、Push 凭证、对象存储 Bucket/Prefix 和 TURN 配置按 TenantID 选择;
|
||||
- 租户没有配置时按明确策略失败,不得静默使用其他租户配置。
|
||||
|
||||
### 12.4 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T12-PUSH-001 | 单元 | A 消息生成 Push Receipt | Receipt/Payload/Channel 均为 A |
|
||||
| T12-PUSH-002 | 集成 | A、B 有同值 UID 和不同设备 | A Push 只读取并投递 A 设备 |
|
||||
| T12-PUSH-003 | 单元 | A、B 使用相同逻辑 Channel | 生成的物理 Channel 不碰撞 |
|
||||
| T12-PUSH-004 | 单元 | TenantID 为零或找不到租户凭证 | 明确失败,不回退到其他租户凭证 |
|
||||
| T12-PLG-001 | 单元 | A 请求发送到 Firehose | Plugin Request TenantID 为 A |
|
||||
| T12-PLG-002 | 安全 | Plugin `REPLACE` 返回 B TenantID | 服务端拒绝或恢复 A,并重新校验 Topic |
|
||||
| T12-PLG-003 | 安全 | Plugin `RESPOND` 返回 B TenantID | 响应不能进入 B 路由 |
|
||||
| T12-EXT-001 | 单元 | 篡改外部请求 TenantContext | 签名校验失败 |
|
||||
| T12-EXT-002 | 集成 | A 请求收到带 B TenantID/错误 nonce 的回调 | 回调拒绝,不更新任何业务状态 |
|
||||
| T12-EXT-003 | 配置 | A 未配置短信/TURN/对象存储 | 按 A 策略失败,不使用 B 或全局 Secret |
|
||||
|
||||
## 13. 实施顺序与提交边界
|
||||
|
||||
### 阶段 A:基础类型和认证
|
||||
|
||||
1. 增加 TopicKey、UserKey、AuthContext 和 `auth.Rec.TenantID`;
|
||||
2. 修改全部认证器和 Mock;
|
||||
3. 实现 Token v2;
|
||||
4. 完成第 5 章测试。
|
||||
|
||||
完成条件:注册、登录、Token 和密码找回全部绑定租户,旧 Token 明确拒绝。
|
||||
|
||||
### 阶段 B:Store、MySQL 与单机消息链路
|
||||
|
||||
1. 增加 TenantStore;
|
||||
2. 改造 Adapter 接口和 MySQL User/Auth/Credential;
|
||||
3. 改造 Topic/Subscription/Message/Device/File/Cache;
|
||||
4. 改造 Session、Topic 和 Hub 调用点;
|
||||
5. 完成第 6 至 8 章测试。
|
||||
|
||||
完成条件:单机模式下两个租户可使用同名登录名、Topic 和 Tag,所有交叉读写失败。
|
||||
|
||||
### 阶段 C:缓存、集群和文件
|
||||
|
||||
1. Hub/User/PCache Key 租户化;
|
||||
2. Cluster RPC 和一致性哈希租户化;
|
||||
3. 文件 Principal、ACL、路径和签名租户化;
|
||||
4. 完成第 9、10 章测试。
|
||||
|
||||
完成条件:双节点路由和文件链路不存在跨租户缓存命中或资源访问。
|
||||
|
||||
### 阶段 D:搜索、Push、插件和外部服务
|
||||
|
||||
1. Find/Tags/消息索引/组织 Repository;
|
||||
2. Push 数据结构、设备查询和凭证选择;
|
||||
3. Plugin protobuf 与 REPLACE/RESPOND 校验;
|
||||
4. 外部 TenantContext 签名与回调验证;
|
||||
5. 完成第 11、12 章测试。
|
||||
|
||||
完成条件:所有出站和异步链路均可按 TenantID 追踪,并通过反向篡改测试。
|
||||
|
||||
## 14. 测试环境与公共数据
|
||||
|
||||
### 14.1 测试租户
|
||||
|
||||
在专用测试库中准备以下数据,不使用开发人员的日常业务数据:
|
||||
|
||||
| 数据 | Tenant A | Tenant B |
|
||||
| --- | --- | --- |
|
||||
| tenant_id | 1001 | 1002 |
|
||||
| code | acme | beta |
|
||||
| state | active | active |
|
||||
| 登录名 | alice | alice |
|
||||
| 手机号 | 13800000000 | 13800000000 |
|
||||
| 逻辑 Topic | sys、grpShared | sys、grpShared |
|
||||
| Tag | email:alice@example.com | email:alice@example.com |
|
||||
| 文件 | file-shared-name | file-shared-name |
|
||||
|
||||
另外准备一个 `suspended` 租户用于握手和已签 Token 的停用测试。测试数据必须由测试事务或 fixture 创建并清理,不能依赖固定自增 ID。
|
||||
|
||||
### 14.2 测试层次
|
||||
|
||||
- 单元测试:Key、AuthContext、Token、Session、Hub、Cluster RPC、Plugin 和签名;
|
||||
- MySQL 集成测试:每个 Adapter 方法至少包含“同租户成功 + 跨租户失败”;
|
||||
- 服务集成测试:两个真实 WebSocket Session 完成握手、登录、建群、发消息和文件访问;
|
||||
- 双节点集群测试:同名 Topic 路由、Proxy、重哈希和节点版本检查;
|
||||
- 安全测试:篡改 TenantID、Token、RPC、Plugin 响应和外部回调;
|
||||
- 并发测试:A、B 同时创建同名业务键,验证唯一约束只在租户内生效。
|
||||
|
||||
## 15. 验证命令
|
||||
|
||||
具体数据库连接参数沿用仓库现有 MySQL 测试配置,不在文档中写死账号密码。实施过程中至少执行:
|
||||
|
||||
```bash
|
||||
go test ./server/auth/... ./server/store/... ./pbx
|
||||
go test ./server
|
||||
go test -tags mysql ./server/db/mysql/tests
|
||||
go test -race ./server ./server/auth/... ./server/store/...
|
||||
go build -tags mysql ./server/...
|
||||
```
|
||||
|
||||
如果仓库现有 MySQL 测试要求特定工作目录或配置文件,应沿用现有测试入口。集群、对象存储、Push 和外部服务使用本地 fake/mock,另在测试环境执行真实双节点和真实供应商沙箱验收。
|
||||
|
||||
## 16. 完成标准
|
||||
|
||||
- [ ] 第 5 至 12 章所有生产调用链都能追踪到可信 TenantID 来源;
|
||||
- [ ] 已认证 Session、User、Topic、消息和认证结果 TenantID 始终一致;
|
||||
- [ ] 业务 Store 不存在可被生产代码调用的无租户入口;
|
||||
- [ ] MySQL 所有业务读写、JOIN、批处理和清理均包含 TenantID;
|
||||
- [ ] Hub、User Cache、PCache 和 Cluster 路由均使用租户 Key;
|
||||
- [ ] Token、文件签名、Plugin 和外部上下文覆盖 TenantID 并通过篡改测试;
|
||||
- [ ] A/B 双租户同名数据测试全部通过;
|
||||
- [ ] 所有跨租户读、写、邀请、关联、删除和回调测试全部失败且不泄露对象存在性;
|
||||
- [ ] 非 MySQL Adapter 无法以未隔离模式启动;
|
||||
- [ ] 单元测试、MySQL 集成测试、race test 和 MySQL build 全部通过;
|
||||
- [ ] 日志、指标和安全审计含 TenantID,但不输出密码、Token、设备 Token 或租户 Secret。
|
||||
@@ -1,713 +0,0 @@
|
||||
-- IM product database changes summary, chapters 1-11.
|
||||
-- Target:
|
||||
-- public backend database: hotgo
|
||||
-- default tenant business database: tinode
|
||||
-- Notes:
|
||||
-- 1. Public tenant registry tables live in hotgo.
|
||||
-- 2. Tenant-scoped business tables live in each tenant database and do not include tenant_id.
|
||||
-- 3. This file is for review and manual execution. Existing legacy columns/indexes should be checked before ALTER/DROP.
|
||||
-- 4. Table definitions are aligned with server/db/mysql/adapter.go plus im_verification_codes used by chapter 1 verification APIs.
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS hotgo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Public database: tenant registry.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hotgo.im_tenant (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
tenant_desc VARCHAR(256),
|
||||
db_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
db_dsn VARCHAR(1024) NOT NULL DEFAULT '',
|
||||
state SMALLINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_tenant_code (code),
|
||||
INDEX idx_im_tenant_db_name (db_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hotgo.im_tenant_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_tenant_config_tenant_id (tenant_id),
|
||||
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY (tenant_id) REFERENCES hotgo.im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 1, tenant-aware login, verification, security.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_security (
|
||||
user_id BIGINT NOT NULL,
|
||||
auth_version INT NOT NULL DEFAULT 1,
|
||||
password_updated_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_verification_codes (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
scene VARCHAR(32) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL DEFAULT '',
|
||||
phone_hash CHAR(64) NOT NULL,
|
||||
phone_mask VARCHAR(32) NOT NULL,
|
||||
target_user BIGINT,
|
||||
code_hash CHAR(64) NOT NULL,
|
||||
verification_token_hash CHAR(64),
|
||||
state SMALLINT NOT NULL,
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
max_attempts INT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
resend_after DATETIME(3) NOT NULL,
|
||||
verified_at DATETIME(3),
|
||||
consumed_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_verification_challenge (challenge_id),
|
||||
INDEX idx_im_verification_scene_phone_state (scene, phone_hash, state, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_realname_verifications (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
name_cipher TEXT NOT NULL,
|
||||
id_type VARCHAR(32) NOT NULL,
|
||||
id_number_cipher TEXT NOT NULL,
|
||||
id_number_hash CHAR(64) NOT NULL,
|
||||
masked_name VARCHAR(64) NOT NULL,
|
||||
masked_id_number VARCHAR(64) NOT NULL,
|
||||
review_reason VARCHAR(256),
|
||||
verified_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_realname_submission (submission_id),
|
||||
INDEX idx_im_realname_user_latest (user_id, created_at),
|
||||
INDEX idx_im_realname_id_hash (id_number_hash)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_action_tokens (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
token_id VARCHAR(64) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
target VARCHAR(128) NOT NULL DEFAULT '',
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
state SMALLINT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
consumed_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_action_tokens_token (token_id),
|
||||
INDEX idx_im_action_tokens_user_action (user_id, action, target, state, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings (
|
||||
user_id BIGINT NOT NULL,
|
||||
purpose VARCHAR(64) NOT NULL,
|
||||
password_hash VARBINARY(255) NOT NULL,
|
||||
enabled TINYINT NOT NULL DEFAULT 0,
|
||||
policy_version INT NOT NULL DEFAULT 1,
|
||||
restrictions JSON,
|
||||
enabled_at DATETIME(3),
|
||||
failed_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id, purpose)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapters 2-7, notifications, messages, friends, files, favorites.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_system_notifications (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
event_id VARCHAR(64) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
summary VARCHAR(512) NOT NULL,
|
||||
action JSON,
|
||||
read_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_system_notifications_event (user_id, event_id),
|
||||
INDEX idx_im_system_notifications_inbox (user_id, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_message_revisions (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seqid INT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
actor BIGINT NOT NULL,
|
||||
head JSON,
|
||||
content JSON,
|
||||
reason VARCHAR(256) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_message_revisions_version (topic, seqid, version)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_friend_requests (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
applicant_id BIGINT NOT NULL,
|
||||
target_id BIGINT NOT NULL,
|
||||
message VARCHAR(200) NOT NULL DEFAULT '',
|
||||
source VARCHAR(32) NOT NULL DEFAULT '',
|
||||
source_topic VARCHAR(64) NOT NULL DEFAULT '',
|
||||
status VARCHAR(16) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
handled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_friend_requests_request (request_id),
|
||||
UNIQUE INDEX uk_im_friend_requests_idempotency (applicant_id, idempotency_key),
|
||||
INDEX idx_im_friend_requests_pair (applicant_id, target_id, status, created_at),
|
||||
INDEX idx_im_friend_requests_inbox (target_id, status, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_friendships (
|
||||
user_id BIGINT NOT NULL,
|
||||
peer_id BIGINT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
remark VARCHAR(64) NOT NULL DEFAULT '',
|
||||
topic CHAR(25) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id, peer_id),
|
||||
INDEX idx_im_friendships_peer (peer_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_blocks (
|
||||
blocker_id BIGINT NOT NULL,
|
||||
blocked_id BIGINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (blocker_id, blocked_id),
|
||||
INDEX idx_im_user_blocks_blocked (blocked_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_favorites (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
favorite_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
item_type VARCHAR(16) NOT NULL,
|
||||
source_topic CHAR(25) NOT NULL,
|
||||
source_seq INT NOT NULL,
|
||||
snapshot JSON,
|
||||
attachment_refs JSON,
|
||||
availability VARCHAR(16) NOT NULL DEFAULT 'available',
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
collected_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_favorites_favorite (favorite_id),
|
||||
UNIQUE INDEX uk_im_favorites_source (user_id, source_topic, source_seq),
|
||||
UNIQUE INDEX uk_im_favorites_idempotency (user_id, ((NULLIF(idempotency_key, '')))),
|
||||
INDEX idx_im_favorites_inbox (user_id, id DESC),
|
||||
INDEX idx_im_favorites_type (user_id, item_type, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- Existing Tinode tables must match the current schema for chapters 2-7:
|
||||
-- topics.aux, fileuploads.etag, and filemsglinks.topic/userid are required by current code.
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'client_msg_id') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'version') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_at') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN recalled_at DATETIME(3) AFTER version',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_by') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN recalled_by BIGINT AFTER recalled_at',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND INDEX_NAME = 'messages_sender_client_mid') = 0,
|
||||
'CREATE UNIQUE INDEX messages_sender_client_mid ON tinode.messages(`from`, client_msg_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 8, realtime call records.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_call_records (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
call_id VARCHAR(64) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seq_id INT NOT NULL,
|
||||
caller_id BIGINT NOT NULL,
|
||||
callee_id BIGINT NOT NULL,
|
||||
call_type VARCHAR(16) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
end_reason VARCHAR(64) NOT NULL DEFAULT '',
|
||||
duration_ms BIGINT NOT NULL DEFAULT 0,
|
||||
started_at DATETIME(3) NOT NULL,
|
||||
accepted_at DATETIME(3),
|
||||
finished_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_call_records_call (call_id),
|
||||
UNIQUE INDEX uk_im_call_records_topic_seq (topic, seq_id),
|
||||
INDEX idx_im_call_records_caller (caller_id, id DESC),
|
||||
INDEX idx_im_call_records_callee (callee_id, id DESC),
|
||||
INDEX idx_im_call_records_status (status, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 9, workbench and office collaboration.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_workbench_apps (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
icon_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
route VARCHAR(128) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
unread_key VARCHAR(64) NOT NULL DEFAULT '',
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
permission VARCHAR(64) NOT NULL DEFAULT 'enabled',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_workbench_apps_code (code),
|
||||
INDEX idx_im_workbench_apps_enabled_order (enabled, sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_org_departments (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
department_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
parent_id VARCHAR(64) NOT NULL DEFAULT 'root',
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_org_departments_department (department_id),
|
||||
INDEX idx_im_org_departments_parent (parent_id, state, sort_order, id),
|
||||
INDEX idx_im_org_departments_name (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_org_members (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
department_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
display_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
position VARCHAR(128) NOT NULL DEFAULT '',
|
||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
mobile VARCHAR(32) NOT NULL DEFAULT '',
|
||||
email VARCHAR(128) NOT NULL DEFAULT '',
|
||||
employment_state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
contactable TINYINT NOT NULL DEFAULT 1,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_org_members_department_user (department_id, user_id),
|
||||
INDEX idx_im_org_members_department (department_id, employment_state, sort_order, id),
|
||||
INDEX idx_im_org_members_user (user_id),
|
||||
INDEX idx_im_org_members_display_name (display_name),
|
||||
CONSTRAINT fk_im_org_members_department FOREIGN KEY (department_id) REFERENCES tinode.im_org_departments(department_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_meetings (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
meeting_id VARCHAR(64) NOT NULL,
|
||||
organizer_id BIGINT NOT NULL,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
start_at DATETIME(3) NOT NULL,
|
||||
end_at DATETIME(3) NOT NULL,
|
||||
location VARCHAR(256) NOT NULL DEFAULT '',
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'scheduled',
|
||||
reminder_minutes JSON,
|
||||
idempotency_key VARCHAR(128),
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
canceled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_meetings_meeting (meeting_id),
|
||||
UNIQUE INDEX uk_im_meetings_idempotency (organizer_id, idempotency_key),
|
||||
INDEX idx_im_meetings_organizer (organizer_id, start_at),
|
||||
INDEX idx_im_meetings_state_time (state, start_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_meeting_participants (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
meeting_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
response VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
response_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_meeting_participants_user (meeting_id, user_id),
|
||||
INDEX idx_im_meeting_participants_user_time (user_id, meeting_id),
|
||||
INDEX idx_im_meeting_participants_response (meeting_id, response),
|
||||
CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY (meeting_id) REFERENCES tinode.im_meetings(meeting_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_daily_reports (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
author_id BIGINT NOT NULL,
|
||||
report_date DATE NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
attachments JSON,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'draft',
|
||||
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_daily_reports_report (report_id),
|
||||
UNIQUE INDEX uk_im_daily_reports_author_date (author_id, report_date),
|
||||
INDEX idx_im_daily_reports_author_id (author_id, id DESC),
|
||||
INDEX idx_im_daily_reports_date_status (report_date, status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_monthly_reports (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
author_id BIGINT NOT NULL,
|
||||
report_month CHAR(7) NOT NULL,
|
||||
work_summary TEXT NOT NULL,
|
||||
next_plan TEXT NOT NULL,
|
||||
attachments JSON,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'draft',
|
||||
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_monthly_reports_report (report_id),
|
||||
UNIQUE INDEX uk_im_monthly_reports_author_month (author_id, report_month),
|
||||
INDEX idx_im_monthly_reports_author_id (author_id, id DESC),
|
||||
INDEX idx_im_monthly_reports_month_status (report_month, status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_mailbox_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
allow_anonymous TINYINT NOT NULL DEFAULT 1,
|
||||
required_fields JSON,
|
||||
max_length INT NOT NULL DEFAULT 5000,
|
||||
attachment_policy JSON,
|
||||
notice_title VARCHAR(128) NOT NULL DEFAULT '',
|
||||
notice_content TEXT NOT NULL,
|
||||
notice_updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_mailbox_submissions (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
sender_id BIGINT NOT NULL,
|
||||
subject VARCHAR(200) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
anonymous TINYINT NOT NULL DEFAULT 0,
|
||||
attachment_file_ids JSON,
|
||||
notice_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
submitted_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_mailbox_submissions_submission (submission_id),
|
||||
UNIQUE INDEX uk_im_mailbox_submissions_idempotency (sender_id, idempotency_key),
|
||||
INDEX idx_im_mailbox_submissions_sender (sender_id, id DESC),
|
||||
INDEX idx_im_mailbox_submissions_status (status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 10, profile, privacy, app release, push, youth mode.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_profiles (
|
||||
user_id BIGINT NOT NULL,
|
||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||
birth_date DATE,
|
||||
signature VARCHAR(256) NOT NULL DEFAULT '',
|
||||
gender VARCHAR(16) NOT NULL DEFAULT '',
|
||||
region VARCHAR(128) NOT NULL DEFAULT '',
|
||||
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings (
|
||||
user_id BIGINT NOT NULL,
|
||||
add_me_methods JSON,
|
||||
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||
read_receipts TINYINT NOT NULL DEFAULT 1,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_app_releases (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
platform VARCHAR(32) NOT NULL,
|
||||
channel VARCHAR(32) NOT NULL DEFAULT 'stable',
|
||||
latest_version VARCHAR(64) NOT NULL,
|
||||
min_supported_version VARCHAR(64) NOT NULL,
|
||||
update_type VARCHAR(16) NOT NULL DEFAULT 'none',
|
||||
release_notes TEXT NOT NULL,
|
||||
store_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
package_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
checksum VARCHAR(128) NOT NULL DEFAULT '',
|
||||
remind_after_seconds INT NOT NULL DEFAULT 0,
|
||||
published_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_app_releases_platform_channel (platform, channel)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens (
|
||||
device_id VARCHAR(128) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
provider VARCHAR(32) NOT NULL,
|
||||
token VARCHAR(512) NOT NULL,
|
||||
app_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
platform VARCHAR(32) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (device_id),
|
||||
UNIQUE INDEX uk_im_device_push_tokens_provider_token (provider, token),
|
||||
INDEX idx_im_device_push_tokens_user (user_id, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 11, risk challenge, abuse report, feedback.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_risk_challenges (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
level VARCHAR(16) NOT NULL,
|
||||
notice_code VARCHAR(64) NOT NULL,
|
||||
notice_params JSON,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
decision VARCHAR(16) NOT NULL DEFAULT '',
|
||||
action_token_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_risk_challenges_challenge (challenge_id),
|
||||
INDEX idx_im_risk_challenges_user_state (user_id, state, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_abuse_reasons (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
target_type VARCHAR(16) NOT NULL,
|
||||
reason_code VARCHAR(64) NOT NULL,
|
||||
label VARCHAR(128) NOT NULL,
|
||||
requires_description TINYINT NOT NULL DEFAULT 0,
|
||||
allows_evidence TINYINT NOT NULL DEFAULT 1,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_abuse_reasons_type_code (target_type, reason_code),
|
||||
INDEX idx_im_abuse_reasons_type_order (target_type, enabled, sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_abuse_reports (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
reporter_id BIGINT NOT NULL,
|
||||
target_type VARCHAR(16) NOT NULL,
|
||||
target_user_id BIGINT NOT NULL DEFAULT 0,
|
||||
target_topic VARCHAR(32) NOT NULL DEFAULT '',
|
||||
target_seq INT NOT NULL DEFAULT 0,
|
||||
reason_code VARCHAR(64) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
evidence_file_ids JSON,
|
||||
snapshot JSON,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_abuse_reports_report (report_id),
|
||||
UNIQUE INDEX uk_im_abuse_reports_idempotency (reporter_id, idempotency_key),
|
||||
INDEX idx_im_abuse_reports_target (target_type, target_topic, target_seq, target_user_id),
|
||||
INDEX idx_im_abuse_reports_reporter (reporter_id, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_feedback_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
categories JSON,
|
||||
max_length INT NOT NULL DEFAULT 1000,
|
||||
attachment_policy JSON,
|
||||
allow_contact TINYINT NOT NULL DEFAULT 1,
|
||||
diagnostics_notice TEXT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_feedback_submissions (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
feedback_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
category VARCHAR(64) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
contact VARCHAR(128) NOT NULL DEFAULT '',
|
||||
attachment_file_ids JSON,
|
||||
client_context JSON,
|
||||
include_diagnostics TINYINT NOT NULL DEFAULT 0,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_feedback_submissions_feedback (feedback_id),
|
||||
UNIQUE INDEX uk_im_feedback_submissions_idempotency (user_id, idempotency_key),
|
||||
INDEX idx_im_feedback_submissions_user (user_id, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Seed data.
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO hotgo.im_tenant(id, code, name, tenant_desc, db_name, db_dsn, state, created_by, updated_by)
|
||||
VALUES (1, 'default', 'Default Tenant', NULL, 'tinode', '', 1, 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
db_name = VALUES(db_name),
|
||||
state = VALUES(state),
|
||||
updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO hotgo.im_tenant_config(tenant_id, max_users, max_groups, max_group_members, created_by, updated_by)
|
||||
SELECT id, 0, 0, 0, 0, 0 FROM hotgo.im_tenant WHERE code = 'default'
|
||||
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_workbench_apps(code, name, route, sort_order, unread_key, enabled, permission)
|
||||
VALUES
|
||||
('organization', '组织架构', '/organization', 10, 'organization', 1, 'enabled'),
|
||||
('meetings', '会议通知', '/meetings', 20, 'meetings', 1, 'enabled'),
|
||||
('daily_reports', '日报', '/reports/daily', 30, 'daily_reports', 1, 'enabled'),
|
||||
('monthly_reports', '月报', '/reports/monthly', 40, 'monthly_reports', 1, 'enabled'),
|
||||
('executive_mailbox', '总经理信箱', '/executive-mailbox', 50, 'executive_mailbox', 1, 'enabled')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
route = VALUES(route),
|
||||
sort_order = VALUES(sort_order),
|
||||
unread_key = VALUES(unread_key),
|
||||
updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_mailbox_config(id, enabled, allow_anonymous, required_fields, max_length, attachment_policy, notice_title, notice_content)
|
||||
VALUES (1, 1, 1, '["subject","content"]', 5000, '{"max_files":5,"purpose":"executive_mailbox"}', '总经理信箱', '请如实填写诉求,匿名提交将按企业政策保护展示身份。')
|
||||
ON DUPLICATE KEY UPDATE updated_at = updated_at;
|
||||
|
||||
INSERT INTO tinode.im_app_releases(platform, channel, latest_version, min_supported_version, update_type, release_notes, remind_after_seconds)
|
||||
VALUES
|
||||
('ios', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0),
|
||||
('android', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0)
|
||||
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_abuse_reasons(target_type, reason_code, label, requires_description, allows_evidence, sort_order)
|
||||
VALUES
|
||||
('user', 'fraud', '诈骗欺诈', 0, 1, 10),
|
||||
('user', 'harassment', '骚扰辱骂', 0, 1, 20),
|
||||
('user', 'illegal', '违法违规', 1, 1, 30),
|
||||
('group', 'fraud', '诈骗欺诈', 0, 1, 10),
|
||||
('group', 'spam', '垃圾广告', 0, 1, 20),
|
||||
('group', 'illegal', '违法违规', 1, 1, 30),
|
||||
('message', 'fraud', '诈骗欺诈', 0, 1, 10),
|
||||
('message', 'spam', '垃圾广告', 0, 1, 20),
|
||||
('message', 'harassment', '骚扰辱骂', 0, 1, 30),
|
||||
('message', 'illegal', '违法违规', 1, 1, 40)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
label = VALUES(label),
|
||||
requires_description = VALUES(requires_description),
|
||||
allows_evidence = VALUES(allows_evidence),
|
||||
sort_order = VALUES(sort_order),
|
||||
updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_feedback_config(id, categories, max_length, attachment_policy, allow_contact, diagnostics_notice)
|
||||
VALUES (1, '["bug","suggestion","complaint","other"]', 1000, '{"max_files":5,"purpose":"feedback"}', 1, '仅在你同意时上传脱敏诊断信息。')
|
||||
ON DUPLICATE KEY UPDATE updated_at = updated_at;
|
||||
|
||||
-- Current MySQL adapter schema version expected by server/db/mysql/adapter.go.
|
||||
UPDATE tinode.kvmeta SET `value` = 123 WHERE `key` = 'version';
|
||||
@@ -1,932 +0,0 @@
|
||||
# IM 服务端多租户改造方案
|
||||
|
||||
> **方案变更说明,2026-08-13:当前多租户技术路线已调整为“控制库 + 每租户独立业务库”。本文原主体内容按“共享库共享表 + tenant_id”编写,只作为旧方案参考,不再作为后续开发准绳。新的落地方案见 [IM 独立库多租户改造方案](IM-独立库多租户改造方案.md)。**
|
||||
>
|
||||
> 新方案下,业务库中的 `users`、`auth`、`topics`、`messages`、`fileuploads` 和后续产品业务表不再增加 `tenant_id` 字段;`TenantID` 仍保留在 Session、Token、Hub、缓存、Push、文件授权、插件上下文和 Store 选库流程中,用于定位租户业务库和运行时隔离。
|
||||
|
||||
> 编写日期:2026-08-10
|
||||
> 适用项目:`im_server` 当前服务端源码
|
||||
> 需求背景:[IM 产品原型需求文档](IM-产品原型需求文档.md) 第 1.1 节“企业码登录”及第 9 章“企业工作台与办公协同”
|
||||
> 关联文档:[IM 产品需求服务端改造分析报告](IM-产品需求服务端改造分析报告.md)
|
||||
|
||||
> 实施状态:第一阶段租户识别已落地。MySQL Adapter schema 117 创建 `im_tenant`、`im_tenant_config` 和默认租户;首次 `hi` 使用企业码解析并固定 Session TenantID。核心业务表的 `tenant_id`、租户化认证和路由隔离尚未完成。
|
||||
|
||||
## 1. 方案目标
|
||||
|
||||
当前项目是单租户 Tinode 服务端。用户、认证记录、Topic、Subscription、Message、File、缓存、集群路由和 Push 都没有租户维度。本方案目标是在一套服务集群和一套 MySQL 数据库中安全承载多个企业租户,并满足以下约束:
|
||||
|
||||
1. 一个企业的数据默认只能被本企业用户和管理员访问;
|
||||
2. 同一手机号、用户名可以在不同企业分别注册;
|
||||
3. 单聊、群聊、搜索、文件、Push、系统消息和后台任务均不可跨租户;
|
||||
4. 租户身份由服务端在握手/认证阶段确定,客户端后续消息不能修改;
|
||||
5. 现有 `usr...`、`grp...`、`p2p...` Topic 格式尽量保持兼容;
|
||||
6. 多租户启用后,可以按租户配置功能、配额、认证方式、数据保留和安全策略。
|
||||
|
||||
## 2. 当前实现为什么不支持多租户
|
||||
|
||||
### 2.1 认证和 Session 没有租户
|
||||
|
||||
- `auth.Rec` 只有 `Uid`、`AuthLevel`、有效期、状态等字段,没有 `TenantID`;
|
||||
- `Session` 只保存 `uid` 和 `authLvl`,登录成功后直接把认证记录写入 Session;
|
||||
- `MsgClientHi`、`MsgClientLogin` 和 Token 中均没有租户;
|
||||
- basic 登录名在数据库中全局唯一,手机号/邮箱凭证也全局唯一;
|
||||
- 密码找回按 `method:value` 全局查找账号;
|
||||
- root 用户可以通过 `Extra.AsUser` 代表任意用户操作,没有租户范围。
|
||||
|
||||
这意味着同一手机号无法在两个企业分别注册,也无法证明当前连接属于哪个企业。
|
||||
|
||||
### 2.2 数据库全部按全局键查询
|
||||
|
||||
改造前 MySQL schema 版本为 116;第一阶段建立租户主表和握手解析后为 117。除租户主数据表外,核心表仍没有 `tenant_id`:
|
||||
|
||||
| 表 | 当前关键唯一约束 | 多租户问题 |
|
||||
| --- | --- | --- |
|
||||
| `users` | `id` | 无租户归属 |
|
||||
| `auth` | `uname` 全局唯一 | 不同企业不能使用相同登录名 |
|
||||
| `credentials` | `synthetic` 全局唯一 | 同一手机号不能加入多个企业 |
|
||||
| `usertags` | 用户/Tag | 搜索可能返回其他企业用户 |
|
||||
| `devices` | 设备 Hash 全局唯一 | 同一设备上的多企业账号会冲突 |
|
||||
| `topics` | `name` 全局唯一 | 没有 Topic 租户边界 |
|
||||
| `subscriptions` | Topic/User | 不能证明用户与 Topic 属于同一租户 |
|
||||
| `messages` | Topic/SeqId | 查询只按 Topic,没有租户条件 |
|
||||
| `dellog` | Topic/User | 删除记录没有租户条件 |
|
||||
| `fileuploads` | File ID | 文件查询和清理没有租户范围 |
|
||||
| `filemsglinks` | File/Message/Topic/User | 附件关联不能强制同租户 |
|
||||
|
||||
Store/Adapter 的方法也只接收 UID 或 Topic,例如:
|
||||
|
||||
```go
|
||||
UserGet(uid)
|
||||
AuthGetUniqueRecord(unique)
|
||||
TopicGet(topic)
|
||||
SubscriptionGet(topic, uid, keepDeleted)
|
||||
MessageGetAll(topic, uid, opts)
|
||||
FileGet(fileID)
|
||||
```
|
||||
|
||||
只在 SQL 表加字段但不修改这些接口,后续开发很容易漏写 `WHERE tenant_id = ?`,仍然会发生越权。
|
||||
|
||||
### 2.3 内存、集群和系统 Topic 没有租户
|
||||
|
||||
- Hub 的 `topics` 使用裸 Topic 名作为 `sync.Map` Key;
|
||||
- 集群一致性哈希只对裸 Topic 名计算;
|
||||
- `ClusterSess` 和集群 RPC 没有租户字段;
|
||||
- User Cache 仅以 UID 为 Key;
|
||||
- Push Receipt、FCM Channel 只有 UID/Topic,没有租户;
|
||||
- Hub 启动时只初始化一个字面值为 `sys` 的全局系统 Topic;
|
||||
- 持久化缓存 Key 没有租户命名空间。
|
||||
|
||||
如果两个租户都使用 `sys` 等逻辑 Topic,同一个内存对象会被共享。即使普通 Topic 名当前由全局 UID 生成,也不能依赖“ID 不碰撞”代替租户授权。
|
||||
|
||||
### 2.4 文件接口只获得 UID
|
||||
|
||||
`authFileRequest` 当前只返回 UID。文件下载查询只按 File ID,无法把文件访问限制在当前租户。对象存储 Location 也没有统一的租户目录规则。
|
||||
|
||||
## 3. 关键设计决策
|
||||
|
||||
### 3.1 目标模式
|
||||
|
||||
本方案采用:
|
||||
|
||||
> **共享服务集群 + 共享 MySQL 数据库 + 共享表 + 每行 `tenant_id` 强隔离**
|
||||
|
||||
该模式最符合“多企业共用一套平台”的目标,但也是改造面最大的模式。若实际只需要企业数据物理隔离,采用“一企一实例/一企一库”会更简单;它不属于本文的共享多租户实现。
|
||||
|
||||
### 3.2 用户模型:租户内账号
|
||||
|
||||
推荐同一个自然人在不同企业中对应不同的 Tinode User:
|
||||
|
||||
```text
|
||||
手机号 138****0000
|
||||
├─ 租户 A -> user A1 -> 独立好友、群、消息和设置
|
||||
└─ 租户 B -> user B1 -> 独立好友、群、消息和设置
|
||||
```
|
||||
|
||||
这样可以最大程度复用当前 User/Topic/Subscription 模型,离职、停用、数据导出和删除也能按企业处理。
|
||||
|
||||
如果以后需要一个账号切换多个企业,可新增 `global_identities` 和 `tenant_memberships`,把全局身份映射到各租户的本地 User。不要在第一阶段直接把当前 User 改造成跨租户共享主体,否则所有 P2P、资料、设备、未读数和隐私规则都会变得更复杂。
|
||||
|
||||
### 3.3 ID 策略:保持全局唯一,额外校验租户
|
||||
|
||||
继续使用现有 Snowflake/加密 UID 生成器,User ID、File ID、普通 Group Topic 名保持全局唯一。外部协议继续使用现有 `usr...`、`grp...`、`p2p...` 格式,不把租户编码暴露在这些 ID 中。
|
||||
|
||||
所有数据库查询仍必须带 `tenant_id`,原因是:
|
||||
|
||||
- 全局唯一只防止碰撞,不能证明调用者有权访问;
|
||||
- 已知或泄露的其他租户 ID 仍然可能被直接请求;
|
||||
- 使用复合租户条件可以让越权请求稳定返回不存在。
|
||||
|
||||
### 3.4 跨租户通信默认禁止
|
||||
|
||||
第一阶段规则:
|
||||
|
||||
- 用户搜索只在当前租户;
|
||||
- P2P 双方必须属于同一租户;
|
||||
- Group Topic 及全部成员必须属于同一租户;
|
||||
- 文件、收藏、搜索结果和系统通知不跨租户;
|
||||
- Tenant Admin 只能管理本租户;
|
||||
- Platform Admin 的跨租户操作只能走独立后台 API,禁止使用普通客户端协议随意 `AsUser`。
|
||||
|
||||
未来若需要跨企业协作,应作为“外部联系人/联邦通信”独立设计,使用明确授权、双边租户策略和审计,不能绕过本方案的同租户不变量。
|
||||
|
||||
## 4. 租户领域模型
|
||||
|
||||
### 4.1 TenantID 与租户上下文
|
||||
|
||||
新增强类型,不使用裸字符串在各层传递:
|
||||
|
||||
```go
|
||||
package types
|
||||
|
||||
type TenantID int64
|
||||
|
||||
const ZeroTenantID TenantID = 0
|
||||
|
||||
type TenantContext struct {
|
||||
TenantID TenantID
|
||||
ActorUID Uid
|
||||
}
|
||||
|
||||
type TopicKey struct {
|
||||
TenantID TenantID
|
||||
Topic string
|
||||
}
|
||||
|
||||
type UserKey struct {
|
||||
TenantID TenantID
|
||||
UID Uid
|
||||
}
|
||||
```
|
||||
|
||||
`TenantID = 0` 仅允许出现在“尚未完成租户握手”的连接和平台级任务中。任何租户业务 Store 调用发现 `ZeroTenantID` 应立即返回错误,不能自动降级为全局查询。
|
||||
|
||||
`context.Context` 继续用于超时和取消;租户 ID 应作为显式的强类型参数或租户作用域对象,不能只隐藏在 `context.Value` 中。
|
||||
|
||||
### 4.2 租户表
|
||||
|
||||
建议新增:
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_tenant (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
tenant_desc VARCHAR(256),
|
||||
state SMALLINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY tenants_code (code)
|
||||
);
|
||||
```
|
||||
|
||||
`created_by` 和 `updated_by` 保存操作人用户 ID;系统初始化默认租户时使用 `0` 表示系统操作。
|
||||
|
||||
当前阶段不在租户主表预留 `config_version` 和 `data_region`。真正建设租户配置中心或多地域路由时,再通过独立的 `tenant_settings` 表增加相应版本/地域字段,避免提前维护没有实际使用场景的列。
|
||||
|
||||
租户状态建议包括:
|
||||
|
||||
| 状态 | 登录 | 读消息 | 发消息/写业务 | 管理操作 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `provisioning` | 禁止 | 禁止 | 禁止 | 平台管理员可配置 |
|
||||
| `active` | 允许 | 允许 | 允许 | 按角色允许 |
|
||||
| `suspended` | 禁止新登录 | 可配置只读 | 禁止 | 平台管理员可恢复/导出 |
|
||||
| `deleting` | 禁止 | 禁止 | 禁止 | 仅执行删除任务 |
|
||||
| `deleted` | 禁止 | 禁止 | 禁止 | 只保留必要审计 |
|
||||
|
||||
### 4.3 租户配置与配额
|
||||
|
||||
第一阶段只建立 `im_tenant_config`,保存用户数、群数和单群成员数上限。每个租户最多一条配置;三个上限字段为 `0` 时表示不限制。
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_tenant_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
|
||||
ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_im_tenant_config_tenant_id (tenant_id),
|
||||
CONSTRAINT fk_im_tenant_config_tenant
|
||||
FOREIGN KEY (tenant_id) REFERENCES im_tenant(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
`created_by` 和 `updated_by` 保存操作人用户 ID。系统初始化时可以使用约定的系统操作人 ID;在平台管理员账号模型确定前,暂不为这两个字段建立用户外键。
|
||||
|
||||
认证、文件、消息频控、功能开关、保留期、合规策略、第三方服务配置及企业品牌等配置本阶段不建字段,待有明确业务需求后再增加。
|
||||
|
||||
## 5. 租户识别与认证流程
|
||||
|
||||
### 5.1 推荐流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as App
|
||||
participant S as Session
|
||||
participant DB as MySQL
|
||||
|
||||
A->>S: 建立 WebSocket
|
||||
A->>S: hi(tenant_code)
|
||||
S->>DB: 按 code 查询租户
|
||||
DB-->>S: tenant
|
||||
S->>S: 校验 active 并固定 TenantID
|
||||
S-->>A: hi success + 公开租户信息
|
||||
A->>S: login(scheme, secret)
|
||||
```
|
||||
|
||||
第一阶段不引入 Tenant Resolve API 和 `tenant_ticket`。企业码不是密码,客户端可直接提交;服务端必须查询数据库并只接受 `active` 租户,客户端不能直接提交数据库 TenantID。
|
||||
|
||||
`tenant_ticket` 的价值在于把一次企业码解析结果变成服务端签名的短期凭证,防止客户端伪造内部 TenantID,并减少 WebSocket 节点的重复查询;它还可以携带目标集群、过期时间和随机数。当前是单集群且刚开始建设,直接查询更简单。以后出现多集群路由或租户解析查询成为性能瓶颈时,可以再增加 ticket,不影响 Session 固定 TenantID 的核心约束。
|
||||
|
||||
### 5.2 协议修改
|
||||
|
||||
在 `MsgClientHi` 增加:
|
||||
|
||||
```go
|
||||
Tenant string `json:"tenant,omitempty"`
|
||||
```
|
||||
|
||||
该字段当前直接传企业码,不传数据库 TenantID。规则如下:
|
||||
|
||||
1. 首次 `hi` 必须设置 Tenant;
|
||||
2. Session 一旦绑定租户,后续 `hi/login/acc/pub/get/set/del/note` 都不能修改;
|
||||
3. 注册、登录和密码找回前必须已绑定租户;
|
||||
4. Tenant 缺失、为空或超过 64 个字符时拒绝握手,不做默认租户兼容;
|
||||
5. 租户不存在时返回 404,租户不是 `active` 状态时返回 403;
|
||||
6. 服务端返回的租户信息仅包含 `code` 和 `name`,不返回内部 ID、状态或密钥。
|
||||
|
||||
当前已实现 `MsgClientHi.Tenant`、MySQL 按企业码查询、Session TenantID 固定、租户公开信息响应,以及未绑定租户时拒绝后续命令。认证器和业务表尚未带 TenantID,因此当前代码只是租户识别基础,不能视为已经完成数据隔离。
|
||||
|
||||
后续 HTTP API 的 TenantID 必须从租户化签名 Token 中获得。禁止只信任 URL、Header 或请求 Body 中客户端自报的 `tenant_id`。
|
||||
|
||||
### 5.3 认证接口修改
|
||||
|
||||
当前认证器接口:
|
||||
|
||||
```go
|
||||
Authenticate(secret []byte, remoteAddr string)
|
||||
```
|
||||
|
||||
建议改为:
|
||||
|
||||
```go
|
||||
type AuthContext struct {
|
||||
TenantID types.TenantID
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
Authenticate(ctx AuthContext, secret []byte)
|
||||
```
|
||||
|
||||
`auth.Rec` 增加:
|
||||
|
||||
```go
|
||||
TenantID types.TenantID `json:"tenant_id,omitempty"`
|
||||
```
|
||||
|
||||
basic、REST、code、token 等认证器和所有 Mock 必须同步修改。认证器返回 TenantID 与 Session 不一致时,统一按认证失败处理,并写安全审计。
|
||||
|
||||
### 5.4 登录名和凭证的租户唯一性
|
||||
|
||||
数据库约束修改为:
|
||||
|
||||
```text
|
||||
auth: UNIQUE (tenant_id, uname)
|
||||
auth: UNIQUE (tenant_id, userid, scheme)
|
||||
credentials: UNIQUE (tenant_id, synthetic)
|
||||
devices: UNIQUE (tenant_id, hash)
|
||||
```
|
||||
|
||||
这样同一手机号可以分别加入多个企业。密码找回必须先有 TenantContext,再按 `(tenant_id, method, value)` 查找,不能全库搜索手机号后选择第一个用户。
|
||||
|
||||
### 5.5 Token v2
|
||||
|
||||
当前 HMAC Token 固定包含 UID、过期时间、认证级别、序列号和特性位,没有 TenantID。需要新增带版本的 Token v2:
|
||||
|
||||
```text
|
||||
[version][tenant_id][uid][expires][auth_level][serial][features][signature]
|
||||
```
|
||||
|
||||
Token 规则:
|
||||
|
||||
1. v2 Token 的签名覆盖 TenantID;
|
||||
2. Token 认证返回 `TenantID + UID`;
|
||||
3. Session 已绑定租户时必须与 Token 一致;
|
||||
4. 不包含 TenantID 的 Token 一律拒绝;
|
||||
5. 租户停用时应支持按租户 Token 版本或撤销时间拒绝已签发 Token,不能只能全平台提升 serial。
|
||||
|
||||
### 5.6 管理员模型
|
||||
|
||||
当前 `AuthLevel` 只有 NONE、ANON、AUTH、ROOT,不足以表达租户管理员。建议保留认证级别,同时增加业务角色:
|
||||
|
||||
- `platform_admin`:平台级租户生命周期和运维;
|
||||
- `tenant_owner`:本租户最高管理员;
|
||||
- `tenant_admin`:组织、用户、群和安全管理;
|
||||
- `security_auditor`:实名、举报、审计读取;
|
||||
- 普通用户。
|
||||
|
||||
Tenant Admin 的权限必须绑定 TenantID。普通 WebSocket 的 `Extra.AsUser` 只能在同租户且有明确授权时使用;平台管理员跨租户代操作应走隔离的后台 API、二次验证和完整审计。
|
||||
|
||||
## 6. Session、消息和 Topic 改造
|
||||
|
||||
### 6.1 Session
|
||||
|
||||
`Session`、`ClusterSess` 增加:
|
||||
|
||||
```go
|
||||
tenantID types.TenantID
|
||||
```
|
||||
|
||||
需要建立以下不变量:
|
||||
|
||||
```text
|
||||
已认证 Session:
|
||||
session.tenantID != 0
|
||||
session.uid != 0
|
||||
User(session.uid).tenant_id == session.tenantID
|
||||
```
|
||||
|
||||
所有内部 `ClientComMessage` 和 `ServerComMessage` 应携带服务端写入的 TenantID,序列化给普通客户端时不必暴露。插件、集群和后台任务可以读取,但不得覆盖。
|
||||
|
||||
### 6.2 内部路由键
|
||||
|
||||
外部 Topic 名保持不变,内部统一使用:
|
||||
|
||||
```go
|
||||
func (k TopicKey) RoutingKey() string {
|
||||
return strconv.FormatUint(uint64(k.TenantID), 10) + ":" + k.Topic
|
||||
}
|
||||
```
|
||||
|
||||
需要修改:
|
||||
|
||||
- Hub `topicGet/topicPut/topicDel`;
|
||||
- Session 的订阅 Map;
|
||||
- Topic Proxy 和 Multiplex Session Key;
|
||||
- 集群一致性哈希 `nodeForTopic/isRemoteTopic`;
|
||||
- ClusterReq、ClusterRoute、ClusterResp;
|
||||
- Topic 重哈希和代理失效;
|
||||
- 调试端点中显示的 Topic 标识。
|
||||
|
||||
客户端仍看到原始 Topic 名,只有服务端内部使用 RoutingKey。不要直接把 `tenantID:` 前缀写入数据库 Topic 名或回传客户端,否则会破坏 Topic 类别解析、P2P 解析和现有客户端。
|
||||
|
||||
### 6.3 `sys`、`me`、`fnd`、`slf`
|
||||
|
||||
- `me`、`fnd`、`slf` 虽由全局 UID 派生,仍必须使用 Session TenantID 加载;
|
||||
- 全局字面值 `sys` 必须改为“租户作用域内的逻辑别名”;
|
||||
- Hub 不再启动一个全局 `sys` Topic;应在租户首次使用或租户创建时初始化 `(tenant_id, sys)`;
|
||||
- 系统消息生产者必须指定 TenantID,不能广播到全平台 `sys`;
|
||||
- `fnd` 搜索只读取当前租户 Tags;
|
||||
- `slf` 收藏不能引用其他租户的源消息或文件。
|
||||
|
||||
### 6.4 P2P
|
||||
|
||||
P2P Topic 名仍由两个全局 UID 生成,但创建前必须执行:
|
||||
|
||||
```text
|
||||
initiator.tenant_id == invited.tenant_id == session.tenant_id
|
||||
```
|
||||
|
||||
订阅、发送、读取、删除和通话时都继续带 TenantID 查询,避免通过已知 P2P 名访问其他企业。
|
||||
|
||||
### 6.5 群聊
|
||||
|
||||
创建 Group Topic 时由 Session 注入 TenantID。邀请、审批、管理员变更和群主转让必须验证:
|
||||
|
||||
- Topic 属于当前租户;
|
||||
- 操作者属于当前租户;
|
||||
- 目标成员属于同一租户;
|
||||
- 所有写入的 Subscription 使用同一 TenantID;
|
||||
- Outbox 和系统通知也使用相同 TenantID。
|
||||
|
||||
不要接受客户端在 `set.desc`、Aux 或 Tags 中提交的 tenant 字段作为授权依据。
|
||||
|
||||
## 7. Store 与 Adapter 改造
|
||||
|
||||
### 7.1 不推荐只靠 `context.Value`
|
||||
|
||||
租户条件应进入编译期可见的接口。推荐增加租户作用域 Store:
|
||||
|
||||
```go
|
||||
tenantStore := store.ForTenant(session.tenantID)
|
||||
|
||||
user, err := tenantStore.Users.Get(uid)
|
||||
topic, err := tenantStore.Topics.Get(topicName)
|
||||
msgs, err := tenantStore.Messages.GetAll(topicName, uid, opts)
|
||||
```
|
||||
|
||||
底层 Adapter 接口显式接收 TenantID:
|
||||
|
||||
```go
|
||||
UserGet(tenantID, uid)
|
||||
AuthGetUniqueRecord(tenantID, unique)
|
||||
TopicGet(tenantID, topic)
|
||||
SubscriptionGet(tenantID, topic, uid, keepDeleted)
|
||||
MessageGetAll(tenantID, topic, uid, opts)
|
||||
FileGet(tenantID, fileID)
|
||||
```
|
||||
|
||||
`store.ForTenant(ZeroTenantID)` 必须失败。平台级扫描和租户生命周期操作使用单独的 `PlatformStore` 接口,不能给业务代码一个可省略 TenantID 的全局 Store。
|
||||
|
||||
### 7.2 需要修改的 Store 接口范围
|
||||
|
||||
| 接口域 | 必须租户化的操作 |
|
||||
| --- | --- |
|
||||
| Users | Create/Get/GetAll/Update/Delete/State/Tags/Topics/Subs/Unread |
|
||||
| Auth | Add/Get/Update/Delete/Unique lookup |
|
||||
| Credentials | Upsert/Get/Confirm/Fail/Delete/Reset lookup |
|
||||
| Topics | Create/Get/Share/Delete/Update/Owner/Find |
|
||||
| Subscriptions | Create/Get/Update/Delete/按 User/Topic 查询 |
|
||||
| Messages | Save/Get/Delete/GetDeleted/更新 Topic SeqId |
|
||||
| Devices | Upsert/Get/Delete/Push Channel |
|
||||
| Files | Start/Finish/Get/Link/DeleteUnused/Access check |
|
||||
| Persistent Cache | Get/Upsert/Delete/Expire |
|
||||
|
||||
这会修改 `server/store/store.go`、`server/db/adapter.go`、MySQL Adapter、Mock Store 以及大量调用点。不要为了减少改动只改少数查询;任何保留的无租户业务接口都会成为后续越权入口。
|
||||
|
||||
### 7.3 数据库支持范围
|
||||
|
||||
当前仓库还有 PostgreSQL、MongoDB、RethinkDB Adapter。多租户改造会使公共 Adapter 接口整体变化。
|
||||
|
||||
推荐产品版第一阶段明确只支持 MySQL:
|
||||
|
||||
- MySQL 完成多租户 schema 和全部集成测试;
|
||||
- 其他 Adapter 编译时可使用明确的 `ErrUnsupportedTenant` 或从产品构建中排除;
|
||||
- 不能让未实现多租户过滤的 Adapter 在配置错误时继续启动;
|
||||
- 如果必须继续支持四种数据库,应将工作量视为四套独立实现和隔离验收。
|
||||
|
||||
## 8. MySQL Schema 改造
|
||||
|
||||
### 8.1 统一规则
|
||||
|
||||
1. 所有租户业务表增加 `tenant_id BIGINT NOT NULL`;
|
||||
2. 唯一索引和高频查询索引以 `tenant_id` 作为第一列;
|
||||
3. 子表使用 `(tenant_id, parent_id)` 复合外键,防止跨租户关联;
|
||||
4. `kvmeta` 中数据库 schema 版本属于平台级元数据,不加 TenantID;
|
||||
5. 持久化业务缓存使用 TenantID 列或强制租户 Key 前缀;
|
||||
6. 所有删除、更新、聚合和后台批处理同样带 TenantID;
|
||||
7. MySQL 没有可直接依赖的应用级 RLS,隔离责任主要在 Adapter 和复合约束。
|
||||
|
||||
### 8.2 核心表与索引
|
||||
|
||||
| 表 | 新增列 | 主要索引调整 |
|
||||
| --- | --- | --- |
|
||||
| `users` | `tenant_id` | `UNIQUE(tenant_id,id)`;状态/最近登录索引前加 tenant |
|
||||
| `usertags` | `tenant_id` | `INDEX(tenant_id,tag)`;`UNIQUE(tenant_id,userid,tag)` |
|
||||
| `devices` | `tenant_id` | `UNIQUE(tenant_id,hash)`;复合外键到 users |
|
||||
| `auth` | `tenant_id` | `UNIQUE(tenant_id,uname)`;`UNIQUE(tenant_id,userid,scheme)` |
|
||||
| `topics` | `tenant_id` | `UNIQUE(tenant_id,name)`;Owner/State 索引前加 tenant |
|
||||
| `topictags` | `tenant_id` | `INDEX(tenant_id,tag)`;`UNIQUE(tenant_id,topic,tag)` |
|
||||
| `subscriptions` | `tenant_id` | `UNIQUE(tenant_id,topic,userid)`;User/Topic 查询前加 tenant |
|
||||
| `messages` | `tenant_id` | `UNIQUE(tenant_id,topic,seqid)`;历史查询索引前加 tenant |
|
||||
| `dellog` | `tenant_id` | Topic/User/DelId 索引前加 tenant |
|
||||
| `credentials` | `tenant_id` | `UNIQUE(tenant_id,synthetic)`;User/Method 索引前加 tenant |
|
||||
| `fileuploads` | `tenant_id` | `INDEX(tenant_id,status,updatedat)`;复合用户归属 |
|
||||
| `filemsglinks` | `tenant_id` | File/Message/Topic/User 索引及复合外键全部带 tenant |
|
||||
|
||||
Topic 名当前通常全局唯一,但仍把数据库唯一约束改为 `(tenant_id, name)`,这样各租户可以拥有自己的逻辑 `sys`,也让 schema 明确表达隔离边界。
|
||||
|
||||
### 8.3 业务表
|
||||
|
||||
后续新增的所有 `im_*` 表,包括好友、群审批、组织、会议、日报、举报、Outbox、审计和消息索引,必须从第一版就包含 TenantID。禁止先以全局表上线再补租户。
|
||||
|
||||
推荐所有 Repository 方法使用如下形态:
|
||||
|
||||
```text
|
||||
Get(tenantID, id)
|
||||
List(tenantID, filter, cursor)
|
||||
Create(tenantID, actorID, data)
|
||||
Update(tenantID, id, expectedVersion, data)
|
||||
Delete(tenantID, id)
|
||||
```
|
||||
|
||||
### 8.4 SQL 示例
|
||||
|
||||
认证查询:
|
||||
|
||||
```sql
|
||||
SELECT userid, secret, expires, authlvl
|
||||
FROM auth
|
||||
WHERE tenant_id = ? AND uname = ?;
|
||||
```
|
||||
|
||||
Topic 查询:
|
||||
|
||||
```sql
|
||||
SELECT ...
|
||||
FROM topics
|
||||
WHERE tenant_id = ? AND name = ? AND state <> ?;
|
||||
```
|
||||
|
||||
消息查询:
|
||||
|
||||
```sql
|
||||
SELECT ...
|
||||
FROM messages
|
||||
WHERE tenant_id = ? AND topic = ? AND seqid < ?
|
||||
ORDER BY seqid DESC
|
||||
LIMIT ?;
|
||||
```
|
||||
|
||||
所有 `UPDATE` 和 `DELETE` 必须同时带业务主键和 `tenant_id`。如果受影响行数为 0,统一返回 NotFound/Denied,不能再用无租户查询判断对象是否属于其他企业,以免泄露对象存在性。
|
||||
|
||||
## 9. 缓存与集群改造
|
||||
|
||||
### 9.1 Hub Topic Cache
|
||||
|
||||
当前 Hub 使用裸 Topic 名作为 Key,应改为 `TopicKey.RoutingKey()`。Topic 对象自身增加 TenantID,初始化后不可修改。
|
||||
|
||||
加载 Topic 时同时检查:
|
||||
|
||||
```text
|
||||
requested tenant == loaded topic tenant == database row tenant
|
||||
```
|
||||
|
||||
如果不一致,记录安全告警并拒绝把 Topic 放入缓存。
|
||||
|
||||
### 9.2 User Cache
|
||||
|
||||
现有 UID 在全平台全局唯一,理论上可以继续只按 UID 缓存。但为避免导入数据、测试固定 UID 或代码误用造成污染,建议改为 `UserKey{TenantID, UID}`。
|
||||
|
||||
`UserCacheReq`、未读数 IO、Pending Push、EvictUser、集群 UserCacheUpdate 全部增加 TenantID。设备注销或账号停用只能影响指定租户的 User。
|
||||
|
||||
### 9.3 Session Store
|
||||
|
||||
Session ID 继续全局随机即可,但以下操作必须带租户断言:
|
||||
|
||||
- 通过 SID 完成文件认证;
|
||||
- `EvictUser`;
|
||||
- Cluster Proxy 恢复 Session;
|
||||
- root/管理员代操作;
|
||||
- 调试页面查看 Session。
|
||||
|
||||
建议新增 `EvictUser(tenantID, uid, skipSID)`,防止固定/导入 UID 冲突时误踢其他企业用户。
|
||||
|
||||
### 9.4 集群路由
|
||||
|
||||
一致性哈希由:
|
||||
|
||||
```text
|
||||
hash(topic)
|
||||
```
|
||||
|
||||
改为:
|
||||
|
||||
```text
|
||||
hash(tenant_id + ":" + topic)
|
||||
```
|
||||
|
||||
`ClusterSess`、`ClusterReq`、`ClusterRoute`、`ClusterResp` 和 User Cache RPC 增加 TenantID。接收端验证:
|
||||
|
||||
- RPC 声明租户与消息内部租户一致;
|
||||
- Session 租户与请求租户一致;
|
||||
- RoutingKey 重新计算结果与目标节点一致;
|
||||
- Plugin 返回的替换消息不能修改租户。
|
||||
|
||||
集群节点加入时必须校验协议版本,不理解 Tenant 字段的节点应拒绝加入集群。
|
||||
|
||||
### 9.5 持久化缓存
|
||||
|
||||
缓存 Key 统一通过函数生成:
|
||||
|
||||
```go
|
||||
TenantCacheKey(tenantID, namespace, key)
|
||||
```
|
||||
|
||||
禁止业务代码自行字符串拼接。租户配置变化时只失效该租户 Key。平台级 schema/version Key 与租户业务 Cache 使用不同接口。
|
||||
|
||||
## 10. 文件与对象存储改造
|
||||
|
||||
### 10.1 HTTP 认证结果
|
||||
|
||||
`authFileRequest` 从只返回 UID 改为:
|
||||
|
||||
```go
|
||||
type RequestPrincipal struct {
|
||||
TenantID types.TenantID
|
||||
UID types.Uid
|
||||
Roles []string
|
||||
}
|
||||
```
|
||||
|
||||
Token 认证和 SID 认证都必须得到相同 Principal。API Key 只表示合法客户端应用,不能替代 Tenant 身份和资源授权。
|
||||
|
||||
### 10.2 文件访问
|
||||
|
||||
新增:
|
||||
|
||||
```go
|
||||
FileCanAccess(tenantID, uid, fileID, action)
|
||||
```
|
||||
|
||||
访问流程:
|
||||
|
||||
1. `FileGet(tenantID, fileID)`;
|
||||
2. 上传未完成时只允许同租户上传者访问;
|
||||
3. 已关联消息时,Subscription 必须为同租户且有 Topic Read 权限;
|
||||
4. 群文件删除检查同租户上传者/管理员/群主;
|
||||
5. 转发时源文件、源消息和目标 Topic 全部属于同租户;
|
||||
6. 生成短时签名 URL,签名上下文包含 TenantID/FileID/过期时间;
|
||||
7. 审计日志记录 TenantID、UID、FileID 和结果。
|
||||
|
||||
### 10.3 存储路径
|
||||
|
||||
新文件统一使用:
|
||||
|
||||
```text
|
||||
tenants/{tenant_id}/yyyy/mm/{file_id}
|
||||
```
|
||||
|
||||
Location 不直接返回客户端。垃圾回收任务按 TenantID 分批并设置公平配额,避免大租户占满清理队列。
|
||||
|
||||
## 11. 搜索、Tags 与组织数据
|
||||
|
||||
### 11.1 用户和 Topic 搜索
|
||||
|
||||
`Find`、`FindOne`、Tag 更新和精确账号搜索都必须带 TenantID。相同手机号/用户名在不同租户生成相同 Tag 没有问题,因为索引和唯一约束均以 TenantID 为前缀。
|
||||
|
||||
跨租户查询统一返回空结果或通用 NotFound,不能返回“该用户属于其他企业”。
|
||||
|
||||
### 11.2 消息搜索
|
||||
|
||||
消息索引记录必须包含 TenantID,搜索引擎索引名或文档路由也包含 TenantID:
|
||||
|
||||
```text
|
||||
tenant_id + topic + seq_id
|
||||
```
|
||||
|
||||
查询至少经过两层过滤:
|
||||
|
||||
1. 搜索请求强制加入 TenantID;
|
||||
2. 返回结果按当前用户的同租户 Topic Read ACL 二次校验。
|
||||
|
||||
如果使用 OpenSearch/Elasticsearch,可以共享索引并按 TenantID 路由,也可以按大租户独立索引。无论哪种方式,TenantID 必须来自 Token,而不是客户端 Query 参数。
|
||||
|
||||
### 11.3 组织架构
|
||||
|
||||
`im_org_units`、`im_org_members` 等第 9 章业务表以 TenantID 为根。部门树的 `parent_id`、负责人、成员和数据权限都必须使用复合租户约束,禁止把其他租户 User 设置为部门负责人。
|
||||
|
||||
## 12. Push、插件和外部服务
|
||||
|
||||
### 12.1 Push
|
||||
|
||||
`push.Receipt`、`ChannelReq` 和 `Payload` 内部增加 TenantID。要求:
|
||||
|
||||
- 设备查询使用 `(tenant_id, uid)`;
|
||||
- FCM Topic/Channel 名加入不可碰撞的租户前缀;
|
||||
- 租户级 Push 模板、应用包和凭证由服务端配置选择;
|
||||
- Push Handler 日志不可输出其他租户的设备 Token;
|
||||
- 未读数 Cache 使用 Tenant User Key;
|
||||
- 锁屏正文按租户安全策略生成。
|
||||
|
||||
如果不同租户使用不同品牌 App,Push Handler 需要根据 TenantID 选择 FCM/APNS 凭证,不能继续假定一套全局凭证。
|
||||
|
||||
### 12.2 Plugin Firehose
|
||||
|
||||
Plugin 请求增加 TenantID、租户策略版本和不可伪造的上下文。插件返回 `REPLACE` 时服务端必须保留原 TenantID,禁止插件把消息换到其他租户/Topic。
|
||||
|
||||
插件配置分两类:
|
||||
|
||||
- 平台级统一审核服务;
|
||||
- 租户级专属审核服务。
|
||||
|
||||
无论哪种,调用超时、熔断、失败开放/关闭策略都按租户配置,审计和指标带 TenantID。
|
||||
|
||||
### 12.3 REST 认证、短信、TURN 和对象存储
|
||||
|
||||
外部请求必须携带服务端签名的租户上下文,并对回调结果校验 TenantID。租户自带密钥时,通过密钥管理系统按 TenantID 选择,不把 Secret 缓存在普通 Topic/User JSON 中。
|
||||
|
||||
## 13. API Key、限流和配额
|
||||
|
||||
当前 `X-Tinode-APIKey` 是应用级准入凭证。多租户后建议保持以下职责:
|
||||
|
||||
- API Key:识别客户端应用、平台、版本和是否允许连接;
|
||||
- Tenant Ticket/Token:确定租户;
|
||||
- User Token:确定租户内用户;
|
||||
- Role/Policy:确定具体操作权限。
|
||||
|
||||
不要仅通过 API Key 推导 Tenant,因为同一个 App 可能登录多个企业。若某企业使用专属 App,可额外把 API Key 限定到一个 Tenant,但仍需验证 Token Tenant 一致。
|
||||
|
||||
限流 Key 至少包含:
|
||||
|
||||
```text
|
||||
tenant + endpoint/action
|
||||
tenant + uid + action
|
||||
tenant + ip + action
|
||||
```
|
||||
|
||||
配额计数包括用户、Topic、消息速率、文件大小、总存储、会议/报告等。超限返回稳定业务错误并记录租户审计,不能让一个大租户耗尽全平台队列。
|
||||
|
||||
## 14. 日志、指标与审计
|
||||
|
||||
### 14.1 日志和 Trace
|
||||
|
||||
认证后所有请求日志增加:
|
||||
|
||||
```text
|
||||
request_id, tenant_id, uid, sid, topic, action, result
|
||||
```
|
||||
|
||||
手机号、证件号、Token、企业码密钥和文件签名不写日志。TenantID 可写内部数值或不可逆短标识。
|
||||
|
||||
### 14.2 指标
|
||||
|
||||
租户 ID 直接作为 Prometheus Label 可能造成高基数。建议:
|
||||
|
||||
- 平台指标默认不带 Tenant Label;
|
||||
- 关键配额和 SLA 写入租户用量表或专用统计系统;
|
||||
- 仅对白名单大租户暴露租户级指标;
|
||||
- 错误日志和 Trace 保留 TenantID 供定位。
|
||||
|
||||
### 14.3 审计
|
||||
|
||||
以下操作必须记录 TenantID:
|
||||
|
||||
- 租户创建、停用、恢复和删除;
|
||||
- Tenant Admin 授权和代操作;
|
||||
- 用户停用、离职和数据导出;
|
||||
- 群主/管理员变更;
|
||||
- 实名审核、举报处置、匿名信箱读取;
|
||||
- 文件下载、批量导出和搜索敏感内容;
|
||||
- 平台管理员跨租户操作。
|
||||
|
||||
审计表只允许追加,普通租户管理员不能修改或删除。
|
||||
|
||||
## 15. 测试方案
|
||||
|
||||
### 15.1 单元和接口测试
|
||||
|
||||
- TenantContext 不可为空;
|
||||
- Session 只能绑定一次 Tenant;
|
||||
- Token Tenant 与 Session Tenant 不一致时登录失败;
|
||||
- Store/Adapter 不提供无 Tenant 的业务查询;
|
||||
- TopicKey、UserKey、CacheKey 和 Push Channel 正确命名空间化;
|
||||
- Plugin 不能替换 Tenant;
|
||||
- Platform Admin 与 Tenant Admin 权限边界正确。
|
||||
|
||||
### 15.2 MySQL 集成测试
|
||||
|
||||
创建 Tenant A、Tenant B,并使用相同用户名、手机号、群显示名和逻辑 `sys`。至少覆盖:
|
||||
|
||||
| 场景 | 预期 |
|
||||
| --- | --- |
|
||||
| 两租户注册相同用户名/手机号 | 均成功,各自登录到不同 UID |
|
||||
| A 使用 B 的账号密码登录 | 统一认证失败 |
|
||||
| A 已知 B 的 UID 执行 Get/Set/Delete | NotFound/Denied,不返回存在性 |
|
||||
| A 已知 B 的 Group Topic 订阅或发布 | 拒绝且不加载到 A 的 Hub Cache |
|
||||
| A 邀请 B 用户进入 A 群 | 拒绝,无 Subscription 写入 |
|
||||
| A 已知 B 的 Message Topic/SeqId | 不能读取、搜索、删除或收藏 |
|
||||
| A 已知 B 的 File ID/URL | 不能下载、关联或转发 |
|
||||
| A/B 同时使用 `sys` | 只收到本租户系统消息 |
|
||||
| A 停用用户 | 不影响相同设备上的 B 用户 |
|
||||
| A Tenant Admin `AsUser` B 用户 | 拒绝并审计 |
|
||||
| 同一设备在 A/B 注册 Push | 设备记录和 Push 不串租户 |
|
||||
| A 大量任务积压 | B 的通知和文件 GC 不被长期饿死 |
|
||||
|
||||
### 15.3 集群测试
|
||||
|
||||
- 相同 Topic 别名在不同 Tenant 生成不同 RoutingKey;
|
||||
- Proxy/Master 传输后 Tenant 不丢失;
|
||||
- 节点重启、重哈希和故障转移不串 Topic;
|
||||
- 混入旧节点时启动检查直接失败;
|
||||
- User Cache、未读数和 Push 在远程节点保持租户一致。
|
||||
|
||||
### 15.4 安全测试
|
||||
|
||||
- 篡改 `hi.tenant`、Token Tenant、HTTP Header 和 Body Tenant;
|
||||
- 枚举其他租户 UID、Topic、File ID、企业码;
|
||||
- SQL 查询遗漏 Tenant 的代码扫描与集成测试;
|
||||
- root/OBO 越权;
|
||||
- 搜索索引、缓存、导出、日志和审计的跨租户泄露;
|
||||
- 对象存储签名 URL 重放和跨租户替换;
|
||||
- Plugin/回调伪造 Tenant。
|
||||
|
||||
## 16. 代码修改清单
|
||||
|
||||
| 文件/目录 | 主要修改 |
|
||||
| --- | --- |
|
||||
| `server/store/types/tenant.go`、`types.go` | TenantID、TenantContext、TopicKey、UserKey;核心实体 TenantID |
|
||||
| `server/auth/auth.go` | AuthContext、`auth.Rec.TenantID`、认证接口 |
|
||||
| `server/auth/basic` | 租户内登录名查询和唯一性 |
|
||||
| `server/auth/token` | Token v2、TenantID 和租户撤销版本 |
|
||||
| `server/auth/rest`、`server/auth/code` | 传递并校验租户上下文 |
|
||||
| `server/datamodel.go` | `MsgClientHi.Tenant`;内部消息 TenantID |
|
||||
| `server/session.go` | Tenant 握手、固定 Session Tenant、登录/注册/发布断言 |
|
||||
| `server/user.go` | User Cache Key、租户级未读/踢出/状态更新 |
|
||||
| `server/sessionstore.go` | `EvictUser(tenant, uid)`、SID 认证租户断言 |
|
||||
| `server/hub.go` | TopicKey 缓存、租户 `sys`、租户路由 |
|
||||
| `server/init_topic.go` | Topic Tenant 初始化和同租户检查 |
|
||||
| `server/topic.go` | Topic/成员/消息 Tenant 不变量 |
|
||||
| `server/cluster.go`、`server/topic_proxy.go` | Cluster RPC Tenant、复合 RoutingKey、协议版本检查 |
|
||||
| `server/store/store.go` | `ForTenant` 作用域和所有 Mapper 租户化 |
|
||||
| `server/db/adapter.go` | Adapter 全接口 Tenant 参数 |
|
||||
| `server/db/mysql/adapter.go` | 所有 SQL Tenant 条件和复合约束 |
|
||||
| `server/db/mysql/schema.sql` | im_tenant、tenant_id、索引、外键和新 schema 版本 |
|
||||
| `server/hdl_files.go` | RequestPrincipal、FileCanAccess、Tenant 文件路径 |
|
||||
| `server/push.go`、`server/push` | Receipt/Channel/设备查询 Tenant 化 |
|
||||
| `server/plugins.go`、`pbx` | 插件协议 Tenant 上下文和不可变校验 |
|
||||
| `server/main.go` | 租户配置缓存和启动自检 |
|
||||
| `server/tinode.conf` | 租户服务和配额配置 |
|
||||
| `server/store/mock_store` | 重新生成所有租户化 Mock |
|
||||
| `server/*_test.go`、`server/db/mysql/tests` | 双租户正向、负向和隔离测试 |
|
||||
|
||||
## 17. 实施阶段与交付物
|
||||
|
||||
### 阶段 0:决策和基线
|
||||
|
||||
- 确认共享表模式;
|
||||
- 确认租户内独立 User;
|
||||
- 确认跨租户通信首期禁止;
|
||||
- 确认产品版数据库支持范围;
|
||||
- 冻结 TenantID、TopicKey 和认证协议设计。
|
||||
|
||||
交付物:架构决策记录、数据字典和接口清单。
|
||||
|
||||
### 阶段 1:Schema 与租户基础服务
|
||||
|
||||
- `im_tenant`、`im_tenant_config`;
|
||||
- 核心表 `tenant_id BIGINT NOT NULL`;
|
||||
- 默认租户;
|
||||
- `hi.tenant` 企业码解析和 Session 绑定;
|
||||
- 租户配置缓存。
|
||||
|
||||
交付物:全新数据库初始化 Schema、租户基础服务和 Schema 约束测试。
|
||||
|
||||
### 阶段 2:认证和 Store
|
||||
|
||||
- Session Tenant;
|
||||
- AuthContext 和 Token v2;
|
||||
- `store.ForTenant`;
|
||||
- MySQL 全查询租户化;
|
||||
- Mock 和单元测试更新。
|
||||
|
||||
交付物:租户内注册、登录和 Store 隔离能力。
|
||||
|
||||
### 阶段 3:实时路由与集群
|
||||
|
||||
- Hub TopicKey;
|
||||
- 租户 `sys/me/fnd/slf`;
|
||||
- Cluster RPC 和一致性哈希;
|
||||
- User Cache 和 Session Evict;
|
||||
- Plugin Tenant。
|
||||
|
||||
交付物:双租户集群隔离测试通过,不兼容节点无法加入集群。
|
||||
|
||||
### 阶段 4:文件、Push、搜索和后台任务
|
||||
|
||||
- 文件 Principal、ACL 和租户路径;
|
||||
- Push Receipt/设备/凭证租户化;
|
||||
- 搜索索引和二次 ACL;
|
||||
- Outbox、审计、GC、配额和限流租户化。
|
||||
|
||||
交付物:资源和异步链路跨租户负向测试通过。
|
||||
|
||||
### 阶段 5:上线验收
|
||||
|
||||
- 内部第二租户;
|
||||
- 安全测试、压测和故障演练;
|
||||
- 上线检查和真实第二企业启用。
|
||||
|
||||
交付物:测试报告、应急预案和多租户上线审批。
|
||||
|
||||
## 18. 主要风险
|
||||
|
||||
| 风险 | 影响 | 控制措施 |
|
||||
| --- | --- | --- |
|
||||
| 某个 SQL 漏 Tenant 条件 | 严重数据泄露 | 显式 Store 接口、复合约束、双租户负向测试和代码检查 |
|
||||
| 只改数据库,缓存/集群未改 | 实时消息串租户 | TopicKey/UserKey、Cluster Tenant 不变量和集群测试 |
|
||||
| 全局 `sys` 未拆分 | 系统通知泄露 | 租户逻辑 sys、Hub 复合 Key、通知生产者强制 Tenant |
|
||||
| 文件 URL 泄露 | 跨租户下载 | FileGet Tenant 条件、Topic ACL、短时签名和审计 |
|
||||
| root/OBO 权限过大 | 管理员跨租户操作 | Platform/Tenant Admin 分离、后台 API、二次验证和审计 |
|
||||
| 混跑旧集群节点 | Tenant 字段丢失 | 集群协议版本检查,不兼容节点拒绝加入 |
|
||||
| 指标 Tenant Label 高基数 | 监控系统压力 | 指标聚合,Tenant 主要进入日志/Trace/用量表 |
|
||||
|
||||
## 19. 上线前必须满足的条件
|
||||
|
||||
1. 业务代码不存在无 Tenant 的 User/Topic/Message/File 查询接口;
|
||||
2. 核心表和全部业务表 TenantID 为 NOT NULL;
|
||||
3. Auth、Credential、Device、Topic 和 Subscription 唯一约束已租户化;
|
||||
4. Token v2、Session Tenant 和 HTTP Principal 全链路一致;
|
||||
5. Hub、User Cache、Cluster、Push、Plugin 和 Persistent Cache 均携带 Tenant;
|
||||
6. `sys` 已按租户隔离;
|
||||
7. 文件下载和搜索完成租户过滤及 ACL 二次校验;
|
||||
8. 双租户自动化测试、集群测试和安全测试全部通过;
|
||||
9. 所有运行节点和后台任务均为租户感知版本;
|
||||
10. 内部第二租户稳定运行后,才允许创建真实第二企业。
|
||||
|
||||
## 20. 最终建议
|
||||
|
||||
多租户改造必须被视为服务端基础架构改造,而不是“给 users/topics/messages 加一列”。当前代码的认证、Store 接口、Hub Cache、Cluster、系统 Topic、文件和 Push 都使用全局 UID/Topic 语义,任何一处遗漏都可能造成跨企业数据泄露。
|
||||
|
||||
建议采用租户内独立 User、全局唯一 UID、共享表 `tenant_id` 强隔离的方案。全新数据库从创建时就使用最终多租户 Schema,并在 Store、认证、Token、路由、缓存、文件和 Push 全链路强制 TenantID;所有负向测试通过后,再开启真实租户。
|
||||
|
||||
工程上最重要的控制手段是:让 TenantID 进入类型和方法签名,使遗漏租户条件尽量在编译或测试阶段暴露;平台级全局查询使用隔离接口,普通业务代码永远只能获得 `store.ForTenant(...)`。这样才能让后续好友、群治理、企业工作台、搜索和安全业务从一开始就在正确的隔离边界内建设。
|
||||
@@ -1,313 +0,0 @@
|
||||
# IM 独立库多租户改造方案
|
||||
|
||||
> 编写日期:2026-08-13
|
||||
> 适用项目:`im_server`
|
||||
> 方案状态:替代原“共享库共享表 + tenant_id”方案,后续多租户开发以本文为准。
|
||||
|
||||
## 1. 方案结论
|
||||
|
||||
多租户调整为:
|
||||
|
||||
> 共享服务集群 + 控制库保存租户元数据 + 每个租户独立业务库
|
||||
|
||||
业务库中的 `users`、`auth`、`credentials`、`topics`、`subscriptions`、`messages`、`fileuploads` 以及后续产品业务表都不再增加 `tenant_id` 字段。租户隔离通过运行时选库实现:客户端先用企业码解析租户,服务端得到 `tenant_id` 后,根据控制库中的 `db_name` 或 `db_dsn` 选择对应租户业务库。
|
||||
|
||||
`TenantID` 仍然要保留,但它的职责从“业务表行级隔离字段”调整为“运行时租户上下文”:
|
||||
|
||||
1. 登录前企业码解析;
|
||||
2. Token 签名和校验;
|
||||
3. Session 当前租户固定;
|
||||
4. Hub、缓存、集群路由、Push、文件路径和插件上下文隔离;
|
||||
5. 选择当前请求使用哪个租户业务库;
|
||||
6. 后台平台侧管理租户、配置配额、停启租户。
|
||||
|
||||
## 2. 为什么独立库仍需要 TenantID
|
||||
|
||||
独立库解决的是数据库物理隔离,不代表服务端运行时没有租户。服务进程仍然同时服务多个企业,如果内存 Topic、用户缓存、系统 Topic、Push、文件下载授权、Token 校验不带租户上下文,仍可能出现:
|
||||
|
||||
1. 两个租户都使用逻辑 Topic `sys` 时共用同一个内存 Topic;
|
||||
2. HTTP 文件下载只拿到 `file_id`,无法判断该去哪个租户库查授权;
|
||||
3. Token 只包含 UID 时,无法知道该 UID 属于哪个租户库;
|
||||
4. 定时任务、插件调用、Push 回调无法定位租户库;
|
||||
5. 后台接口可能把平台租户 ID 和业务租户数据混在一起。
|
||||
|
||||
所以新方案的边界是:
|
||||
|
||||
```text
|
||||
TenantID 负责运行时路由和授权上下文
|
||||
tenant_id 字段不进入租户业务库表结构
|
||||
```
|
||||
|
||||
## 3. 数据库拆分
|
||||
|
||||
### 3.1 控制库
|
||||
|
||||
控制库可以使用现有 `hotgo`,只保存平台级租户元数据和配额配置。控制库表需要保留 `tenant_id`,因为这些表本身是在管理所有租户;这里的 `tenant_id` 不是业务库行级隔离字段。
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_tenant (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
tenant_desc VARCHAR(256),
|
||||
db_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
db_dsn VARCHAR(1024) NOT NULL DEFAULT '',
|
||||
state SMALLINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_im_tenant_code (code),
|
||||
KEY idx_im_tenant_db_name (db_name)
|
||||
);
|
||||
|
||||
CREATE TABLE im_tenant_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_im_tenant_config_tenant_id (tenant_id),
|
||||
CONSTRAINT fk_im_tenant_config_tenant
|
||||
FOREIGN KEY (tenant_id) REFERENCES im_tenant(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
`db_name` 适合所有租户共用同一个 MySQL 实例时使用,例如 `im_tenant_1001`。`db_dsn` 适合某些租户需要独立 MySQL 实例时使用。优先级建议为:`db_dsn` 非空时使用完整 DSN,否则使用主 DSN + `db_name`。
|
||||
|
||||
如果要求 `db_name` 不能重复,建议在租户创建服务里校验“非空 db_name 唯一”。MySQL 普通唯一索引不能表达“只对非空字符串唯一”,直接给 `db_name` 建唯一索引会导致多个使用独立 `db_dsn` 的租户因为空 `db_name` 冲突。
|
||||
|
||||
### 3.2 租户业务库
|
||||
|
||||
每个租户一个业务库,例如:
|
||||
|
||||
```text
|
||||
hotgo
|
||||
├─ im_tenant
|
||||
└─ im_tenant_config
|
||||
|
||||
im_tenant_1001
|
||||
├─ users
|
||||
├─ auth
|
||||
├─ credentials
|
||||
├─ topics
|
||||
├─ subscriptions
|
||||
├─ messages
|
||||
├─ fileuploads
|
||||
└─ 后续产品业务表
|
||||
```
|
||||
|
||||
租户业务库表结构恢复为租户内唯一约束,不加 `tenant_id`。同一个手机号、用户名可以在两个租户库中各自存在,不会冲突。
|
||||
|
||||
## 4. 当前代码如何调整
|
||||
|
||||
当前代码已经有一部分独立库路由雏形,可以继续利用:
|
||||
|
||||
1. `server/store/store.go` 已经预留 `TenantRoutedAdapter` 分支;
|
||||
2. `server/db/adapter.go` 已定义 `TenantAdapterFor(tenantID)`;
|
||||
3. `server/db/mysql/adapter.go` 已支持 `tenant_registry_db`、`tenant_db_routing`、`db_name`、`db_dsn`;
|
||||
4. Token、Session、Hub TopicKey、UserKey、插件上下文已经带 `TenantID`。
|
||||
|
||||
需要调整的重点是:
|
||||
|
||||
| 模块 | 旧共享表方案 | 新独立库方案 |
|
||||
| --- | --- | --- |
|
||||
| 控制库 | 与业务表混在一个库 | 单独保存 `im_tenant`、`im_tenant_config` |
|
||||
| 业务库 schema | 所有表加 `tenant_id` | 所有业务表不加 `tenant_id` |
|
||||
| MySQL Adapter | SQL 使用 `WHERE tenant_id=?` | 先根据 TenantID 选库,再执行租户库内 SQL |
|
||||
| Store 接口 | TenantID 作为 SQL 条件 | TenantID 作为选库参数 |
|
||||
| 唯一索引 | `(tenant_id, business_key)` | 租户库内 `business_key` 唯一 |
|
||||
| 外键 | 复合外键带 `tenant_id` | 普通外键 |
|
||||
| 数据迁移 | 共享库内补列回填 | 新租户直接建独立库,无历史迁移 |
|
||||
|
||||
## 5. 代码落地步骤
|
||||
|
||||
### 5.1 拆分 schema
|
||||
|
||||
把 MySQL schema 拆成两个层次:
|
||||
|
||||
1. `registry_schema.sql`:只创建控制库表 `im_tenant`、`im_tenant_config`;
|
||||
2. `tenant_schema.sql`:创建 Tinode 核心表和产品业务表,不包含 `tenant_id`;
|
||||
3. `schema.sql` 可以作为文档索引,说明不能直接混用两套 schema。
|
||||
|
||||
`CreateDb(reset)` 也要拆分:
|
||||
|
||||
1. 初始化控制库;
|
||||
2. 初始化默认租户业务库;
|
||||
3. 根据 `im_tenant.db_name/db_dsn` 初始化指定租户库;
|
||||
4. 禁止在同一个事务中同时创建控制库和多个租户业务库,避免失败后状态难恢复。
|
||||
|
||||
### 5.2 改 MySQL Adapter 路由
|
||||
|
||||
保留 `TenantAdapterFor(tenantID)`,但租户 Adapter 内执行的 SQL 应该是无 `tenant_id` 版本。
|
||||
|
||||
推荐结构:
|
||||
|
||||
```text
|
||||
mysql.adapter
|
||||
├─ registryDB: 连接控制库 hotgo
|
||||
├─ defaultDB: 默认租户库
|
||||
├─ tenantAdapters: map[TenantID]*adapter
|
||||
└─ TenantAdapterFor(tenantID): 从 registryDB 读取 db_name/db_dsn 并返回租户库 adapter
|
||||
```
|
||||
|
||||
租户业务方法仍保留 `TenantUserGet(tenantID, uid)` 这类签名,但实现改为“先选库,后执行租户库内原 SQL”。实际写代码时要避免递归,可以把“无租户 SQL”的方法放在租户库 adapter 上,把“路由包装方法”放在根 adapter 上。
|
||||
|
||||
### 5.3 删除业务表 tenant_id 依赖
|
||||
|
||||
需要回退或重写这些位置:
|
||||
|
||||
1. `server/db/mysql/schema.sql` 中核心业务表的 `tenant_id` 字段;
|
||||
2. `server/db/mysql/tenant_adapter.go` 中所有 `WHERE tenant_id=?`、`INSERT tenant_id`、复合 JOIN;
|
||||
3. `server/db/mysql/tests/mysql_test.go` 中依赖共享表隔离的测试;
|
||||
4. `docs/sql/mysql-multitenant-tenant-id-review.sql` 这份共享表迁移脚本应标记为废弃;
|
||||
5. 后续产品业务表设计也不再带 `tenant_id`。
|
||||
|
||||
不建议删除 Store、Session、Token、Hub、Push 和插件里的 `TenantID`,这些属于运行时隔离边界。
|
||||
|
||||
### 5.4 租户创建流程
|
||||
|
||||
后台创建租户时建议按以下顺序:
|
||||
|
||||
1. 在控制库 `im_tenant` 创建记录,状态为 `provisioning`;
|
||||
2. 生成业务库名,例如 `im_tenant_{id}`;
|
||||
3. 创建业务库并执行 `tenant_schema.sql`;
|
||||
4. 写入默认系统数据,例如 `kvmeta.version`、系统账号、`sys` Topic;
|
||||
5. 回写 `im_tenant.db_name`,状态改为 `active`;
|
||||
6. 创建失败时租户保持不可登录状态,并记录错误供后台重试。
|
||||
|
||||
第一阶段可以不做自动建库接口,先由运维执行建库 SQL,再在控制库写入 `im_tenant`。
|
||||
|
||||
### 5.5 认证与登录
|
||||
|
||||
登录入口仍然是:
|
||||
|
||||
1. 客户端携带 `X-Tinode-APIKey` 请求准入;
|
||||
2. `hi` 或登录前 HTTP 接口提交企业码;
|
||||
3. 服务端在控制库查 `im_tenant.code`;
|
||||
4. 租户状态必须是 `active`;
|
||||
5. Session 固定 `TenantID`;
|
||||
6. 登录认证根据 `TenantID` 选择租户业务库,然后查该库内 `auth`、`credentials`、`users`;
|
||||
7. Token 签名内容继续包含 `TenantID`,HTTP 后续请求可从 Token 反推出该使用哪个租户库。
|
||||
|
||||
客户端不能通过 URL、Header、Body 直接指定业务库名或 `tenant_id`。
|
||||
|
||||
## 6. 产品业务表如何设计
|
||||
|
||||
后续根据《IM 产品原型需求文档》补充的业务表也按“租户库内表”设计,不加 `tenant_id`。所有新业务表仍统一要求:
|
||||
|
||||
```sql
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
```
|
||||
|
||||
例如好友申请:
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_friend_requests (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
request_id CHAR(26) NOT NULL,
|
||||
applicant_id BIGINT NOT NULL,
|
||||
target_id BIGINT NOT NULL,
|
||||
message VARCHAR(256),
|
||||
status SMALLINT NOT NULL,
|
||||
handled_by BIGINT,
|
||||
handled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_im_friend_requests_request_id (request_id),
|
||||
KEY idx_im_friend_requests_target_status (target_id, status, created_at),
|
||||
KEY idx_im_friend_requests_applicant_status (applicant_id, status, created_at)
|
||||
);
|
||||
```
|
||||
|
||||
## 7. 文件和对象存储
|
||||
|
||||
数据库独立库后,文件元数据在租户库内隔离,但对象存储路径仍建议带租户目录:
|
||||
|
||||
```text
|
||||
tenants/{tenant_id}/yyyy/mm/{file_id}
|
||||
```
|
||||
|
||||
对象存储通常仍是共享 Bucket。带租户目录便于权限策略、容量统计、清理和迁移。
|
||||
|
||||
文件下载流程:
|
||||
|
||||
1. HTTP Token 解析得到 `tenant_id` 和 `uid`;
|
||||
2. 根据 `tenant_id` 选择租户业务库;
|
||||
3. 在该库查 `fileuploads` 和 `filemsglinks`;
|
||||
4. 校验当前用户是否有 Topic 或消息访问权;
|
||||
5. 返回文件或预签名 URL。
|
||||
|
||||
## 8. 测试策略
|
||||
|
||||
需要把旧共享表测试改成独立库测试:
|
||||
|
||||
| 用例 | 准备 | 预期 |
|
||||
| --- | --- | --- |
|
||||
| 企业码解析 | 控制库有 A、B 两个租户 | A、B 返回不同 `tenant_id` 和不同 `db_name` |
|
||||
| 同手机号注册 | A、B 租户库分别注册同手机号 | 两边都成功 |
|
||||
| 登录选库 | A 用户用 B 企业码登录 | B 库查不到该用户,登录失败 |
|
||||
| Token 选库 | A Token 请求文件接口 | 只访问 A 租户库 |
|
||||
| 内存 Topic 隔离 | A、B 都使用 `sys` | Hub 中是两个 TopicKey |
|
||||
| 租户停用 | 控制库把 A 设为 suspended | 新登录失败 |
|
||||
| 租户库不可用 | A 的 DSN 错误或库不存在 | A 请求失败,B 不受影响 |
|
||||
| 连接池复用 | 多次访问 A | 只创建一个 A 租户 adapter,后续复用 |
|
||||
| 配额检查 | A 配置 max_users=1 | A 超额注册失败,B 不受影响 |
|
||||
|
||||
## 9. 对旧方案的处理
|
||||
|
||||
以下内容停止继续开发:
|
||||
|
||||
1. 给核心业务表增加 `tenant_id`;
|
||||
2. 把唯一索引改成 `(tenant_id, xxx)`;
|
||||
3. 所有业务 SQL 加 `WHERE tenant_id=?`;
|
||||
4. 共享库内的数据迁移、回填、复合外键改造;
|
||||
5. 产品业务表中的 `tenant_id` 字段设计。
|
||||
|
||||
以下内容继续保留:
|
||||
|
||||
1. 企业码识别;
|
||||
2. 控制库 `im_tenant`、`im_tenant_config`;
|
||||
3. `TenantID` 类型;
|
||||
4. Session TenantID;
|
||||
5. Token TenantID;
|
||||
6. Hub、缓存、集群、Push、插件上下文中的 TenantID;
|
||||
7. `TenantAdapterFor` 选库机制。
|
||||
|
||||
## 10. 推荐下一步
|
||||
|
||||
下一步代码改造建议从 MySQL 层开始:
|
||||
|
||||
1. 把 `server/db/mysql/schema.sql` 拆为控制库 schema 和租户业务库 schema;
|
||||
2. 把 `tenant_adapter.go` 改成路由包装,不再写 `tenant_id` SQL;
|
||||
3. 恢复或复用原 `adapter.go` 中无租户 SQL 作为租户库真实执行逻辑;
|
||||
4. 修改 MySQL 测试,创建两个独立业务库验证隔离;
|
||||
5. 再继续补产品业务表,所有业务表按租户库内表设计。
|
||||
|
||||
## 11. 当前代码落地状态
|
||||
|
||||
2026-08-13 已开始按独立库方案改造 MySQL 底座:
|
||||
|
||||
1. 默认业务库为 `tinode`;
|
||||
2. 控制库为 `hotgo`;
|
||||
3. `CreateDb` 会在 `hotgo` 创建 `im_tenant`、`im_tenant_config`,默认租户 `id=1/code=default/db_name=tinode`;
|
||||
4. `CreateDb` 会在 `tinode` 创建核心业务表,核心表不再包含 `tenant_id`;
|
||||
5. `TenantAdapterFor` 开启路由后默认租户也会从控制库解析,不再直接退回根连接;
|
||||
6. 租户库 adapter 禁用二次路由,避免递归;
|
||||
7. 用户、认证、凭证、Topic、Subscription、消息、设备、文件等核心 Tenant 方法已改为“先选库,再调用租户库内无 tenant_id 方法”;
|
||||
8. 旧共享表 `tenant_id` 迁移 SQL 已标记废弃。
|
||||
|
||||
剩余项:
|
||||
|
||||
1. 产品扩展表和 CRUD,例如好友、收藏、实名、青少年、系统通知,还需要继续从 `tenant_id` SQL 改成租户库内无 `tenant_id` SQL;
|
||||
2. `server/db/mysql/schema.sql` 仍是参考文件,已标注正在从旧共享表草稿拆分,后续应拆成 `registry_schema.sql` 和 `tenant_schema.sql`;
|
||||
3. 旧版本 `UpgradeDb` 迁移链仍包含共享表历史步骤。因为当前项目不考虑历史数据,优先使用新建库 `CreateDb`;如后续需要升级老库,应单独重写迁移链。
|
||||
@@ -1,377 +0,0 @@
|
||||
# IM 现有实现与新需求差异对照总结
|
||||
|
||||
> 对照日期:2026-08-10
|
||||
> 新需求来源:[IM 产品原型需求文档](IM-产品原型需求文档.md)
|
||||
> 详细改造分析:[IM 产品需求服务端改造分析报告](IM-产品需求服务端改造分析报告.md)
|
||||
> 对照对象:`chat-0.25.3` 当前服务端源码
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文从“现有服务端已经实现了什么”出发,对照新产品需求逐项给出处理结论:
|
||||
|
||||
- **保留**:现有实现已基本满足需求,只补配置或测试;
|
||||
- **需修改**:现有能力可复用,但协议、数据模型、权限或流程不完整;
|
||||
- **新增**:现有系统没有对应实现,需要新增模块;
|
||||
- **客户端为主**:主要是移动端交互,服务端只提供状态、配置或资源;
|
||||
- **废弃**:现有产品能力或接入方式在新需求中没有对应章节,不再作为新产品能力继续建设。
|
||||
|
||||
“新需求无直接章节”的数据库、对象存储、集群、监控等运行基础设施不按产品功能废弃,而标记为“保留基础设施”。真正需要废弃的能力单列在第 5 节。
|
||||
|
||||
## 2. 总体结论
|
||||
|
||||
当前系统可以继续作为新产品的即时通讯内核,不需要重写。现有 P2P Topic、群 Topic、Subscription ACL、消息序号、消息广播、回执、文件上传、Push 和 WebRTC 信令均可复用。
|
||||
|
||||
新需求与现状的主要关系如下:
|
||||
|
||||
| 新需求章节 | 功能域 | 当前覆盖程度 | 结论 |
|
||||
| --- | --- | --- | --- |
|
||||
| 第 1 章 | 账号、企业码、短信、实名、独立密码 | 部分覆盖 | 认证内核保留,产品流程需修改,企业码/实名需新增 |
|
||||
| 第 2 章 | 会话首页、网络状态、消息操作、系统消息 | 部分覆盖 | 会话与连接保留,产品状态和系统通知需修改 |
|
||||
| 第 3 章 | 单聊、好友状态、回执、媒体和名片 | 部分覆盖 | P2P/回执保留,好友约束和结构化消息需修改 |
|
||||
| 第 4 章 | 输入、@、语音、文件、收藏、转发 | 少量覆盖 | 文件和通用消息可复用,大部分产品语义需新增 |
|
||||
| 第 5 章 | 通讯录、好友申请、主页、黑名单 | 少量覆盖 | 用户搜索可改造,关系状态机需新增 |
|
||||
| 第 6 章 | 群创建、审批、公告、管理、禁言、群文件 | 部分覆盖 | 群和 ACL 保留,群治理业务需新增 |
|
||||
| 第 7 章 | 收藏、下载、聊天记录搜索 | 少量覆盖 | `slf` 和文件可复用,授权和索引必须修改 |
|
||||
| 第 8 章 | 语音/视频通话 | 部分覆盖 | P2P WebRTC 信令保留,通话业务状态需修改 |
|
||||
| 第 9 章 | 组织、会议、日报、月报、信箱 | 未覆盖 | 全部新增业务模块 |
|
||||
| 第 10 章 | 资料、设置、版本、隐私、青少年模式 | 少量覆盖 | 通用资料可改造,其余多数新增或客户端实现 |
|
||||
| 第 11 章 | 反诈、举报、反馈 | 未形成业务闭环 | 插件机制保留,业务模型和处置流程新增 |
|
||||
|
||||
必须优先处理三个基础问题:
|
||||
|
||||
1. 消息发布缺少客户端消息 ID 幂等校验,断线重试可能产生重复消息;
|
||||
2. 文件下载只校验登录,没有校验用户对文件关联 Topic/消息的读取权限;
|
||||
3. 当前核心表没有 `tenant_id`,共享数据库部署多个企业前必须完成全链路租户隔离。
|
||||
|
||||
## 3. 现有核心实现与新需求关系
|
||||
|
||||
| 现有实现 | 源码位置 | 对应新需求章节 | 结论 | 修改说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| basic 用户名/密码认证 | `server/auth/basic` | 1.2、1.3、1.4、1.6 | 需修改 | 保留认证器,增加协议确认、密码策略、错误次数、锁定、找回密码编排和旧 Token 撤销 |
|
||||
| HMAC Token 登录 | `server/auth/token` | 1.2、1.6;2.2 | 保留 | 继续作为登录态;补设备会话、单设备撤销和密码重置后的 Token 失效 |
|
||||
| 电话号码及短信验证器 | `server/validate/tel` | 1.4、1.5、1.6、1.7 | 需修改 | 保留验证码能力,增加用途隔离、发送频控、`retry_after`、一次性验证票据和审计 |
|
||||
| REST 外部认证器 | `server/auth/rest` | 1.1、1.2 | 需修改 | 可连接企业账号中心;企业码解析仍需新增独立入口,认证后绑定企业上下文 |
|
||||
| Session/WebSocket 协议分发 | `server/session.go` | 2.2;第 3、4、8 章 | 保留 | 作为 App 的主实时入口;补幂等、产品策略和结构化错误 |
|
||||
| Topic 消息顺序、保存和广播 | `server/topic.go` | 第 2、3、4、6、7 章 | 需修改 | 保留核心循环;持久化前增加好友、黑名单、禁言、青少年和内容风险校验 |
|
||||
| P2P Topic | `server/topic.go` | 3.1-3.12;5.2-5.14 | 需修改 | 保留单聊容器;是否允许创建、发送和呼叫改由好友/黑名单/隐私策略控制 |
|
||||
| Group Topic | `server/topic.go` | 6.1-6.28;4.2-4.4 | 需修改 | 保留群消息和成员基础;增加审批、公告、二维码、禁言和群文件业务层 |
|
||||
| Subscription ACL | `server/store/types/types.go`、`server/topic.go` | 3.1-3.8;6.4-6.26 | 需修改 | 保留 Join/Read/Write/Admin/Owner;不能用 ACL 代替好友申请、待审批和禁言记录 |
|
||||
| Presence、输入状态 | `server/topic.go`、`server/datamodel.go` | 2.1;3.1-3.4;4.9 | 需修改 | 在线/输入状态可复用;增加隐私控制、节流和过期清理 |
|
||||
| `recv`、`read` 回执 | `server/topic.go` | 3.4、3.7 | 保留 | 定义客户端状态映射;群聊是否显示具体已读人由产品规则控制 |
|
||||
| 通用 JSON `head/content` 与 Drafty | `server/datamodel.go`、`server/drafty` | 3.6、3.10-3.12;4.1-4.20 | 需修改 | 增加消息类型、schema 版本、字段白名单、引用、@、转发和审核状态,不再允许各端自定义语义 |
|
||||
| 消息删除 | `server/session.go`、`server/topic.go` | 2.4;3.8;4.15 | 需修改 | 明确仅自己删除、全员撤回、撤回期限、管理员权限和审计,不直接复用模糊的硬删除语义 |
|
||||
| `slf` 自收藏 Topic | `server/store/types/types.go`、`server/topic.go` | 4.16;7.1-7.8 | 需修改 | 继续保存收藏消息,同时增加收藏索引、来源、去重、类型筛选和源消息删除策略 |
|
||||
| 文件上传与消息附件关联 | `server/hdl_files.go`、`server/store/store.go` | 3.6、3.10-3.12;4.10-4.20;6.27-6.28;7.3、7.7-7.11 | 需修改 | 保留上传;增加附件所有权、病毒扫描、群文件索引、短时下载 URL 和资源级授权 |
|
||||
| Push Handler | `server/push.go`、`server/push` | 2.1、2.5;4.4;8.1-8.11;9.3-9.4;10.18 | 需修改 | 保留 Push;扩展 @、审批、会议、通话和安全通知,增加锁屏内容隐私设置 |
|
||||
| P2P WebRTC 信令 | `server/calls.go` | 8.1-8.11 | 需修改 | 保留 Invite/Offer/Answer/ICE/Hang-up;新增 `call_id`、设备抢答、结束原因、详单和 TURN 指标 |
|
||||
| Plugin Firehose | `server/plugins.go` | 11.1-11.4;2.5 | 需修改 | 用于外部内容审核和反诈;增加超时、熔断、失败策略和审核结果落库 |
|
||||
| User Public/Trusted/Tags | `server/store/types/types.go` | 5.1、5.5-5.8;10.1-10.6 | 需修改 | 可保存基础展示资料;生日、可见性、实名和组织关系应使用明确字段/业务表 |
|
||||
| Topic Public/Trusted/Aux/Tags | `server/store/types/types.go` | 6.10-6.16 | 需修改 | 可保存群基础展示资料;公告、审批、二维码和禁言不能只存任意 JSON |
|
||||
| `fnd` 用户/Topic 查找 | `server/topic.go` | 5.1、5.3-5.5 | 需修改 | 改成租户内精准 ID/受控搜索;执行隐私策略并防止手机号、用户 ID 枚举 |
|
||||
| `sys` 系统 Topic | `server/store/types/types.go`、`server/topic.go` | 2.5;第 9、11 章通知 | 需修改 | 作为可信系统消息通道;定义消息类型、模板版本、跳转参数和去重键 |
|
||||
| MySQL 核心表和 Adapter | `server/db/mysql` | 所有服务端需求的存储基础 | 需修改 | schema 118 已建立 `im_tenant`;仍需为核心表增加 tenant_id,并新增产品表、幂等、搜索索引和文件授权查询 |
|
||||
| 文件系统/S3 媒体后端 | `server/media/fs`、`server/media/s3` | 第 3、4、6、7、11 章附件 | 保留基础设施 | 生产使用 S3/兼容对象存储,本地文件系统仅开发;补扫描和私有签名地址 |
|
||||
|
||||
## 4. 按新需求章节逐项对照
|
||||
|
||||
### 4.1 第 1 章:账号注册、登录与身份安全
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1.1 | 企业码登录 | 无企业/租户模型 | 新增 | 增加企业码解析 API、企业状态、服务地址和认证策略;共享库模式需先加 `tenant_id` |
|
||||
| 1.2-1.3 | 账号密码登录及错误提示 | basic + token 已有 | 需修改 | 增加协议确认、统一错误码、失败计数、账号锁定、设备登录记录和安全通知 |
|
||||
| 1.4-1.5 | 短信注册和倒计时 | 电话验证器已有 | 需修改 | 新增注册流程 API;验证码按用途隔离,返回有效期和重发时间,服务端强制限频 |
|
||||
| 1.6 | 忘记密码 | 验证器与密码更新能力分散 | 需修改 | 建立“发码 → 临时票据 → 重置”状态机;成功后撤销旧 Token |
|
||||
| 1.7 | 敏感操作二次验证 | 无统一业务流程 | 新增 | 增加验证挑战、一次性票据、有效期、错误次数和操作绑定 |
|
||||
| 1.8-1.10 | 实名认证及状态提醒 | 无 | 新增 | 独立实名表、字段加密、审核状态、脱敏查询、审核回调和提醒消息 |
|
||||
| 1.11 | 四位独立密码 | 无 | 新增 | 保存强哈希、失败锁定和找回流程;禁止写入普通设置 JSON 或明文日志 |
|
||||
|
||||
### 4.2 第 2 章:消息首页、全局导航与系统通知
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 2.1 | 会话列表、摘要、未读数 | Subscription/Topic 已有基础 | 需修改 | 增加置顶、免打扰、隐藏、草稿和结构化消息摘要;文件助手使用固定系统 Topic |
|
||||
| 2.2 | 断网、重连、增量同步 | Session 与序号拉取已有 | 需修改 | 明确同步游标、重连退避、重复消息幂等和连接状态指标 |
|
||||
| 2.3 | 快捷菜单 | 无服务端页面逻辑 | 客户端为主 | 服务端只返回能力/权限开关,菜单和关闭交互由客户端实现 |
|
||||
| 2.4 | 引用、复制、收藏、编辑、转发、撤回、删除、多选、群回执 | 仅删除、通用消息、回执基础 | 需修改 | 增加引用/编辑/转发协议,撤回规则和批量操作校验;复制/多选 UI 在客户端 |
|
||||
| 2.5 | 新设备登录等系统消息 | `sys` Topic 可用,无完整事件生产 | 需修改 | 建立可信系统账号、设备/IP 审计事件、模板、跳转参数和防伪造规则 |
|
||||
|
||||
### 4.3 第 3 章:单聊会话、会话建立与消息状态
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 3.1-3.5 | 好友申请前后单聊状态 | P2P Topic 有,无好友状态机 | 新增/修改 | 新增好友申请和关系表;申请通过后再开放 Topic Write 与媒体能力 |
|
||||
| 3.4、3.7 | 发送中、送达、已读、失败 | 消息序号和 read/recv 已有 | 需修改 | 增加客户端消息 ID、稳定状态映射和业务失败原因;拉黑消息不得持久化给对方 |
|
||||
| 3.6 | 图片/语音/视频进度 | 文件上传与消息可用 | 需修改 | 统一 `file_id`、消息类型和上传完成确认;进度主要由客户端维护 |
|
||||
| 3.7-3.8 | 拉黑、删除好友后的发送限制 | 无关系策略 | 新增 | 在建会话、发布、通话、名片和文件入口统一执行关系策略 |
|
||||
| 3.9 | 键盘收起布局 | 无服务端职责 | 客户端为主 | 服务端无需修改 |
|
||||
| 3.10-3.11 | 多类型文件/混合消息 | 通用 JSON + 文件已有 | 需修改 | 定义 text/image/voice/video/file 类型和字段校验,禁止任意 URL |
|
||||
| 3.12 | 名片消息 | 无标准类型 | 新增 | 增加 `contact_card`,只传用户 ID 与安全快照,打开时重新检查资料权限 |
|
||||
|
||||
### 4.4 第 4 章:消息输入、媒体内容与消息操作
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 4.1 | 键盘、草稿 | 无专用服务端草稿 | 客户端为主 | 草稿默认本地;如需多端同步,增加每用户每 Topic 的草稿设置 |
|
||||
| 4.2-4.4 | @成员、@所有人 | 无结构化 @语义 | 新增 | 消息保存稳定用户 ID;校验群成员和 @所有人权限,生成定向未读与 Push |
|
||||
| 4.5-4.6 | 表情面板和表情消息 | Drafty 可保存文本 | 保留/客户端为主 | Unicode 表情按文本发送;独立表情包需要资源 ID 和版本,不保存本地路径 |
|
||||
| 4.7-4.12 | 录音、图片/语音/视频上传与展示 | 文件和通用消息基础已有 | 需修改 | 增加媒体类型、时长/尺寸/缩略图、扫描状态;录音交互和播放进度由客户端实现 |
|
||||
| 4.13-4.14 | 文件上传和文件卡片 | 已有上传与附件关联 | 需修改 | 补格式/大小策略、所有权、病毒扫描、可预览信息和安全下载 |
|
||||
| 4.15 | 消息多选批量操作 | 无服务端批量语义 | 需修改 | 客户端选择;服务端逐条鉴权,限制批量数量并返回逐项结果 |
|
||||
| 4.16 | 批量收藏 | `slf` 可承载 | 需修改 | 增加收藏唯一约束、来源索引、类型和批量幂等 |
|
||||
| 4.17-4.19 | 单选/多目标逐条转发 | 无标准转发模型 | 新增 | 校验每个目标发送权和附件权;限制目标数;每条新消息使用独立幂等 ID |
|
||||
| 4.20 | 合并转发 | 无 | 新增 | 创建不可变 `merged_forward` 快照,限制子项和总大小,清理会话 ID 与敏感字段 |
|
||||
|
||||
### 4.5 第 5 章:通讯录、好友关系与黑名单
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 5.1 | 好友通讯录和企业成员 | `fnd`/Tags 仅提供基础查找 | 需修改 | 新增好友关系和组织成员查询;备注优先;支持字母/拼音索引 |
|
||||
| 5.2 | 新朋友列表与审批 | 无 | 新增 | 新增好友申请状态机、幂等接受/拒绝、过期和系统通知 |
|
||||
| 5.3-5.5 | ID、扫码添加好友 | 基础查找可部分复用 | 需修改 | 限定租户内精准搜索;二维码使用签名短 Token;执行隐私和反枚举策略 |
|
||||
| 5.6 | 好友验证留言 | 无明确模型 | 新增 | 好友申请表保存验证信息、来源、过期时间和风控结果 |
|
||||
| 5.7-5.8 | 好友/非好友个人主页 | Public/Trusted 可显示基础资料 | 需修改 | 根据关系、企业和隐私设置返回不同字段,不让客户端自行裁剪敏感资料 |
|
||||
| 5.9-5.10 | 拉黑开关 | 无完整黑名单模型 | 新增 | 关系表记录单方 block;立即影响消息、搜索、呼叫和资料访问 |
|
||||
| 5.11 | 删除好友 | 无好友关系真源 | 新增 | 事务更新双方关系;默认保留历史但禁止继续发送,规则需产品确认 |
|
||||
| 5.12-5.14 | 黑名单列表及提示 | 无 | 新增 | 提供黑名单查询、解除接口和统一业务错误,不向被拉黑人泄露过多信息 |
|
||||
|
||||
### 4.6 第 6 章:群聊创建、资料维护与成员治理
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 6.1-6.2 | 群列表和空状态 | Group Topic 已有 | 需修改 | 复用订阅列表;增加群业务状态、群头像摘要和已解散处理 |
|
||||
| 6.3-6.6 | 加群、邀请与审批 | ACL 可邀请成员,但无申请状态机 | 新增/修改 | 新增入群申请/邀请表;审批通过后才更新 Subscription,写 Outbox 通知 |
|
||||
| 6.7-6.9 | 群公告和已读 | 无公告模型 | 新增 | 公告主表、版本、发布权限、置顶和成员已读表;变更通过群系统消息通知 |
|
||||
| 6.10-6.16 | 群设置、资料、头像、昵称、简介、标识、二维码 | Public/Aux/Private 可部分承载 | 需修改 | 定义可索引字段和修改权限;二维码新增签名 Token、有效期、使用次数和撤销 |
|
||||
| 6.17-6.22 | 管理员、群主转让 | Admin/Owner ACL 已有 | 需修改 | 复用权限位;补管理员上限、二次确认、操作审计和可靠通知 |
|
||||
| 6.23-6.24 | 成员禁言 | 当前通知静音不等于发言禁用 | 新增 | 新增全员/成员禁言规则,在消息持久化前强制执行,定义管理员豁免 |
|
||||
| 6.25-6.26 | 选择新群主及确认 | 已有 Owner 转移逻辑 | 需修改 | 增加风险校验、幂等、候选人有效性、二次确认和审计 |
|
||||
| 6.27-6.28 | 群文件及上传 | 文件消息可用,无群文件目录 | 新增/修改 | 建群文件索引,支持类型/时间/上传者查询和管理员删除,复用安全上传链路 |
|
||||
|
||||
### 4.7 第 7 章:文件下载、收藏管理与聊天记录检索
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 7.1-7.8 | 收藏分类、查看和分享 | `slf` Topic 只有基础容器 | 需修改 | 增加收藏索引、类型、去重和源消息引用;分享时重新执行附件和目标会话权限 |
|
||||
| 7.9-7.11 | 文件下载状态 | 已有 HTTP 下载 | 必须修改 | 下载前检查上传者或关联 Topic Read ACL;使用短时签名 URL;进度/完成状态在客户端 |
|
||||
| 7.12 | 关键词搜索并定位上下文 | 现有查询只按序号范围 | 新增 | 建消息搜索索引,返回 Topic/SeqId 后重新检查 ACL,并支持上下文拉取 |
|
||||
| 7.13-7.16 | 文件、链接、图片、视频分类检索 | 无类型索引 | 新增 | 建 `message_type`、`mime_group`、file/link 字段和游标分页,支持历史回填 |
|
||||
|
||||
### 4.8 第 8 章:实时语音与视频通话
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 8.1-8.4 | 邀请、等待、接听、语音通话 | P2P WebRTC 信令已有 | 需修改 | 增加独立 `call_id`、呼叫类型、响铃/接听时间、忙线/拒接/超时原因和通话记录 |
|
||||
| 8.5-8.6 | 麦克风与扬声器状态 | 信令可传临时事件 | 客户端为主 | 本地媒体控制由客户端负责,仅在对端需要展示时同步状态 |
|
||||
| 8.7-8.10 | 视频等待、摄像头关闭、视频中 | WebRTC Offer/Answer/ICE 已有 | 需修改 | 增加视频类型和摄像头状态事件;配置 TURN,监控 ICE 成功率 |
|
||||
| 8.11 | 通话悬浮状态 | 无服务端职责 | 客户端为主 | 服务端保持呼叫会话和重连状态,悬浮窗由客户端实现 |
|
||||
|
||||
当前实现仅适合一对一通话。新需求没有群音视频章节,因此首期不新增 SFU 或群会议通话。
|
||||
|
||||
### 4.9 第 9 章:企业工作台与办公协同
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 9.1 | 工作台入口 | 无业务模块 | 新增 | 新增工作台应用配置和用户可见权限;页面布局由客户端实现 |
|
||||
| 9.2 | 部门组织架构 | 无 | 新增 | 新建组织单元和成员表,支持树、搜索、主部门、职位、在职状态和数据范围 |
|
||||
| 9.3-9.4 | 会议通知 | 仅能发送普通消息 | 新增 | 新建会议和参与人模型;创建/变更/取消通过 Outbox 发送系统消息和定时提醒 |
|
||||
| 9.5-9.8 | 日报及普通/管理员视图 | 无 | 新增 | 建报告表和提交状态机;管理员查询按组织数据权限过滤 |
|
||||
| 9.9-9.11 | 月报及详情 | 无 | 新增 | 复用报告模型,以类型/周期区分;增加汇总和附件能力 |
|
||||
| 9.12-9.13 | 总经理信箱 | 无 | 新增 | 建信箱工单、匿名展示、附件、回复和审计;匿名对业务角色隐藏但保留受控追溯 |
|
||||
|
||||
工作台 CRUD 建议走 `/v1/im/*` HTTP API,状态提醒走系统 Topic,不把报告和组织树直接塞进聊天消息协议。
|
||||
|
||||
### 4.10 第 10 章:个人资料、通用设置、隐私与青少年模式
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 10.1-10.5 | 我的、个人信息、昵称、生日、签名 | Public/Trusted 有通用字段 | 需修改 | 定义资料字段、格式、可见性、敏感词、变更版本和审计;实名信息单独存储 |
|
||||
| 10.6 | 个人二维码 | 无标准 Token | 新增 | 生成短时签名 Token,扫码后仍执行租户、隐私和账号状态校验 |
|
||||
| 10.7-10.8 | iOS/Android 设置页 | 无服务端页面逻辑 | 客户端为主 | 服务端提供用户设置和能力开关 |
|
||||
| 10.9 | 清空缓存 | 不属于服务端数据删除 | 客户端为主 | 只清理设备缓存,不删除云端消息;无需服务端接口 |
|
||||
| 10.10-10.11 | 版本更新 | 无版本配置服务 | 新增 | 提供平台/渠道最低版本、推荐版本、强更开关、下载地址和文案 API |
|
||||
| 10.12-10.13 | 隐私设置及添加方式 | 无统一隐私模型 | 新增 | 建隐私设置表,并在搜索、加好友、资料和在线状态接口中强制执行 |
|
||||
| 10.14-10.17 | 青少年模式 | 无 | 新增 | 服务端保存启用状态、PIN 强哈希、失败锁定和策略版本;所有关键入口强制检查 |
|
||||
| 10.18 | 推送权限 | Push Token 能力已有 | 需修改/客户端为主 | 系统授权由客户端申请;服务端保存 Token、开关、免打扰和预览策略 |
|
||||
| 10.19-10.21 | 提醒、相机、麦克风说明 | 无服务端职责 | 客户端为主 | 权限说明和系统授权由客户端实现,可由配置服务下发版本化文案 |
|
||||
|
||||
### 4.11 第 11 章:反诈安全、举报与意见反馈
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 11.1-11.2 | 反诈一次/二次提示 | Plugin 可拦截消息,无产品策略 | 新增/修改 | 建风险事件和策略版本;同步硬规则本地执行,外部审核设置超时/熔断;客户端按等级弹窗 |
|
||||
| 11.3-11.4 | 举报提交和分类 | 无举报工单 | 新增 | 建举报、证据、处理状态和审计;服务端在有权限时固化消息快照与哈希 |
|
||||
| 11.5 | 意见反馈 | 无 | 新增 | 建反馈工单、分类、附件、联系方式和处理状态;附件复用私有文件授权 |
|
||||
|
||||
## 5. 新需求未覆盖的现有能力:废弃清单
|
||||
|
||||
以下能力在新需求文档中没有对应章节。按新产品范围标注为“废弃”,但应采用“停止新接入 → 观察存量 → 迁移 → 删除”的方式处理,不能在未确认线上使用情况时直接删除源码或数据。
|
||||
|
||||
| 现有能力 | 源码/配置 | 新需求章节 | 处理结论 | 废弃方式 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 匿名登录 | `server/auth/anon` | 无 | **废弃** | 新环境不配置、不向客户端公开;确认无匿名会话后移除注册和文档 |
|
||||
| 邮箱注册/邮箱验证码/邮箱找回 | `server/validate/email` | 无,新需求只定义手机号短信 | **废弃** | 产品 API 不再暴露 email credential;存量账号先绑定手机号再停用 |
|
||||
| Channel 只读广播 Topic | `TopicCatGrp` 的 channel 模式 | 无 | **废弃** | 禁止新建 Channel;如有存量,迁移为普通群或公告业务后再移除入口 |
|
||||
| Long Poll 客户端接入 | `/v0/channels/lp` | 无,新移动端按 WebSocket 设计 | **废弃** | 先记录调用量并公告;无流量后关闭路由,WebSocket 保留 |
|
||||
| 客户端 gRPC 接入 | `grpc_listen`、客户端 gRPC 服务 | 无 | **废弃** | App 不再接入;注意保留插件所需的 gRPC 能力,两者不能一起误删 |
|
||||
| 示例 Python Chat Bot | `tinode.conf` 中 `python_chat_bot` | 无 | **废弃** | 从生产配置删除;插件框架因第 11 章风控继续保留 |
|
||||
| 静态网页托管 | `main.go` 静态文件路由/配置 | 无 Web 产品章节 | **废弃** | 新产品服务端不托管 Web UI;确认运维页面用途后关闭 |
|
||||
| stdout Push Handler | `server/push/stdout` | 无生产需求 | **废弃(生产)** | 仅允许本地测试使用,生产配置删除;FCM/TNPG 等真实 Push 保留 |
|
||||
| PostgreSQL Adapter | `server/db/postgres` | 无数据库兼容要求 | **废弃(产品版候选)** | 若确认生产统一 MySQL,则停止产品功能适配和验收;删除前核查部署 |
|
||||
| MongoDB Adapter | `server/db/mongodb` | 无数据库兼容要求 | **废弃(产品版候选)** | 同上,不继续为新增 `im_*` 业务表实现双写逻辑 |
|
||||
| RethinkDB Adapter | `server/db/rethinkdb` | 无数据库兼容要求 | **废弃(产品版候选)** | 同上;保留上游代码还是物理删除由仓库维护策略决定 |
|
||||
|
||||
以下能力虽然没有独立的新需求章节,但属于满足新需求所需的技术底座,不废弃:
|
||||
|
||||
| 基础能力 | 保留原因 |
|
||||
| --- | --- |
|
||||
| `me` Topic 和设备同步 | 会话列表、未读数、多端状态需要 |
|
||||
| `fnd` 查找 Topic | 第 5 章精准用户搜索可在其基础上收紧 |
|
||||
| `sys` Topic | 第 2、9、11 章的可信系统通知需要 |
|
||||
| `slf` Topic | 第 4、7 章收藏同步可复用 |
|
||||
| gRPC Plugin/Firehose | 第 11 章内容审核和反诈外部服务需要 |
|
||||
| FCM/TNPG Push | 第 2、4、8、9、10 章离线提醒需要,生产只选择实际使用的 Handler |
|
||||
| 文件系统/S3 Media Adapter | 第 3、4、6、7、11 章附件需要,按环境选择 |
|
||||
| 集群、缓存、监控、TLS | 属于可用性和安全基础,不是产品页面功能 |
|
||||
| root/后台管理能力 | 实名审核、举报处置、组织管理和审计需要,但必须收紧权限 |
|
||||
|
||||
## 6. 需要新增的服务端模块
|
||||
|
||||
| 新模块 | 对应新需求章节 | 主要职责 |
|
||||
| --- | --- | --- |
|
||||
| `imapi` | 第 1、5、6、7、9、10、11 章 | HTTP API、认证中间件、输入校验、统一错误和游标分页 |
|
||||
| `impolicy` | 3.7-3.8;5.9-5.14;6.23-6.24;10.12-10.17 | 好友、黑名单、禁言、隐私和青少年同步授权 |
|
||||
| `immessage` | 2.4;3.6、3.10-3.12;第 4 章 | 消息类型、schema 校验、@、引用、编辑、转发和摘要 |
|
||||
| `imbiz/relationship` | 第 5 章 | 好友申请、联系人、备注、拉黑和删除状态机 |
|
||||
| `imbiz/group` | 6.3-6.28 | 群审批、公告、二维码、角色、禁言和群文件 |
|
||||
| `imbiz/identity` | 1.7-1.10 | 二次验证、实名认证、审核和脱敏 |
|
||||
| `imbiz/workbench` | 第 9 章 | 组织、会议、日报、月报和信箱 |
|
||||
| `imbiz/safety` | 第 11 章 | 风险事件、举报证据、处置、反馈和审计 |
|
||||
| `imsearch` | 7.12-7.16 | 消息索引、历史回填、检索和 ACL 二次过滤 |
|
||||
| `imnotify` | 2.5;第 5、6、9、11 章 | Outbox 消费、系统消息和 Push 通知 |
|
||||
| `imjobs` | 1.5;5.2;6.3-6.6;9.3-9.4 | 申请过期、会议提醒、重试、清理和索引回填 |
|
||||
|
||||
## 7. 主要数据修改
|
||||
|
||||
### 7.1 核心表必须修改
|
||||
|
||||
| 对象 | 对应新需求章节 | 修改内容 |
|
||||
| --- | --- | --- |
|
||||
| Message | 2.2、2.4;第 3、4、7 章 | 增加或关联客户端幂等 ID、类型、审核状态、搜索索引;重复发送返回同一 SeqId |
|
||||
| FileDef/File links | 第 3、4、6、7、11 章 | 增加扫描状态、业务用途和访问查询;下载按消息/Topic ACL 授权 |
|
||||
| User | 1.1-1.10;5.1;9.2;第 10 章 | 共享 SaaS 时加 tenant;基础资料字段规范化,敏感身份数据独立存储 |
|
||||
| Topic/Subscription | 第 2、3、5、6 章 | 共享 SaaS 时加 tenant;会话个性设置、群策略和关系状态使用独立业务表 |
|
||||
|
||||
### 7.2 新增业务表
|
||||
|
||||
| 数据域 | 建议表 | 对应新需求章节 |
|
||||
| --- | --- | --- |
|
||||
| 好友与会话 | `im_user_relations`、`im_friend_requests`、`im_conversation_settings`、`im_privacy_settings` | 第 2、3、5 章;10.12-10.13 |
|
||||
| 群治理 | `im_group_settings`、`im_group_join_requests`、`im_group_mutes`、`im_group_announcements`、`im_group_announcement_reads`、`im_group_invite_tokens` | 第 6 章 |
|
||||
| 消息与收藏 | `im_message_idempotency`、`im_message_index`、`im_favorites` | 2.2、2.4;第 4、7 章 |
|
||||
| 身份与设置 | `im_identity_verifications`、`im_user_settings`、`im_teen_mode` | 第 1、10 章 |
|
||||
| 企业办公 | `im_org_units`、`im_org_members`、`im_meetings`、`im_meeting_participants`、`im_work_reports`、`im_mailbox_messages` | 第 9 章 |
|
||||
| 安全治理 | `im_reports`、`im_report_evidence`、`im_feedback`、`im_risk_events`、`im_audit_logs` | 第 11 章 |
|
||||
| 可靠事件 | `im_outbox`、`im_idempotency_keys` | 第 5、6、9、11 章的审批和通知 |
|
||||
|
||||
共享数据库的多企业模式下,核心表和全部 `im_*` 表都必须包含 `tenant_id`,并把租户放入唯一索引、查询条件、缓存键和文件路径。若采用一企一实例/一企一库,则企业码负责服务发现,物理隔离优先。
|
||||
|
||||
## 8. 主要代码修改位置
|
||||
|
||||
| 文件/目录 | 对应新需求章节 | 修改内容 |
|
||||
| --- | --- | --- |
|
||||
| `server/datamodel.go` | 第 2、3、4、7、8 章 | 定义 `x-im-schema`、消息类型、幂等 ID、引用、转发、搜索和通话扩展 |
|
||||
| `server/session.go` | 第 2、3、4、5、8、10、11 章 | 消息幂等、策略调用、租户上下文和统一业务错误 |
|
||||
| `server/topic.go` | 第 2、3、4、6、7 章 | 保存前执行好友/黑名单/禁言/青少年/风险规则,发送结构化系统消息 |
|
||||
| `server/calls.go` | 第 8 章 | call_id、设备抢答、标准结束原因、通话记录和指标 |
|
||||
| `server/hdl_files.go` | 第 3、4、6、7、11 章 | 抽取 HTTP 认证、资源 ACL、签名下载 URL 和扫描状态检查 |
|
||||
| `server/plugins.go` | 第 11 章 | 审核超时、熔断、失败开放/关闭策略和可观测性 |
|
||||
| `server/push.go` | 2.5;4.4;第 8、9、10、11 章 | 产品消息类型、@、会议、审批、通话和隐私 Push |
|
||||
| `server/main.go` | 第 1、5、6、7、9、10、11 章 | 注册 `/v1/im/*` API、业务服务和后台任务;逐步下线废弃路由 |
|
||||
| `server/store` | 所有服务端章节 | 新增核心事务、幂等、文件授权、搜索接口;明确产品版支持的数据库 |
|
||||
| `server/db/mysql` | 所有服务端章节 | 从当前 schema 118 继续迁移核心 tenant_id、业务表、索引和 MySQL 实现 |
|
||||
| `server/tinode.conf` | 所有章节 | 企业、产品模块、搜索、审核、TURN、文件扫描、限流和废弃能力开关 |
|
||||
|
||||
## 9. 修改优先级
|
||||
|
||||
### P0:先保证新系统不会产生安全和数据问题
|
||||
|
||||
| 工作 | 对应新需求章节 |
|
||||
| --- | --- |
|
||||
| 确定一企一实例或共享 SaaS | 1.1、5.1、9.2 |
|
||||
| 确定产品版只支持 MySQL 还是继续支持多数据库 | 所有服务端章节的存储基础 |
|
||||
| 消息客户端 ID 幂等 | 2.2;3.4、3.7;第 4 章 |
|
||||
| 文件下载资源级授权 | 3.10-3.12;4.13-4.20;6.27-6.28;7.3、7.7-7.11 |
|
||||
| 消息类型、错误码和兼容协议 | 2.4;第 3、4、7、8 章 |
|
||||
| 停止新客户端使用匿名、Long Poll、Channel 和客户端 gRPC | 新需求无对应章节,废弃项 |
|
||||
|
||||
### P1:完成即时通讯产品主链路
|
||||
|
||||
| 工作 | 对应新需求章节 |
|
||||
| --- | --- |
|
||||
| 企业码、短信注册/找回、二次验证、实名 | 第 1 章 |
|
||||
| 好友申请、联系人、隐私、黑名单 | 第 5 章;3.1-3.8 |
|
||||
| 结构化消息、@、引用、转发、收藏 | 2.4;第 3、4 章 |
|
||||
| 会话设置和可信系统通知 | 第 2 章 |
|
||||
|
||||
### P2:完成群、文件和搜索
|
||||
|
||||
| 工作 | 对应新需求章节 |
|
||||
| --- | --- |
|
||||
| 群申请、公告、管理员、禁言、二维码、群文件 | 第 6 章 |
|
||||
| 收藏分类和分享 | 7.1-7.8 |
|
||||
| 关键词和媒体类型搜索、历史回填 | 7.12-7.16 |
|
||||
|
||||
### P3:完成通话、设置和安全
|
||||
|
||||
| 工作 | 对应新需求章节 |
|
||||
| --- | --- |
|
||||
| 通话 ID、跨设备状态、TURN 和详单 | 第 8 章 |
|
||||
| 资料、版本、隐私和青少年模式 | 第 10 章 |
|
||||
| 反诈、举报、证据、反馈和后台处置 | 第 11 章 |
|
||||
|
||||
### P4:完成企业办公
|
||||
|
||||
| 工作 | 对应新需求章节 |
|
||||
| --- | --- |
|
||||
| 组织与部门 | 9.2 |
|
||||
| 会议通知 | 9.3-9.4 |
|
||||
| 日报/月报 | 9.5-9.11 |
|
||||
| 总经理信箱 | 9.12-9.13 |
|
||||
|
||||
## 10. 废弃实施规则
|
||||
|
||||
废弃项不要直接删除,应按以下顺序执行:
|
||||
|
||||
1. 在配置和对接文档中标记 deprecated,禁止新客户端接入;
|
||||
2. 为废弃入口增加调用指标,至少观察一个完整客户端版本周期;
|
||||
3. 对匿名账号、邮箱账号、Channel 等有数据的能力先做存量扫描;
|
||||
4. 给存量用户提供迁移方案,例如邮箱账号绑定手机号、Channel 迁移普通群;
|
||||
5. 新版本服务端默认关闭,保留临时回退开关;
|
||||
6. 确认无调用和无未迁移数据后,再删除路由、配置、代码和测试;
|
||||
7. 数据库删除列或表必须单独发布,保留备份和回滚窗口。
|
||||
|
||||
客户端 gRPC 和 Plugin gRPC 必须分开处理:新需求没有客户端 gRPC,因此前者废弃;第 11 章需要外部审核插件,因此 Plugin Firehose 及其 gRPC 通道保留。
|
||||
|
||||
## 11. 验收口径
|
||||
|
||||
每项改造只有同时满足以下条件才算完成:
|
||||
|
||||
1. 文档中的新需求章节有对应 API/协议、数据表和权限规则;
|
||||
2. 旧客户端兼容行为有明确结论,不依赖未定义 JSON 字段;
|
||||
3. 重试、并发、断网恢复不会产生重复业务数据;
|
||||
4. 用户不能通过直接构造协议绕过好友、黑名单、禁言、隐私和青少年策略;
|
||||
5. 文件、搜索、转发和收藏均重新执行当前用户的资源权限;
|
||||
6. 共享 SaaS 模式下通过跨租户负向测试;
|
||||
7. 审批、实名、举报、群管理和匿名信箱敏感操作有审计记录;
|
||||
8. 废弃入口有调用量证据和存量迁移结果,不能仅凭新原型没有页面就直接删除;
|
||||
9. MySQL schema、升级脚本、回滚脚本和代码中的建表定义保持一致;
|
||||
10. 服务端单元测试、MySQL 集成测试、WebSocket/HTTP 端到端测试和真实设备通话测试全部通过。
|
||||
|
||||
## 12. 最终处理建议
|
||||
|
||||
新系统应保留 Tinode 的实时消息内核,重点修改认证流程、消息协议、权限策略、文件授权和通话状态;好友、群治理、搜索、企业工作台、实名、青少年和举报作为新的产品业务层建设。
|
||||
|
||||
新需求未覆盖的匿名登录、邮箱注册验证、Channel、Long Poll、客户端 gRPC、示例聊天机器人和静态网页托管标记为废弃。多数据库 Adapter 是否物理删除取决于产品数据库决策;若产品版确定只支持 MySQL,则 PostgreSQL、MongoDB 和 RethinkDB 不再承担新业务适配与验收。
|
||||
|
||||
改造顺序应先处理租户、消息幂等和文件授权,再完成好友与基础消息,随后完成群治理、搜索、通话与安全,最后建设企业工作台。这样可以最大限度复用现有实现,同时避免继续维护新产品不需要的入口和功能。
|
||||
@@ -1,298 +0,0 @@
|
||||
# IM 第一至十一章后续增强项
|
||||
|
||||
> 日期:2026-08-13
|
||||
> 范围:第一章登录注册与安全、第二章消息首页/导航/系统通知、第三章单聊关系、第四章群消息与 @、第五章通讯录/好友/黑名单、第六章群聊创建/资料/治理、第七章文件/收藏/聊天记录检索、第八章实时语音与视频通话、第九章企业工作台与组织架构、第十章个人设置与客户端能力、第十一章反诈安全/举报/反馈
|
||||
> 用途:记录当前已完成基线之外的增强项,后续统一排期处理。
|
||||
|
||||
## 1. 第一章:登录、注册与安全
|
||||
|
||||
### 1.1 当前已完成基线
|
||||
|
||||
1. 企业码解析、租户状态校验和公开租户配置返回。
|
||||
2. 注册、登录前验证码发送与验证。
|
||||
3. 注册验证码 Token 消费,避免绕过验证码直接注册。
|
||||
4. 忘记密码挑战验证与重置确认。
|
||||
5. Token 绑定 `auth_version`,密码重置后旧 Token 失效。
|
||||
6. 登录后敏感操作二次验证挑战。
|
||||
7. 实名认证状态查询与提交,证件信息脱敏/保护存储。
|
||||
8. 独立密码设置与验证,包含弱密码规则和失败锁定。
|
||||
9. 登录后 bootstrap 返回实名、安全与菜单基础能力。
|
||||
|
||||
### 1.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | HTTP `Idempotency-Key` 统一存储与请求摘要冲突检测 | 当前部分接口要求幂等键,但还没有统一的 `tenant_id + uid + method + route + key + body_hash` 处理层。 | 相同键相同 body 返回同一结果;相同键不同 body 返回冲突。 |
|
||||
| P0 | 验证码真实发送通道 | 当前验证码生成、校验和限频链路已具备,短信/邮件供应商发送仍需接入真实服务。 | 验证码可通过供应商下发;供应商失败有可观测错误和重试策略。 |
|
||||
| P0 | 验证码限频维度补强 | 需要按租户、手机号、IP、设备、场景组合限频,并落库或接入 Redis。 | 高频请求返回 `VERIFICATION_RATE_LIMITED`,不同场景互不误伤。 |
|
||||
| P0 | 安全事件通知联动 | 密码重置、实名提交/审核、独立密码变更、新设备登录等应写入系统通知。 | 操作成功后 `/v1/im/system-notifications` 能看到对应事件。 |
|
||||
| P1 | 实名审核工作流 | 当前支持提交和状态查询,后续需要后台审核、驳回原因、通过后状态变更。 | 审核通过后 `realname.status=verified`;驳回可再次提交。 |
|
||||
| P1 | 独立密码用途扩展 | 当前按 `purpose` 存储,可继续接入青少年模式关闭、敏感设置修改等业务入口。 | 不同 `purpose` 相互隔离;错误次数和锁定互不串扰。 |
|
||||
| P1 | 设备与会话管理 | 需要登录设备列表、主动踢出设备、Token 刷新/撤销审计。 | 修改密码或踢设备后对应 Token 立即不可用。 |
|
||||
| P1 | API Key 轮换策略 | 当前 API Key 校验兼容现有逻辑,尚未实现最低可接受序号或撤销表。 | 旧序号 Key 可按配置禁用,新 Key 不影响正常客户端。 |
|
||||
| P2 | 风控与反诈策略接入 | 第一章安全动作后续应接入风险评分、反诈弹窗和二次确认策略。 | 高风险动作触发二次验证或反诈提示,低风险动作直通。 |
|
||||
|
||||
## 2. 第二章:消息首页、全局导航与系统通知
|
||||
|
||||
### 2.1 当前已完成基线
|
||||
|
||||
1. `GET /v1/im/bootstrap` 返回首页菜单、能力开关、权限和 `message_policy`。
|
||||
2. `GET /v1/im/search/conversations` 支持会话搜索和基础分页。
|
||||
3. WebSocket 发消息支持 `Head["x-im-client-mid"]` 幂等,重连重发不重复落库/广播。
|
||||
4. 消息 Header 基础校验:`x-im-schema`、`type`、`x-im-client-mid`。
|
||||
5. 普通用户禁止发布 `sys` Topic,且禁止伪造 `Head.type=system`。
|
||||
6. `GET /v1/im/topics/{topic}/messages/{seq}/receipts` 返回群消息送达/已读基础列表。
|
||||
7. `POST /v1/im/topics/{topic}/messages/{seq}/recall` 支持版本校验、权限校验和时限内撤回。
|
||||
8. `POST /v1/im/topics/{topic}/messages/{seq}/revisions` 支持文本消息编辑、版本递增和修订记录。
|
||||
9. `GET /v1/im/system-notifications` 和 `POST /v1/im/system-notifications/{id}/read`。
|
||||
|
||||
### 2.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 编辑/撤回多端实时事件广播 | 当前 HTTP 更新 DB 并返回成功,后续需要向 Topic 在线成员广播编辑/撤回事件。 | A 端撤回/编辑后,B 端无需刷新即可收到事件并更新 UI。 |
|
||||
| P0 | 编辑/撤回事件 Outbox | DB 更新和实时广播应通过事务事件或 Outbox 衔接,避免更新成功但事件丢失。 | 模拟广播失败后,后台任务可补发事件。 |
|
||||
| P0 | 消息变更查询占位策略 | 已撤回消息在普通拉取中应返回撤回占位,而不是仅靠客户端本地事件。 | 新设备登录拉历史时能看到撤回占位和版本。 |
|
||||
| P0 | `Idempotency-Key` 接入编辑/撤回 | 编辑/撤回目前依赖乐观版本控制,后续需要同时支持 HTTP 幂等键。 | 客户端超时重试同一请求不会重复写 revision。 |
|
||||
| P1 | 群回执隐私和角色策略 | 当前返回基础 read/recv 列表,后续需按租户策略、群规模、角色决定是否展示具体成员。 | 普通成员/管理员在不同策略下看到不同粒度回执。 |
|
||||
| P1 | 群回执 cursor 精确分页 | 当前 cursor 是基础位置分页,后续可升级为稳定游标,避免成员变动造成跳页。 | 成员新增/删除时分页不重复、不漏主要数据。 |
|
||||
| P1 | `message_policy` 租户配置化 | 当前 recall/edit 窗口和菜单能力是固定值,后续需要来自租户配置。 | 不同租户可配置不同撤回/编辑时限和菜单能力。 |
|
||||
| P1 | 系统通知可信生产通道 | 当前已提供通知存储和读取接口,后续需要统一服务端生产 API/事件消费者,禁止业务散落直写。 | 安全、群、组织等模块通过统一通道创建通知。 |
|
||||
| P1 | 系统通知 action 白名单 | `action.type` 和参数需要统一注册、校验和版本化。 | 非白名单 action 写入失败;客户端只收到可识别跳转。 |
|
||||
| P1 | 会话搜索排序与类型过滤 | 当前按现有订阅数据过滤,后续需支持会话类型、置顶、免打扰、最近活跃排序等。 | 搜索结果与消息首页排序、筛选规则一致。 |
|
||||
| P2 | 离线重连恢复编排测试 | 需要端到端覆盖 `hi -> token login -> sub me -> 恢复 Topic -> get data`。 | 断网重连后未丢消息、未重复消息、未错乱回执。 |
|
||||
| P2 | 消息 Header schema 版本演进 | 当前做基础格式/枚举校验,后续需要按 `x-im-schema` 版本维护兼容矩阵。 | 老客户端消息可兼容,新客户端非法字段被拒绝。 |
|
||||
|
||||
## 3. 第三章:单聊关系、权限与会话上下文
|
||||
|
||||
### 3.1 当前已完成基线
|
||||
|
||||
1. 单聊会话上下文接口返回双方关系、发送能力、对端公开资料和 P2P Topic。
|
||||
2. P2P 发送路径接入好友关系校验,非好友、拉黑状态下禁止直接发送。
|
||||
3. 好友申请、通过、拒绝、删除好友与单聊关系状态联动。
|
||||
4. 拉黑/解除拉黑后关系上下文即时反映 `blocked_by_me`、`blocked_by_peer` 等状态。
|
||||
5. 会话侧可根据 `can_send` 锁定输入框,并引导重新添加好友。
|
||||
|
||||
### 3.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | P2P 策略统一入口 | 当前核心路径已校验好友/黑名单,后续需要把发消息、通话、名片、文件、历史查看统一收敛到同一策略服务。 | 任一 P2P 业务入口对同一关系状态返回一致权限。 |
|
||||
| P0 | 关系变更实时同步 | 好友通过、删除、拉黑、解除拉黑后,应向双方在线端推送关系和会话能力变化。 | A 拉黑 B 后,B 端输入框无需刷新自动锁定。 |
|
||||
| P0 | 关系事件 Outbox | 关系 DB 状态变化与系统通知、会话事件、Push 需要事务级衔接。 | 模拟推送失败后后台可补发,不产生重复事件。 |
|
||||
| P1 | Pending 会话体验增强 | 待处理好友申请对应的单聊页应携带申请摘要、来源、过期时间和快速处理入口。 | 进入 pending 会话可直接通过/拒绝,不需要跳转新朋友页。 |
|
||||
| P1 | 黑名单历史消息策略 | 当前发送/通话阻断已具备,后续需明确拉黑后历史消息、资料、文件和已读回执可见范围。 | 被拉黑人在不同策略下看到一致的历史与资料裁剪结果。 |
|
||||
| P1 | 非好友资料隐私裁剪 | 单聊上下文中的对端资料需要接入隐私设置、企业可见性和青少年模式。 | 非好友只能看到允许公开字段;好友看到扩展字段。 |
|
||||
| P1 | 好友关系审计 | 删除、拉黑、解除拉黑、申请处理等敏感关系操作写审计日志。 | 后台可按租户、操作者、目标用户查询操作流水。 |
|
||||
| P2 | 关系状态迁移测试矩阵 | 需要覆盖 none、pending_inbound、pending_outbound、friend、blocked_by_me、blocked_by_peer 的全迁移。 | 任意状态迁移后 DB、HTTP 上下文、WS 权限一致。 |
|
||||
|
||||
## 4. 第四章:群消息、群成员与 @ 能力
|
||||
|
||||
### 4.1 当前已完成基线
|
||||
|
||||
1. 群成员列表接口支持基础分页、搜索和 `mentionable` 过滤。
|
||||
2. 返回成员角色、公开资料、ACL 和是否可被 @。
|
||||
3. 群 @ 消息头部字段校验,支持 `mentioned_user_ids`、`mention_all` 等基础结构。
|
||||
4. 群 @ 权限接入成员 ACL,管理员/群主才具备 @ 全体能力。
|
||||
5. 群消息读取链路保留 @ 元数据,客户端可据此展示提醒。
|
||||
|
||||
### 4.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | @ 实时通知与离线 Push | 当前消息元数据已落库,后续需要对被 @ 成员生成高优先级提醒和离线 Push。 | 被 @ 用户在在线、离线、多端场景均收到一次提醒。 |
|
||||
| P0 | @ 全体频控 | @ 全体需要按租户、群、角色、时间窗口限频,避免刷屏。 | 超限返回稳定错误码,管理员可配置限额。 |
|
||||
| P0 | 群成员稳定游标分页 | 当前成员列表以基础 cursor 分页,后续应使用稳定游标避免成员变动跳页。 | 成员入群/退群时翻页不重复、不漏主要数据。 |
|
||||
| P1 | 群成员资料隐私裁剪 | 群内非好友主页需要按群关系、好友关系、隐私设置裁剪资料字段。 | 同一成员从群内和好友主页进入时字段符合策略差异。 |
|
||||
| P1 | 群昵称/角色变更事件 | 群昵称、角色、禁言、移除等变化需要实时推送给群成员并刷新成员列表缓存。 | 群主修改成员角色后在线端成员列表自动更新。 |
|
||||
| P1 | @ 候选排序优化 | @ 面板候选应支持拼音首字母、最近互动、角色优先和本地缓存协同。 | 大群搜索响应稳定,候选排序符合产品规则。 |
|
||||
| P1 | 群治理策略配置化 | 成员私聊、成员邀请、全员禁言、上传权限等应统一由群治理配置控制。 | 不同群可配置不同治理能力,HTTP 与 WS 权限一致。 |
|
||||
| P2 | 大群性能压测 | 需要覆盖 500、2000、10000 成员下的成员列表、@ 搜索和发送性能。 | 达到目标延迟和资源占用阈值。 |
|
||||
|
||||
## 5. 第五章:通讯录、好友关系与黑名单
|
||||
|
||||
### 5.1 当前已完成基线
|
||||
|
||||
1. 好友列表支持查询、首字母、分页,返回备注、头像、显示名来源和会话 Topic。
|
||||
2. 新朋友列表、好友申请、通过、拒绝链路已具备。
|
||||
3. 用户搜索支持账号、精准账号和手机号类型,返回公开资料、关系和 `action`。
|
||||
4. 添加好友二维码支持短期签名 Token 生成与解析。
|
||||
5. 好友个人主页返回关系、资料和操作能力。
|
||||
6. 群内非好友个人主页返回群角色、关系、资料和操作能力。
|
||||
7. 好友备注更新、删除好友、拉黑/解除拉黑已具备。
|
||||
8. 黑名单列表和批量移出黑名单已具备。
|
||||
|
||||
### 5.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 好友/黑名单操作统一幂等 | 拉黑、解除、删除、批量移出、好友申请等需要统一接入 `Idempotency-Key` 存储和请求摘要。 | 客户端超时重试不会重复创建申请或重复触发事件。 |
|
||||
| P0 | 好友关系实时事件 | 申请、通过、拒绝、删除、备注、拉黑等状态变化需要向相关在线端实时同步。 | 两端关系页、会话页和通讯录无需刷新自动更新。 |
|
||||
| P0 | 用户搜索隐私策略 | 当前搜索基线可用,后续需要接入 `allow_user_search`、添加方式、手机号可搜索等租户和个人隐私设置。 | 禁止手机号添加的用户无法通过手机号被搜索到,且不泄露存在性。 |
|
||||
| P0 | 二维码吊销与一次性策略 | 当前二维码是短期签名 Token,后续如需主动失效、刷新覆盖、扫码审计,需要落库或接入 Redis。 | 用户刷新二维码后旧码按策略失效;后台可查询扫码记录。 |
|
||||
| P1 | 好友资料字段精细裁剪 | 个人主页需按好友、非好友、群内关系、黑名单、实名/青少年等规则返回不同字段。 | 不同入口返回字段符合隐私矩阵。 |
|
||||
| P1 | 通讯录排序与拼音索引 | 当前支持基础 initial,后续需维护昵称/备注拼音、特殊字符分组和本地增量同步。 | 好友列表与客户端通讯录排序一致,中文首字母稳定。 |
|
||||
| P1 | 黑名单占位资料 | 资料删除、离职或不可见时,黑名单列表仍需返回可解除的受控占位用户。 | 黑名单中失效用户可展示占位并成功移出。 |
|
||||
| P1 | 好友申请防骚扰 | 需要按目标用户、申请人、IP、设备、来源和群来源限频,并支持拒收策略。 | 高频申请被限制;群来源申请校验双方仍在群内。 |
|
||||
| P1 | 关系审计和安全通知 | 添加、删除、拉黑、解除、二维码解析等应写审计并按策略生成系统通知。 | 用户可在通知中看到关键关系事件,后台可追溯。 |
|
||||
| P2 | 批量接口部分失败语义增强 | 批量移出黑名单目前返回逐项结果,后续需要统一批量接口错误码、重试建议和幂等结果缓存。 | 部分失败时客户端能只重试失败项。 |
|
||||
|
||||
## 6. 第六章:群聊创建、资料维护与成员治理
|
||||
|
||||
### 6.1 当前已完成基线
|
||||
|
||||
1. 群列表、创建群、群资料查询与更新、群头像和群简介保存接口已具备。
|
||||
2. 群设置、个人群设置、退出群、群昵称接口已具备。
|
||||
3. 群二维码生成与扫码解析支持 `join_group` 场景。
|
||||
4. 群邀请、进群申请、申请列表、同意/拒绝、群 summary 申请计数已具备。
|
||||
5. 群公告最新查询、发布/更新、详情和已读接口已具备。
|
||||
6. 群治理配置 `GET/PATCH /governance` 已具备。
|
||||
7. 管理员候选、管理员上限、批量添加管理员、删除管理员和群主转让接口已具备。
|
||||
8. 禁言候选、批量禁言、禁言列表和解除禁言接口已具备。
|
||||
9. 当前实现优先复用 Topic、Subscription 和 Topic Aux,把产品主链路接口跑通。
|
||||
|
||||
### 6.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 群业务独立表拆分 | 当前群公告、进群申请、治理配置、禁言记录先存 Topic Aux,后续需要拆为独立表。 | 公告、申请、治理、禁言均有 tenant/topic 维度索引,可稳定分页和审计。 |
|
||||
| P0 | 群治理强事务状态机 | 邀请、申请审批、管理员变更、群主转让、禁言需在事务内同步业务表、订阅 ACL、成员数和事件。 | 并发审批/转让/禁言不会出现 ACL 与业务状态不一致。 |
|
||||
| P0 | 第六章幂等键落库 | 邀请、申请处理、公告发布、治理 PATCH、管理员批量、群主转让、禁言批量都需要统一幂等。 | 超时重试同一请求返回同一结果,不重复加人、升权或发事件。 |
|
||||
| P0 | 系统事件、Push 与 Outbox | 第六章所有成员/公告/治理变化需要生成可信群系统事件,并通过 Outbox 保证推送。 | 在线端实时刷新;模拟推送失败后可补偿且不重复。 |
|
||||
| P1 | 群公告已读独立记录 | 当前公告已读返回基础结果,后续需记录每个成员的公告已读时间。 | 新公告只提醒未读成员,已读状态跨端同步。 |
|
||||
| P1 | 进群申请隐私与限频 | 申请入口需要按群治理、租户策略、用户状态、来源、IP/设备限频。 | 高频申请被限制;普通成员不可看到敏感申请人信息。 |
|
||||
| P1 | 管理员/群主角色层级策略 | 管理员互相操作、管理员禁言、群主转让二次认证等需要完整策略矩阵。 | 群主、管理员、普通成员在每个治理动作上的权限稳定一致。 |
|
||||
| P1 | 禁言发送链路强校验 | 当前禁言接口已具备,后续需要在 WebSocket 发消息路径校验 `GROUP_MUTED`。 | 被禁言成员发群消息被服务端拒绝,过期后自动恢复。 |
|
||||
| P1 | 群文件索引 | 6.27-6.28 需要独立群文件表,绑定上传文件、群成员权限、下载状态和删除权限。 | 群文件列表可搜索/分页,退群后不可下载旧 URL。 |
|
||||
| P1 | 群解散与二维码吊销 | 群解散、二维码撤销、邀请失效需要状态联动和权限回收。 | 解散后 Topic、二维码、邀请、文件新增权限全部失效。 |
|
||||
| P2 | 大群性能与稳定分页 | 成员、申请、公告、禁言、文件列表都需要稳定游标和大群压测。 | 大群场景下分页不重复不漏,接口延迟达标。 |
|
||||
|
||||
## 7. 第七章:文件下载、收藏管理与聊天记录检索
|
||||
|
||||
### 7.1 当前已完成基线
|
||||
|
||||
1. 收藏创建、列表、详情和删除接口已具备,支持文本、文件、语音、链接等类型。
|
||||
2. 收藏批量创建和基础分享前的数据结构已具备。
|
||||
3. Topic 消息关键词搜索、文件/图片/视频等媒体类型检索接口已具备。
|
||||
4. 消息上下文定位接口可围绕指定 seq 返回前后消息。
|
||||
5. 文件访问已接入用户与 Topic 权限校验,下载前可判断资源可见性。
|
||||
|
||||
### 7.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 收藏幂等与去重策略 | 收藏同一消息、同一文件或同一链接需要统一去重和幂等键处理。 | 重复点击收藏不会产生重复记录;超时重试返回同一收藏 ID。 |
|
||||
| P0 | 收藏分享权限二次校验 | 收藏文件或消息分享给好友/群时,需要重新校验接收方是否可访问原资源或生成受控副本。 | 无权限接收方不能通过收藏分享拿到原会话附件。 |
|
||||
| P0 | 文件下载短时签名 URL | 当前权限判断已具备,后续应生成短时下载 URL,并记录下载审计。 | URL 过期不可访问;退群/拉黑后旧 URL 失效或受策略限制。 |
|
||||
| P0 | 文件安全扫描与 MIME 校验 | 上传后的文件需要扩展名、MIME、内容嗅探、病毒扫描和图片/视频解码安全检查。 | 高风险文件不可下载或转发,客户端收到稳定错误码。 |
|
||||
| P1 | 收藏分类与全文索引 | 收藏列表需要按类型、来源、时间、关键词、发送人等维度检索。 | 大量收藏下搜索响应稳定,类型筛选结果准确。 |
|
||||
| P1 | 聊天记录稳定游标 | 消息搜索和媒体列表需要稳定游标,避免新消息插入导致翻页重复或漏项。 | 搜索期间新增消息不影响当前翻页结果。 |
|
||||
| P1 | 历史消息权限快照策略 | 用户退群、删除好友、拉黑后,历史消息和附件的可见范围需要统一规则。 | 各入口对同一历史资源权限判断一致。 |
|
||||
| P1 | 下载状态与断点续传 | 大文件下载需要断点续传、失败重试、进度状态和下载完成事件。 | 网络中断后可续传;下载完成状态可跨端同步或按端隔离。 |
|
||||
| P2 | 聊天记录高亮与命中片段 | 搜索结果可返回命中摘要、高亮范围和消息类型摘要。 | 客户端无需二次解析即可展示命中片段。 |
|
||||
| P2 | 收藏导出与批量管理 | 后续可支持收藏批量删除、批量转发、导出和云端容量统计。 | 批量操作有部分失败语义和可重试结果。 |
|
||||
|
||||
## 8. 第八章:实时语音与视频通话
|
||||
|
||||
### 8.1 当前已完成基线
|
||||
|
||||
1. 通话 invite、accept、offer、answer、ice-candidate、hang-up 等事件沿用 Tinode call event 链路。
|
||||
2. 通话记录表 `im_call_records` 已具备,记录 `call_id`、Topic、seq、主叫/被叫、类型、状态、时长和结束原因。
|
||||
3. 通话记录创建、接听和结束状态更新已接入 store/adapter。
|
||||
4. 通话记录查询接口已具备:全局 `GET /v1/im/calls` 与 Topic 内 `GET /v1/im/topics/{topic}/calls`。
|
||||
5. 查询结果返回对端用户、方向、通话类型、状态、时长和关键时间点,客户端可先完成通话记录页对接。
|
||||
|
||||
### 8.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 跨设备抢答状态机 | 同一账号多端收到来电时,需要一个设备接听后自动取消其他端待接状态。 | A 手机接听后,A 平板和 Web 端立即变为已在其他设备接听。 |
|
||||
| P0 | 通话状态强一致与幂等 | invite、accept、hang-up 等事件需要幂等键或事件序号,避免乱序/重复导致状态回退。 | 重复 hang-up 不改变最终状态;迟到 accept 不会覆盖已结束通话。 |
|
||||
| P0 | 通话事件 Outbox 与通知 | 来电、未接、挂断、忙线等需要可信事件和 Push,DB 状态与通知通过 Outbox 衔接。 | 离线用户收到一次来电/未接提醒,失败可补偿。 |
|
||||
| P0 | TURN/STUN 配置与媒体准入 | 服务端应按租户/区域返回可用 ICE 配置,并限制未授权用户创建媒体会话。 | 客户端拉取到有效 TURN;无权限通话被服务端拒绝。 |
|
||||
| P1 | 结束原因标准化 | 需要统一 `missed`、`declined`、`busy`、`timeout`、`network_error` 等原因和客户端展示文案。 | 不同端同一结束原因展示一致,统计口径一致。 |
|
||||
| P1 | 通话质量指标 | 需要采集建立耗时、ICE 成功率、丢包、抖动、断连等指标。 | 后台可按租户和版本查看通话质量趋势。 |
|
||||
| P1 | 群通话/会议房间拆分 | 群音视频不宜复用 P2P call,需要会议房间、参与人状态和 SFU 接入。 | 群通话有独立 room_id、成员列表和退出/重进状态。 |
|
||||
| P1 | 通话权限统一策略 | 通话入口应复用 P2P/群关系策略、黑名单、禁言、青少年模式和企业策略。 | 被拉黑、非好友或受限制用户无法发起通话。 |
|
||||
| P2 | 通话记录聚合与清理 | 通话记录可按会话、日期、未接分类聚合,并支持保留周期清理。 | 记录列表分页稳定,超过保留期按策略清理。 |
|
||||
|
||||
## 9. 第九章:企业工作台与办公协同
|
||||
|
||||
### 9.1 当前已完成基线
|
||||
|
||||
1. 工作台接口 `GET /v1/im/workbench` 已具备,返回模块、布局版本和企业功能开关。
|
||||
2. 工作台模块表 `im_workbench_apps` 已具备,默认包含组织架构、会议通知、日报、月报、总经理信箱入口。
|
||||
3. 服务端对工作台 route 做内部白名单过滤,避免客户端执行任意服务端 URL。
|
||||
4. 组织架构部门表 `im_org_departments` 和成员表 `im_org_members` 已具备。
|
||||
5. 部门列表 `GET /v1/im/organization/departments`、部门成员 `GET /v1/im/organization/departments/{id}/members`、组织搜索 `GET /v1/im/organization/search` 已具备。
|
||||
6. 会议通知 `POST/GET/PATCH/CANCEL/REMIND/MY-RESPONSE` 已具备基线,包含参与人和版本冲突控制。
|
||||
7. 日报/月报草稿、提交、详情、列表、统计与提醒基线已具备。
|
||||
8. 总经理信箱配置与提交接口已具备,包含匿名策略、文案版本和幂等键。
|
||||
|
||||
### 9.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 工作台权限与功能开关配置化 | 当前模块基线可返回,后续需要按租户、部门、角色、后台配置动态裁剪。 | 不同用户登录同一租户看到不同授权模块。 |
|
||||
| P0 | 工作台未读角标事件 | 文档要求 `workbench.badge.updated`,后续需要模块未读聚合和实时增量事件。 | 会议、日报、信箱未读变化后工作台角标实时更新。 |
|
||||
| P0 | 组织数据后台维护链路 | 当前前台读接口和表已具备,后续需要后台导入、增删改部门、调整成员和离职状态。 | 后台修改组织后前台接口版本变化,客户端可刷新。 |
|
||||
| P0 | 组织权限与可见字段裁剪 | 成员手机号、邮箱、职位等字段需要按组织角色、隐私和企业策略裁剪。 | 普通员工和管理员看到的成员字段不同且不可绕过。 |
|
||||
| P1 | 组织搜索索引优化 | 当前基础 LIKE 搜索可用,后续需要拼音、工号、手机号脱敏匹配和稳定排序。 | 大组织下搜索延迟达标,结果排序符合组织规则。 |
|
||||
| P1 | 组织版本与增量同步 | 部门和成员返回版本,后续需要全局组织版本、变更事件和客户端增量刷新。 | 切换部门或人员变更后客户端无需全量重拉。 |
|
||||
| P1 | 会议通知实时提醒 | 当前接口和表已具备,后续需要会议创建、更新、取消、响应后的在线事件和 Push。 | 参与人在线端无需刷新即可收到会议变化。 |
|
||||
| P1 | 日报/月报后台统计口径 | 当前基础统计可用,后续需要按部门、周期、迟交、补交、未读等维度扩展。 | 管理员看板与业务口径一致。 |
|
||||
| P1 | 总经理信箱后台流转 | 当前前台配置/提交已具备,后续需要后台处理、回复、转派、归档和通知。 | 信件全生命周期可追踪,匿名身份受保护。 |
|
||||
| P1 | 办公协同审计 | 组织查询、会议变更、报告查看、信箱提交等敏感动作需要审计。 | 后台可按租户、用户、对象查询办公操作流水。 |
|
||||
| P2 | 办公模块统一附件策略 | 会议、日报、月报、信箱附件需要复用第七章文件 ACL、用途和病毒扫描。 | 附件上传、查看、下载权限与业务状态一致。 |
|
||||
|
||||
## 10. 个人设置与客户端能力
|
||||
|
||||
### 10.1 当前已完成基线
|
||||
|
||||
1. 个人资料 `GET/PATCH /v1/im/me/profile`、个人页 `GET /v1/im/me` 已具备。
|
||||
2. 隐私设置 `GET/PATCH /v1/im/me/privacy` 已具备,支持版本冲突控制。
|
||||
3. 最新版本检查 `GET /v1/im/app/releases/latest` 已具备,支持未登录按租户解析调用。
|
||||
4. 青年模式查询、开启、关闭接口已具备,复用敏感操作 `action_token` 和独立密码校验链路。
|
||||
5. 设备 Push Token 覆盖写入 `PUT /v1/im/devices/{device_id}/push-token` 已具备。
|
||||
|
||||
### 10.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 个人资料变更实时同步 | 资料、头像、隐私变更后需要向多端推送 `profile.updated`、`privacy.updated`。 | 同账号多端无需重登即可刷新设置。 |
|
||||
| P0 | 精准登出与会话撤销 | 现有 token 主要靠 `auth_version` 批量失效,后续需要 access/refresh/session 表支持当前设备登出和指定 session 删除。 | 当前设备退出不影响其他设备;删除指定 session 后该设备 token 立即失效。 |
|
||||
| P0 | 青年模式策略配置化 | 当前返回默认 restrictions,后续需要后台按租户配置可用时段、内容限制、消费保护等规则。 | 不同租户返回不同限制策略且能热更新。 |
|
||||
| P0 | Push Token 生命周期 | 当前可覆盖写入,后续需要 token 失效回收、设备解绑、平台厂商错误码处理。 | 推送失败后自动禁用无效 token,重新登录可恢复。 |
|
||||
| P1 | 版本灰度发布 | 当前按 platform/channel 查最新版本,后续需要按租户、用户、灰度比例、最低版本强制升级。 | 灰度用户收到新版本,非灰度用户保持稳定版本。 |
|
||||
| P1 | 资料隐私裁剪落地 | 隐私设置需要接入用户搜索、组织架构、会话资料页等读路径。 | 非授权用户无法通过其他接口绕过隐私配置。 |
|
||||
| P2 | 客户端能力上报 | 后续增加设备能力、通知权限、厂商通道状态和前台活跃状态上报。 | 后台可定位推送不可达和版本兼容问题。 |
|
||||
|
||||
## 11. 反诈安全、举报与意见反馈
|
||||
|
||||
### 11.1 当前已完成基线
|
||||
|
||||
1. URL 安全检查 `POST /v1/im/security/urls/check` 已接入风控 challenge 创建。
|
||||
2. 风控确认 `POST /v1/im/security/risk-challenges/{id}/confirm` 已具备,`continue` 返回短时 `risk_action_token`。
|
||||
3. 举报原因 `GET /v1/im/reports/abuse/reasons` 已具备,默认覆盖用户、群、消息目标。
|
||||
4. 举报提交 `POST /v1/im/reports/abuse` 已具备,支持幂等键、证据文件 ID、目标基础可见性校验和审核快照。
|
||||
5. 意见反馈配置 `GET /v1/im/feedback/config`、提交 `POST /v1/im/feedback` 已具备,支持附件规则、诊断授权和幂等键。
|
||||
|
||||
### 11.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 风控策略引擎 | 当前 URL 检查仅基础规则,后续需要按动作、关系、内容、设备、频率和租户策略综合判定。 | 高风险发消息、加好友、打开链接等动作统一返回风控 challenge。 |
|
||||
| P0 | 风控 token 消费接入业务接口 | 当前确认可签发 `risk_action_token`,后续需要原业务接口校验并单次消费该 token。 | 二次确认后只允许重试原动作一次,重复使用返回 Token 已消费。 |
|
||||
| P0 | 举报目标完整权限校验 | 当前做基础可见性校验,后续需要结合用户侧删除、撤回、退群、黑名单、文件 ACL 和审核状态。 | 无权限目标无法提交举报,也不泄露目标存在性。 |
|
||||
| P0 | 审核后台流转 | 举报和反馈当前仅 received,后续需要后台分派、处理、关闭、回复和通知。 | 处理状态可追踪,举报人按策略收到处理结果。 |
|
||||
| P1 | 证据文件安全与保留期 | 证据文件需要 `abuse_evidence` 专用 ACL、病毒扫描、不可转发和保留期策略。 | 证据只能审核链路访问,过期按政策清理。 |
|
||||
| P1 | 反诈系统事件可信生产 | 低风险提示需通过可信 `security.risk_notice` 系统事件发送,并限制普通消息伪装。 | 普通用户无法伪造反诈弹窗,客户端只信任系统事件。 |
|
||||
| P1 | 举报合并与限频 | 重复举报需要合并、返回原受理号或限频错误,避免刷单。 | 短时间重复举报同一目标不会生成大量工单。 |
|
||||
| P2 | 反馈诊断包脱敏 | 诊断包需统一脱敏、大小限制、客户端日志授权和服务端下载审计。 | 未同意时没有诊断包,同意后包内不含明文 token/手机号。 |
|
||||
|
||||
## 12. 统一处理建议
|
||||
|
||||
1. 先做 P0:统一幂等、实时事件/Outbox、验证码真实发送与限频、P2P/好友/搜索隐私策略、第六章群业务独立表与事务状态机、文件下载签名、通话强状态机、工作台/组织权限。
|
||||
2. 再做 P1:租户策略配置、系统通知生产通道、实名审核、设备管理、资料隐私裁剪、关系审计、群禁言发送校验、群文件索引、通话质量指标、会议/日报/月报/信箱完整业务表。
|
||||
3. 最后做 P2:端到端重连测试、schema 版本演进、大群压测、批量接口体验优化、收藏导出、通话记录聚合和办公模块附件统一策略。
|
||||
4. 每个增强项落地前同步更新 `IM-系统设计文档.md` 和 `IM-前后端对接文档.md` 中对应章节状态。
|
||||
@@ -1,316 +0,0 @@
|
||||
-- IM 第一至七章数据库增量审核稿
|
||||
-- 仅供审核,不要直接在生产库盲执行。
|
||||
-- 适用目标:已有 Tinode MySQL 库,补齐本项目 1-7 章业务层依赖表与关键字段。
|
||||
-- 注意:本稿使用 MySQL 8 functional index 处理空 idempotency_key 唯一性。
|
||||
-- 第一阶段多库方案:本 SQL 的 im_tenant 放 hotgo 公共库;tinode/各租户业务库表暂时保留 tenant_id 字段。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- hotgo 公共后台库需要先存在;如果你审核通过,可先执行:
|
||||
-- CREATE DATABASE IF NOT EXISTS hotgo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
-- USE hotgo;
|
||||
--
|
||||
-- 租户业务库 tinode 也需要存在并完成 Tinode 基础表初始化:
|
||||
-- CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 租户、认证、安全基础表
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_tenant(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
tenant_desc VARCHAR(256),
|
||||
db_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'tenant business database name, e.g. tinode_acme',
|
||||
db_dsn VARCHAR(1024) NOT NULL DEFAULT '' COMMENT 'optional full tenant DB DSN, empty means reuse base MySQL config',
|
||||
state SMALLINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_tenant_code(code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
INSERT INTO im_tenant(code,name,tenant_desc,db_name,db_dsn,state,created_by,updated_by)
|
||||
VALUES('default','Default Tenant',NULL,'tinode','',1,0,0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), db_name=VALUES(db_name), updated_at=CURRENT_TIMESTAMP(3);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_tenant_config(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_tenant_config_tenant_id(tenant_id),
|
||||
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_verification_codes(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
scene VARCHAR(32) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL DEFAULT '',
|
||||
phone_hash CHAR(64) NOT NULL,
|
||||
phone_mask VARCHAR(32) NOT NULL,
|
||||
target_user BIGINT,
|
||||
code_hash CHAR(64) NOT NULL,
|
||||
verification_token_hash CHAR(64),
|
||||
state SMALLINT NOT NULL,
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
max_attempts INT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
resend_after DATETIME(3) NOT NULL,
|
||||
verified_at DATETIME(3),
|
||||
consumed_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_verification_tenant_challenge(tenant_id, challenge_id),
|
||||
INDEX idx_im_verification_tenant_scene_phone_state(tenant_id, scene, phone_hash, state, expires_at),
|
||||
CONSTRAINT fk_im_verification_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_user_security(
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
auth_version INT NOT NULL DEFAULT 1,
|
||||
password_updated_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(tenant_id,user_id),
|
||||
CONSTRAINT fk_im_user_security_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_realname_verifications(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
name_cipher TEXT NOT NULL,
|
||||
id_type VARCHAR(32) NOT NULL,
|
||||
id_number_cipher TEXT NOT NULL,
|
||||
id_number_hash CHAR(64) NOT NULL,
|
||||
masked_name VARCHAR(64) NOT NULL,
|
||||
masked_id_number VARCHAR(64) NOT NULL,
|
||||
review_reason VARCHAR(256),
|
||||
verified_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_realname_submission(tenant_id, submission_id),
|
||||
INDEX idx_im_realname_user_latest(tenant_id, user_id, created_at),
|
||||
INDEX idx_im_realname_id_hash(tenant_id, id_number_hash),
|
||||
CONSTRAINT fk_im_realname_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_action_tokens(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
token_id VARCHAR(64) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
target VARCHAR(128) NOT NULL DEFAULT '',
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
state SMALLINT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
consumed_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_action_tokens_token(tenant_id, token_id),
|
||||
INDEX idx_im_action_tokens_user_action(tenant_id, user_id, action, target, state, expires_at),
|
||||
CONSTRAINT fk_im_action_tokens_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_youth_mode_settings(
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
purpose VARCHAR(64) NOT NULL,
|
||||
password_hash VARBINARY(255) NOT NULL,
|
||||
failed_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(tenant_id, user_id, purpose),
|
||||
CONSTRAINT fk_im_youth_settings_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_system_notifications(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
event_id VARCHAR(64) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
summary VARCHAR(512) NOT NULL,
|
||||
action JSON,
|
||||
read_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_system_notifications_event(tenant_id, user_id, event_id),
|
||||
INDEX idx_im_system_notifications_inbox(tenant_id, user_id, id DESC),
|
||||
CONSTRAINT fk_im_system_notifications_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 好友、黑名单
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_friend_requests(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
applicant_id BIGINT NOT NULL,
|
||||
target_id BIGINT NOT NULL,
|
||||
message VARCHAR(200) NOT NULL DEFAULT '',
|
||||
source VARCHAR(32) NOT NULL DEFAULT '',
|
||||
source_topic VARCHAR(64) NOT NULL DEFAULT '',
|
||||
status VARCHAR(16) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
handled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_friend_requests_request(tenant_id, request_id),
|
||||
INDEX idx_im_friend_requests_pair(tenant_id, applicant_id, target_id, status, created_at),
|
||||
INDEX idx_im_friend_requests_inbox(tenant_id, target_id, status, created_at),
|
||||
CONSTRAINT fk_im_friend_requests_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE UNIQUE INDEX uk_im_friend_requests_idempotency
|
||||
ON im_friend_requests(tenant_id, applicant_id, (NULLIF(idempotency_key, '')));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_friendships(
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
peer_id BIGINT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
remark VARCHAR(64) NOT NULL DEFAULT '',
|
||||
topic CHAR(25) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(tenant_id, user_id, peer_id),
|
||||
INDEX idx_im_friendships_peer(tenant_id, peer_id, status),
|
||||
CONSTRAINT fk_im_friendships_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_user_blocks(
|
||||
tenant_id BIGINT NOT NULL,
|
||||
blocker_id BIGINT NOT NULL,
|
||||
blocked_id BIGINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(tenant_id, blocker_id, blocked_id),
|
||||
INDEX idx_im_user_blocks_blocked(tenant_id, blocked_id),
|
||||
CONSTRAINT fk_im_user_blocks_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- 3. 收藏、转发、消息增强
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_favorites(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
favorite_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
item_type VARCHAR(16) NOT NULL,
|
||||
source_topic CHAR(25) NOT NULL,
|
||||
source_seq INT NOT NULL,
|
||||
snapshot JSON,
|
||||
attachment_refs JSON,
|
||||
availability VARCHAR(16) NOT NULL DEFAULT 'available',
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
collected_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_favorites_favorite(tenant_id, favorite_id),
|
||||
UNIQUE INDEX uk_im_favorites_source(tenant_id, user_id, source_topic, source_seq),
|
||||
INDEX idx_im_favorites_inbox(tenant_id, user_id, id DESC),
|
||||
INDEX idx_im_favorites_type(tenant_id, user_id, item_type, id DESC),
|
||||
CONSTRAINT fk_im_favorites_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE UNIQUE INDEX uk_im_favorites_idempotency
|
||||
ON im_favorites(tenant_id, user_id, (NULLIF(idempotency_key, '')));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_message_revisions(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seqid INT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
actor BIGINT NOT NULL,
|
||||
head JSON,
|
||||
content JSON,
|
||||
reason VARCHAR(256) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_message_revisions_version(tenant_id, topic, seqid, version),
|
||||
CONSTRAINT fk_im_message_revisions_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Tinode 基础表需补齐的字段/索引
|
||||
-- 如果你的库是用当前 schema.sql 初始化,这些已经存在。
|
||||
-- 如果是旧库,需要逐项核对后再 ALTER。
|
||||
-- ============================================================
|
||||
|
||||
-- users:租户隔离字段和索引。
|
||||
-- ALTER TABLE users ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- CREATE INDEX users_tenant_id_id ON users(tenant_id, id);
|
||||
|
||||
-- topics:租户隔离、群治理 Aux、租户维度唯一索引。
|
||||
-- 第六章群邀请、入群审批、群公告、群治理配置、成员禁言目前存放在 topics.aux JSON。
|
||||
-- 后续若要强状态机和审计,应拆 im_group_join_requests、im_group_announcements、im_group_mutes 等表。
|
||||
-- ALTER TABLE topics ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- ALTER TABLE topics ADD COLUMN aux JSON AFTER tags;
|
||||
-- CREATE UNIQUE INDEX topics_tenant_name ON topics(tenant_id, name);
|
||||
-- CREATE INDEX topics_tenant_owner ON topics(tenant_id, owner);
|
||||
|
||||
-- subscriptions:租户字段和按租户查成员索引。
|
||||
-- ALTER TABLE subscriptions ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- CREATE INDEX subscriptions_tenant_userid ON subscriptions(tenant_id, userid);
|
||||
-- CREATE INDEX subscriptions_tenant_topic ON subscriptions(tenant_id, topic);
|
||||
-- CREATE INDEX subscriptions_userid_topic_deletedat ON subscriptions(userid, topic, deletedat);
|
||||
|
||||
-- messages:租户、客户端消息幂等、编辑撤回字段、搜索/文件授权需要的索引。
|
||||
-- ALTER TABLE messages ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- ALTER TABLE messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`;
|
||||
-- ALTER TABLE messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id;
|
||||
-- ALTER TABLE messages ADD COLUMN recalled_at DATETIME(3) AFTER version;
|
||||
-- ALTER TABLE messages ADD COLUMN recalled_by BIGINT AFTER recalled_at;
|
||||
-- CREATE INDEX messages_tenant_topic ON messages(tenant_id, topic);
|
||||
-- CREATE UNIQUE INDEX messages_sender_client_mid ON messages(tenant_id, `from`, client_msg_id);
|
||||
|
||||
-- dellog:用户侧删除过滤需要 tenant_id。
|
||||
-- ALTER TABLE dellog ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- CREATE INDEX dellog_tenant_topic ON dellog(tenant_id, topic);
|
||||
-- CREATE INDEX dellog_topic_deletedfor_low_hi ON dellog(topic, deletedfor, low, hi);
|
||||
|
||||
-- fileuploads:文件 ACL、租户隔离、ETag。
|
||||
-- ALTER TABLE fileuploads ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- ALTER TABLE fileuploads ADD COLUMN etag VARCHAR(128) AFTER size;
|
||||
-- CREATE INDEX fileuploads_tenant_status ON fileuploads(tenant_id, status);
|
||||
|
||||
-- filemsglinks:文件资源级 ACL 需要租户、topic 和 userid 引用。
|
||||
-- ALTER TABLE filemsglinks ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- ALTER TABLE filemsglinks ADD COLUMN topic CHAR(25) AFTER msgid;
|
||||
-- ALTER TABLE filemsglinks ADD COLUMN userid BIGINT AFTER topic;
|
||||
-- CREATE INDEX filemsglinks_tenant_fileid ON filemsglinks(tenant_id, fileid);
|
||||
|
||||
-- usertags/topictags/devices/auth/credentials 也已按租户增加 tenant_id;
|
||||
-- 若旧库缺字段,需要参照当前 server/db/mysql/schema.sql 逐项补齐。
|
||||
@@ -1,898 +0,0 @@
|
||||
# IM 系统设计文档
|
||||
|
||||
> 版本:v1.1<br>
|
||||
> 日期:2026-08-11<br>
|
||||
> 状态:后续开发基线<br>
|
||||
> 需求依据:[IM 产品原型需求文档](IM-产品原型需求文档.md)<br>
|
||||
> 关联文档:[IM 产品需求服务端改造分析报告](IM-产品需求服务端改造分析报告.md)、[IM 现有实现与新需求差异对照总结](IM-现有实现与新需求差异对照总结.md)、[IM 服务端多租户改造方案](IM-服务端多租户改造方案.md)
|
||||
|
||||
| 版本 | 日期 | 变更 |
|
||||
| --- | --- | --- |
|
||||
| v1.0 | 2026-08-11 | 建立总体架构和业务模块设计 |
|
||||
| v1.1 | 2026-08-11 | 冻结 MySQL 范围;补齐认证、配额、消息变更、好友/群真源、审核、事务、Outbox、搜索和测试设计 |
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文作为后续 IM 产品开发的系统设计指导文件,用于统一服务端、客户端和测试团队对产品架构、模块边界、数据模型、接口风格、权限规则、消息事件和开发顺序的理解。
|
||||
|
||||
本文不替代详细接口文档和数据库 DDL。后续每个业务模块进入开发前,应在本文定义的边界内补充独立的 API 文档、状态机文档、表结构 SQL 和测试用例。
|
||||
|
||||
本文是产品业务开发的设计基线。关联分析报告用于记录调研过程;表名、唯一约束、状态机或技术选择与本文冲突时,以本文为准。设计发生变化时,必须先修改本文并记录版本,再修改接口和代码。
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
系统面向企业内部沟通与协作场景,核心目标如下:
|
||||
|
||||
1. 支持企业码进入租户,所有账号、好友、群、消息、文件、组织和办公数据均按 `tenant_id` 强隔离。
|
||||
2. 复用现有 Tinode 实时通信内核,保留 WebSocket 消息、Topic、Subscription、回执、文件、Push 和 WebRTC 信令能力。
|
||||
3. 在实时内核外新增产品业务层,承载好友关系、群治理、收藏、搜索、组织、会议、日报、月报、实名、青少年、举报和反诈等业务。
|
||||
4. 服务端必须成为权限和状态机的唯一可信执行方,客户端只负责交互展示和本地体验优化。
|
||||
5. 所有敏感动作具备幂等、防重、审计和可测试的错误码。
|
||||
6. 支持 iOS、Android 多端一致同步,网络断开后可增量恢复。
|
||||
|
||||
### 2.1 首期技术范围
|
||||
|
||||
1. 产品版首期只支持 MySQL 8,共享服务、共享数据库,通过 `tenant_id` 隔离租户。
|
||||
2. PostgreSQL、MongoDB 和 RethinkDB 不在首期产品业务支持范围内。启用产品模块但 Adapter 未实现完整租户业务接口时,服务必须拒绝启动,不能静默退回无租户接口。
|
||||
3. 实时消息继续使用现有 Tinode WebSocket 协议;事务型产品业务使用 HTTP API。
|
||||
4. 搜索首期使用 MySQL,达到第 11.3 节定义的切换条件后再引入独立搜索引擎。
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
系统采用“实时通信内核 + 产品业务服务 + 异步事件”的分层架构。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
APP["iOS / Android 客户端"]
|
||||
|
||||
subgraph Gateway["接入层"]
|
||||
WS["WebSocket 实时协议"]
|
||||
API["HTTP /v1/im/* 业务 API"]
|
||||
FILE["HTTP 文件上传下载"]
|
||||
end
|
||||
|
||||
subgraph Core["通信内核"]
|
||||
SESSION["Session"]
|
||||
HUB["Hub / Topic Actor"]
|
||||
STORE["Tenant Store"]
|
||||
CALL["WebRTC Call Signaling"]
|
||||
end
|
||||
|
||||
subgraph Product["产品业务层"]
|
||||
POLICY["策略引擎"]
|
||||
REL["好友与黑名单"]
|
||||
GROUP["群治理"]
|
||||
FAV["收藏与转发"]
|
||||
SEARCH["搜索索引"]
|
||||
ORG["组织与工作台"]
|
||||
SAFE["实名 / 青少年 / 举报 / 反诈"]
|
||||
end
|
||||
|
||||
subgraph Infra["基础设施"]
|
||||
DB["MySQL"]
|
||||
CACHE["内存缓存 / 持久化缓存"]
|
||||
OUTBOX["Outbox 异步事件"]
|
||||
PUSH["Push / 短信"]
|
||||
MEDIA["对象存储"]
|
||||
PLUGIN["审核 / 风控插件"]
|
||||
end
|
||||
|
||||
APP --> WS
|
||||
APP --> API
|
||||
APP --> FILE
|
||||
WS --> SESSION --> HUB --> STORE --> DB
|
||||
API --> Product --> DB
|
||||
FILE --> MEDIA
|
||||
HUB --> CALL
|
||||
HUB --> POLICY
|
||||
POLICY --> REL
|
||||
POLICY --> GROUP
|
||||
POLICY --> SAFE
|
||||
Product --> OUTBOX --> PUSH
|
||||
OUTBOX --> HUB
|
||||
POLICY --> PLUGIN
|
||||
```
|
||||
|
||||
## 4. 核心分层职责
|
||||
|
||||
| 层 | 职责 | 禁止事项 |
|
||||
| --- | --- | --- |
|
||||
| 接入层 | 解析 Token、Session、文件认证和租户上下文;做基础参数校验 | 不从客户端自报字段信任内部 `tenant_id` |
|
||||
| 通信内核 | 维护连接、Topic 路由、消息顺序、广播、回执、Push 触发和通话信令 | 不承载复杂审批、报表、组织树等事务业务 |
|
||||
| 产品策略层 | 发消息前校验好友、黑名单、群禁言、青少年、文件权限和内容风险 | 不把策略只放客户端 |
|
||||
| 业务服务层 | 处理好友申请、群审批、公告、收藏、组织、会议、日报、举报等 CRUD 和状态机 | 不把主数据塞进 Topic Public/Aux 任意 JSON |
|
||||
| 存储层 | 所有业务表带 `tenant_id`,更新删除必须带租户条件 | 不保留无租户业务查询入口 |
|
||||
| 异步层 | 系统消息、Push、搜索索引、审核任务、定时提醒和审计导出 | 不在 Topic 事件循环中执行耗时远程调用;核心审计记录仍在业务事务内落库 |
|
||||
|
||||
## 5. 多租户基础规则
|
||||
|
||||
多租户是本系统的底座,后续所有业务模块必须遵守:
|
||||
|
||||
1. 首次 `hi.tenant` 使用企业码解析租户,Session 固定 `TenantID`。
|
||||
2. Token、认证记录、设备、Push、文件和插件调用均携带服务端可信 `TenantID`。
|
||||
3. 普通业务 Store 只能通过租户作用域访问,禁止全局查用户、Topic、消息、文件。
|
||||
4. 新增租户业务表必须包含 `tenant_id BIGINT NOT NULL`。平台级表和租户根表 `im_tenant` 例外;例外表必须在 DDL 评审中明确说明作用域。
|
||||
5. 唯一索引优先使用 `(tenant_id, business_key)`。
|
||||
6. 子表外键优先使用 `(tenant_id, parent_id)` 复合约束。
|
||||
7. 跨租户通信首期禁止;搜索、加好友、入群、转发和文件访问均限制在当前租户。
|
||||
8. 越权访问统一返回 NotFound/Denied,不泄露目标对象是否存在于其他租户。
|
||||
|
||||
### 5.1 租户配置与配额
|
||||
|
||||
`im_tenant_config` 是租户配额的配置真源,首期使用以下字段:
|
||||
|
||||
| 字段 | 含义 | `0` 的含义 |
|
||||
| --- | --- | --- |
|
||||
| `max_users` | 租户有效用户数上限 | 不限制 |
|
||||
| `max_groups` | 租户有效群数上限 | 不限制 |
|
||||
| `max_group_members` | 单群有效成员数上限 | 不限制 |
|
||||
|
||||
配额必须由服务端在事务内强制执行:
|
||||
|
||||
1. 注册或启用用户时锁定租户配置记录,统计有效用户后再创建账号。
|
||||
2. 创建群时锁定租户配置记录,统计有效群后再创建 Topic 和群业务数据。
|
||||
3. 邀请、扫码入群或审批入群时锁定目标群业务记录,统计有效成员并校验 `max_group_members` 后再写成员关系和 Subscription。
|
||||
4. 配额检查和资源创建必须在同一事务中完成,禁止先查询、提交后再创建。
|
||||
5. 超限返回稳定错误码,并记录 `tenant_id`、当前用量、限制值和操作类型;响应不得泄露其他租户信息。
|
||||
|
||||
## 6. 通道与接口风格
|
||||
|
||||
### 6.1 WebSocket 实时协议
|
||||
|
||||
继续使用现有 Tinode 协议承载以下实时能力:
|
||||
|
||||
| 能力 | 协议方向 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 登录、注册、Token 登录 | client -> server | 已租户化,继续复用 |
|
||||
| 发消息 | `pub` | 增加产品策略校验、消息类型和客户端幂等 ID |
|
||||
| 拉消息、拉元数据 | `get` | 保留原序号查询;复杂搜索走 HTTP |
|
||||
| 已读、送达、输入中 | `note` | 保留并增加隐私和节流规则 |
|
||||
| 订阅/退订 Topic | `sub` / `leave` | 由好友、群审批等业务控制是否允许 |
|
||||
| 通话信令 | `note` 或现有 call 信令 | 增加 `call_id`、通话类型和结束原因 |
|
||||
| 系统消息 | 服务端发布的 `system` 类型消息 | 个人事件进入服务端管理的系统通知 Topic;群内事件进入对应群 Topic;普通用户不能发布该类型 |
|
||||
|
||||
### 6.2 HTTP 业务 API
|
||||
|
||||
新增业务 API 使用 `/v1/im/*` 前缀,按模块划分:
|
||||
|
||||
```text
|
||||
/v1/im/friends/*
|
||||
/v1/im/groups/*
|
||||
/v1/im/favorites/*
|
||||
/v1/im/search/*
|
||||
/v1/im/org/*
|
||||
/v1/im/meetings/*
|
||||
/v1/im/reports/*
|
||||
/v1/im/profile/*
|
||||
/v1/im/settings/*
|
||||
/v1/im/security/*
|
||||
/v1/im/reports-abuse/*
|
||||
/v1/im/feedback/*
|
||||
```
|
||||
|
||||
HTTP API 通用规则:
|
||||
|
||||
1. HTTP 请求使用 `Authorization: Bearer <token>`;统一认证中间件校验 Token 后生成只读 Principal,并从中获取 `tenant_id` 和 `uid`。请求参数中的同名字段不参与授权。
|
||||
2. 写接口必须支持幂等,使用 `Idempotency-Key` 或业务唯一键。HTTP 幂等键作用域固定为 `tenant_id + uid + method + route + key`;相同键但请求体摘要不同返回冲突错误。
|
||||
3. 列表接口统一使用游标分页,不使用不稳定的大 offset。
|
||||
4. 错误响应使用稳定业务错误码,客户端不解析数据库错误。
|
||||
5. 敏感接口记录审计日志。
|
||||
|
||||
### 6.3 文件 API
|
||||
|
||||
继续复用现有文件上传下载入口,但必须满足:
|
||||
|
||||
1. 文件记录带 `tenant_id`。
|
||||
2. 上传路径使用 `tenants/{tenant_id}/yyyy/mm/{file_id}`。
|
||||
3. 下载前校验文件上传者或关联消息/Topic 读取权限。
|
||||
4. 群文件、收藏文件、举报证据和反馈附件复用同一文件基础设施,但业务用途单独记录。
|
||||
5. 客户端不可见内部 `location`。
|
||||
|
||||
## 7. 核心领域与认证设计
|
||||
|
||||
### 7.1 已有通信模型
|
||||
|
||||
| 模型 | 继续复用 | 新增约束 |
|
||||
| --- | --- | --- |
|
||||
| User | 用户账号和基础资料 | 租户归属、实名状态、隐私设置、青少年状态 |
|
||||
| Topic | 单聊、群聊、系统 Topic、收藏 Topic | 租户归属、产品业务类型、群治理策略 |
|
||||
| Subscription | 用户与 Topic 的关系和 ACL | 好友/群状态不能只靠 ACL 表达 |
|
||||
| Message | 话题内顺序消息 | 消息类型、幂等 ID、审核状态、搜索索引 |
|
||||
| FileDef | 上传文件元数据 | 租户归属、业务用途、访问授权、扫描状态 |
|
||||
|
||||
### 7.2 新增产品模型
|
||||
|
||||
| 模块 | 目标表 | 主要职责 |
|
||||
| --- | --- | --- |
|
||||
| 好友关系 | `im_friendships`、`im_friend_requests`、`im_user_blocks` | 好友申请、同意、拒绝、删除、拉黑 |
|
||||
| 用户资料 | `im_user_profiles`、`im_user_privacy_settings` | 昵称、生日、签名、可见性和添加方式 |
|
||||
| 群治理 | `im_group_settings`、`im_group_join_requests`、`im_group_announcements`、`im_group_mutes`、`im_group_admin_logs` | 入群审批、公告、禁言、管理员、转让群主 |
|
||||
| 群文件 | `im_group_files` | 群文件目录、搜索、删除权限 |
|
||||
| 收藏 | `im_favorites` | 收藏去重、分类、来源、搜索和分享 |
|
||||
| 搜索 | `im_message_index` 或外部搜索索引 | 关键词、类型、附件、链接、图片、视频检索 |
|
||||
| 通话 | `im_call_records`、`im_call_participants` | 通话记录、状态、时长、结束原因 |
|
||||
| 组织 | `im_org_units`、`im_org_members` | 部门树、成员、岗位、数据范围 |
|
||||
| 工作台 | `im_workbench_apps`、`im_workbench_user_apps` | 企业应用入口和权限 |
|
||||
| 会议 | `im_meetings`、`im_meeting_participants` | 会议通知、确认状态、提醒和取消 |
|
||||
| 日报月报 | `im_reports`、`im_report_read_states` | 日报/月报填写、提交、查看和统计 |
|
||||
| 总经理信箱 | `im_manager_mailbox` | 意见提交、匿名规则、处理状态 |
|
||||
| 实名认证 | `im_realname_verifications` | 实名信息加密、审核状态、脱敏展示 |
|
||||
| 青少年模式 | `im_youth_mode_settings` | 启用状态、PIN 哈希、策略版本 |
|
||||
| 举报反馈 | `im_abuse_reports`、`im_feedbacks`、`im_evidence_files` | 举报、证据、反馈和处理状态 |
|
||||
| 审计 | `im_audit_logs` | 敏感操作追加日志 |
|
||||
| 异步事件 | `im_outbox_events` | 系统消息、Push、搜索索引、定时任务 |
|
||||
|
||||
新增以下基础模型,避免各业务模块重复实现认证、会话设置和消息变更:
|
||||
|
||||
| 模块 | 表 | 主要职责 |
|
||||
| --- | --- | --- |
|
||||
| 账号安全 | `im_user_security` | `auth_version`、密码更新时间、登录失败和锁定状态 |
|
||||
| 短信验证 | `im_verification_codes` | 验证码用途、目标号码哈希、有效期、错误次数和发送频控 |
|
||||
| 二次验证 | `im_auth_challenges` | 敏感操作挑战、一次性票据哈希、绑定操作和过期时间 |
|
||||
| 登录设备 | `im_login_sessions` | 设备会话、Token 标识、最近活动和撤销状态 |
|
||||
| 会话设置 | `im_conversation_settings` | 置顶、免打扰、隐藏和用户侧会话状态 |
|
||||
| 群成员 | `im_group_members` | 群成员业务状态、角色、入群来源和操作者 |
|
||||
| 消息版本 | `im_message_revisions` | 编辑前内容、版本、操作者和变更原因 |
|
||||
|
||||
### 7.3 企业码与登录流程
|
||||
|
||||
1. 未认证客户端先通过 `hi.tenant` 提交企业码;服务端只按 `im_tenant.code` 查找启用租户并把 `TenantID` 固定到 Session。
|
||||
2. 企业不存在、已停用或不允许登录时返回统一企业不可用错误,不能暴露内部状态和数据库信息。
|
||||
3. 后续注册、登录、验证码和找回密码全部使用该服务端租户上下文;不接受客户端在业务请求中重新指定租户。
|
||||
4. HTTP 场景使用 `/v1/im/tenants/resolve` 完成企业码解析,只返回公开品牌信息和服务地址。首期不设计 `tenant_ticket`;注册、登录等未认证请求由服务端根据企业码重新解析租户,认证成功后只信任正式用户 Token。
|
||||
5. 用户 Token 必须包含可信 `tenant_id`、`uid`、`auth_version`、签发时间和过期时间。Token 租户与当前接入租户不一致时拒绝认证。
|
||||
|
||||
### 7.4 验证码、密码重置和二次验证
|
||||
|
||||
验证码按 `register`、`login`、`reset_password`、`sensitive_action` 用途隔离,相同验证码不能跨用途使用。数据库只保存验证码强哈希和手机号规范化哈希,明文只在发送时短暂存在。
|
||||
|
||||
密码重置必须在同一事务中完成:
|
||||
|
||||
1. 消费有效验证码或二次验证挑战。
|
||||
2. 更新密码强哈希和密码更新时间。
|
||||
3. 递增 `im_user_security.auth_version`。
|
||||
4. 撤销该用户现有 `im_login_sessions`。
|
||||
5. 写安全审计和 Outbox 通知。
|
||||
|
||||
二次验证成功后签发一次性操作票据。票据必须绑定 `tenant_id + uid + action + target + expires_at`,只保存票据哈希,成功使用后立即失效。青少年模式独立密码不与登录密码共用,哈希保存在 `im_youth_mode_settings`。
|
||||
|
||||
### 7.5 认证安全约束
|
||||
|
||||
1. 用户名和手机号唯一约束均包含 `tenant_id`。
|
||||
2. 验证码发送和验证按租户、手机号、IP、设备分别限频。
|
||||
3. 登录错误使用统一提示;服务端仍按账号、IP 和设备记录失败次数并执行指数退避或临时锁定。
|
||||
4. 新设备登录、密码修改、会话撤销和异常登录通过 Outbox 发送安全通知。
|
||||
5. 手机号、验证码、密码、票据、Token 和证件信息不得写入普通日志或消息内容。
|
||||
|
||||
## 8. 消息体系设计
|
||||
|
||||
### 8.1 消息类型
|
||||
|
||||
服务端需要定义稳定的消息类型,不允许各端随意约定 JSON 字段。
|
||||
|
||||
产品消息协议 v1 使用现有 `pub`,固定字段放在 `head`:
|
||||
|
||||
| 字段 | 规则 |
|
||||
| --- | --- |
|
||||
| `x-im-schema` | 产品消息必填,首版固定为 `1` |
|
||||
| `x-im-type` | 产品消息必填,值必须在下表白名单中 |
|
||||
| `x-im-client-mid` | 普通客户端消息必填,对应服务端 `client_msg_id` |
|
||||
| `x-im-reply-to` | 引用消息时填写同 Topic 的 SeqId |
|
||||
| `x-im-forward-mode` | 转发时为 `single` 或 `merged` |
|
||||
| `x-im-risk-state` | 仅服务端生成;客户端提交时删除并记录安全日志 |
|
||||
|
||||
`content` 只保存对应消息类型的业务内容。每种类型进入开发前必须冻结字段、长度、必填条件和兼容规则;客户端未知字段应忽略,未知消息类型显示安全占位而不能崩溃。
|
||||
|
||||
| 类型 | 用途 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `text` | 普通文本和 Unicode 表情 | `text`、`mentions` |
|
||||
| `image` | 图片消息 | `file_id`、`width`、`height`、`thumb_file_id` |
|
||||
| `voice` | 语音消息 | `file_id`、`duration_ms`、`waveform` |
|
||||
| `video` | 视频消息 | `file_id`、`duration_ms`、`thumb_file_id` |
|
||||
| `file` | 普通文件 | `file_id`、`name`、`size`、`mime` |
|
||||
| `contact_card` | 名片 | `user_id`、`display_name_snapshot`、`avatar_snapshot` |
|
||||
| `call` | 通话结果消息 | `call_id`、`call_type`、`result`、`duration_sec` |
|
||||
| `system` | 系统提示 | `event_type`、`actor_id`、`target_ids`、`params` |
|
||||
| `forward_single` | 单条转发 | `source_message_ref`、`snapshot` |
|
||||
| `forward_merged` | 合并转发 | `summary`、`items`、`source_refs` |
|
||||
|
||||
### 8.2 消息幂等
|
||||
|
||||
客户端发消息必须带不超过 64 字符的 `client_msg_id`,推荐使用 UUID/ULID。服务端在当前租户和发送人范围内保证唯一,不把 Topic 放入唯一键,避免客户端误把同一个发送请求投递到多个会话:
|
||||
|
||||
```text
|
||||
UNIQUE(tenant_id, from_uid, client_msg_id)
|
||||
```
|
||||
|
||||
断线重试时,如果服务端发现重复 `client_msg_id`,返回原消息的 Topic、`seq_id`、状态和响应摘要,不能再次插入消息。服务端生成的系统消息也必须生成稳定幂等 ID,并以 Outbox 事件 ID 作为去重来源。
|
||||
|
||||
### 8.3 发消息前策略校验
|
||||
|
||||
每条消息持久化前执行统一策略链:
|
||||
|
||||
1. Session 租户和 Topic 租户一致。
|
||||
2. 发送人是同租户有效用户。
|
||||
3. Topic 存在且未删除。
|
||||
4. Subscription 具备写权限。
|
||||
5. 单聊检查好友关系、黑名单、删除状态和隐私策略。
|
||||
6. 群聊检查禁言、全员禁言、群状态和成员身份。
|
||||
7. 文件消息检查文件归属、扫描状态和目标 Topic 权限。
|
||||
8. 青少年模式和反诈策略检查。
|
||||
9. 内容审核通过或进入待审核策略。
|
||||
|
||||
### 8.4 消息编辑、引用、撤回与删除
|
||||
|
||||
消息在 Topic 中的 `seq_id` 和原发送人不可变。产品消息增加 `message_type`、`client_msg_id`、`version`、`moderation_state`、`recalled_at` 和 `recalled_by` 等明确字段,不能依赖客户端任意 JSON 决定服务端状态。
|
||||
|
||||
| 操作 | 服务端规则 | 多端同步 |
|
||||
| --- | --- | --- |
|
||||
| 引用 | 保存 `topic + seq_id` 引用和发送时的安全摘要;禁止跨租户引用 | 新消息正常广播;原消息删除后仍显示受控摘要 |
|
||||
| 编辑 | 仅原发送人、允许类型、规定时限内可编辑;事务内递增 `version` 并写 `im_message_revisions` | 广播带版本号的编辑事件,旧版本事件不得覆盖新版本 |
|
||||
| 全员撤回 | 校验发送人或群管理员权限及撤回时限;保留受保护审计内容,普通查询只返回撤回占位 | 广播撤回事件,收藏、搜索和引用按保留策略更新 |
|
||||
| 仅自己删除 | 使用现有用户侧删除日志,不改变其他成员可见性 | 只同步当前用户的其他设备 |
|
||||
| 批量删除/撤回 | 限制单次数量,逐项授权,返回逐项结果 | 每个成功项产生独立事件和审计记录 |
|
||||
|
||||
编辑、撤回与索引更新必须使用乐观版本控制。请求携带期望 `version`,版本不一致返回冲突错误,客户端重新拉取当前消息状态。
|
||||
|
||||
### 8.5 内容审核时序
|
||||
|
||||
发送链路先执行本地权限、限频和硬规则,再按租户及消息类型选择以下一种审核模式:
|
||||
|
||||
| 模式 | 处理方式 | 适用场景 |
|
||||
| --- | --- | --- |
|
||||
| `sync_block` | 在短超时内同步审核,通过后持久化和广播;超时按租户配置拒绝或降级 | 高风险文本、小请求 |
|
||||
| `async_hold` | 以 `pending_review` 持久化但不广播;Outbox 审核通过后发布,拒绝后标记 `rejected` | 文件、图片和高风险内容 |
|
||||
| `async_post_audit` | 先发布,异步审核;命中后执行系统撤回并审计 | 租户明确允许的低风险内容 |
|
||||
|
||||
状态转换固定为:
|
||||
|
||||
```text
|
||||
pending_review -> approved -> published
|
||||
pending_review -> rejected
|
||||
published -> recalled_by_review
|
||||
```
|
||||
|
||||
客户端提交的审核状态一律忽略。审核 Worker 必须幂等,同一消息只能完成一次终态转换。
|
||||
|
||||
## 9. 好友与黑名单设计
|
||||
|
||||
好友关系是单聊产品状态的真源,不直接用 P2P Topic 是否存在表示好友。
|
||||
|
||||
### 9.1 好友申请与关系状态
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> none
|
||||
none --> requested: 发起申请
|
||||
requested --> accepted: 通过
|
||||
requested --> rejected: 拒绝
|
||||
requested --> expired: 过期
|
||||
accepted --> friends: 事务创建好友关系
|
||||
friends --> deleted: 删除好友关系
|
||||
deleted --> requested: 重新申请
|
||||
rejected --> requested: 重新申请
|
||||
```
|
||||
|
||||
`im_friend_requests` 记录申请状态,`im_friendships` 记录双方好友关系,`im_user_blocks` 记录单向拉黑。拉黑不修改好友关系状态;取消拉黑只删除当前用户的黑名单记录,不自动恢复已经删除的好友关系。
|
||||
|
||||
### 9.2 服务端规则
|
||||
|
||||
1. 申请、同意、拒绝、删除、拉黑必须幂等。
|
||||
2. 好友申请保存验证留言、来源、过期时间和处理人。
|
||||
3. 拉黑是单向行为,唯一约束为 `(tenant_id, blocker_id, blocked_id)`;被拉黑人发消息、通话、名片、查看资料时按策略阻断。
|
||||
4. 删除好友后历史消息默认保留,但双方不能继续发送好友消息。
|
||||
5. 好友资料字段由关系和隐私设置共同决定。
|
||||
6. 接受申请必须在一个事务内更新申请、创建好友关系、创建或激活 P2P Topic/Subscription,并写入 Outbox。
|
||||
7. 单聊策略按“双方账号有效 -> 好友关系有效 -> 任一方向黑名单 -> 隐私/青少年/风控”的顺序判定。
|
||||
|
||||
## 10. 群治理设计
|
||||
|
||||
现有 Group Topic 继续作为群消息容器。`im_group_settings` 与 `im_group_members` 是群业务状态真源;Topic、Subscription 和 ACL 是通信内核投影。所有成员和角色变化必须通过产品业务接口完成,并在同一 MySQL 事务内同步业务真源与通信投影。
|
||||
|
||||
### 10.1 群角色
|
||||
|
||||
| 角色 | 业务真源 | 通信投影 | 权限 |
|
||||
| --- | --- | --- | --- |
|
||||
| 群主 | `im_group_settings.owner_user_id` + `im_group_members.role=owner` | Topic Owner + Owner ACL | 全部群管理、转让、解散 |
|
||||
| 管理员 | `im_group_members.role=admin` | Admin ACL | 审批、公告、禁言、成员管理 |
|
||||
| 普通成员 | `im_group_members.role=member` | 有效 Subscription | 收发消息、查看公告和群文件 |
|
||||
| 游客/待审批 | `im_group_join_requests` | 无 Subscription | 不具备群消息读写权限 |
|
||||
|
||||
禁止客户端直接修改群 Topic Owner 或管理员 ACL。服务启动或定时一致性任务发现业务真源与通信投影不一致时,记录告警并以业务真源修复投影。
|
||||
|
||||
### 10.2 群能力
|
||||
|
||||
| 能力 | 设计 |
|
||||
| --- | --- |
|
||||
| 创建群 | 同一事务创建 Topic、群设置、群主成员记录、Subscription、ACL、审计和 Outbox |
|
||||
| 入群邀请 | 写邀请/申请记录,审批通过后在同一事务写成员记录和 Subscription |
|
||||
| 入群验证 | 群设置控制是否需要审批 |
|
||||
| 群公告 | 公告表保存版本,群内发送系统消息 |
|
||||
| 群二维码 | 签名短 Token,包含群 ID、租户、过期时间和用途 |
|
||||
| 管理员 | 更新 `im_group_members.role`,同一事务同步 ACL |
|
||||
| 禁言 | 全员禁言和成员禁言都在发消息前强制检查 |
|
||||
| 群文件 | 文件消息成功后进入群文件索引,按群权限查询 |
|
||||
| 转让群主 | 二次确认;同一事务更新 owner、双方角色、Topic Owner/ACL、审计和 Outbox |
|
||||
| 解散群聊 | 标记群状态、踢出成员、停用二维码和审批入口 |
|
||||
|
||||
## 11. 收藏、转发与搜索设计
|
||||
|
||||
### 11.1 收藏
|
||||
|
||||
收藏使用独立 `im_favorites` 表作为查询和去重真源,不仅依赖 `slf` Topic。
|
||||
|
||||
收藏记录需要保存:
|
||||
|
||||
1. 租户、收藏人、来源 Topic、来源 SeqId、来源消息 ID。
|
||||
2. 消息类型、摘要、文件 ID、发送人、发送时间。
|
||||
3. 收藏时间、去重键和删除状态。
|
||||
4. 原消息删除后的可见策略。
|
||||
|
||||
### 11.2 转发
|
||||
|
||||
逐条转发和合并转发都必须重新执行目标会话权限和文件权限。
|
||||
|
||||
| 转发类型 | 规则 |
|
||||
| --- | --- |
|
||||
| 逐条转发 | 每条源消息生成一条新消息;保持顺序;每条使用新 `client_msg_id` |
|
||||
| 合并转发 | 生成不可变快照;限制条数、总大小和敏感字段;支持点击查看详情 |
|
||||
| 文件转发 | 源文件和目标 Topic 必须同租户;必要时创建新的引用关系 |
|
||||
|
||||
### 11.3 搜索
|
||||
|
||||
搜索分三类:
|
||||
|
||||
1. 用户和群搜索:使用租户内规范化 Tags,并执行隐私策略。
|
||||
2. 聊天记录搜索:`im_message_index` 保存 `tenant_id`、Topic、SeqId、消息类型、发送人、时间、规范化检索文本和资源元数据,返回前二次校验 ACL。
|
||||
3. 文件/图片/视频/链接搜索:依赖消息类型索引和文件元数据索引。
|
||||
|
||||
首期固定使用 MySQL 8:结构化条件使用 B-Tree 复合索引,消息正文使用独立 `im_message_index.search_text` 和适合中文的 FULLTEXT/ngram 索引。分页游标使用稳定的 `created_at + message_id`,不能使用大 Offset。
|
||||
|
||||
搜索索引不是授权来源。查询必须先限定 `tenant_id` 和当前用户可读 Topic 集合,返回前再次校验 Topic Read ACL、成员状态、用户侧删除、全员撤回和审核状态。退群、拉黑或权限变更后必须立即阻断查询,即使异步索引尚未删除。
|
||||
|
||||
出现以下任一情况时重新评估 OpenSearch/Elasticsearch:单租户可检索消息达到千万级、MySQL 搜索 P95 超过 500ms、需要复杂中文相关性/高亮,或索引写入明显影响消息主库。切换时保持 HTTP 搜索契约不变。
|
||||
|
||||
## 12. 通话设计
|
||||
|
||||
现有 P2P WebRTC 信令继续复用,新增业务通话记录。
|
||||
|
||||
### 12.1 通话状态
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> ringing: 发起呼叫
|
||||
ringing --> accepted: 接听
|
||||
ringing --> canceled: 发起方取消
|
||||
ringing --> rejected: 接收方拒绝
|
||||
ringing --> timeout: 超时
|
||||
ringing --> busy: 对方忙线
|
||||
accepted --> ended: 正常挂断
|
||||
accepted --> interrupted: 异常断开
|
||||
```
|
||||
|
||||
### 12.2 关键字段
|
||||
|
||||
`im_call_records` 至少包含:
|
||||
|
||||
```text
|
||||
tenant_id, call_id, topic, caller_id, callee_id, call_type,
|
||||
state, start_at, accepted_at, ended_at, duration_sec,
|
||||
end_reason, created_at, updated_at
|
||||
```
|
||||
|
||||
通话结束后生成 `call` 类型消息,双方看到的文案由统一结果码生成。
|
||||
|
||||
## 13. 企业工作台设计
|
||||
|
||||
工作台是独立业务域,不能依赖聊天 Topic 保存主数据。
|
||||
|
||||
| 模块 | 入口 | 通知方式 |
|
||||
| --- | --- | --- |
|
||||
| 组织架构 | `/v1/im/org/*` | 组织变更一般不发聊天消息,必要时发系统通知 |
|
||||
| 会议通知 | `/v1/im/meetings/*` | 创建、变更、取消和提醒通过系统消息 + Push |
|
||||
| 日报/月报 | `/v1/im/reports/*` | 未提交提醒、提交通知和管理员提醒通过 Outbox |
|
||||
| 总经理信箱 | `/v1/im/manager-mailbox/*` | 提交成功给本人通知;处理结果按匿名规则通知 |
|
||||
| 工作台应用 | `/v1/im/workbench/apps` | 返回租户配置和用户权限范围内的入口 |
|
||||
|
||||
组织数据权限用于日报、月报、会议统计和通讯录查询。管理员只能查看权限范围内成员数据。
|
||||
|
||||
## 14. 个人资料、隐私与青少年模式
|
||||
|
||||
### 14.1 个人资料
|
||||
|
||||
基础展示资料可以同步到 User Public,但可索引、可审计或敏感字段应进入独立表。
|
||||
|
||||
| 字段 | 存储建议 |
|
||||
| --- | --- |
|
||||
| 头像、昵称、签名 | User Public + `im_user_profiles` |
|
||||
| 生日、地区、性别 | `im_user_profiles`,按隐私设置返回 |
|
||||
| 实名姓名、证件号 | 实名表加密存储,不进入 Public |
|
||||
| 群昵称 | `im_group_members`,按需投影到 Subscription Private |
|
||||
|
||||
### 14.2 隐私设置
|
||||
|
||||
隐私设置影响以下入口:
|
||||
|
||||
1. 账号/手机号/二维码/群聊添加好友。
|
||||
2. 个人主页字段展示。
|
||||
3. 在线状态和最后在线时间展示。
|
||||
4. 非好友消息和通话发起。
|
||||
|
||||
客户端可以隐藏入口,但最终阻断必须在服务端执行。
|
||||
|
||||
### 14.3 青少年模式
|
||||
|
||||
青少年模式状态保存在服务端,PIN 使用强哈希,不保存明文。
|
||||
|
||||
受限能力由租户策略决定,首期建议覆盖:
|
||||
|
||||
1. 添加好友。
|
||||
2. 加群和创建群。
|
||||
3. 文件传输。
|
||||
4. 音视频通话。
|
||||
5. 工作台外部链接。
|
||||
|
||||
### 14.4 会话与通知设置
|
||||
|
||||
`im_conversation_settings` 以 `(tenant_id, user_id, topic)` 唯一,保存置顶、免打扰、隐藏等用户侧设置。它不改变其他成员的会话状态,也不能替代 Topic ACL。
|
||||
|
||||
Push 发送前同时检查租户通知策略、用户全局通知设置、会话免打扰、青少年模式和系统权限登记状态。客户端系统通知权限只用于展示引导,服务端保存的是用户偏好和最近一次上报状态,不把客户端上报当作可靠授权依据。
|
||||
|
||||
## 15. 安全、实名、举报与反诈
|
||||
|
||||
### 15.1 实名认证
|
||||
|
||||
实名认证采用独立状态机:
|
||||
|
||||
```text
|
||||
none -> pending -> approved
|
||||
none -> pending -> rejected
|
||||
approved -> appeal_pending -> approved/rejected
|
||||
```
|
||||
|
||||
证件号、姓名等敏感字段加密存储,普通查询只返回脱敏状态。
|
||||
|
||||
### 15.2 反诈策略
|
||||
|
||||
反诈由“本地规则 + 外部风控插件”共同组成:
|
||||
|
||||
1. 本地规则用于稳定阻断和提示,例如陌生人、敏感词、频繁加好友、异常文件。
|
||||
2. 外部插件用于内容审核、风险评分和策略建议。
|
||||
3. 插件调用遵守第 8.5 节的三种审核模式、超时和降级规则,不能在 Topic Actor 中执行无界远程等待。
|
||||
4. 风险提示和二次确认结果写审计。
|
||||
|
||||
### 15.3 举报与反馈
|
||||
|
||||
举报和反馈都按工单处理。
|
||||
|
||||
| 类型 | 特点 |
|
||||
| --- | --- |
|
||||
| 举报 | 绑定被举报对象、会话、消息、原因、证据和处理状态 |
|
||||
| 意见反馈 | 绑定提交人、设备、版本、问题描述、附件和处理状态 |
|
||||
|
||||
举报证据应固化消息快照和文件引用,防止原消息删除后无法处理。
|
||||
|
||||
## 16. 系统消息与通知设计
|
||||
|
||||
需要产生异步副作用的业务状态变化,在同一数据库事务中写入 Outbox,由 Worker 生成系统消息、Push、索引或定时任务。没有异步副作用的更新不要求创建空事件。
|
||||
|
||||
### 16.1 系统消息格式
|
||||
|
||||
系统消息固定使用以下结构;业务模块只能登记 `event_type` 和参数 Schema,不能自行定义另一套消息信封:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "system",
|
||||
"event_type": "group_join_approved",
|
||||
"template_version": 1,
|
||||
"actor_id": "usr...",
|
||||
"target_ids": ["usr..."],
|
||||
"params": {
|
||||
"group_id": "grp...",
|
||||
"group_name": "项目群"
|
||||
},
|
||||
"dedupe_key": "tenant:1:group:grpxxx:join:request123"
|
||||
}
|
||||
```
|
||||
|
||||
### 16.2 事件类型
|
||||
|
||||
首期至少定义:
|
||||
|
||||
| 业务 | 事件 |
|
||||
| --- | --- |
|
||||
| 账号安全 | 新设备登录、密码修改、实名提醒 |
|
||||
| 好友 | 新申请、通过、拒绝、删除 |
|
||||
| 群 | 邀请、申请、审批、公告、禁言、管理员变更、群主转让、解散 |
|
||||
| 文件 | 群文件上传、文件失效 |
|
||||
| 通话 | 未接、拒绝、取消、忙线、通话结束 |
|
||||
| 工作台 | 会议通知、会议变更、日报提醒、月报提醒、信箱处理 |
|
||||
| 安全 | 反诈提示、举报受理、反馈受理 |
|
||||
|
||||
## 17. 权限模型
|
||||
|
||||
### 17.1 角色层级
|
||||
|
||||
| 层级 | 角色 |
|
||||
| --- | --- |
|
||||
| 平台 | `platform_admin` |
|
||||
| 租户 | `tenant_owner`、`tenant_admin` |
|
||||
| 组织 | 部门负责人、报表管理员 |
|
||||
| 群 | 群主、管理员、成员 |
|
||||
| 关系 | 好友、非好友、黑名单 |
|
||||
| 账号 | 普通用户、实名用户、青少年模式用户 |
|
||||
|
||||
### 17.2 权限判定顺序
|
||||
|
||||
```text
|
||||
租户状态 -> 用户状态 -> 业务角色 -> Topic ACL -> 产品策略 -> 风控策略
|
||||
```
|
||||
|
||||
任何一步失败,写操作不得继续。
|
||||
|
||||
## 18. 数据一致性与异步任务
|
||||
|
||||
### 18.1 事务边界
|
||||
|
||||
必须在同一事务中完成:
|
||||
|
||||
1. 好友申请处理和关系状态更新。
|
||||
2. 群审批通过和 Subscription 写入。
|
||||
3. 群主转让和权限更新。
|
||||
4. 消息保存和附件引用建立。
|
||||
5. 文件记录从 `uploaded` 切换为 `ready`、附件引用和业务索引写入。
|
||||
6. 举报提交和证据引用。
|
||||
|
||||
业务层不能在 Handler 中连续调用多个独立 Store 方法来模拟事务。`imstore` 必须提供按用例划分的事务方法,例如 `AcceptFriendRequest`、`ApproveGroupJoin`、`TransferGroupOwner` 和 `SaveMessageWithAttachments`,方法内部同时更新产品业务表、Tinode 核心表、审计记录和 Outbox。
|
||||
|
||||
对象存储不参与 MySQL 事务,文件采用以下状态机:
|
||||
|
||||
```text
|
||||
pending -> uploaded -> ready
|
||||
pending/uploaded -> failed
|
||||
uploaded(超时且无引用) -> orphaned -> deleted
|
||||
```
|
||||
|
||||
对象上传成功后,再在 MySQL 事务中把文件切换为 `ready` 并建立业务引用。若数据库事务失败,文件保持 `uploaded`,由带宽受控的租户级垃圾回收任务清理;消息只能引用 `ready` 文件。
|
||||
|
||||
### 18.2 Outbox
|
||||
|
||||
需要通知、Push、索引或审计的业务写操作,在事务内写入 `im_outbox_events`。异步 Worker 负责:
|
||||
|
||||
1. 发送系统消息。
|
||||
2. 推送离线通知。
|
||||
3. 更新搜索索引。
|
||||
4. 调用外部审核/反诈。
|
||||
5. 发送会议、日报、月报定时提醒。
|
||||
6. 记录可观测指标。
|
||||
|
||||
`im_outbox_events` 至少包含:
|
||||
|
||||
```text
|
||||
id, tenant_id, aggregate_type, aggregate_id, event_type,
|
||||
payload, dedupe_key, state, available_at, attempts,
|
||||
locked_by, locked_at, last_error, created_at, processed_at
|
||||
```
|
||||
|
||||
Outbox 处理规则:
|
||||
|
||||
1. `dedupe_key` 在租户内唯一,生产事件和消费副作用都必须幂等。
|
||||
2. Worker 使用 `SELECT ... FOR UPDATE SKIP LOCKED` 分批领取事件,设置最大批量和租户公平调度,避免单个租户占满队列。
|
||||
3. 失败采用有上限的指数退避;超过上限进入 `dead_letter`,触发告警并支持人工重放。
|
||||
4. 系统消息通过集群安全的内部发布接口进入目标 Topic,不能直接假设 Outbox Worker 与目标 Topic 位于同一进程。
|
||||
5. 标记事件成功前必须确认目标副作用已获得稳定幂等结果;进程崩溃后的重复执行不得产生重复消息或 Push。
|
||||
6. 外部审核事件必须按第 8.5 节推进消息审核状态,不能绕过状态机直接广播。
|
||||
|
||||
## 19. API 与错误码规范
|
||||
|
||||
### 19.1 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {},
|
||||
"request_id": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 19.2 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 30001,
|
||||
"message": "permission denied",
|
||||
"reason": "GROUP_MUTED",
|
||||
"request_id": "..."
|
||||
}
|
||||
```
|
||||
|
||||
错误码按模块分段:
|
||||
|
||||
| 范围 | 模块 |
|
||||
| --- | --- |
|
||||
| 10000-19999 | 通用、认证、租户 |
|
||||
| 20000-29999 | 好友、黑名单、隐私 |
|
||||
| 30000-39999 | 群、公告、群文件 |
|
||||
| 40000-49999 | 消息、收藏、搜索、转发 |
|
||||
| 50000-59999 | 文件、媒体、通话 |
|
||||
| 60000-69999 | 组织、会议、日报、月报 |
|
||||
| 70000-79999 | 实名、青少年、安全、举报、反馈 |
|
||||
|
||||
HTTP 状态码表达协议层结果,业务 `code` 表达稳定失败原因:参数错误使用 400,未认证使用 401,无权限使用 403,不可见资源使用 404,版本或幂等冲突使用 409,限频使用 429,服务端故障使用 5xx。客户端不得只判断 HTTP 状态而忽略业务 `code`。
|
||||
|
||||
## 20. 可观测性与审计
|
||||
|
||||
### 20.1 日志字段
|
||||
|
||||
认证后的请求日志至少包含:
|
||||
|
||||
```text
|
||||
request_id, tenant_id, uid, sid, topic, action, result, latency_ms
|
||||
```
|
||||
|
||||
手机号、证件号、Token、验证码、文件签名和设备 Push Token 不写明文日志。
|
||||
|
||||
### 20.2 审计事件
|
||||
|
||||
必须审计:
|
||||
|
||||
1. 租户、组织和管理员变更。
|
||||
2. 登录、密码修改、实名审核和二次验证。
|
||||
3. 好友删除、拉黑、黑名单解除。
|
||||
4. 群主转让、管理员变更、禁言、解散群。
|
||||
5. 文件下载、批量转发、收藏导出。
|
||||
6. 举报处理、反馈处理和反诈二次确认。
|
||||
7. 平台管理员代操作。
|
||||
|
||||
## 21. 开发阶段规划
|
||||
|
||||
### 阶段 0:基础底座收口
|
||||
|
||||
目标:让后续业务都站在正确地基上。
|
||||
|
||||
交付:
|
||||
|
||||
1. 多租户核心链路完成并通过双租户隔离测试。
|
||||
2. 消息 `client_msg_id` 幂等。
|
||||
3. 文件下载资源级授权。
|
||||
4. 系统消息结构和 Outbox 基础。
|
||||
5. 统一错误码和请求日志。
|
||||
6. 产品版明确只支持 MySQL 8,不支持的 Adapter 在启动时快速失败。
|
||||
7. `im_tenant_config` 三项配额在注册、建群和入群入口强制生效。
|
||||
8. 建立专用业务事务 Store,禁止跨多个 Store 调用拼接关键事务。
|
||||
|
||||
### 阶段 1:账号、资料、好友和黑名单
|
||||
|
||||
交付:
|
||||
|
||||
1. 注册、登录、找回密码、二次验证。
|
||||
2. `auth_version`、登录设备、Token 撤销和安全通知。
|
||||
3. 个人资料、会话设置和隐私设置。
|
||||
4. 好友申请、通讯录、主页、删除好友。
|
||||
5. 独立黑名单和单聊发送/通话策略。
|
||||
|
||||
### 阶段 2:群聊产品化
|
||||
|
||||
交付:
|
||||
|
||||
1. 群列表、创建群、群资料、群头像。
|
||||
2. 入群审批、邀请、群二维码。
|
||||
3. 群公告、管理员、群主转让。
|
||||
4. 全员禁言、成员禁言、群文件。
|
||||
5. 群业务真源与 Topic/Subscription/ACL 投影一致性检查。
|
||||
|
||||
### 阶段 3:消息增强、收藏、搜索和转发
|
||||
|
||||
交付:
|
||||
|
||||
1. 结构化消息类型。
|
||||
2. @成员、引用、编辑、撤回、批量删除及消息版本事件。
|
||||
3. 收藏分类和分享。
|
||||
4. 聊天记录、文件、图片、视频和链接搜索。
|
||||
5. 逐条转发和合并转发。
|
||||
6. `sync_block`、`async_hold` 和 `async_post_audit` 审核状态机。
|
||||
|
||||
### 阶段 4:通话和通知体验
|
||||
|
||||
交付:
|
||||
|
||||
1. `call_id` 和通话记录。
|
||||
2. 通话状态消息。
|
||||
3. 忙线、拒绝、未接、超时和异常断开。
|
||||
4. Push 文案、免打扰、锁屏隐私。
|
||||
|
||||
### 阶段 5:企业工作台
|
||||
|
||||
交付:
|
||||
|
||||
1. 工作台应用配置。
|
||||
2. 组织架构。
|
||||
3. 会议通知和参会确认。
|
||||
4. 日报、月报。
|
||||
5. 总经理信箱。
|
||||
|
||||
### 阶段 6:安全合规
|
||||
|
||||
交付:
|
||||
|
||||
1. 实名认证。
|
||||
2. 青少年模式。
|
||||
3. 反诈策略。
|
||||
4. 举报和意见反馈。
|
||||
5. 审计后台和处置流程。
|
||||
|
||||
## 22. 测试策略
|
||||
|
||||
### 22.1 自动化测试
|
||||
|
||||
每个模块至少包含:
|
||||
|
||||
1. 单元测试:状态机、权限判断、错误码。
|
||||
2. Store 测试:租户隔离、唯一约束、分页、软删除。
|
||||
3. API 测试:参数校验、幂等、越权、并发。
|
||||
4. WebSocket 测试:消息顺序、回执、离线重连、系统消息。
|
||||
5. 安全测试:跨租户、跨用户、跨群、文件越权和枚举。
|
||||
6. 事务测试:在每个数据库写入点注入失败,验证业务真源、通信投影、审计和 Outbox 不出现部分提交。
|
||||
7. Worker 测试:重复消费、锁超时、进程崩溃、指数退避、死信和人工重放。
|
||||
8. Schema 测试:MySQL 初始化 DDL、升级 DDL 和 Adapter SQL 保持一致,唯一约束与外键真实生效。
|
||||
9. 性能测试:大群广播、并发发消息、重连风暴、文件上传下载、中文搜索和 Outbox 积压恢复。
|
||||
|
||||
性能测试不能只记录平均值。每次发布前根据部署规模给出 P95/P99 延迟、错误率、吞吐、数据库连接和队列积压报告;未确定正式 SLO 前,以当前稳定版本基线为回归门槛。
|
||||
|
||||
### 22.2 重点验收场景
|
||||
|
||||
| 场景 | 预期 |
|
||||
| --- | --- |
|
||||
| 两个租户使用相同手机号注册 | 均成功,互不影响 |
|
||||
| A 租户尝试搜索 B 租户用户 | 返回空或通用 NotFound |
|
||||
| 被拉黑用户发送消息 | 不进入对方会话,返回稳定失败原因 |
|
||||
| 禁言成员发群消息 | 服务端拒绝,其他成员不可见 |
|
||||
| 文件 URL 被其他用户重放 | 无权限返回 403/404 |
|
||||
| 断线重试同一消息 | 不重复入库,返回原消息状态 |
|
||||
| 同一发送人把相同 `client_msg_id` 投递到不同 Topic | 第二次返回首次投递结果,不写入另一 Topic |
|
||||
| 并发注册超过 `max_users` | 成功数不超过配额,失败请求返回同一超限错误 |
|
||||
| 并发审批入群超过 `max_group_members` | 最终有效成员数不超过配额 |
|
||||
| 群主转让并发提交 | 最终只有一个成功,审计完整 |
|
||||
| 群主转让在任一写入点失败 | owner、成员角色和 Topic ACL 全部回滚 |
|
||||
| `async_hold` 消息等待审核 | 审核前其他成员不可见,通过后只广播一次 |
|
||||
| Outbox 发送成功但标记前进程崩溃 | 重启重放不产生重复系统消息或 Push |
|
||||
| 用户退群后搜索旧群消息 | 即使索引尚在也不返回结果 |
|
||||
| 举报消息后原消息被撤回 | 举报证据仍可处理 |
|
||||
|
||||
## 23. 开发约束
|
||||
|
||||
1. 新业务表一律以 `im_` 前缀命名。
|
||||
2. 所有租户业务表必须带 `tenant_id`;平台级表和 `im_tenant` 等根表例外,例外必须经过 DDL 评审。
|
||||
3. 新接口不使用无租户 Store。
|
||||
4. 不在 Topic Actor 中执行慢查询、外部 HTTP 或复杂报表统计。
|
||||
5. 不把敏感字段放入 User Public、Topic Public、普通消息 JSON 或日志。
|
||||
6. 新增消息类型必须先定义 schema,再实现客户端渲染。
|
||||
7. 所有危险操作必须有二次确认、幂等和审计。
|
||||
8. 所有批量操作必须限制数量并返回逐项结果。
|
||||
9. 所有列表必须分页。
|
||||
10. 所有外部服务调用必须有超时、重试上限和降级策略。
|
||||
11. 关键业务状态变化必须通过专用事务 Store 完成,不允许 Handler 直接修改 Topic ACL 绕过业务真源。
|
||||
12. 产品模块只允许在 `TenantBusinessReady()` 为真且数据库版本满足要求时启动。
|
||||
|
||||
## 24. 待产品确认事项
|
||||
|
||||
以下问题会影响接口和表结构,开发前需要产品或业务方确认:
|
||||
|
||||
对应阶段冻结接口和 DDL 前必须确认本节事项。在产品未给出结论时,可以按本文其他章节声明的默认行为开发内部能力,但不得将未确认交互作为正式验收口径。
|
||||
|
||||
1. 删除好友后是否同时删除双方会话,还是仅禁止继续发送。
|
||||
2. 拉黑后是否允许被拉黑人看到历史消息和个人资料。
|
||||
3. 群聊已读回执是否展示具体已读成员,是否只对管理员开放。
|
||||
4. 消息撤回时限和管理员撤回范围。
|
||||
5. 合并转发是否允许包含已撤回、已删除或涉敏消息。
|
||||
6. 群文件是否等同于群内所有文件消息,还是独立上传目录。
|
||||
7. 实名认证是否强制,未实名能否继续登录、聊天、加好友、入群。
|
||||
8. 青少年模式限制范围和默认策略。
|
||||
9. 总经理信箱是否匿名,以及哪些角色可以查看真实提交人。
|
||||
10. 日报/月报是否支持草稿、补交、撤回和修改。
|
||||
|
||||
## 25. 总结
|
||||
|
||||
后续开发应以现有 Tinode 服务端作为实时通信内核,在其外建设清晰的产品业务层。核心原则是:聊天实时链路保持轻,业务状态机走 HTTP 和数据库事务,状态变化通过系统消息和 Push 回到聊天体验中。
|
||||
|
||||
多租户、消息幂等、文件授权、系统消息和审计是所有功能之前的基础工程。好友、群治理、收藏搜索、通话、工作台和安全合规应按阶段推进,每个阶段都必须同时完成数据模型、接口、权限、通知和测试闭环。
|
||||
|
Before Width: | Height: | Size: 108 KiB |