优化文档相关
This commit is contained in:
@@ -69,6 +69,9 @@ func (c *BackendOaDocumentController) oaDocFail(err error) {
|
||||
status, code, msg = 404, 404, err.Error()
|
||||
case services.ErrDocLinkDuplicate:
|
||||
status, code, msg = 409, 409, err.Error()
|
||||
case services.ErrDocCategorySystem, services.ErrDocCategorySystemLocked,
|
||||
services.ErrDocCategoryNameKept:
|
||||
status, code, msg = 403, 403, err.Error()
|
||||
default:
|
||||
msg = err.Error()
|
||||
}
|
||||
@@ -95,14 +98,15 @@ func (c *BackendOaDocumentController) oaDocActor(claims *jwtutil.Claims) service
|
||||
|
||||
// ---------------- 分类 ----------------
|
||||
|
||||
// Categories GET /backend/oa/document/categories 分类树(含文档数量)
|
||||
// Categories GET /backend/oa/document/categories?scope=shared|personal 文档分类树(含各分类文档数量)
|
||||
func (c *BackendOaDocumentController) Categories() {
|
||||
claims, err := c.oaDocClaims()
|
||||
if err != nil {
|
||||
c.oaDocFail(err)
|
||||
return
|
||||
}
|
||||
tree, err := services.OaDocCategoryTree(c.oaDocActor(claims))
|
||||
scope := strings.TrimSpace(c.GetString("scope"))
|
||||
tree, err := services.OaDocCategoryTree(c.oaDocActor(claims), scope)
|
||||
if err != nil {
|
||||
c.oaDocFail(err)
|
||||
return
|
||||
@@ -118,6 +122,7 @@ func (c *BackendOaDocumentController) CreateCategory() {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Scope string `json:"scope"`
|
||||
ParentID uint64 `json:"parent_id"`
|
||||
Name string `json:"name"`
|
||||
Remark string `json:"remark"`
|
||||
@@ -126,7 +131,8 @@ func (c *BackendOaDocumentController) CreateCategory() {
|
||||
if !c.oaDocParseBody(&body) {
|
||||
return
|
||||
}
|
||||
item, err := services.OaDocCategoryCreate(claims.TenantId, body.ParentID, body.Name, body.Remark, body.Sort)
|
||||
item, err := services.OaDocCategoryCreate(
|
||||
c.oaDocActor(claims), body.Scope, body.ParentID, body.Name, body.Remark, body.Sort)
|
||||
if err != nil {
|
||||
c.oaDocFail(err)
|
||||
return
|
||||
@@ -169,7 +175,7 @@ func (c *BackendOaDocumentController) UpdateCategory() {
|
||||
if body.Sort != nil {
|
||||
sort = *body.Sort
|
||||
}
|
||||
item, err := services.OaDocCategoryUpdate(claims.TenantId, id, name, remark, sort, parentID)
|
||||
item, err := services.OaDocCategoryUpdate(c.oaDocActor(claims), id, name, remark, sort, parentID)
|
||||
if err != nil {
|
||||
c.oaDocFail(err)
|
||||
return
|
||||
@@ -188,7 +194,7 @@ func (c *BackendOaDocumentController) DeleteCategory() {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := services.OaDocCategoryDelete(claims.TenantId, id); err != nil {
|
||||
if err := services.OaDocCategoryDelete(c.oaDocActor(claims), id); err != nil {
|
||||
c.oaDocFail(err)
|
||||
return
|
||||
}
|
||||
@@ -211,13 +217,19 @@ func (c *BackendOaDocumentController) List() {
|
||||
docType, _ := c.GetInt("doc_type", -1)
|
||||
star, _ := c.GetInt("star", -1)
|
||||
categoryID, _ := c.GetUint64("category_id")
|
||||
visibility, _ := c.GetInt("visibility", -1)
|
||||
mine := strings.TrimSpace(c.GetString("mine")) == "1"
|
||||
scope := strings.TrimSpace(c.GetString("scope"))
|
||||
|
||||
actor := c.oaDocActor(claims)
|
||||
list, total, err := services.OaDocList(actor, services.OaDocListParams{
|
||||
Actor: actor,
|
||||
Scope: scope,
|
||||
Keyword: strings.TrimSpace(c.GetString("keyword")),
|
||||
CategoryID: categoryID,
|
||||
Uncategorized: strings.TrimSpace(c.GetString("uncategorized")) == "1",
|
||||
Visibility: visibility,
|
||||
Mine: mine,
|
||||
Status: status,
|
||||
DocType: docType,
|
||||
Tag: strings.TrimSpace(c.GetString("tag")),
|
||||
@@ -232,14 +244,15 @@ func (c *BackendOaDocumentController) List() {
|
||||
c.oaDocOk(map[string]interface{}{"list": list, "total": total})
|
||||
}
|
||||
|
||||
// Stats GET /backend/oa/document/stats 文档库概览
|
||||
// Stats GET /backend/oa/document/stats?scope=shared|personal|all 文档库概览
|
||||
func (c *BackendOaDocumentController) Stats() {
|
||||
claims, err := c.oaDocClaims()
|
||||
if err != nil {
|
||||
c.oaDocFail(err)
|
||||
return
|
||||
}
|
||||
stats, err := services.OaDocStats(c.oaDocActor(claims))
|
||||
scope := strings.TrimSpace(c.GetString("scope"))
|
||||
stats, err := services.OaDocStats(c.oaDocActor(claims), scope)
|
||||
if err != nil {
|
||||
c.oaDocFail(err)
|
||||
return
|
||||
@@ -266,6 +279,25 @@ func (c *BackendOaDocumentController) Detail() {
|
||||
c.oaDocOk(item)
|
||||
}
|
||||
|
||||
// Content GET /backend/oa/document/content/:id 读取在线文档的编辑源(不累加查看次数)
|
||||
func (c *BackendOaDocumentController) Content() {
|
||||
claims, err := c.oaDocClaims()
|
||||
if err != nil {
|
||||
c.oaDocFail(err)
|
||||
return
|
||||
}
|
||||
id, ok := c.oaDocID()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
item, err := services.OaDocGet(c.oaDocActor(claims), id)
|
||||
if err != nil {
|
||||
c.oaDocFail(err)
|
||||
return
|
||||
}
|
||||
c.oaDocOk(map[string]interface{}{"content": item.Content})
|
||||
}
|
||||
|
||||
// Create POST /backend/oa/document/create
|
||||
func (c *BackendOaDocumentController) Create() {
|
||||
claims, err := c.oaDocClaims()
|
||||
@@ -406,10 +438,10 @@ func (c *BackendOaDocumentController) Download() {
|
||||
return
|
||||
}
|
||||
c.oaDocOk(map[string]interface{}{
|
||||
"id": item.ID,
|
||||
"title": item.Title,
|
||||
"file_url": item.FileURL,
|
||||
"file_name": item.FileName,
|
||||
"id": item.ID,
|
||||
"title": item.Title,
|
||||
"file_url": item.FileURL,
|
||||
"file_name": item.FileName,
|
||||
"download_count": item.DownCount,
|
||||
})
|
||||
}
|
||||
@@ -605,38 +637,44 @@ func (c *BackendOaDocumentController) oaDocParseSaveBody() (services.OaDocSavePa
|
||||
var body struct {
|
||||
CategoryID *uint64 `json:"category_id"`
|
||||
Title string `json:"title"`
|
||||
FileID uint64 `json:"file_id"`
|
||||
FileURL string `json:"file_url"`
|
||||
FileName string `json:"file_name"`
|
||||
Ext string `json:"ext"`
|
||||
Size uint64 `json:"size"`
|
||||
Tags string `json:"tags"`
|
||||
Summary string `json:"summary"`
|
||||
Status *int8 `json:"status"`
|
||||
Version int `json:"version"`
|
||||
OwnerID uint64 `json:"owner_id"`
|
||||
OwnerName string `json:"owner_name"`
|
||||
IsStar *int8 `json:"is_star"`
|
||||
Visibility *int8 `json:"visibility"`
|
||||
FileID uint64 `json:"file_id"`
|
||||
FileURL string `json:"file_url"`
|
||||
FileName string `json:"file_name"`
|
||||
Ext string `json:"ext"`
|
||||
Size uint64 `json:"size"`
|
||||
Tags string `json:"tags"`
|
||||
Summary string `json:"summary"`
|
||||
Content *string `json:"content"`
|
||||
Status *int8 `json:"status"`
|
||||
Version int `json:"version"`
|
||||
OwnerID uint64 `json:"owner_id"`
|
||||
OwnerName string `json:"owner_name"`
|
||||
IsStar *int8 `json:"is_star"`
|
||||
Visibility *int8 `json:"visibility"`
|
||||
}
|
||||
if !c.oaDocParseBody(&body) {
|
||||
return services.OaDocSaveParams{}, false
|
||||
}
|
||||
params := services.OaDocSaveParams{
|
||||
Title: body.Title,
|
||||
FileID: body.FileID,
|
||||
FileURL: body.FileURL,
|
||||
FileName: body.FileName,
|
||||
Ext: body.Ext,
|
||||
Size: body.Size,
|
||||
Tags: body.Tags,
|
||||
Summary: body.Summary,
|
||||
Version: body.Version,
|
||||
OwnerID: body.OwnerID,
|
||||
OwnerName: body.OwnerName,
|
||||
Title: body.Title,
|
||||
FileID: body.FileID,
|
||||
FileURL: body.FileURL,
|
||||
FileName: body.FileName,
|
||||
Ext: body.Ext,
|
||||
Size: body.Size,
|
||||
Tags: body.Tags,
|
||||
Summary: body.Summary,
|
||||
Version: body.Version,
|
||||
OwnerID: body.OwnerID,
|
||||
OwnerName: body.OwnerName,
|
||||
}
|
||||
if body.Content != nil {
|
||||
params.SetContent = true
|
||||
params.Content = *body.Content
|
||||
}
|
||||
if body.Visibility != nil {
|
||||
params.Visibility = *body.Visibility
|
||||
params.SetVisibility = true
|
||||
}
|
||||
if body.CategoryID != nil {
|
||||
params.SetCategory = true
|
||||
|
||||
@@ -18,18 +18,40 @@
|
||||
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 '分类归属用户:共享空间共享=0(租户级);私密空间=创建者的 yz_system_tenant_user.uid',
|
||||
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 '所属空间 shared-共享文档空间 personal-私密文档空间(绑定用户级别)',
|
||||
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文档分类表';
|
||||
|
||||
-- 1.1 历史库补齐列(特性上线前已建表时执行,已存在则报错可忽略)
|
||||
-- ALTER TABLE yz_backend_oa_doc_category
|
||||
-- ADD COLUMN is_system tinyint NOT NULL DEFAULT 0 COMMENT '是否系统内置分类 0-否(可删除) 1-是(不可删除)';
|
||||
-- ALTER TABLE yz_backend_oa_doc_category
|
||||
-- ADD COLUMN scope varchar(16) NOT NULL DEFAULT 'shared' COMMENT '所属空间 shared-共享 personal-私密';
|
||||
-- ALTER TABLE yz_backend_oa_doc_category
|
||||
-- ADD COLUMN user_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '分类归属用户(私密空间=租户用户 uid)';
|
||||
|
||||
-- 1.2 分类归属规则(双空间模型,2026-09 起):
|
||||
-- 文档库按「共享文档 / 私密文档」两个空间组织,每个空间各自一棵分类树:
|
||||
-- - 共享空间:scope=shared、user_id=0(租户级,全租户共用一套,任何人可建/删,内置分类除外);
|
||||
-- - 私密空间:scope=personal、user_id=当前用户的 yz_system_tenant_user.uid(绑定到用户级别,各用户独立一套)。
|
||||
-- 文档可见性(OaDoc.visibility)与所在空间一一对应:共享空间→0(租户公开),私密空间→1(私密,仅创建者与被共享者可见)。
|
||||
-- 系统内置分类「项目文档」(scope=shared, user_id=0, is_system=1) 属于共享空间,按租户自动补建
|
||||
-- (models.EnsureOaDocDefaultCategories),一般无需手工插入。
|
||||
-- 历史脏数据(scope 不在 shared/personal)在 EnsureOaDocumentTables 中自动归为共享空间(user_id=0, scope=shared)。
|
||||
-- 说明:「未分类」是虚拟分类,文档 category_id = 0 即表示未分类,两个空间各自独立计算,不落库、不可删除。
|
||||
|
||||
-- 2. 文档表
|
||||
-- doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频
|
||||
-- status : 0-草稿 1-已发布 2-已归档
|
||||
|
||||
+204
-44
@@ -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
|
||||
}
|
||||
|
||||
@@ -306,6 +306,7 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/oa/document/links", &controllers.BackendOaDocumentController{}, "get:LinkList;post:LinkCreate")
|
||||
beego.Router("/backend/oa/document/links/:id", &controllers.BackendOaDocumentController{}, "delete:LinkDelete")
|
||||
beego.Router("/backend/oa/document/detail/:id", &controllers.BackendOaDocumentController{}, "get:Detail")
|
||||
beego.Router("/backend/oa/document/content/:id", &controllers.BackendOaDocumentController{}, "get:Content")
|
||||
beego.Router("/backend/oa/document/create", &controllers.BackendOaDocumentController{}, "post:Create")
|
||||
beego.Router("/backend/oa/document/update/:id", &controllers.BackendOaDocumentController{}, "post:Update")
|
||||
beego.Router("/backend/oa/document/delete/:id", &controllers.BackendOaDocumentController{}, "delete:Delete")
|
||||
|
||||
+476
-129
@@ -18,20 +18,23 @@ import (
|
||||
|
||||
// 业务错误
|
||||
var (
|
||||
ErrDocCategoryNotFound = errors.New("分类不存在")
|
||||
ErrDocCategoryHasChild = errors.New("存在子分类,请先删除子分类")
|
||||
ErrDocCategoryHasDoc = errors.New("分类下仍有文档,请先移动或删除")
|
||||
ErrDocCategoryNameEmpty = errors.New("请输入分类名称")
|
||||
ErrDocCategoryCircular = errors.New("不能将分类移动到自己或其子分类下")
|
||||
ErrDocNotFound = errors.New("文档不存在")
|
||||
ErrDocTitleEmpty = errors.New("请输入文档标题")
|
||||
ErrDocCategoryInvalid = errors.New("所属分类不存在")
|
||||
ErrDocLinkSelf = errors.New("不能关联文档自身")
|
||||
ErrDocLinkDuplicate = errors.New("该关联关系已存在")
|
||||
ErrDocLinkTargetNotFound = errors.New("被关联的文档不存在")
|
||||
ErrDocLinkNotFound = errors.New("关联关系不存在")
|
||||
ErrDocIDEmpty = errors.New("请选择要操作的文档")
|
||||
ErrDocNoPermission = errors.New("无权操作该私密文档")
|
||||
ErrDocCategoryNotFound = errors.New("分类不存在")
|
||||
ErrDocCategoryHasChild = errors.New("存在子分类,请先删除子分类")
|
||||
ErrDocCategoryHasDoc = errors.New("分类下仍有文档,请先移动或删除")
|
||||
ErrDocCategoryNameEmpty = errors.New("请输入分类名称")
|
||||
ErrDocCategoryCircular = errors.New("不能将分类移动到自己或其子分类下")
|
||||
ErrDocCategorySystem = errors.New("系统内置分类不可删除")
|
||||
ErrDocCategoryNameKept = errors.New("该分类名称为系统保留,不可使用")
|
||||
ErrDocCategorySystemLocked = errors.New("系统内置分类固定为一级分类,不可移动")
|
||||
ErrDocNotFound = errors.New("文档不存在")
|
||||
ErrDocTitleEmpty = errors.New("请输入文档标题")
|
||||
ErrDocCategoryInvalid = errors.New("所属分类不存在")
|
||||
ErrDocLinkSelf = errors.New("不能关联文档自身")
|
||||
ErrDocLinkDuplicate = errors.New("该关联关系已存在")
|
||||
ErrDocLinkTargetNotFound = errors.New("被关联的文档不存在")
|
||||
ErrDocLinkNotFound = errors.New("关联关系不存在")
|
||||
ErrDocIDEmpty = errors.New("请选择要操作的文档")
|
||||
ErrDocNoPermission = errors.New("无权操作该私密文档")
|
||||
)
|
||||
|
||||
// ---------------- 访问者上下文与可见性 ----------------
|
||||
@@ -54,6 +57,8 @@ type OaDocActor struct {
|
||||
func NewOaDocActor(tid int, uid uint64) OaDocActor {
|
||||
models.EnsureOaDocumentTables()
|
||||
models.EnsureTenantUserOrgColumn()
|
||||
// 补建租户自己的系统内置分类(项目文档),按 tid 隔离
|
||||
models.EnsureOaDocDefaultCategories(tid)
|
||||
|
||||
actor := OaDocActor{Tid: tid, UID: uid, OrgIDs: []uint64{}}
|
||||
|
||||
@@ -164,6 +169,48 @@ func oaDocApplyVisible(qs orm.QuerySeter, a OaDocActor) orm.QuerySeter {
|
||||
return qs.SetCond(qs.GetCond().AndCond(cond))
|
||||
}
|
||||
|
||||
// ---------------- 可见性(共享 / 私密) ----------------
|
||||
//
|
||||
// 文档库按「共享文档 / 私密文档」两个空间组织,每个空间各自一棵分类树;
|
||||
// 文档可见性由 visibility 决定,并与所在空间一一对应:0=租户公开(共享空间),1=私密(私密空间,仅创建者及被共享者可见)。
|
||||
// "我的文档"由 creator_id 决定,不与空间字段强耦合。
|
||||
|
||||
// oaDocVisibilityRestrictSQL 生成 Raw SQL 场景复用的「用户级可见性」条件片段
|
||||
// (非管理员才附加:公开 OR 本人创建 OR 已共享给我)。prefix 为表别名前缀。
|
||||
func oaDocVisibilityRestrictSQL(a OaDocActor, prefix string) (string, []interface{}) {
|
||||
if a.IsAdmin {
|
||||
return "", nil
|
||||
}
|
||||
shared := oaDocSharedDocIDs(a)
|
||||
if len(shared) == 0 {
|
||||
shared = []uint64{0} // 避免 SQL 出现 IN ()
|
||||
}
|
||||
placeholders := make([]string, len(shared))
|
||||
for i := range shared {
|
||||
placeholders[i] = "?"
|
||||
}
|
||||
args := []interface{}{a.UID}
|
||||
for _, id := range shared {
|
||||
args = append(args, id)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
" AND (%svisibility = 0 OR %screator_id = ? OR %sid IN (%s))",
|
||||
prefix, prefix, prefix, strings.Join(placeholders, ",")), args
|
||||
}
|
||||
|
||||
// oaDocApplyVisibility 给查询集叠加绝对可见性过滤(按文档自身的 visibility 字段)。
|
||||
// v<0 不过滤;0=公开;1=私密。
|
||||
func oaDocApplyVisibility(qs orm.QuerySeter, v int) orm.QuerySeter {
|
||||
switch v {
|
||||
case 0:
|
||||
return qs.Filter("visibility", models.DocVisibilityPublic)
|
||||
case 1:
|
||||
return qs.Filter("visibility", models.DocVisibilityPrivate)
|
||||
default:
|
||||
return qs
|
||||
}
|
||||
}
|
||||
|
||||
// oaDocVisibleIDSet 从给定文档 ID 中筛出当前访问者可见的部分。
|
||||
func oaDocVisibleIDSet(a OaDocActor, ids []uint64) map[uint64]bool {
|
||||
out := make(map[uint64]bool, len(ids))
|
||||
@@ -217,24 +264,114 @@ type OaDocCategoryItem struct {
|
||||
Children []*OaDocCategoryItem `json:"children"`
|
||||
}
|
||||
|
||||
// oaDocCategoryBase 租户内未删除分类的基础查询集
|
||||
func oaDocCategoryBase(tid int) orm.QuerySeter {
|
||||
// ---------------- 范围(共享 / 私密) ----------------
|
||||
|
||||
// oaDocScopeOf 归一化范围参数:空或未知一律视为共享文档空间。
|
||||
func oaDocScopeOf(scope string) string {
|
||||
if scope == models.DocScopePersonal {
|
||||
return models.DocScopePersonal
|
||||
}
|
||||
if scope == models.DocScopeAll {
|
||||
return models.DocScopeAll
|
||||
}
|
||||
return models.DocScopeShared
|
||||
}
|
||||
|
||||
// oaDocScopeVisibility 返回某范围对应的文档可见性:私密→私密(1),其余→租户公开(0)。
|
||||
func oaDocScopeVisibility(scope string) int8 {
|
||||
if scope == models.DocScopePersonal {
|
||||
return models.DocVisibilityPrivate
|
||||
}
|
||||
return models.DocVisibilityPublic
|
||||
}
|
||||
|
||||
// oaDocScopeOfVisibility 由文档可见性反推范围:私密(1)→私密文档空间,其余→共享文档空间。
|
||||
func oaDocScopeOfVisibility(v int8) string {
|
||||
if v == models.DocVisibilityPrivate {
|
||||
return models.DocScopePersonal
|
||||
}
|
||||
return models.DocScopeShared
|
||||
}
|
||||
|
||||
// oaDocCategoryUserID 返回某范围下分类的归属用户:共享→0(租户级),私密→当前用户(绑定到用户级别)。
|
||||
func oaDocCategoryUserID(a OaDocActor, scope string) uint64 {
|
||||
if scope == models.DocScopePersonal {
|
||||
return a.UID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// oaDocCategoryBaseAny 租户内未删除分类的基础查询集(不限范围,用于按 ID 检索 / 权限校验)
|
||||
func oaDocCategoryBaseAny(tid int) orm.QuerySeter {
|
||||
return models.Orm.QueryTable(new(models.OaDocCategory)).
|
||||
Filter("tid", tid).
|
||||
Filter("is_deleted", 0)
|
||||
}
|
||||
|
||||
// OaDocCategoryTree 返回分类树,每个节点附带该分类下的文档数量(仅统计直属文档)。
|
||||
// 数量统计同样受可见性约束:无权限的私密文档不计入。
|
||||
func OaDocCategoryTree(a OaDocActor) ([]*OaDocCategoryItem, error) {
|
||||
// oaDocCategoryBase 返回指定范围内的未删除分类基础查询集。
|
||||
// 共享空间:scope=shared 且 user_id=0(租户级,所有人共用);
|
||||
// 私密空间:scope=personal 且 user_id=当前用户(绑定到用户级别,互不干扰);
|
||||
// all:跨两个空间(仅图谱等全局视图使用)。
|
||||
func oaDocCategoryBase(a OaDocActor, scope string) orm.QuerySeter {
|
||||
s := oaDocScopeOf(scope)
|
||||
if s == models.DocScopeAll {
|
||||
// 跨空间:不加 scope / user_id 过滤
|
||||
return oaDocCategoryBaseAny(a.Tid)
|
||||
}
|
||||
qs := oaDocCategoryBaseAny(a.Tid).Filter("scope", s)
|
||||
if s == models.DocScopePersonal {
|
||||
qs = qs.Filter("user_id", a.UID)
|
||||
} else if s == models.DocScopeShared {
|
||||
qs = qs.Filter("user_id", 0)
|
||||
}
|
||||
return qs
|
||||
}
|
||||
|
||||
// oaDocCategoryIDs 返回指定范围内全部未删除分类的 ID。
|
||||
func oaDocCategoryIDs(a OaDocActor, scope string) ([]uint64, error) {
|
||||
var list []models.OaDocCategory
|
||||
qs := oaDocCategoryBaseAny(a.Tid)
|
||||
if scope != models.DocScopeAll {
|
||||
s := oaDocScopeOf(scope)
|
||||
qs = qs.Filter("scope", s)
|
||||
if s == models.DocScopePersonal {
|
||||
qs = qs.Filter("user_id", a.UID)
|
||||
} else if s == models.DocScopeShared {
|
||||
qs = qs.Filter("user_id", 0)
|
||||
}
|
||||
}
|
||||
if _, err := qs.All(&list); err != nil && err != orm.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]uint64, 0, len(list))
|
||||
for _, c := range list {
|
||||
ids = append(ids, c.ID)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// oaDocApplyUncategorized 筛选"未分类"文档:category_id=0 或不属于当前范围任何分类。
|
||||
func oaDocApplyUncategorized(qs orm.QuerySeter, a OaDocActor, scope string) orm.QuerySeter {
|
||||
ids, err := oaDocCategoryIDs(a, scope)
|
||||
if err != nil || len(ids) == 0 {
|
||||
return qs // 范围内没有任何分类,全部视为未分类
|
||||
}
|
||||
return qs.Exclude("category_id__in", ids)
|
||||
}
|
||||
|
||||
// OaDocCategoryTree 返回指定范围内的文档分类树,每个节点附带该分类下的文档数量。
|
||||
// scope="shared" 返回共享空间树(租户级),scope="personal" 返回当前用户的私密空间树。
|
||||
// 系统内置分类(项目文档)置顶;数量统计受可见性约束:无权限的私密文档不计入。
|
||||
func OaDocCategoryTree(a OaDocActor, scope string) ([]*OaDocCategoryItem, error) {
|
||||
models.EnsureOaDocumentTables()
|
||||
|
||||
scope = oaDocScopeOf(scope)
|
||||
var list []models.OaDocCategory
|
||||
if _, err := oaDocCategoryBase(a.Tid).OrderBy("sort", "id").All(&list); err != nil && err != orm.ErrNoRows {
|
||||
if _, err := oaDocCategoryBase(a, scope).OrderBy("-is_system", "sort", "id").All(&list); err != nil && err != orm.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counts, err := OaDocCountByCategory(a)
|
||||
counts, err := OaDocCountByCategory(a, scope, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -249,6 +386,7 @@ func OaDocCategoryTree(a OaDocActor) ([]*OaDocCategoryItem, error) {
|
||||
}
|
||||
nodes[item.ID] = item
|
||||
}
|
||||
// 父节点必须也在本范围内(私密空间的父节点不会是共享空间的分类),否则归入根
|
||||
for i := range list {
|
||||
item := nodes[list[i].ID]
|
||||
if parent, ok := nodes[list[i].ParentID]; ok && list[i].ParentID != 0 {
|
||||
@@ -257,33 +395,44 @@ func OaDocCategoryTree(a OaDocActor) ([]*OaDocCategoryItem, error) {
|
||||
roots = append(roots, item)
|
||||
}
|
||||
}
|
||||
// 汇总子孙分类的文档数到父级:父级展示的是"包含所有子分类"的文档总数,
|
||||
// 避免出现父级有子分类却显示 0 的情况。
|
||||
var rollup func(item *OaDocCategoryItem) int64
|
||||
rollup = func(item *OaDocCategoryItem) int64 {
|
||||
total := item.DocCount
|
||||
for _, child := range item.Children {
|
||||
total += rollup(child)
|
||||
}
|
||||
item.DocCount = total
|
||||
return total
|
||||
}
|
||||
for _, root := range roots {
|
||||
rollup(root)
|
||||
}
|
||||
return roots, nil
|
||||
}
|
||||
|
||||
// OaDocCountByCategory 统计各分类下未删除文档的数量(仅统计当前访问者可见的文档)。
|
||||
func OaDocCountByCategory(a OaDocActor) (map[uint64]int64, error) {
|
||||
// OaDocCountByCategory 统计指定范围内各分类下未删除文档的数量。
|
||||
// visibility<0 时按范围自动取对应可见性(共享→公开,私密→私密);可见性约束同样生效:无权限的私密文档不计入。
|
||||
// scope="all" 时跨两个空间统计(不限定 visibility)。
|
||||
func OaDocCountByCategory(a OaDocActor, scope string, visibility int) (map[uint64]int64, error) {
|
||||
models.EnsureOaDocumentTables()
|
||||
|
||||
scope = oaDocScopeOf(scope)
|
||||
sql := "SELECT category_id, COUNT(*) AS cnt FROM yz_backend_oa_doc WHERE tid = ? AND is_deleted = 0"
|
||||
args := []interface{}{a.Tid}
|
||||
if !a.IsAdmin {
|
||||
shared := oaDocSharedDocIDs(a)
|
||||
if len(shared) == 0 {
|
||||
shared = []uint64{0}
|
||||
if scope != models.DocScopeAll {
|
||||
vis := int8(0)
|
||||
if visibility >= 0 {
|
||||
vis = int8(visibility)
|
||||
} else {
|
||||
vis = oaDocScopeVisibility(scope)
|
||||
}
|
||||
placeholders := make([]string, len(shared))
|
||||
for i := range shared {
|
||||
placeholders[i] = "?"
|
||||
}
|
||||
// 占位符顺序需与 args 一致:先 creator_id,再 IN (...)
|
||||
args = append(args, a.UID)
|
||||
for _, id := range shared {
|
||||
args = append(args, id)
|
||||
}
|
||||
sql += fmt.Sprintf(
|
||||
" AND (visibility = 0 OR creator_id = ? OR id IN (%s))",
|
||||
strings.Join(placeholders, ","))
|
||||
sql += fmt.Sprintf(" AND visibility = %d", vis)
|
||||
}
|
||||
extra, extraArgs := oaDocVisibilityRestrictSQL(a, "")
|
||||
sql += extra
|
||||
args = append(args, extraArgs...)
|
||||
sql += " GROUP BY category_id"
|
||||
|
||||
var rows []struct {
|
||||
@@ -300,16 +449,30 @@ func OaDocCountByCategory(a OaDocActor) (map[uint64]int64, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// OaDocCategoryCreate 新建分类。
|
||||
func OaDocCategoryCreate(tid int, parentID uint64, name, remark string, sort int) (*models.OaDocCategory, error) {
|
||||
// oaDocIsReservedName 判断分类名是否为系统保留名。
|
||||
// 「未分类」是虚拟分类(文档 category_id=0),不允许新建同名真实分类;
|
||||
// 「项目文档」为系统内置分类名(共享空间),禁止重复创建。
|
||||
func oaDocIsReservedName(name string) bool {
|
||||
name = strings.TrimSpace(name)
|
||||
return name == "未分类" || name == models.DocCategoryNameProject
|
||||
}
|
||||
|
||||
// OaDocCategoryCreate 在指定空间下新建分类。
|
||||
// scope="shared" 创建到共享空间(user_id=0,租户级);scope="personal" 创建到当前用户的私密空间(user_id=uid)。
|
||||
// 父级分类必须与目标空间一致,否则视为不存在。
|
||||
func OaDocCategoryCreate(a OaDocActor, scope string, parentID uint64, name, remark string, sort int) (*models.OaDocCategory, error) {
|
||||
models.EnsureOaDocumentTables()
|
||||
|
||||
scope = oaDocScopeOf(scope)
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, ErrDocCategoryNameEmpty
|
||||
}
|
||||
if oaDocIsReservedName(name) {
|
||||
return nil, ErrDocCategoryNameKept
|
||||
}
|
||||
if parentID != 0 {
|
||||
ok, err := oaDocCategoryExists(tid, parentID)
|
||||
ok, err := oaDocCategoryExists(a, scope, parentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -319,11 +482,13 @@ func OaDocCategoryCreate(tid int, parentID uint64, name, remark string, sort int
|
||||
}
|
||||
|
||||
item := &models.OaDocCategory{
|
||||
Tid: tid,
|
||||
Tid: a.Tid,
|
||||
UserID: oaDocCategoryUserID(a, scope),
|
||||
ParentID: parentID,
|
||||
Name: name,
|
||||
Remark: strings.TrimSpace(remark),
|
||||
Sort: sort,
|
||||
Scope: scope,
|
||||
IsDeleted: 0,
|
||||
}
|
||||
if _, err := models.Orm.Insert(item); err != nil {
|
||||
@@ -333,16 +498,22 @@ func OaDocCategoryCreate(tid int, parentID uint64, name, remark string, sort int
|
||||
}
|
||||
|
||||
// OaDocCategoryUpdate 修改分类名称/备注/排序,parentID < 0 表示不调整父级。
|
||||
func OaDocCategoryUpdate(tid int, id uint64, name, remark string, sort int, parentID int64) (*models.OaDocCategory, error) {
|
||||
func OaDocCategoryUpdate(a OaDocActor, id uint64, name, remark string, sort int, parentID int64) (*models.OaDocCategory, error) {
|
||||
models.EnsureOaDocumentTables()
|
||||
|
||||
var item models.OaDocCategory
|
||||
if err := oaDocCategoryBase(tid).Filter("id", id).One(&item); err != nil {
|
||||
if err := oaDocCategoryBaseAny(a.Tid).Filter("id", id).One(&item); err != nil {
|
||||
return nil, ErrDocCategoryNotFound
|
||||
}
|
||||
if err := oaDocCheckCategoryOwner(a, &item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fields := []string{"UpdateTime"}
|
||||
if name = strings.TrimSpace(name); name != "" && name != item.Name {
|
||||
if item.IsSystem != models.DocCategorySystem && oaDocIsReservedName(name) {
|
||||
return nil, ErrDocCategoryNameKept
|
||||
}
|
||||
item.Name = name
|
||||
fields = append(fields, "Name")
|
||||
}
|
||||
@@ -355,20 +526,22 @@ func OaDocCategoryUpdate(tid int, id uint64, name, remark string, sort int, pare
|
||||
fields = append(fields, "Sort")
|
||||
}
|
||||
if parentID >= 0 && uint64(parentID) != item.ParentID {
|
||||
if item.IsSystem == models.DocCategorySystem {
|
||||
return nil, ErrDocCategorySystemLocked
|
||||
}
|
||||
newParent := uint64(parentID)
|
||||
if newParent == id {
|
||||
return nil, ErrDocCategoryCircular
|
||||
}
|
||||
if newParent != 0 {
|
||||
ok, err := oaDocCategoryExists(tid, newParent)
|
||||
ok, err := oaDocCategoryExists(a, item.Scope, newParent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, ErrDocCategoryNotFound
|
||||
}
|
||||
// 禁止挂到自己的子孙节点下,避免形成环
|
||||
childIDs, err := OaDocCategoryDescendants(tid, id)
|
||||
childIDs, err := OaDocCategoryDescendants(a, newParent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -394,9 +567,10 @@ func OaDocCategoryUpdate(tid int, id uint64, name, remark string, sort int, pare
|
||||
}
|
||||
|
||||
// OaDocCategoryDescendants 收集某分类的全部子孙 ID(不含自身)。
|
||||
func OaDocCategoryDescendants(tid int, rootID uint64) ([]uint64, error) {
|
||||
// 遍历租户内全部未删除分类(跨空间),用于移动时的环路检测。
|
||||
func OaDocCategoryDescendants(a OaDocActor, rootID uint64) ([]uint64, error) {
|
||||
var list []models.OaDocCategory
|
||||
if _, err := oaDocCategoryBase(tid).All(&list); err != nil && err != orm.ErrNoRows {
|
||||
if _, err := oaDocCategoryBaseAny(a.Tid).All(&list); err != nil && err != orm.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
children := make(map[uint64][]uint64)
|
||||
@@ -420,27 +594,35 @@ func OaDocCategoryDescendants(tid int, rootID uint64) ([]uint64, error) {
|
||||
}
|
||||
|
||||
// oaDocCategoryIDsWithChildren 返回指定分类及其所有子孙 ID;rootID=0 时返回 nil 表示不限制。
|
||||
func oaDocCategoryIDsWithChildren(tid int, rootID uint64) ([]uint64, error) {
|
||||
func oaDocCategoryIDsWithChildren(a OaDocActor, scope string, rootID uint64) ([]uint64, error) {
|
||||
if rootID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
children, err := OaDocCategoryDescendants(tid, rootID)
|
||||
children, err := OaDocCategoryDescendants(a, rootID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append([]uint64{rootID}, children...), nil
|
||||
}
|
||||
|
||||
// OaDocCategoryDelete 删除分类:存在子分类或文档时拒绝删除。
|
||||
func OaDocCategoryDelete(tid int, id uint64) error {
|
||||
// OaDocCategoryDelete 删除分类:系统内置分类、存在子分类或文档时拒绝删除。
|
||||
// 共享空间分类任何租户成员均可删除(内置分类除外);私密空间分类仅本人可删除。
|
||||
func OaDocCategoryDelete(a OaDocActor, id uint64) error {
|
||||
models.EnsureOaDocumentTables()
|
||||
|
||||
var item models.OaDocCategory
|
||||
if err := oaDocCategoryBase(tid).Filter("id", id).One(&item); err != nil {
|
||||
if err := oaDocCategoryBaseAny(a.Tid).Filter("id", id).One(&item); err != nil {
|
||||
return ErrDocCategoryNotFound
|
||||
}
|
||||
// 内置分类(如「项目文档」)为默认分类,任何租户都不可删除
|
||||
if item.IsSystem == models.DocCategorySystem {
|
||||
return ErrDocCategorySystem
|
||||
}
|
||||
if err := oaDocCheckCategoryOwner(a, &item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
childCount, err := oaDocCategoryBase(tid).Filter("parent_id", id).Count()
|
||||
childCount, err := oaDocCategoryBase(a, item.Scope).Filter("parent_id", id).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -448,7 +630,7 @@ func OaDocCategoryDelete(tid int, id uint64) error {
|
||||
return ErrDocCategoryHasChild
|
||||
}
|
||||
|
||||
docCount, err := oaDocBase(tid).Filter("category_id", id).Count()
|
||||
docCount, err := oaDocBase(a.Tid).Filter("category_id", id).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -457,7 +639,7 @@ func OaDocCategoryDelete(tid int, id uint64) error {
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
_, err = oaDocCategoryBase(tid).Filter("id", id).Update(orm.Params{
|
||||
_, err = oaDocCategoryBaseAny(a.Tid).Filter("id", id).Update(orm.Params{
|
||||
"IsDeleted": 1,
|
||||
"DeleteTime": now,
|
||||
"UpdateTime": now,
|
||||
@@ -465,14 +647,37 @@ func OaDocCategoryDelete(tid int, id uint64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func oaDocCategoryExists(tid int, id uint64) (bool, error) {
|
||||
n, err := oaDocCategoryBase(tid).Filter("id", id).Count()
|
||||
// oaDocCategoryExists 判断指定范围内是否存在该分类(私密空间按 user_id 隔离)。
|
||||
func oaDocCategoryExists(a OaDocActor, scope string, id uint64) (bool, error) {
|
||||
n, err := oaDocCategoryBase(a, scope).Filter("id", id).Count()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
// oaDocCheckCategoryOwner 校验分类是否可被该访问者操作。
|
||||
// 共享分类(user_id=0)租户内任何成员均可操作;私密分类(user_id>0)仅归属用户本人可操作。
|
||||
func oaDocCheckCategoryOwner(a OaDocActor, item *models.OaDocCategory) error {
|
||||
if item.UserID == 0 {
|
||||
return nil
|
||||
}
|
||||
if item.UserID == a.UID && a.UID != 0 {
|
||||
return nil
|
||||
}
|
||||
return ErrDocCategoryNotFound
|
||||
}
|
||||
|
||||
// oaDocCategoryUsable 读取指定范围内的分类并校验当前访问者是否可以使用它(用于文档归类)。
|
||||
// 不存在或不属于当前范围/用户时返回 ok=false。
|
||||
func oaDocCategoryUsable(a OaDocActor, scope string, id uint64) (models.OaDocCategory, bool, error) {
|
||||
var item models.OaDocCategory
|
||||
if err := oaDocCategoryBase(a, scope).Filter("id", id).One(&item); err != nil {
|
||||
return item, false, nil
|
||||
}
|
||||
return item, true, nil
|
||||
}
|
||||
|
||||
// ---------------- 文档 ----------------
|
||||
|
||||
// oaDocBase 租户内未删除文档的基础查询集
|
||||
@@ -483,13 +688,17 @@ func oaDocBase(tid int) orm.QuerySeter {
|
||||
}
|
||||
|
||||
// OaDocListParams 文档列表查询条件。
|
||||
// Status/DocType/Star 使用 -1 表示"全部";CategoryID > 0 时包含其全部子孙分类。
|
||||
// Actor 为访问者上下文,用于租户内用户级可见性过滤(私密文档只对创建者/被共享者可见)。
|
||||
// Scope 限定空间:"shared"=共享文档(租户公开),"personal"=私密文档(绑定用户级别),"all"=跨两个空间。
|
||||
// Status/DocType/Star 使用 -1 表示"全部";CategoryID > 0 时包含其全部子孙分类(须与 Scope 同空间)。
|
||||
// Mine 为 true 时只看本人创建的文档。Actor 用于租户内用户级可见性过滤。
|
||||
type OaDocListParams struct {
|
||||
Actor OaDocActor
|
||||
Actor OaDocActor
|
||||
Scope string
|
||||
Keyword string
|
||||
CategoryID uint64
|
||||
Uncategorized bool // true 时只看未分类(category_id = 0)的文档
|
||||
Visibility int // -1 不限(由 Scope 推导);0 公开;1 私密
|
||||
Mine bool // true 时只看本人创建的文档
|
||||
Status int
|
||||
DocType int
|
||||
Tag string
|
||||
@@ -498,11 +707,18 @@ type OaDocListParams struct {
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// OaDocList 分页查询文档(自动应用可见性过滤)。
|
||||
// OaDocList 分页查询文档(自动应用可见性与范围过滤)。
|
||||
func OaDocList(a OaDocActor, p OaDocListParams) ([]models.OaDoc, int64, error) {
|
||||
models.EnsureOaDocumentTables()
|
||||
|
||||
scope := oaDocScopeOf(p.Scope)
|
||||
qs := oaDocApplyVisible(oaDocBase(a.Tid), a)
|
||||
if scope != models.DocScopeAll {
|
||||
qs = qs.Filter("visibility", oaDocScopeVisibility(scope))
|
||||
}
|
||||
if p.Mine {
|
||||
qs = qs.Filter("creator_id", a.UID)
|
||||
}
|
||||
if kw := strings.TrimSpace(p.Keyword); kw != "" {
|
||||
cond := orm.NewCondition()
|
||||
cond = cond.Or("title__icontains", kw).
|
||||
@@ -513,14 +729,17 @@ func OaDocList(a OaDocActor, p OaDocListParams) ([]models.OaDoc, int64, error) {
|
||||
qs = qs.SetCond(qs.GetCond().AndCond(cond))
|
||||
}
|
||||
if p.Uncategorized {
|
||||
qs = qs.Filter("category_id", 0)
|
||||
qs = oaDocApplyUncategorized(qs, a, scope)
|
||||
} else if p.CategoryID > 0 {
|
||||
ids, err := oaDocCategoryIDsWithChildren(a.Tid, p.CategoryID)
|
||||
ids, err := oaDocCategoryIDsWithChildren(a, scope, p.CategoryID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
qs = qs.Filter("category_id__in", ids)
|
||||
} else {
|
||||
// 所选分类不在当前空间,直接返回空结果
|
||||
qs = qs.Filter("category_id__in", []uint64{})
|
||||
}
|
||||
}
|
||||
if p.Status >= 0 {
|
||||
@@ -559,6 +778,13 @@ func OaDocList(a OaDocActor, p OaDocListParams) ([]models.OaDoc, int64, error) {
|
||||
if list == nil {
|
||||
list = []models.OaDoc{}
|
||||
}
|
||||
// 列表不返回体积较大的编辑源,仅标记是否在线制作
|
||||
for i := range list {
|
||||
if strings.TrimSpace(list[i].Content) != "" {
|
||||
list[i].IsOnline = 1
|
||||
}
|
||||
list[i].Content = ""
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
@@ -603,20 +829,26 @@ func OaDocDownload(a OaDocActor, id uint64) (*models.OaDoc, error) {
|
||||
type OaDocSaveParams struct {
|
||||
SetCategory bool
|
||||
CategoryID uint64
|
||||
Title string
|
||||
FileID uint64
|
||||
FileURL string
|
||||
FileName string
|
||||
Ext string
|
||||
Size uint64
|
||||
Tags string
|
||||
Summary string
|
||||
Status int8
|
||||
Version int
|
||||
OwnerID uint64
|
||||
OwnerName string
|
||||
IsStar int8
|
||||
Visibility int8 // 0-租户公开 1-私密
|
||||
// SetVisibility 为 true 时才以 Visibility 覆盖原值,
|
||||
// 避免"未传该字段"被当成"改为租户公开"(int8 零值无法区分未传与显式 0)
|
||||
SetVisibility bool
|
||||
Title string
|
||||
FileID uint64
|
||||
FileURL string
|
||||
FileName string
|
||||
Ext string
|
||||
Size uint64
|
||||
Tags string
|
||||
Summary string
|
||||
// Content 在线制作文档的编辑源(富文本 HTML),SetContent 为 true 时才覆盖
|
||||
SetContent bool
|
||||
Content string
|
||||
Status int8
|
||||
Version int
|
||||
OwnerID uint64
|
||||
OwnerName string
|
||||
IsStar int8
|
||||
Visibility int8 // 0-租户公开 1-私密
|
||||
}
|
||||
|
||||
// OaDocCreate 新增文档。
|
||||
@@ -632,12 +864,12 @@ func OaDocCreate(a OaDocActor, operatorName string, p OaDocSaveParams) (*models.
|
||||
if title == "" {
|
||||
return nil, ErrDocTitleEmpty
|
||||
}
|
||||
// 分类须与文档可见性同空间:共享(visibility=0)→共享空间;私密(visibility=1)→私密空间(绑定用户级别)。
|
||||
if p.CategoryID > 0 {
|
||||
ok, err := oaDocCategoryExists(tid, p.CategoryID)
|
||||
if err != nil {
|
||||
scope := oaDocScopeOfVisibility(p.Visibility)
|
||||
if _, ok, err := oaDocCategoryUsable(a, scope, p.CategoryID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
} else if !ok {
|
||||
return nil, ErrDocCategoryInvalid
|
||||
}
|
||||
}
|
||||
@@ -666,6 +898,7 @@ func OaDocCreate(a OaDocActor, operatorName string, p OaDocSaveParams) (*models.
|
||||
DocType: models.DocTypeByExt(ext),
|
||||
Tags: strings.TrimSpace(p.Tags),
|
||||
Summary: strings.TrimSpace(p.Summary),
|
||||
Content: p.Content,
|
||||
Status: p.Status,
|
||||
Version: version,
|
||||
IsStar: p.IsStar,
|
||||
@@ -703,23 +936,29 @@ func OaDocUpdate(a OaDocActor, id uint64, p OaDocSaveParams) (*models.OaDoc, err
|
||||
}
|
||||
|
||||
fields := []string{"UpdateTime"}
|
||||
if v := visibilityOf(p.Visibility); v != item.Visibility {
|
||||
item.Visibility = v
|
||||
fields = append(fields, "Visibility")
|
||||
}
|
||||
if p.SetCategory && p.CategoryID != item.CategoryID {
|
||||
// 分类须与文档可见性同空间:以"更新后"的可见性推导范围
|
||||
if p.CategoryID > 0 {
|
||||
ok, err := oaDocCategoryExists(a.Tid, p.CategoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
v := item.Visibility
|
||||
if p.SetVisibility {
|
||||
v = p.Visibility
|
||||
}
|
||||
if !ok {
|
||||
scope := oaDocScopeOfVisibility(v)
|
||||
if _, ok, err := oaDocCategoryUsable(a, scope, p.CategoryID); err != nil {
|
||||
return nil, err
|
||||
} else if !ok {
|
||||
return nil, ErrDocCategoryInvalid
|
||||
}
|
||||
}
|
||||
item.CategoryID = p.CategoryID
|
||||
fields = append(fields, "CategoryID")
|
||||
}
|
||||
if p.SetVisibility {
|
||||
if v := visibilityOf(p.Visibility); v != item.Visibility {
|
||||
item.Visibility = v
|
||||
fields = append(fields, "Visibility")
|
||||
}
|
||||
}
|
||||
if title := strings.TrimSpace(p.Title); title != "" && title != item.Title {
|
||||
item.Title = title
|
||||
fields = append(fields, "Title")
|
||||
@@ -775,6 +1014,11 @@ func OaDocUpdate(a OaDocActor, id uint64, p OaDocSaveParams) (*models.OaDoc, err
|
||||
item.OwnerName = owner
|
||||
fields = append(fields, "OwnerName")
|
||||
}
|
||||
// 在线文档编辑源:仅在显式传入时覆盖
|
||||
if p.SetContent && p.Content != item.Content {
|
||||
item.Content = p.Content
|
||||
fields = append(fields, "Content")
|
||||
}
|
||||
|
||||
if len(fields) == 1 {
|
||||
return &item, nil
|
||||
@@ -840,13 +1084,16 @@ func OaDocMove(a OaDocActor, ids []uint64, categoryID uint64) (int64, error) {
|
||||
if len(ids) == 0 {
|
||||
return 0, ErrDocIDEmpty
|
||||
}
|
||||
// 移动仅改变文档归属分类;分类须与每篇文档的可见性同空间(共享/私密)
|
||||
if categoryID > 0 {
|
||||
ok, err := oaDocCategoryExists(a.Tid, categoryID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !ok {
|
||||
return 0, ErrDocCategoryInvalid
|
||||
var sample models.OaDoc
|
||||
if _, err := oaDocApplyVisible(oaDocBase(a.Tid).Filter("id__in", ids), a).OrderBy("id").Limit(1).All(&sample); err == nil {
|
||||
scope := oaDocScopeOfVisibility(sample.Visibility)
|
||||
if _, ok, err := oaDocCategoryUsable(a, scope, categoryID); err != nil {
|
||||
return 0, err
|
||||
} else if !ok {
|
||||
return 0, ErrDocCategoryInvalid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,10 +1112,11 @@ func OaDocMove(a OaDocActor, ids []uint64, categoryID uint64) (int64, error) {
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return oaDocBase(a.Tid).Filter("id__in", allowed).Update(orm.Params{
|
||||
params := orm.Params{
|
||||
"CategoryID": categoryID,
|
||||
"UpdateTime": now,
|
||||
})
|
||||
}
|
||||
return oaDocBase(a.Tid).Filter("id__in", allowed).Update(params)
|
||||
}
|
||||
|
||||
// OaDocToggleStar 收藏 / 取消收藏切换,返回切换后的状态。
|
||||
@@ -892,16 +1140,20 @@ func OaDocToggleStar(a OaDocActor, id uint64) (int8, error) {
|
||||
|
||||
// OaDocStatsResult 文档库概览统计
|
||||
type OaDocStatsResult struct {
|
||||
Total int64 `json:"total"`
|
||||
Published int64 `json:"published"`
|
||||
Draft int64 `json:"draft"`
|
||||
Archived int64 `json:"archived"`
|
||||
Starred int64 `json:"starred"`
|
||||
Uncategorized int64 `json:"uncategorized"`
|
||||
LinkCount int64 `json:"link_count"`
|
||||
TypeCounts []OaDocTypeCount `json:"type_counts"`
|
||||
RecentDocs []models.OaDoc `json:"recent_docs"`
|
||||
HotDocs []models.OaDoc `json:"hot_docs"`
|
||||
Total int64 `json:"total"`
|
||||
Published int64 `json:"published"`
|
||||
Draft int64 `json:"draft"`
|
||||
Archived int64 `json:"archived"`
|
||||
Starred int64 `json:"starred"`
|
||||
Uncategorized int64 `json:"uncategorized"`
|
||||
LinkCount int64 `json:"link_count"`
|
||||
// 可见性维度概览(不受当前 visibility 筛选影响,始终反映全库口径):
|
||||
PublicCount int64 `json:"public_count"` // 团队公开文档数
|
||||
PrivateCount int64 `json:"private_count"` // 私密文档数
|
||||
MineCount int64 `json:"mine_count"` // 我创建的文档数
|
||||
TypeCounts []OaDocTypeCount `json:"type_counts"`
|
||||
RecentDocs []models.OaDoc `json:"recent_docs"`
|
||||
HotDocs []models.OaDoc `json:"hot_docs"`
|
||||
}
|
||||
|
||||
// OaDocTypeCount 按文档类型统计
|
||||
@@ -910,26 +1162,71 @@ type OaDocTypeCount struct {
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// OaDocStats 文档库概览:总数、状态分布、类型分布、最近更新与热门文档(均受可见性约束)。
|
||||
func OaDocStats(a OaDocActor) (*OaDocStatsResult, error) {
|
||||
// OaDocStats 文档库概览:总数、状态分布、类型分布、最近更新与热门文档。
|
||||
// scope 限定统计口径("shared" 共享文档空间 / "personal" 私密文档空间 / "all" 跨空间);可见性约束同样生效。
|
||||
func OaDocStats(a OaDocActor, scope string) (*OaDocStatsResult, error) {
|
||||
models.EnsureOaDocumentTables()
|
||||
|
||||
base := oaDocApplyVisible(oaDocBase(a.Tid), a)
|
||||
scope = oaDocScopeOf(scope)
|
||||
vis := int8(-1)
|
||||
if scope != models.DocScopeAll {
|
||||
vis = oaDocScopeVisibility(scope)
|
||||
}
|
||||
|
||||
// 当前范围内的全部可见文档(私密仅创建者/被共享者可见)
|
||||
allVisible := oaDocApplyVisible(oaDocBase(a.Tid), a)
|
||||
if vis >= 0 {
|
||||
allVisible = allVisible.Filter("visibility", vis)
|
||||
}
|
||||
mineCount, _ := allVisible.Filter("creator_id", a.UID).Count()
|
||||
|
||||
base := allVisible
|
||||
total, _ := base.Count()
|
||||
published, _ := base.Filter("status", models.DocStatusPublish).Count()
|
||||
draft, _ := base.Filter("status", models.DocStatusDraft).Count()
|
||||
archived, _ := base.Filter("status", models.DocStatusArchive).Count()
|
||||
starred, _ := base.Filter("is_star", 1).Count()
|
||||
uncategorized, _ := base.Filter("category_id", 0).Count()
|
||||
linkCount, _ := models.Orm.QueryTable(new(models.OaDocLink)).Filter("tid", a.Tid).Count()
|
||||
uncategorized, _ := oaDocApplyUncategorized(base, a, scope).Count()
|
||||
|
||||
// 关联数只统计"当前空间口径下文档发起"的连线
|
||||
linkCount := int64(0)
|
||||
linkExtra, linkExtraArgs := oaDocVisibilityRestrictSQL(a, "d.")
|
||||
linkArgs := []interface{}{a.Tid}
|
||||
if vis >= 0 {
|
||||
linkArgs = append(linkArgs, vis)
|
||||
}
|
||||
linkArgs = append(linkArgs, linkExtraArgs...)
|
||||
var linkRows []struct {
|
||||
Cnt int64 `orm:"column(cnt)"`
|
||||
}
|
||||
visClause := ""
|
||||
if vis >= 0 {
|
||||
visClause = " AND d.visibility = ?"
|
||||
}
|
||||
_, _ = models.Orm.Raw(
|
||||
`SELECT COUNT(*) AS cnt FROM yz_backend_oa_doc_link l
|
||||
WHERE l.tid = ?`+visClause+` AND EXISTS (
|
||||
SELECT 1 FROM yz_backend_oa_doc d
|
||||
WHERE d.id = l.source_id AND d.is_deleted = 0`+linkExtra+")",
|
||||
linkArgs...).QueryRows(&linkRows)
|
||||
if len(linkRows) > 0 {
|
||||
linkCount = linkRows[0].Cnt
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
DocType int8 `orm:"column(doc_type)"`
|
||||
Cnt int64 `orm:"column(cnt)"`
|
||||
}
|
||||
typeExtra, typeExtraArgs := oaDocVisibilityRestrictSQL(a, "")
|
||||
typeArgs := []interface{}{a.Tid}
|
||||
if vis >= 0 {
|
||||
typeArgs = append(typeArgs, vis)
|
||||
}
|
||||
typeArgs = append(typeArgs, typeExtraArgs...)
|
||||
_, _ = models.Orm.Raw(
|
||||
"SELECT doc_type, COUNT(*) AS cnt FROM yz_backend_oa_doc WHERE tid = ? AND is_deleted = 0 GROUP BY doc_type ORDER BY cnt DESC",
|
||||
a.Tid).QueryRows(&rows)
|
||||
"SELECT doc_type, COUNT(*) AS cnt FROM yz_backend_oa_doc WHERE tid = ? AND is_deleted = 0"+
|
||||
visClause+typeExtra+" GROUP BY doc_type ORDER BY cnt DESC",
|
||||
typeArgs...).QueryRows(&rows)
|
||||
typeCounts := make([]OaDocTypeCount, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
typeCounts = append(typeCounts, OaDocTypeCount{DocType: r.DocType, Count: r.Cnt})
|
||||
@@ -946,6 +1243,17 @@ func OaDocStats(a OaDocActor) (*OaDocStatsResult, error) {
|
||||
hot = []models.OaDoc{}
|
||||
}
|
||||
|
||||
publicCount, privateCount := int64(0), int64(0)
|
||||
switch scope {
|
||||
case models.DocScopeShared:
|
||||
publicCount = total
|
||||
case models.DocScopePersonal:
|
||||
privateCount = total
|
||||
case models.DocScopeAll:
|
||||
publicCount, _ = oaDocApplyVisible(oaDocBase(a.Tid), a).Filter("visibility", models.DocVisibilityPublic).Count()
|
||||
privateCount, _ = oaDocApplyVisible(oaDocBase(a.Tid), a).Filter("visibility", models.DocVisibilityPrivate).Count()
|
||||
}
|
||||
|
||||
return &OaDocStatsResult{
|
||||
Total: total,
|
||||
Published: published,
|
||||
@@ -954,6 +1262,9 @@ func OaDocStats(a OaDocActor) (*OaDocStatsResult, error) {
|
||||
Starred: starred,
|
||||
Uncategorized: uncategorized,
|
||||
LinkCount: linkCount,
|
||||
PublicCount: publicCount,
|
||||
PrivateCount: privateCount,
|
||||
MineCount: mineCount,
|
||||
TypeCounts: typeCounts,
|
||||
RecentDocs: recent,
|
||||
HotDocs: hot,
|
||||
@@ -1371,13 +1682,13 @@ type OaDocRelationCount struct {
|
||||
|
||||
// OaDocGraphStats 图谱概览统计
|
||||
type OaDocGraphStats struct {
|
||||
DocCount int64 `json:"doc_count"`
|
||||
LinkCount int64 `json:"link_count"`
|
||||
CategoryCount int64 `json:"category_count"`
|
||||
IsolatedCount int64 `json:"isolated_count"`
|
||||
AvgDegree float64 `json:"avg_degree"`
|
||||
DocCount int64 `json:"doc_count"`
|
||||
LinkCount int64 `json:"link_count"`
|
||||
CategoryCount int64 `json:"category_count"`
|
||||
IsolatedCount int64 `json:"isolated_count"`
|
||||
AvgDegree float64 `json:"avg_degree"`
|
||||
RelationCounts []OaDocRelationCount `json:"relation_counts"`
|
||||
TopNodes []OaDocGraphNode `json:"top_nodes"`
|
||||
TopNodes []OaDocGraphNode `json:"top_nodes"`
|
||||
}
|
||||
|
||||
// OaDocGraphResult 图谱数据
|
||||
@@ -1402,7 +1713,8 @@ func OaDocGraph(a OaDocActor, p OaDocGraphParams) (*OaDocGraphResult, error) {
|
||||
}
|
||||
|
||||
docs, _, err := OaDocList(a, OaDocListParams{
|
||||
Actor: a,
|
||||
Actor: a,
|
||||
Scope: models.DocScopeAll,
|
||||
Keyword: p.Keyword,
|
||||
CategoryID: p.CategoryID,
|
||||
Status: -1,
|
||||
@@ -1476,18 +1788,31 @@ func OaDocGraph(a OaDocActor, p OaDocGraphParams) (*OaDocGraphResult, error) {
|
||||
// 归属分类连线 + 分类中心节点
|
||||
categoryCount := int64(0)
|
||||
if p.IncludeCategory {
|
||||
// 图谱包含两类分类节点:共享分类(user_id=0,Category=1)与当前用户自己的私密分类(Category=2)。
|
||||
// 只取用户可见范围(共享 + 本人私密),避免把其他用户的私密分类暴露出来。
|
||||
catCond := orm.NewCondition().
|
||||
And("tid", a.Tid).
|
||||
And("is_deleted", 0).
|
||||
AndCond(orm.NewCondition().Or("user_id", 0).Or("user_id", a.UID))
|
||||
var cats []models.OaDocCategory
|
||||
if _, err := oaDocCategoryBase(a.Tid).OrderBy("sort", "id").All(&cats); err != nil && err != orm.ErrNoRows {
|
||||
if _, err := models.Orm.QueryTable(new(models.OaDocCategory)).SetCond(catCond).OrderBy("sort", "id").All(&cats); err != nil && err != orm.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
categoryCount = int64(len(cats))
|
||||
|
||||
used := make(map[uint64]bool, len(cats))
|
||||
catByID := make(map[uint64]models.OaDocCategory, len(cats))
|
||||
for _, c := range cats {
|
||||
catByID[c.ID] = c
|
||||
}
|
||||
|
||||
// 文档 → 分类 归属连线(仅当分类在当前可见范围内,避免出现指向不存在节点的悬空连线)
|
||||
for _, d := range docs {
|
||||
if d.CategoryID == 0 {
|
||||
continue
|
||||
}
|
||||
used[d.CategoryID] = true
|
||||
if _, ok := catByID[d.CategoryID]; !ok {
|
||||
continue
|
||||
}
|
||||
links = append(links, OaDocGraphLink{
|
||||
Source: fmt.Sprintf("d%d", d.ID),
|
||||
Target: fmt.Sprintf("c%d", d.CategoryID),
|
||||
@@ -1498,14 +1823,36 @@ func OaDocGraph(a OaDocActor, p OaDocGraphParams) (*OaDocGraphResult, error) {
|
||||
degree[fmt.Sprintf("d%d", d.ID)]++
|
||||
degree[fmt.Sprintf("c%d", d.CategoryID)]++
|
||||
}
|
||||
|
||||
// 分类 → 父分类 层级连线(补全分类树的上下级结构,避免顶级/上级分类缺失)
|
||||
for _, c := range cats {
|
||||
if !used[c.ID] {
|
||||
if c.ParentID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := catByID[c.ParentID]; !ok {
|
||||
continue
|
||||
}
|
||||
links = append(links, OaDocGraphLink{
|
||||
Source: fmt.Sprintf("c%d", c.ID),
|
||||
Target: fmt.Sprintf("c%d", c.ParentID),
|
||||
Relation: models.DocRelationBelong,
|
||||
Label: "子分类",
|
||||
Kind: "cate",
|
||||
})
|
||||
degree[fmt.Sprintf("c%d", c.ID)]++
|
||||
degree[fmt.Sprintf("c%d", c.ParentID)]++
|
||||
}
|
||||
|
||||
// 当前可见范围内的全部分类都作为中心节点,保证分类树完整
|
||||
for _, c := range cats {
|
||||
catKind := 1
|
||||
if c.UserID != 0 {
|
||||
catKind = 2 // 私密分类
|
||||
}
|
||||
nodes = append(nodes, OaDocGraphNode{
|
||||
ID: fmt.Sprintf("c%d", c.ID),
|
||||
Name: c.Name,
|
||||
Category: 1,
|
||||
Category: catKind,
|
||||
CategoryID: c.ID,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user