Files
yunzerwebsiteallinone/go/models/oa_document.go
T
2026-09-10 12:57:16 +08:00

436 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package models
import (
"strings"
"sync"
"time"
"github.com/beego/beego/v2/client/orm"
)
// OA 文档管理(文档库 / 文档图谱)数据模型。
//
// 三张表按租户 tid 隔离:
// - yz_backend_oa_doc_category 文档分类(树形,parent_id=0 为一级)
// - yz_backend_oa_doc 文档主体(附件信息冗余存储,避免每次联查文件表)
// - 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"`
// 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"`
}
func (m *OaDocCategory) TableName() string {
return "yz_backend_oa_doc_category"
}
// OaDoc OA文档: yz_backend_oa_doc
// 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"`
// 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"`
}
func (m *OaDoc) TableName() string {
return "yz_backend_oa_doc"
}
// OaDocLink OA文档关联: yz_backend_oa_doc_link
// relation: reference-引用 related-相关 version-版本迭代 belong-从属
type OaDocLink struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid int `orm:"column(tid)" json:"tid"`
SourceID uint64 `orm:"column(source_id);default(0)" json:"source_id"`
TargetID uint64 `orm:"column(target_id);default(0)" json:"target_id"`
Relation string `orm:"column(relation);size(32);default(related)" json:"relation"`
Remark string `orm:"column(remark);size(255);default()" json:"remark"`
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
}
func (m *OaDocLink) TableName() string {
return "yz_backend_oa_doc_link"
}
// 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,含其全部子部门)
type OaDocShare struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid int `orm:"column(tid)" json:"tid"`
DocID uint64 `orm:"column(doc_id);default(0)" json:"doc_id"`
ShareType int8 `orm:"column(share_type);default(0)" json:"share_type"`
TargetID uint64 `orm:"column(target_id);default(0)" json:"target_id"`
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
}
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
)
// 文档状态常量(status)
const (
DocStatusDraft int8 = 0
DocStatusPublish int8 = 1
DocStatusArchive int8 = 2
)
// 文档关联关系常量(relation)
const (
DocRelationReference = "reference"
DocRelationRelated = "related"
DocRelationVersion = "version"
DocRelationBelong = "belong"
)
// 文档可见性常量(visibility)
const (
DocVisibilityPublic int8 = 0 // 租户公开
DocVisibilityPrivate int8 = 1 // 私密
)
// 文档共享目标类型常量(share_type)
const (
DocShareTypeUser int8 = 0 // 共享给指定用户(target_id = yz_system_tenant_user.uid)
DocShareTypeOrg int8 = 1 // 共享给部门(target_id = yz_backend_organization.id,含子部门)
)
// extDocTypeMap 扩展名 -> 文档类型
var extDocTypeMap = map[string]int8{
"doc": DocTypeDoc, "docx": DocTypeDoc, "txt": DocTypeDoc, "md": DocTypeDoc,
"rtf": DocTypeDoc, "wps": DocTypeDoc, "odt": DocTypeDoc,
"xls": DocTypeSheet, "xlsx": DocTypeSheet, "csv": DocTypeSheet, "ods": DocTypeSheet,
"ppt": DocTypeSlide, "pptx": DocTypeSlide, "odp": DocTypeSlide,
"pdf": DocTypePDF,
"jpg": DocTypeImage, "jpeg": DocTypeImage, "png": DocTypeImage,
"gif": DocTypeImage, "bmp": DocTypeImage, "webp": DocTypeImage, "svg": DocTypeImage,
"zip": DocTypeZip, "rar": DocTypeZip, "7z": DocTypeZip, "tar": DocTypeZip, "gz": DocTypeZip,
"mp4": DocTypeVideo, "webm": DocTypeVideo, "mov": DocTypeVideo, "avi": DocTypeVideo,
"mp3": DocTypeAudio, "wav": DocTypeAudio, "ogg": DocTypeAudio,
}
// DocTypeByExt 根据扩展名推断文档类型,未知返回"其他"。
func DocTypeByExt(ext string) int8 {
if t, ok := extDocTypeMap[strings.ToLower(strings.TrimPrefix(strings.TrimSpace(ext), "."))]; ok {
return t
}
return DocTypeOther
}
var oaDocumentTableOnce sync.Once
// EnsureOaDocumentTables 首次访问文档接口时自动建表(若不存在)。
// 与通知公告/日程保持一致的运行期自愈策略,避免新功能上线还需手工执行建表脚本。
func EnsureOaDocumentTables() error {
if Orm == nil {
return nil
}
var err error
oaDocumentTableOnce.Do(func() {
_, err = Orm.Raw(`
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_scope_user (tid, scope, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表'`).Exec()
if err != nil {
return
}
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
category_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '所属分类ID,0为未分类',
title varchar(255) NOT NULL DEFAULT '' COMMENT '文档标题',
file_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '关联附件ID(yz_system_files)',
file_url varchar(512) NOT NULL DEFAULT '' COMMENT '附件访问地址',
file_name varchar(255) NOT NULL DEFAULT '' COMMENT '原始文件名',
ext varchar(16) NOT NULL DEFAULT '' COMMENT '扩展名',
size bigint unsigned NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
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-是',
owner_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '负责人ID',
owner_name varchar(100) NOT NULL DEFAULT '' COMMENT '负责人姓名',
view_count int NOT NULL DEFAULT 0 COMMENT '查看次数',
download_count int NOT NULL DEFAULT 0 COMMENT '下载次数',
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
creator_name varchar(100) NOT NULL DEFAULT '' COMMENT '创建人姓名',
visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)',
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_cate (tid, category_id, is_deleted),
KEY idx_tid_status (tid, status, is_deleted),
KEY idx_tid_title (tid, title)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档表'`).Exec()
if err != nil {
return
}
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_link (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
source_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '源文档ID',
target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '目标文档ID',
relation varchar(32) NOT NULL DEFAULT 'related' COMMENT '关系 reference-引用 related-相关 version-版本 belong-从属',
remark varchar(255) NOT NULL DEFAULT '' COMMENT '关系说明',
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
create_time datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_doc_pair (tid, source_id, target_id, relation),
KEY idx_tid_source (tid, source_id),
KEY idx_tid_target (tid, target_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档关联表'`).Exec()
if err != nil {
return
}
// 历史库补齐 visibility 列(已存在时 MySQL 报 duplicate column,忽略)
_, _ = Orm.Raw(`
ALTER TABLE yz_backend_oa_doc
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',
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
doc_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '文档ID',
share_type tinyint NOT NULL DEFAULT 0 COMMENT '共享类型 0-用户(target_id=uid) 1-部门(target_id=org_id,含子部门)',
target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '共享目标ID',
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '授权人ID',
create_time datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_doc_target (tid, doc_id, share_type, target_id),
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
}