package controllers import ( "encoding/json" "errors" "io" "strconv" "strings" "server/pkg/jwtutil" "server/services" beego "github.com/beego/beego/v2/server/web" ) // 控制器层专用错误,仅用于响应转换 var ( errOaDocUnauthorized = errors.New("未登录或无权限") errOaDocBadID = errors.New("无效ID") errOaDocBadParam = errors.New("参数错误") ) // BackendOaDocumentController OA 文档管理(文档库 + 文档图谱)。 // 数据按租户 tid 隔离;所有业务逻辑委托 services 层,本层只做鉴权、参数解析与响应格式化。 type BackendOaDocumentController struct { beego.Controller } func (c *BackendOaDocumentController) oaDocClaims() (*jwtutil.Claims, error) { auth := c.Ctx.Request.Header.Get("Authorization") if auth == "" { return nil, errOaDocUnauthorized } parts := strings.SplitN(auth, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { return nil, errOaDocUnauthorized } claims, err := jwtutil.ParseToken(parts[1]) if err != nil { return nil, err } if claims.UserType != "backend" { return nil, errOaDocUnauthorized } return claims, nil } func (c *BackendOaDocumentController) oaDocOk(data interface{}) { c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} _ = c.ServeJSON() } // oaDocFail 将业务错误转换为统一响应;未识别的错误按 500 处理。 func (c *BackendOaDocumentController) oaDocFail(err error) { status, code, msg := 400, 400, "操作失败" if err != nil { switch err { case errOaDocUnauthorized: status, code, msg = 401, 401, "未登录或无权限" case services.ErrDocNotFound, services.ErrDocLinkNotFound, services.ErrDocCategoryNotFound, services.ErrDocCategoryInvalid, services.ErrDocLinkTargetNotFound: 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() } } c.Ctx.Output.SetStatus(status) c.Data["json"] = map[string]interface{}{"code": code, "msg": msg} _ = c.ServeJSON() } // oaDocID 解析路径参数 :id func (c *BackendOaDocumentController) oaDocID() (uint64, bool) { id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) if err != nil || id == 0 { c.oaDocFail(errOaDocBadID) return 0, false } return id, true } // oaDocActor 由 JWT 构造文档访问者上下文(含用户、所属部门与是否全权限)。 func (c *BackendOaDocumentController) oaDocActor(claims *jwtutil.Claims) services.OaDocActor { return services.NewOaDocActor(claims.TenantId, uint64(claims.UserID)) } // ---------------- 分类 ---------------- // Categories GET /backend/oa/document/categories?scope=shared|personal 文档分类树(含各分类文档数量) func (c *BackendOaDocumentController) Categories() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } scope := strings.TrimSpace(c.GetString("scope")) tree, err := services.OaDocCategoryTree(c.oaDocActor(claims), scope) if err != nil { c.oaDocFail(err) return } c.oaDocOk(tree) } // CreateCategory POST /backend/oa/document/categories func (c *BackendOaDocumentController) CreateCategory() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } var body struct { Scope string `json:"scope"` ParentID uint64 `json:"parent_id"` Name string `json:"name"` Remark string `json:"remark"` Sort int `json:"sort"` } if !c.oaDocParseBody(&body) { return } item, err := services.OaDocCategoryCreate( c.oaDocActor(claims), body.Scope, body.ParentID, body.Name, body.Remark, body.Sort) if err != nil { c.oaDocFail(err) return } c.oaDocOk(item) } // UpdateCategory POST /backend/oa/document/categories/:id // parent_id < 0 表示不调整父级 func (c *BackendOaDocumentController) UpdateCategory() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } id, ok := c.oaDocID() if !ok { return } var body struct { ParentID *int64 `json:"parent_id"` Name *string `json:"name"` Remark *string `json:"remark"` Sort *int `json:"sort"` } if !c.oaDocParseBody(&body) { return } parentID := int64(-1) if body.ParentID != nil { parentID = *body.ParentID } name, remark, sort := "", "", -1 if body.Name != nil { name = *body.Name } if body.Remark != nil { remark = *body.Remark } if body.Sort != nil { sort = *body.Sort } item, err := services.OaDocCategoryUpdate(c.oaDocActor(claims), id, name, remark, sort, parentID) if err != nil { c.oaDocFail(err) return } c.oaDocOk(item) } // DeleteCategory DELETE /backend/oa/document/categories/:id func (c *BackendOaDocumentController) DeleteCategory() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } id, ok := c.oaDocID() if !ok { return } if err := services.OaDocCategoryDelete(c.oaDocActor(claims), id); err != nil { c.oaDocFail(err) return } c.oaDocOk(nil) } // ---------------- 文档 ---------------- // List GET /backend/oa/document/list func (c *BackendOaDocumentController) List() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) status, _ := c.GetInt("status", -1) 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")), Star: star, Page: page, PageSize: pageSize, }) if err != nil { c.oaDocFail(err) return } c.oaDocOk(map[string]interface{}{"list": list, "total": total}) } // 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 } scope := strings.TrimSpace(c.GetString("scope")) stats, err := services.OaDocStats(c.oaDocActor(claims), scope) if err != nil { c.oaDocFail(err) return } c.oaDocOk(stats) } // Detail GET /backend/oa/document/detail/:id 详情(累加查看次数) func (c *BackendOaDocumentController) Detail() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } id, ok := c.oaDocID() if !ok { return } item, err := services.OaDocView(c.oaDocActor(claims), id) if err != nil { c.oaDocFail(err) return } 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() if err != nil { c.oaDocFail(err) return } body, ok := c.oaDocParseSaveBody() if !ok { return } item, err := services.OaDocCreate(c.oaDocActor(claims), claims.Username, body) if err != nil { c.oaDocFail(err) return } c.oaDocOk(item) } // Update POST /backend/oa/document/update/:id func (c *BackendOaDocumentController) Update() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } id, ok := c.oaDocID() if !ok { return } body, ok := c.oaDocParseSaveBody() if !ok { return } item, err := services.OaDocUpdate(c.oaDocActor(claims), id, body) if err != nil { c.oaDocFail(err) return } c.oaDocOk(item) } // Delete DELETE /backend/oa/document/delete/:id 软删除 func (c *BackendOaDocumentController) Delete() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } id, ok := c.oaDocID() if !ok { return } n, err := services.OaDocDelete(c.oaDocActor(claims), []uint64{id}) if err != nil { c.oaDocFail(err) return } c.oaDocOk(map[string]interface{}{"count": n}) } // DeleteBatch POST /backend/oa/document/delete-batch 批量软删除 func (c *BackendOaDocumentController) DeleteBatch() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } var body struct { IDs []uint64 `json:"ids"` } if !c.oaDocParseBody(&body) { return } n, err := services.OaDocDelete(c.oaDocActor(claims), body.IDs) if err != nil { c.oaDocFail(err) return } c.oaDocOk(map[string]interface{}{"count": n}) } // Move POST /backend/oa/document/move 批量移动分类 func (c *BackendOaDocumentController) Move() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } var body struct { IDs []uint64 `json:"ids"` CategoryID uint64 `json:"category_id"` } if !c.oaDocParseBody(&body) { return } n, err := services.OaDocMove(c.oaDocActor(claims), body.IDs, body.CategoryID) if err != nil { c.oaDocFail(err) return } c.oaDocOk(map[string]interface{}{"count": n}) } // Star POST /backend/oa/document/star/:id 收藏 / 取消收藏 func (c *BackendOaDocumentController) Star() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } id, ok := c.oaDocID() if !ok { return } isStar, err := services.OaDocToggleStar(c.oaDocActor(claims), id) if err != nil { c.oaDocFail(err) return } c.oaDocOk(map[string]interface{}{"is_star": isStar}) } // Download POST /backend/oa/document/download/:id 记录下载次数并返回下载地址 func (c *BackendOaDocumentController) Download() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } id, ok := c.oaDocID() if !ok { return } item, err := services.OaDocDownload(c.oaDocActor(claims), id) if err != nil { c.oaDocFail(err) return } c.oaDocOk(map[string]interface{}{ "id": item.ID, "title": item.Title, "file_url": item.FileURL, "file_name": item.FileName, "download_count": item.DownCount, }) } // ---------------- 文档关联 ---------------- // LinkList GET /backend/oa/document/links?doc_id=&all=1 func (c *BackendOaDocumentController) LinkList() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } docID, _ := c.GetUint64("doc_id") all := strings.TrimSpace(c.GetString("all")) == "1" list, err := services.OaDocLinkList(c.oaDocActor(claims), docID, all) if err != nil { c.oaDocFail(err) return } c.oaDocOk(list) } // LinkCreate POST /backend/oa/document/links func (c *BackendOaDocumentController) LinkCreate() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } var body struct { SourceID uint64 `json:"source_id"` TargetID uint64 `json:"target_id"` Relation string `json:"relation"` Remark string `json:"remark"` } if !c.oaDocParseBody(&body) { return } item, err := services.OaDocLinkCreate(c.oaDocActor(claims), uint64(claims.UserID), body.SourceID, body.TargetID, body.Relation, body.Remark) if err != nil { c.oaDocFail(err) return } c.oaDocOk(item) } // LinkDelete DELETE /backend/oa/document/links/:id func (c *BackendOaDocumentController) LinkDelete() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } id, ok := c.oaDocID() if !ok { return } if err := services.OaDocLinkDelete(c.oaDocActor(claims), id); err != nil { c.oaDocFail(err) return } c.oaDocOk(nil) } // ---------------- 文档图谱 ---------------- // Graph GET /backend/oa/document/graph func (c *BackendOaDocumentController) Graph() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } categoryID, _ := c.GetUint64("category_id") limit, _ := c.GetInt("limit", 300) includeCategory := strings.TrimSpace(c.GetString("include_category")) onlyLinked := strings.TrimSpace(c.GetString("only_linked")) == "1" result, err := services.OaDocGraph(c.oaDocActor(claims), services.OaDocGraphParams{ Keyword: strings.TrimSpace(c.GetString("keyword")), CategoryID: categoryID, Relation: strings.TrimSpace(c.GetString("relation")), IncludeCategory: includeCategory != "0", OnlyLinked: onlyLinked, Limit: limit, }) if err != nil { c.oaDocFail(err) return } c.oaDocOk(result) } // ---------------- 文档共享(私密文档可见性) ---------------- // Members GET /backend/oa/document/members 租户用户列表(私密文档共享选人) func (c *BackendOaDocumentController) Members() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } keyword := strings.TrimSpace(c.GetString("keyword")) limit, _ := c.GetInt("limit", 100) members, err := services.OaDocMembers(claims.TenantId, keyword, limit) if err != nil { c.oaDocFail(err) return } c.oaDocOk(members) } // ShareList GET /backend/oa/document/shares/:id 读取文档共享清单 func (c *BackendOaDocumentController) ShareList() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } id, ok := c.oaDocID() if !ok { return } list, err := services.OaDocShareList(c.oaDocActor(claims), id) if err != nil { c.oaDocFail(err) return } c.oaDocOk(list) } // ShareSet POST /backend/oa/document/shares/:id 全量设置文档共享(创建者/全权限用户) func (c *BackendOaDocumentController) ShareSet() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } id, ok := c.oaDocID() if !ok { return } var body struct { Shares []services.OaDocShareItem `json:"shares"` } if !c.oaDocParseBody(&body) { return } list, err := services.OaDocShareSet(c.oaDocActor(claims), id, body.Shares) if err != nil { c.oaDocFail(err) return } c.oaDocOk(list) } // ShareRemove DELETE /backend/oa/document/shares/:sid 删除单条共享 func (c *BackendOaDocumentController) ShareRemove() { claims, err := c.oaDocClaims() if err != nil { c.oaDocFail(err) return } sid, err := strconv.ParseUint(c.Ctx.Input.Param(":sid"), 10, 64) if err != nil || sid == 0 { c.oaDocFail(errOaDocBadID) return } if err := services.OaDocShareRemove(c.oaDocActor(claims), sid); err != nil { c.oaDocFail(err) return } c.oaDocOk(nil) } // ---------------- 辅助 ---------------- // oaDocParseBody 读取并解析 JSON 请求体 func (c *BackendOaDocumentController) oaDocParseBody(out interface{}) bool { raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil || json.Unmarshal(raw, out) != nil { c.oaDocFail(errOaDocBadParam) return false } return true } // oaDocParseSaveBody 解析文档新增/编辑请求体 func (c *BackendOaDocumentController) oaDocParseSaveBody() (services.OaDocSaveParams, bool) { 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"` 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, } 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 params.CategoryID = *body.CategoryID } // -1 表示"不修改该字段" if body.Status != nil { params.Status = *body.Status } else { params.Status = -1 } if body.IsStar != nil { params.IsStar = *body.IsStar } else { params.IsStar = -1 } return params, true }