287 lines
13 KiB
Go
287 lines
13 KiB
Go
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
|
||
}
|
||
|
||
// oaDocDefaultCategoryOnce 记录已完成内置分类补建的租户,避免每次请求重复查库。
|
||
// key 为 tid(int),value 为 struct{}{};补建失败时不写缓存,下次请求继续重试。
|
||
var oaDocDefaultCategoryOnce sync.Map
|
||
|
||
// EnsureOaDocDefaultCategories 保证指定租户的系统内置分类齐全(当前为「项目文档」)。
|
||
// 【租户级初始化】新租户首次使用时补建默认业务数据(非表结构/全局预置数据;结构变更须走 docs/sql/ SQL 文件)。
|
||
//
|
||
// 租户隔离:内置分类是真实数据行,每个租户各自持有一份(tid 不同),
|
||
// 首次访问文档接口时自动补建,租户之间互不可见、互不影响。
|
||
//
|
||
// 范围隔离:「项目文档」属于共享文档空间(scope=shared、user_id=0),租户内所有人共用。
|
||
// 私密文档空间不预置内置分类,由用户自行建立自己的文件夹。
|
||
//
|
||
// 「未分类」不落库:它是虚拟分类,文档 category_id = 0 即表示未分类,因此天然不可删除;
|
||
// 同时在 services 层把「未分类」「项目文档」列为保留名,禁止新建同名分类与之混淆。
|
||
//
|
||
// 任何失败均静默忽略,不阻断主流程(与其它租户级初始化保持一致)。
|
||
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
|
||
}
|