优化文档相关

This commit is contained in:
2026-09-10 12:57:16 +08:00
parent 8a0334a020
commit e340d9a7d1
17 changed files with 3131 additions and 457 deletions
+204 -44
View File
@@ -4,6 +4,8 @@ import (
"strings"
"sync"
"time"
"github.com/beego/beego/v2/client/orm"
)
// OA 文档管理(文档库 / 文档图谱)数据模型。
@@ -14,14 +16,28 @@ import (
// - yz_backend_oa_doc_link 文档关联关系(引用/相关/版本/从属),文档图谱的连线来源
// OaDocCategory OA文档分类: yz_backend_oa_doc_category
//
// 双空间模型:文档库按「共享文档 / 私密文档」两个空间组织,每个空间各自有一棵分类树。
// - 共享文档空间(Scope="shared"、UserID=0):租户级,租户内所有成员共用同一套文件夹结构;
// - 私密文档空间(Scope="personal"、UserID=登录用户的 yz_system_tenant_user.uid):
// 绑定到租户的用户级别,每位用户拥有自己独立的一套文件夹,互不干扰;
// - 文档的可见性(OaDoc.Visibility)与所在空间一一对应:共享空间→租户公开(0),私密空间→私密(1);
// - 系统内置的「项目文档」属于共享空间(Scope="shared"、UserID=0);
// - 「未分类」是虚拟分类(文档 category_id=0),不落库、不可删除,两个空间各自独立计算。
type OaDocCategory struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid int `orm:"column(tid)" json:"tid"`
ParentID uint64 `orm:"column(parent_id);default(0)" json:"parent_id"`
Name string `orm:"column(name);size(128)" json:"name"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Remark string `orm:"column(remark);size(255);default()" json:"remark"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid int `orm:"column(tid)" json:"tid"`
// UserID 分类归属用户:共享空间恒为 0(租户级);私密空间为创建者的 yz_system_tenant_user.uid。
UserID uint64 `orm:"column(user_id);default(0)" json:"user_id"`
ParentID uint64 `orm:"column(parent_id);default(0)" json:"parent_id"`
Name string `orm:"column(name);size(128)" json:"name"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Remark string `orm:"column(remark);size(255);default()" json:"remark"`
IsSystem int8 `orm:"column(is_system);default(0)" json:"is_system"`
// Scope 所属空间:shared=共享文档空间,personal=私密文档空间(绑定到用户级别)。
Scope string `orm:"column(scope);size(16);default(shared)" json:"scope"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
@@ -35,33 +51,38 @@ func (m *OaDocCategory) TableName() string {
// status: 0-草稿 1-已发布 2-已归档
// doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频
type OaDoc struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid int `orm:"column(tid)" json:"tid"`
CategoryID uint64 `orm:"column(category_id);default(0)" json:"category_id"`
Title string `orm:"column(title);size(255)" json:"title"`
FileID uint64 `orm:"column(file_id);default(0)" json:"file_id"`
FileURL string `orm:"column(file_url);size(512);default()" json:"file_url"`
FileName string `orm:"column(file_name);size(255);default()" json:"file_name"`
Ext string `orm:"column(ext);size(16);default()" json:"ext"`
Size uint64 `orm:"column(size);default(0)" json:"size"`
DocType int8 `orm:"column(doc_type);default(0)" json:"doc_type"`
Tags string `orm:"column(tags);size(255);default()" json:"tags"`
Summary string `orm:"column(summary);size(1000);default()" json:"summary"`
Status int8 `orm:"column(status);default(0)" json:"status"`
Version int `orm:"column(version);default(1)" json:"version"`
IsStar int8 `orm:"column(is_star);default(0)" json:"is_star"`
OwnerID uint64 `orm:"column(owner_id);default(0)" json:"owner_id"`
OwnerName string `orm:"column(owner_name);size(100);default()" json:"owner_name"`
ViewCount int `orm:"column(view_count);default(0)" json:"view_count"`
DownCount int `orm:"column(download_count);default(0)" json:"download_count"`
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
CreatorName string `orm:"column(creator_name);size(100);default()" json:"creator_name"`
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid int `orm:"column(tid)" json:"tid"`
CategoryID uint64 `orm:"column(category_id);default(0)" json:"category_id"`
Title string `orm:"column(title);size(255)" json:"title"`
FileID uint64 `orm:"column(file_id);default(0)" json:"file_id"`
FileURL string `orm:"column(file_url);size(512);default()" json:"file_url"`
FileName string `orm:"column(file_name);size(255);default()" json:"file_name"`
Ext string `orm:"column(ext);size(16);default()" json:"ext"`
Size uint64 `orm:"column(size);default(0)" json:"size"`
DocType int8 `orm:"column(doc_type);default(0)" json:"doc_type"`
Tags string `orm:"column(tags);size(255);default()" json:"tags"`
Summary string `orm:"column(summary);size(1000);default()" json:"summary"`
// Content 在线制作文档的编辑源(富文本 HTML),仅用于再次编辑;上传文件或普通文档为空。
// 注意:文档对外的写入/下载格式为 docx(由该源导出),此字段只是编辑器的中间态。
Content string `orm:"column(content);type(longtext);null" json:"content,omitempty"`
// IsOnline 是否在线制作(非持久化字段,列表接口按 Content 是否为空计算)
IsOnline int8 `orm:"-" json:"is_online"`
Status int8 `orm:"column(status);default(0)" json:"status"`
Version int `orm:"column(version);default(1)" json:"version"`
IsStar int8 `orm:"column(is_star);default(0)" json:"is_star"`
OwnerID uint64 `orm:"column(owner_id);default(0)" json:"owner_id"`
OwnerName string `orm:"column(owner_name);size(100);default()" json:"owner_name"`
ViewCount int `orm:"column(view_count);default(0)" json:"view_count"`
DownCount int `orm:"column(download_count);default(0)" json:"download_count"`
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
CreatorName string `orm:"column(creator_name);size(100);default()" json:"creator_name"`
// Visibility 可见性:0-租户公开(默认,租户内所有后台用户可见)1-私密(仅创建者与被共享者可见)
Visibility int8 `orm:"column(visibility);default(0)" json:"visibility"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
Visibility int8 `orm:"column(visibility);default(0)" json:"visibility"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *OaDoc) TableName() string {
@@ -88,7 +109,8 @@ func (m *OaDocLink) TableName() string {
// OaDocShare OA文档共享: yz_backend_oa_doc_share
// 私密文档(visibility=1)在"创建者可见"之外,额外授权给指定用户或部门。
// share_type: 0-用户(target_id 为 yz_system_tenant_user.uid)
// 1-部门(target_id 为 yz_backend_organization.id,含其全部子部门)
//
// 1-部门(target_id 为 yz_backend_organization.id,含其全部子部门)
type OaDocShare struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid int `orm:"column(tid)" json:"tid"`
@@ -103,17 +125,39 @@ func (m *OaDocShare) TableName() string {
return "yz_backend_oa_doc_share"
}
// 文档分类内置标记常量(is_system)
const (
DocCategoryNormal int8 = 0 // 普通分类,租户可自行删除
DocCategorySystem int8 = 1 // 系统内置分类,任何租户都不可删除
)
// 文档范围(scope):文档库按「共享文档 / 私密文档」两个空间组织,分类同样按此隔离。
// 共享空间(shared):租户级,所有人共用;私密空间(personal):绑定到用户级别,每人独立一套。
// 文档侧复用 visibility 字段:personal 对应私密(1),shared 对应租户公开(0)。
const (
DocScopeAll = "all"
DocScopePersonal = "personal"
DocScopeShared = "shared"
)
// DocCategoryNameProject 系统内置分类名称:项目文档,只属于共享文档范围。
// 每个租户首次访问文档接口时自动补建自己那一份(按 tid 隔离),租户之间互不可见。
const DocCategoryNameProject = "项目文档"
// docCategorySystemRemark 内置分类的默认备注
const docCategorySystemRemark = "系统内置分类,不可删除"
// 文档类型常量(doc_type)
const (
DocTypeOther int8 = 0
DocTypeDoc int8 = 1 // doc / docx / txt / md / rtf
DocTypeSheet int8 = 2 // xls / xlsx / csv
DocTypeSlide int8 = 3 // ppt / pptx
DocTypePDF int8 = 4 // pdf
DocTypeImage int8 = 5 // jpg / png / gif / bmp / webp
DocTypeZip int8 = 6 // zip / rar / 7z / tar / gz
DocTypeVideo int8 = 7
DocTypeAudio int8 = 8
DocTypeOther int8 = 0
DocTypeDoc int8 = 1 // doc / docx / txt / md / rtf
DocTypeSheet int8 = 2 // xls / xlsx / csv
DocTypeSlide int8 = 3 // ppt / pptx
DocTypePDF int8 = 4 // pdf
DocTypeImage int8 = 5 // jpg / png / gif / bmp / webp
DocTypeZip int8 = 6 // zip / rar / 7z / tar / gz
DocTypeVideo int8 = 7
DocTypeAudio int8 = 8
)
// 文档状态常量(status)
@@ -179,16 +223,20 @@ func EnsureOaDocumentTables() error {
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_category (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
user_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '归属用户ID 0-租户级(共享分类) >0-该用户的个人分类',
parent_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级ID,0为一级分类',
name varchar(128) NOT NULL DEFAULT '' COMMENT '分类名称',
sort int NOT NULL DEFAULT 0 COMMENT '排序,越小越靠前',
remark varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
is_system tinyint NOT NULL DEFAULT 0 COMMENT '是否系统内置分类 0-否(可删除) 1-是(不可删除)',
scope varchar(16) NOT NULL DEFAULT 'shared' COMMENT '所属范围 personal-个人文档 shared-共享文档',
is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
create_time datetime DEFAULT NULL COMMENT '创建时间',
update_time datetime DEFAULT NULL COMMENT '更新时间',
delete_time datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (id),
KEY idx_tid_parent (tid, parent_id)
KEY idx_tid_parent (tid, parent_id),
KEY idx_tid_scope_user (tid, scope, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表'`).Exec()
if err != nil {
return
@@ -208,6 +256,7 @@ CREATE TABLE IF NOT EXISTS yz_backend_oa_doc (
doc_type tinyint NOT NULL DEFAULT 0 COMMENT '类型 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频',
tags varchar(255) NOT NULL DEFAULT '' COMMENT '标签,逗号分隔',
summary varchar(1000) NOT NULL DEFAULT '' COMMENT '摘要/描述',
content longtext NULL COMMENT '在线制作文档的编辑源(富文本HTML),仅用于再次编辑',
status tinyint NOT NULL DEFAULT 0 COMMENT '状态 0-草稿 1-已发布 2-已归档',
version int NOT NULL DEFAULT 1 COMMENT '版本号',
is_star tinyint NOT NULL DEFAULT 0 COMMENT '是否收藏 0-否 1-是',
@@ -256,6 +305,30 @@ CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_link (
ADD COLUMN visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)'
`).Exec()
// 历史库补齐在线文档编辑源列 content(在线制作功能上线前已建表)
_, _ = Orm.Raw(`
ALTER TABLE yz_backend_oa_doc
ADD COLUMN content longtext NULL COMMENT '在线制作文档的编辑源(富文本HTML),仅用于再次编辑'
`).Exec()
// 历史库补齐分类表的 is_system 列(内置分类特性上线前已建表)
_, _ = Orm.Raw(`
ALTER TABLE yz_backend_oa_doc_category
ADD COLUMN is_system tinyint NOT NULL DEFAULT 0 COMMENT '是否系统内置分类 0-否(可删除) 1-是(不可删除)'
`).Exec()
// 历史库补齐分类表的 scope 列:既有分类默认归入共享文档,保证升级后仍可见
_, _ = Orm.Raw(`
ALTER TABLE yz_backend_oa_doc_category
ADD COLUMN scope varchar(16) NOT NULL DEFAULT 'shared' COMMENT '所属范围 personal-个人文档 shared-共享文档'
`).Exec()
// 历史库补齐分类表的 user_id 列:既有分类默认归租户级(共享分类)
_, _ = Orm.Raw(`
ALTER TABLE yz_backend_oa_doc_category
ADD COLUMN user_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '归属用户ID 0-租户级(共享分类) >0-该用户的个人分类'
`).Exec()
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_share (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
@@ -270,6 +343,93 @@ CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_share (
KEY idx_tid_doc (tid, doc_id),
KEY idx_target (tid, share_type, target_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档共享表'`).Exec()
if err != nil {
return
}
// 双空间模型:兼容存量数据,将不在 (shared, personal) 范围内的分类统一归为共享空间
// (user_id 置 0、scope 置 shared),避免历史脏数据导致分类不可见。
// 注意:不再把 personal 并入 shared,私密空间需保留各自的 user_id 隔离。
_, _ = Orm.Raw(`
UPDATE yz_backend_oa_doc_category
SET user_id = 0, scope = 'shared'
WHERE scope NOT IN ('shared', 'personal') AND is_deleted = 0
`).Exec()
})
return err
}
// oaDocDefaultCategoryOnce 记录已完成内置分类补建的租户,避免每次请求重复查库。
// key 为 tid(int),value 为 struct{}{};补建失败时不写缓存,下次请求继续重试。
var oaDocDefaultCategoryOnce sync.Map
// EnsureOaDocDefaultCategories 保证指定租户的系统内置分类齐全(当前为「项目文档」)。
//
// 租户隔离:内置分类是真实数据行,每个租户各自持有一份(tid 不同),
// 首次访问文档接口时自动补建,租户之间互不可见、互不影响。
//
// 范围隔离:「项目文档」属于共享文档空间(scope=shared、user_id=0),租户内所有人共用。
// 私密文档空间不预置内置分类,由用户自行建立自己的文件夹。
//
// 「未分类」不落库:它是虚拟分类,文档 category_id = 0 即表示未分类,因此天然不可删除;
// 同时在 services 层把「未分类」「项目文档」列为保留名,禁止新建同名分类与之混淆。
//
// 任何失败均静默忽略,不阻断主流程(与 EnsureDefaultTenantRoles 保持一致)。
func EnsureOaDocDefaultCategories(tid int) {
if Orm == nil || tid <= 0 {
return
}
if _, done := oaDocDefaultCategoryOnce.Load(tid); done {
return
}
if err := ensureOaDocProjectCategory(tid); err == nil {
oaDocDefaultCategoryOnce.Store(tid, struct{}{})
}
}
// ensureOaDocProjectCategory 补建租户的「项目文档」内置分类:
// 1. 已有任意内置分类(is_system=1):说明租户已初始化过(可能被改名),直接返回;
// 2. 已有同名普通分类:升级为内置分类,避免出现两个「项目文档」;
// 3. 都没有:插入一条一级内置分类。
func ensureOaDocProjectCategory(tid int) error {
// 「项目文档」为统一租户分类树下的内置分类(scope=shared)
qs := Orm.QueryTable(new(OaDocCategory)).
Filter("tid", tid).
Filter("scope", DocScopeShared)
sysCount, err := qs.Filter("is_system", DocCategorySystem).Count()
if err != nil {
return err
}
if sysCount > 0 {
return nil
}
var existed []OaDocCategory
if _, err := qs.Filter("name", DocCategoryNameProject).
Filter("is_deleted", 0).
OrderBy("id").Limit(1).All(&existed); err != nil && err != orm.ErrNoRows {
return err
}
if len(existed) > 0 {
item := existed[0]
item.IsSystem = DocCategorySystem
if item.Remark == "" {
item.Remark = docCategorySystemRemark
}
_, err := Orm.Update(&item, "IsSystem", "Remark")
return err
}
_, err = Orm.Insert(&OaDocCategory{
Tid: tid,
ParentID: 0,
Name: DocCategoryNameProject,
Sort: 0,
Remark: docCategorySystemRemark,
IsSystem: DocCategorySystem,
Scope: DocScopeShared,
IsDeleted: 0,
})
return err
}