Files
2026-09-15 10:44:31 +08:00

270 lines
8.8 KiB
Go
Raw Permalink 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 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
}