增加文档管理功能

This commit is contained in:
2026-09-10 00:44:15 +08:00
parent 63bf972c17
commit 8a0334a020
18 changed files with 5958 additions and 4 deletions
+657
View File
@@ -0,0 +1,657 @@
package controllers
import (
"encoding/json"
"errors"
"io"
"strconv"
"strings"
"server/models"
"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
}
// Prepare 所有接口执行前确保文档相关表存在。
func (c *BackendOaDocumentController) Prepare() {
_ = models.EnsureOaDocumentTables()
}
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()
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 分类树(含文档数量)
func (c *BackendOaDocumentController) Categories() {
claims, err := c.oaDocClaims()
if err != nil {
c.oaDocFail(err)
return
}
tree, err := services.OaDocCategoryTree(c.oaDocActor(claims))
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 {
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(claims.TenantId, 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(claims.TenantId, 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(claims.TenantId, 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")
actor := c.oaDocActor(claims)
list, total, err := services.OaDocList(actor, services.OaDocListParams{
Actor: actor,
Keyword: strings.TrimSpace(c.GetString("keyword")),
CategoryID: categoryID,
Uncategorized: strings.TrimSpace(c.GetString("uncategorized")) == "1",
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 文档库概览
func (c *BackendOaDocumentController) Stats() {
claims, err := c.oaDocClaims()
if err != nil {
c.oaDocFail(err)
return
}
stats, err := services.OaDocStats(c.oaDocActor(claims))
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)
}
// 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"`
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.Visibility != nil {
params.Visibility = *body.Visibility
}
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
}
+9
View File
@@ -32,6 +32,7 @@ type tenantUserPayload struct {
Password *string `json:"password"`
IsDefault *int8 `json:"is_default"`
Status *int8 `json:"status"`
OrgID *uint64 `json:"org_id"`
Remark *string `json:"remark"`
}
@@ -200,6 +201,11 @@ func (c *PlatformTenantUserController) CreateTenantUser() {
_ = c.ServeJSON()
return
}
if p.OrgID != nil && *p.OrgID > 0 {
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("id", id).
Update(map[string]interface{}{"org_id": *p.OrgID})
}
if isDefault == 1 {
_ = services.SetDefaultTenant(p.Uid, p.Tid)
// 租户第一个账号(普通租户)即租户管理员:自动绑定「租户管理员」角色,
@@ -265,6 +271,9 @@ func (c *PlatformTenantUserController) EditTenantUser() {
if p.Status != nil {
update["status"] = *p.Status
}
if p.OrgID != nil {
update["org_id"] = *p.OrgID
}
if p.Remark != nil {
update["remark"] = p.Remark
}
+107
View File
@@ -0,0 +1,107 @@
-- OA 文档管理(文档库 / 文档图谱)建表脚本
--
-- 说明:正常情况【不需要执行本脚本】。
-- 服务在首次访问 /backend/oa/document/* 接口时,会由
-- controllers/backend_oa_document.go 的 Prepare() 调用
-- models.EnsureOaDocumentTables() 自动建表(CREATE TABLE IF NOT EXISTS,
-- sync.Once 保证单进程只建一次)。
--
-- 本脚本仅用于以下场景手工预建:
-- 1. 生产库账号没有 DDL 权限,自动建表失败;
-- 2. DBA 要求先在预发/生产环境审核建表语句;
-- 3. 需要排查表结构是否符合预期。
--
-- 幂等:全部使用 IF NOT EXISTS,可反复执行,不会覆盖已有数据。
-- 与代码保持一致:models/oa_document.go
-- 1. 文档分类表(树形)
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',
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_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)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表';
-- 2. 文档表
-- doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频
-- status : 0-草稿 1-已发布 2-已归档
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
category_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '所属分类ID,0为未分类',
title varchar(255) NOT NULL DEFAULT '' COMMENT '文档标题',
file_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '关联附件ID(yz_system_files)',
file_url varchar(512) NOT NULL DEFAULT '' COMMENT '附件访问地址',
file_name varchar(255) NOT NULL DEFAULT '' COMMENT '原始文件名',
ext varchar(16) NOT NULL DEFAULT '' COMMENT '扩展名',
size bigint unsigned NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
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 '摘要/描述',
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-是',
owner_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '负责人ID',
owner_name varchar(100) NOT NULL DEFAULT '' COMMENT '负责人姓名',
view_count int NOT NULL DEFAULT 0 COMMENT '查看次数',
download_count int NOT NULL DEFAULT 0 COMMENT '下载次数',
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
creator_name varchar(100) NOT NULL DEFAULT '' COMMENT '创建人姓名',
visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)',
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_cate (tid, category_id, is_deleted),
KEY idx_tid_status (tid, status, is_deleted),
KEY idx_tid_title (tid, title)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档表';
-- 3. 文档关联表(文档图谱的连线来源)
-- relation: reference-引用 related-相关 version-版本 belong-从属
-- 说明:代码在建立关联时会同时写入正向与反向两条记录,便于图谱无向检索;
-- uk_doc_pair 保证同一对文档的同一关系不重复。
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_link (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
source_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '源文档ID',
target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '目标文档ID',
relation varchar(32) NOT NULL DEFAULT 'related' COMMENT '关系 reference-引用 related-相关 version-版本 belong-从属',
remark varchar(255) NOT NULL DEFAULT '' COMMENT '关系说明',
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
create_time datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_doc_pair (tid, source_id, target_id, relation),
KEY idx_tid_source (tid, source_id),
KEY idx_tid_target (tid, target_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档关联表';
-- 4. 文档共享表(私密文档可见性授权)
-- share_type: 0-用户(target_id=uid) 1-部门(target_id=org_id,含子部门)
-- uk_doc_target 保证同一文档对同一目标不重复授权。
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_share (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
doc_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '文档ID',
share_type tinyint NOT NULL DEFAULT 0 COMMENT '共享类型 0-用户(target_id=uid) 1-部门(target_id=org_id,含子部门)',
target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '共享目标ID',
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '授权人ID',
create_time datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_doc_target (tid, doc_id, share_type, target_id),
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文档共享表';
-- 5. 校验(应看到 4 张表)
-- SHOW TABLES LIKE 'yz_backend_oa_doc%';
+5
View File
@@ -114,6 +114,10 @@ func Init(_ string) {
new(OaSchedule),
new(OaNotice),
new(OaDocCategory),
new(OaDoc),
new(OaDocLink),
new(OaDocShare),
new(BackendOaCompensationScheme),
new(BackendOaPayroll),
new(BackendOaPayrollItem),
@@ -127,4 +131,5 @@ func Init(_ string) {
// 补齐历史库缺失的列(已存在时 MySQL 报 duplicate column,统一忽略)
EnsureAdminRoleTenantColumn()
EnsureAdminRoleCustomColumn()
EnsureTenantUserOrgColumn()
}
+275
View File
@@ -0,0 +1,275 @@
package models
import (
"strings"
"sync"
"time"
)
// OA 文档管理(文档库 / 文档图谱)数据模型。
//
// 三张表按租户 tid 隔离:
// - yz_backend_oa_doc_category 文档分类(树形,parent_id=0 为一级)
// - yz_backend_oa_doc 文档主体(附件信息冗余存储,避免每次联查文件表)
// - yz_backend_oa_doc_link 文档关联关系(引用/相关/版本/从属),文档图谱的连线来源
// OaDocCategory OA文档分类: yz_backend_oa_doc_category
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"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *OaDocCategory) TableName() string {
return "yz_backend_oa_doc_category"
}
// OaDoc OA文档: yz_backend_oa_doc
// status: 0-草稿 1-已发布 2-已归档
// doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频
type OaDoc struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid int `orm:"column(tid)" json:"tid"`
CategoryID uint64 `orm:"column(category_id);default(0)" json:"category_id"`
Title string `orm:"column(title);size(255)" json:"title"`
FileID uint64 `orm:"column(file_id);default(0)" json:"file_id"`
FileURL string `orm:"column(file_url);size(512);default()" json:"file_url"`
FileName string `orm:"column(file_name);size(255);default()" json:"file_name"`
Ext string `orm:"column(ext);size(16);default()" json:"ext"`
Size uint64 `orm:"column(size);default(0)" json:"size"`
DocType int8 `orm:"column(doc_type);default(0)" json:"doc_type"`
Tags string `orm:"column(tags);size(255);default()" json:"tags"`
Summary string `orm:"column(summary);size(1000);default()" json:"summary"`
Status int8 `orm:"column(status);default(0)" json:"status"`
Version int `orm:"column(version);default(1)" json:"version"`
IsStar int8 `orm:"column(is_star);default(0)" json:"is_star"`
OwnerID uint64 `orm:"column(owner_id);default(0)" json:"owner_id"`
OwnerName string `orm:"column(owner_name);size(100);default()" json:"owner_name"`
ViewCount int `orm:"column(view_count);default(0)" json:"view_count"`
DownCount int `orm:"column(download_count);default(0)" json:"download_count"`
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
CreatorName string `orm:"column(creator_name);size(100);default()" json:"creator_name"`
// Visibility 可见性:0-租户公开(默认,租户内所有后台用户可见)1-私密(仅创建者与被共享者可见)
Visibility int8 `orm:"column(visibility);default(0)" json:"visibility"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *OaDoc) TableName() string {
return "yz_backend_oa_doc"
}
// OaDocLink OA文档关联: yz_backend_oa_doc_link
// relation: reference-引用 related-相关 version-版本迭代 belong-从属
type OaDocLink struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid int `orm:"column(tid)" json:"tid"`
SourceID uint64 `orm:"column(source_id);default(0)" json:"source_id"`
TargetID uint64 `orm:"column(target_id);default(0)" json:"target_id"`
Relation string `orm:"column(relation);size(32);default(related)" json:"relation"`
Remark string `orm:"column(remark);size(255);default()" json:"remark"`
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
}
func (m *OaDocLink) TableName() string {
return "yz_backend_oa_doc_link"
}
// OaDocShare OA文档共享: yz_backend_oa_doc_share
// 私密文档(visibility=1)在"创建者可见"之外,额外授权给指定用户或部门。
// share_type: 0-用户(target_id 为 yz_system_tenant_user.uid)
// 1-部门(target_id 为 yz_backend_organization.id,含其全部子部门)
type OaDocShare struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid int `orm:"column(tid)" json:"tid"`
DocID uint64 `orm:"column(doc_id);default(0)" json:"doc_id"`
ShareType int8 `orm:"column(share_type);default(0)" json:"share_type"`
TargetID uint64 `orm:"column(target_id);default(0)" json:"target_id"`
CreatorID uint64 `orm:"column(creator_id);default(0)" json:"creator_id"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
}
func (m *OaDocShare) TableName() string {
return "yz_backend_oa_doc_share"
}
// 文档类型常量(doc_type)
const (
DocTypeOther int8 = 0
DocTypeDoc int8 = 1 // doc / docx / txt / md / rtf
DocTypeSheet int8 = 2 // xls / xlsx / csv
DocTypeSlide int8 = 3 // ppt / pptx
DocTypePDF int8 = 4 // pdf
DocTypeImage int8 = 5 // jpg / png / gif / bmp / webp
DocTypeZip int8 = 6 // zip / rar / 7z / tar / gz
DocTypeVideo int8 = 7
DocTypeAudio int8 = 8
)
// 文档状态常量(status)
const (
DocStatusDraft int8 = 0
DocStatusPublish int8 = 1
DocStatusArchive int8 = 2
)
// 文档关联关系常量(relation)
const (
DocRelationReference = "reference"
DocRelationRelated = "related"
DocRelationVersion = "version"
DocRelationBelong = "belong"
)
// 文档可见性常量(visibility)
const (
DocVisibilityPublic int8 = 0 // 租户公开
DocVisibilityPrivate int8 = 1 // 私密
)
// 文档共享目标类型常量(share_type)
const (
DocShareTypeUser int8 = 0 // 共享给指定用户(target_id = yz_system_tenant_user.uid)
DocShareTypeOrg int8 = 1 // 共享给部门(target_id = yz_backend_organization.id,含子部门)
)
// extDocTypeMap 扩展名 -> 文档类型
var extDocTypeMap = map[string]int8{
"doc": DocTypeDoc, "docx": DocTypeDoc, "txt": DocTypeDoc, "md": DocTypeDoc,
"rtf": DocTypeDoc, "wps": DocTypeDoc, "odt": DocTypeDoc,
"xls": DocTypeSheet, "xlsx": DocTypeSheet, "csv": DocTypeSheet, "ods": DocTypeSheet,
"ppt": DocTypeSlide, "pptx": DocTypeSlide, "odp": DocTypeSlide,
"pdf": DocTypePDF,
"jpg": DocTypeImage, "jpeg": DocTypeImage, "png": DocTypeImage,
"gif": DocTypeImage, "bmp": DocTypeImage, "webp": DocTypeImage, "svg": DocTypeImage,
"zip": DocTypeZip, "rar": DocTypeZip, "7z": DocTypeZip, "tar": DocTypeZip, "gz": DocTypeZip,
"mp4": DocTypeVideo, "webm": DocTypeVideo, "mov": DocTypeVideo, "avi": DocTypeVideo,
"mp3": DocTypeAudio, "wav": DocTypeAudio, "ogg": DocTypeAudio,
}
// DocTypeByExt 根据扩展名推断文档类型,未知返回"其他"。
func DocTypeByExt(ext string) int8 {
if t, ok := extDocTypeMap[strings.ToLower(strings.TrimPrefix(strings.TrimSpace(ext), "."))]; ok {
return t
}
return DocTypeOther
}
var oaDocumentTableOnce sync.Once
// EnsureOaDocumentTables 首次访问文档接口时自动建表(若不存在)。
// 与通知公告/日程保持一致的运行期自愈策略,避免新功能上线还需手工执行建表脚本。
func EnsureOaDocumentTables() error {
if Orm == nil {
return nil
}
var err error
oaDocumentTableOnce.Do(func() {
_, err = Orm.Raw(`
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',
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_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)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表'`).Exec()
if err != nil {
return
}
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
category_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '所属分类ID,0为未分类',
title varchar(255) NOT NULL DEFAULT '' COMMENT '文档标题',
file_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '关联附件ID(yz_system_files)',
file_url varchar(512) NOT NULL DEFAULT '' COMMENT '附件访问地址',
file_name varchar(255) NOT NULL DEFAULT '' COMMENT '原始文件名',
ext varchar(16) NOT NULL DEFAULT '' COMMENT '扩展名',
size bigint unsigned NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
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 '摘要/描述',
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-是',
owner_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '负责人ID',
owner_name varchar(100) NOT NULL DEFAULT '' COMMENT '负责人姓名',
view_count int NOT NULL DEFAULT 0 COMMENT '查看次数',
download_count int NOT NULL DEFAULT 0 COMMENT '下载次数',
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
creator_name varchar(100) NOT NULL DEFAULT '' COMMENT '创建人姓名',
visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)',
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_cate (tid, category_id, is_deleted),
KEY idx_tid_status (tid, status, is_deleted),
KEY idx_tid_title (tid, title)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档表'`).Exec()
if err != nil {
return
}
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_link (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
source_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '源文档ID',
target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '目标文档ID',
relation varchar(32) NOT NULL DEFAULT 'related' COMMENT '关系 reference-引用 related-相关 version-版本 belong-从属',
remark varchar(255) NOT NULL DEFAULT '' COMMENT '关系说明',
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
create_time datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_doc_pair (tid, source_id, target_id, relation),
KEY idx_tid_source (tid, source_id),
KEY idx_tid_target (tid, target_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档关联表'`).Exec()
if err != nil {
return
}
// 历史库补齐 visibility 列(已存在时 MySQL 报 duplicate column,忽略)
_, _ = Orm.Raw(`
ALTER TABLE yz_backend_oa_doc
ADD COLUMN visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)'
`).Exec()
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_share (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
doc_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '文档ID',
share_type tinyint NOT NULL DEFAULT 0 COMMENT '共享类型 0-用户(target_id=uid) 1-部门(target_id=org_id,含子部门)',
target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '共享目标ID',
creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '授权人ID',
create_time datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_doc_target (tid, doc_id, share_type, target_id),
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()
})
return err
}
+19
View File
@@ -19,6 +19,9 @@ type SystemTenantUser struct {
Password *string `orm:"column(password);size(255);null" json:"password"`
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"`
Status int8 `orm:"column(status);default(1)" json:"status"`
// OrgID 所属组织(部门),关联 yz_backend_organization(id);0 表示未分配。
// 用于文档私密共享等需要按部门判定可见性的场景。
OrgID uint64 `orm:"column(org_id);default(0)" json:"org_id"`
Remark *string `orm:"column(remark);size(255);null" json:"remark"`
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"`
@@ -44,3 +47,19 @@ func EnsureTenantUserGroupColumn() {
`).Exec()
})
}
var tenantUserOrgColOnce sync.Once
// EnsureTenantUserOrgColumn 为租户用户表补齐 org_id(所属部门)列。
// 与 group_id 同样的幂等策略:字段已存在时 MySQL 报 duplicate column,统一忽略。
func EnsureTenantUserOrgColumn() {
if Orm == nil {
return
}
tenantUserOrgColOnce.Do(func() {
_, _ = Orm.Raw(`
ALTER TABLE yz_system_tenant_user
ADD COLUMN org_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '所属组织ID 关联 yz_backend_organization(id),0-未分配'
`).Exec()
})
}
+20
View File
@@ -296,6 +296,26 @@ func RegisterAuthRoutes() {
beego.Router("/backend/oa/notice/delete/:id", &controllers.BackendOaNoticeController{}, "delete:Delete")
beego.Router("/backend/oa/notice/publish/:id", &controllers.BackendOaNoticeController{}, "post:Publish")
beego.Router("/backend/oa/notice/top/:id", &controllers.BackendOaNoticeController{}, "post:Top")
// OA文档管理(文档库 / 文档图谱)
beego.Router("/backend/oa/document/categories", &controllers.BackendOaDocumentController{}, "get:Categories;post:CreateCategory")
beego.Router("/backend/oa/document/categories/:id", &controllers.BackendOaDocumentController{}, "post:UpdateCategory;delete:DeleteCategory")
beego.Router("/backend/oa/document/list", &controllers.BackendOaDocumentController{}, "get:List")
beego.Router("/backend/oa/document/stats", &controllers.BackendOaDocumentController{}, "get:Stats")
beego.Router("/backend/oa/document/graph", &controllers.BackendOaDocumentController{}, "get:Graph")
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/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")
beego.Router("/backend/oa/document/delete-batch", &controllers.BackendOaDocumentController{}, "post:DeleteBatch")
beego.Router("/backend/oa/document/move", &controllers.BackendOaDocumentController{}, "post:Move")
beego.Router("/backend/oa/document/star/:id", &controllers.BackendOaDocumentController{}, "post:Star")
beego.Router("/backend/oa/document/download/:id", &controllers.BackendOaDocumentController{}, "post:Download")
beego.Router("/backend/oa/document/members", &controllers.BackendOaDocumentController{}, "get:Members")
beego.Router("/backend/oa/document/shares/:id", &controllers.BackendOaDocumentController{}, "get:ShareList;post:ShareSet")
beego.Router("/backend/oa/document/share/:sid", &controllers.BackendOaDocumentController{}, "delete:ShareRemove")
}
// registerOrganizationRoutes 为指定模块前缀注册组织架构路由。
File diff suppressed because it is too large Load Diff