270 lines
8.8 KiB
Go
270 lines
8.8 KiB
Go
package services
|
||
|
||
import (
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"server/models"
|
||
|
||
"github.com/beego/beego/v2/client/orm"
|
||
)
|
||
|
||
// 项目文档(CRM 项目 ↔ OA 文档库)绑定业务逻辑。
|
||
//
|
||
// 约定:每个项目在文档库「共享文档 / 项目文档」下拥有一个与项目同名的文件夹,
|
||
// 项目管理侧负责调用与文件上传,文件统一写入文档库(yz_backend_oa_doc),
|
||
// 文档库侧负责浏览、图谱与共享。项目表用 doc_category_id 记住自己文件夹的分类ID,
|
||
// 项目改名时同步重命名文件夹;存量项目在首次访问项目文档时自动补建。
|
||
|
||
// EnsureCrmProjectDocCategory 确保项目在文档库「共享文档 / 项目文档」下拥有同名文件夹,返回文件夹分类ID。
|
||
//
|
||
// 兼容存量项目:项目上的分类ID为空(0)或指向已删除分类时自动补建,并回写项目表;
|
||
// 若「项目文档」下已有同名且未被其它项目占用的文件夹则复用,避免重复建目录。
|
||
// 失败时返回 error(由调用方决定是否阻断,建议静默忽略,不影响项目主流程)。
|
||
func EnsureCrmProjectDocCategory(tid int, projectID uint64, projectName string, currentCategoryID uint64) (uint64, error) {
|
||
if models.Orm == nil || tid <= 0 {
|
||
return 0, nil
|
||
}
|
||
projectName = strings.TrimSpace(projectName)
|
||
if projectName == "" {
|
||
return 0, nil
|
||
}
|
||
|
||
models.EnsureOaDocDefaultCategories(tid)
|
||
|
||
// 1. 已绑定且分类仍有效:直接返回
|
||
if currentCategoryID > 0 {
|
||
n, err := models.Orm.QueryTable(new(models.OaDocCategory)).
|
||
Filter("tid", tid).
|
||
Filter("id", currentCategoryID).
|
||
Filter("is_deleted", 0).Count()
|
||
if err == nil && n > 0 {
|
||
return currentCategoryID, nil
|
||
}
|
||
}
|
||
|
||
// 2. 定位「项目文档」内置分类(首次访问时已由 EnsureOaDocDefaultCategories 补建)
|
||
rootID, err := crmProjectDocRootCategory(tid)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
// 3. 复用「项目文档」下同名且未被其它项目占用的文件夹(同名项目共存时各用各的)
|
||
var sameName []models.OaDocCategory
|
||
if _, err := models.Orm.QueryTable(new(models.OaDocCategory)).
|
||
Filter("tid", tid).
|
||
Filter("parent_id", rootID).
|
||
Filter("scope", models.DocScopeShared).
|
||
Filter("user_id", 0).
|
||
Filter("name", projectName).
|
||
Filter("is_deleted", 0).
|
||
OrderBy("id").All(&sameName); err != nil && err != orm.ErrNoRows {
|
||
return 0, err
|
||
}
|
||
for _, c := range sameName {
|
||
bound, err := crmProjectDocCategoryBound(tid, c.ID, projectID)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
if !bound {
|
||
return c.ID, crmProjectDocBindCategory(tid, projectID, c.ID)
|
||
}
|
||
}
|
||
|
||
// 4. 新建项目同名文件夹
|
||
item := &models.OaDocCategory{
|
||
Tid: tid,
|
||
UserID: 0,
|
||
ParentID: rootID,
|
||
Name: projectName,
|
||
Remark: "项目文档:" + projectName,
|
||
Sort: 0,
|
||
IsSystem: models.DocCategoryNormal,
|
||
Scope: models.DocScopeShared,
|
||
IsDeleted: 0,
|
||
}
|
||
id, err := models.Orm.Insert(item)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
categoryID := uint64(id)
|
||
return categoryID, crmProjectDocBindCategory(tid, projectID, categoryID)
|
||
}
|
||
|
||
// RenameCrmProjectDocCategory 项目改名时同步重命名文档库中的项目文件夹(失败由调用方处理)。
|
||
func RenameCrmProjectDocCategory(tid int, categoryID uint64, newName string) error {
|
||
newName = strings.TrimSpace(newName)
|
||
if models.Orm == nil || tid <= 0 || categoryID == 0 || newName == "" {
|
||
return nil
|
||
}
|
||
var item models.OaDocCategory
|
||
if err := models.Orm.QueryTable(new(models.OaDocCategory)).
|
||
Filter("tid", tid).Filter("id", categoryID).
|
||
Filter("is_deleted", 0).One(&item); err != nil {
|
||
return ErrDocCategoryNotFound
|
||
}
|
||
if item.Name == newName {
|
||
return nil
|
||
}
|
||
now := time.Now()
|
||
_, err := models.Orm.QueryTable(new(models.OaDocCategory)).
|
||
Filter("id", categoryID).Filter("tid", tid).
|
||
Update(orm.Params{"name": newName, "update_time": now})
|
||
return err
|
||
}
|
||
|
||
// CrmProjectDocCategoryNameMap 返回租户内未删除分类的 ID -> 名称映射(项目文档列表展示文件夹名用)。
|
||
func CrmProjectDocCategoryNameMap(tid int) map[uint64]string {
|
||
result := make(map[uint64]string)
|
||
if models.Orm == nil || tid <= 0 {
|
||
return result
|
||
}
|
||
var list []models.OaDocCategory
|
||
if _, err := models.Orm.QueryTable(new(models.OaDocCategory)).
|
||
Filter("tid", tid).Filter("is_deleted", 0).All(&list); err != nil && err != orm.ErrNoRows {
|
||
return result
|
||
}
|
||
for _, c := range list {
|
||
result[c.ID] = c.Name
|
||
}
|
||
return result
|
||
}
|
||
|
||
// CrmProjectDocCategoryInScope 判断分类是否位于项目文件夹内(含项目文件夹自身)。
|
||
// 用于约束项目侧的文件夹创建、文件上传与删除只能在项目自己的目录下操作。
|
||
func CrmProjectDocCategoryInScope(a OaDocActor, rootID, categoryID uint64) bool {
|
||
if rootID == 0 || categoryID == 0 {
|
||
return false
|
||
}
|
||
if categoryID == rootID {
|
||
return true
|
||
}
|
||
children, err := OaDocCategoryDescendants(a, rootID)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
for _, id := range children {
|
||
if id == categoryID {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ProjectDocFolder 项目文档文件夹树节点(含该文件夹直接挂载的文档数量)。
|
||
type ProjectDocFolder struct {
|
||
ID uint64 `json:"id"`
|
||
ParentID uint64 `json:"parent_id"`
|
||
Name string `json:"name"`
|
||
DocCount int64 `json:"doc_count"`
|
||
Children []*ProjectDocFolder `json:"children"`
|
||
}
|
||
|
||
// CrmProjectDocFolderTree 构建项目文件夹树(根为项目文件夹,含全部子孙文件夹)。
|
||
// 「项目文档」下的同名文件夹即项目根,用户在项目内新建的文件夹为其子分类。
|
||
func CrmProjectDocFolderTree(a OaDocActor, rootID uint64) ([]*ProjectDocFolder, error) {
|
||
empty := []*ProjectDocFolder{}
|
||
if rootID == 0 {
|
||
return empty, nil
|
||
}
|
||
var list []models.OaDocCategory
|
||
if _, err := models.Orm.QueryTable(new(models.OaDocCategory)).
|
||
Filter("tid", a.Tid).Filter("is_deleted", 0).OrderBy("id").All(&list); err != nil && err != orm.ErrNoRows {
|
||
return nil, err
|
||
}
|
||
nodes := make(map[uint64]*ProjectDocFolder, len(list))
|
||
for i := range list {
|
||
// 项目文件夹属于共享空间(scope=shared、user_id=0)
|
||
if list[i].Scope != models.DocScopeShared || list[i].UserID != 0 {
|
||
continue
|
||
}
|
||
nodes[list[i].ID] = &ProjectDocFolder{
|
||
ID: list[i].ID,
|
||
ParentID: list[i].ParentID,
|
||
Name: list[i].Name,
|
||
Children: []*ProjectDocFolder{},
|
||
}
|
||
}
|
||
root, ok := nodes[rootID]
|
||
if !ok {
|
||
return empty, nil
|
||
}
|
||
// 按 id 顺序挂接父子关系;只有能与根连通的节点才会出现在项目子树中
|
||
for i := range list {
|
||
node, ok := nodes[list[i].ID]
|
||
if !ok || node.ID == rootID {
|
||
continue
|
||
}
|
||
if parent, ok := nodes[list[i].ParentID]; ok {
|
||
parent.Children = append(parent.Children, node)
|
||
}
|
||
}
|
||
// 附带各文件夹直接挂载的文档数量(仅展示用,不汇总子孙)
|
||
if counts, err := OaDocCountByCategory(a, models.DocScopeShared, -1); err == nil {
|
||
for id, cnt := range counts {
|
||
if n, ok := nodes[id]; ok {
|
||
n.DocCount = cnt
|
||
}
|
||
}
|
||
}
|
||
return []*ProjectDocFolder{root}, nil
|
||
}
|
||
|
||
// crmProjectDocRootCategory 查找租户「项目文档」内置分类;缺失时兜底重建(正常由文档库初始化保证存在)。
|
||
func crmProjectDocRootCategory(tid int) (uint64, error) {
|
||
var root models.OaDocCategory
|
||
err := models.Orm.QueryTable(new(models.OaDocCategory)).
|
||
Filter("tid", tid).
|
||
Filter("scope", models.DocScopeShared).
|
||
Filter("is_system", models.DocCategorySystem).
|
||
Filter("is_deleted", 0).
|
||
OrderBy("id").One(&root)
|
||
if err == nil {
|
||
return root.ID, nil
|
||
}
|
||
id, insertErr := models.Orm.Insert(&models.OaDocCategory{
|
||
Tid: tid,
|
||
UserID: 0,
|
||
ParentID: 0,
|
||
Name: models.DocCategoryNameProject,
|
||
Sort: 0,
|
||
Remark: "系统内置分类,不可删除",
|
||
IsSystem: models.DocCategorySystem,
|
||
Scope: models.DocScopeShared,
|
||
IsDeleted: 0,
|
||
})
|
||
if insertErr != nil {
|
||
return 0, insertErr
|
||
}
|
||
return uint64(id), nil
|
||
}
|
||
|
||
// crmProjectDocCategoryBound 判断分类是否已被同租户的其它项目占用(excludeProjectID 为当前项目)。
|
||
func crmProjectDocCategoryBound(tid int, categoryID, excludeProjectID uint64) (bool, error) {
|
||
qs := models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||
Filter("tenant_id", strconv.Itoa(tid)).
|
||
Filter("doc_category_id", categoryID).
|
||
Filter("delete_time__isnull", true)
|
||
if excludeProjectID > 0 {
|
||
qs = qs.Exclude("id", excludeProjectID)
|
||
}
|
||
n, err := qs.Count()
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return n > 0, nil
|
||
}
|
||
|
||
// crmProjectDocBindCategory 回写项目与文档分类的绑定关系(projectID=0 时跳过)。
|
||
func crmProjectDocBindCategory(tid int, projectID, categoryID uint64) error {
|
||
if projectID == 0 {
|
||
return nil
|
||
}
|
||
_, err := models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||
Filter("id", projectID).
|
||
Filter("tenant_id", strconv.Itoa(tid)).
|
||
Update(orm.Params{"doc_category_id": categoryID, "update_time": time.Now()})
|
||
return err
|
||
}
|