[转]Office文档在线编辑的实现之二

简介: From:http://www.cnblogs.com/jianyi0115/archive/2007/07/15/818566.html 本篇将讲解如何实现客户端的office直接编辑数据库中的二进制形式保存的office文件。
From: http://www.cnblogs.com/jianyi0115/archive/2007/07/15/818566.html

本篇将讲解如何实现客户端的office直接编辑数据库中的二进制形式保存的office文件。

实现的关键:模拟IIS,自己实现一个webdav的服务器端。

首先,我们简单了解一下webdav:
webdav,中文可以翻译为网络分布式协作协议,它解决了http协议中一个问题:http无法实现版本和单访问控制。
什么是单访问控制呢?假设我们有一个页面编辑某条数据,这个页面可以同时被多个用户使用,那么最终的数据是最后一个用户提交的数据,
而其他用户是不知道的.我们的99%的web程序都存在此问题,当然通过编码可以解决,但http协议本身并没有提供对这种情形的支持。

webdav协议在标准的http协议的基础上,扩展了以下请求动作(verb):
PUT:用于客户端推送二进制文件。(好像http有这个verb)
LOCK:用户锁定一个资源,保证资源的单访问
UNLOCK:解锁一个资源
OPTIONS:获取服务器可以支持的请求类型
DELETE:删除服务器文件
PROPFIND:查询文件属性
其他动作: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH
要详细地了解webdav,大家可以google一下,或访问 http://en.wikipedia.org/wiki/WebDAV

笔者在实现这个解决方案的时候,是采用fiddler,debug IE的http请求,才搞懂了IIS本身的实现机制,为了形象化,可以看一下webdav请求相应的
数据:
发起一个OPTIONS请求
OPTIONS /PMDemo/Test/待办事务.doc HTTP/1.1
User-Agent: Fiddler
Host: localhost

响应如下:
HTTP/1.1 200 OK
Date: Wed, 27 Dec 2006 11:34:03 GMT
Server: Microsoft-IIS/6.0
MicrosoftOfficeWebServer: 5.0_Pub
X-Powered-By: ASP.NET
MS-Author-Via: DAV
Content-Length: 0
Accept-Ranges: bytes
DASL: <DAV:sql>
DAV: 1, 2
Public: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH
Allow: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, COPY, MOVE, PROPFIND, PROPPATCH, SEARCH, LOCK, UNLOCK
Cache-Control: private

搞清楚了这些,下面我们的任务就是如何在asp.net中实现一个wevdav服务器.
显然,这要求我们需要在底层截获http请求,幸运的是asp.net中支持这种技术:HttpHandler.它可以让我们自己的代码来处理http请求.

首先,我们在web.config中做如下配置:
     < httpHandlers >
            
< remove  verb ="*"  path ="*" />             
            
< add  verb ="GET,PUT,UNLOCK,LOCK,OPTIONS"  path ="*.doc,*.xml"  type ="Webdav.WebdavProtocolHandler, Webdav " />
    
</ httpHandlers >
通过这个配置,使我们的 WebdavProtocolHandler可以来处理webdav请求.

WebdavProtocolHandler类是一个标准的httphandler,实现了IHttpHandler接口,它按照客户端的请求类型,返回符合wevdav协议的数据.

WebdavProtocolHandler类需要按照不同的webdav请求动作,做不同的处理,那么怎么来实现这个类呢?
这里就要用到一个设计模式:命令模式.

首先定义一个接口:

public   interface  IVerbHandler
{
      
void  Process( HttpContext context );
}

实现对Options请求的处理:
class  OptionsHandler : IVerbHandler
    {
        
#region  IVerbHandler 成员

        
public   void  Process(System.Web.HttpContext context)
        {
            context.Response.AppendHeader(
" DASL " " <DAV:sql> " );
            context.Response.AppendHeader(
" DAV " " 1, 2 " );

            context.Response.AppendHeader(
" Public " " OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH " );

            context.Response.AppendHeader(
" Allow " " OPTIONS, TRACE, GET, HEAD, DELETE, PUT, COPY, MOVE, PROPFIND, PROPPATCH, SEARCH, LOCK, UNLOCK " );
        }

        
#endregion
    }
webdav的请求verb多达15个以上,大多数情况下,我们并不需要一个完整的webdav支持,故我们只要对其中的几个进行实现即可。

实现对LOCK的支持:
  class  LockHandler : IVerbHandler
    {
        
#region  IVerbHandler 成员

        
public   void  Process(System.Web.HttpContext context)
        {
            context.Response.ContentType 
=   " text/xml " ;

            
string  token  =  Guid.NewGuid().ToString()  +   " : "   +  DateTime.Now.Ticks.ToString() ; 

            context.Response.AppendHeader(
" Lock-Token " " <opaquelocktoken: "   +  token  +   " > " );
          
            
string  xml  =   @" <?xml version=""1.0""?>
<a:prop xmlns:a=""DAV:""><a:lockdiscovery>
<a:activelock><a:locktype><a:write/></a:locktype>
<a:lockscope><a:exclusive/></a:lockscope><owner xmlns=""DAV:"">Administrator</owner><a:locktoken>
<a:href>opaquelocktoken:{0}</a:href></a:locktoken>
<a:depth>0</a:depth><a:timeout>Second-180</a:timeout></a:activelock></a:lockdiscovery>
</a:prop>
" ;

            context.Response.Write( String.Format( xml , token ) );
            context.Response.End();
        }

        
#endregion
    }

注意这篇文章的主题:实现在线编辑。并没有版本控制等其他内容,大家仔细看以上的代码,服务器并没有真正实现"锁定",只是假装告诉客户端,你要的资源已经给你锁定了,你可以放心的编辑了。当然,有兴趣的朋友可以实现真正的锁定,无非可以通过给数据做一个状态字段来实现。但注意,要考虑一些复杂的情况,如自动解锁(用户打开一个文档,然后关机了,文档岂不永远锁定了?)等等。

接着,我们实现UNLOCK,同样是假的:
class  UnLockHandler : IVerbHandler
    {
        
#region  IVerbHandler 成员

        
public   void  Process(System.Web.HttpContext context)
        {            
        }

        
#endregion
    }

下面,我们将实现两个最重要的请求动作的处理:Get和Put, office请求打开一个服务器上的文件时,采用get请求,office保存一个文件到服务器上时,发送put请求。

首先,我们要考虑一种数据项标识的传递策略,即:客户端发起访问数据库的office文件行,那么如何确认数据行的主键?
有两种策略:
1)通过不同的文件名 , 如,请求http://localhost/weboffice/1.doc  这个请求主键 为1的文件。
2)通过文件路径, 如,请求http://localhost/weboffice/1/文件名.doc  这个请求主键为1的文件。
我们将采用策略2。

再返回到我们对web.config做的配置:

< add  verb ="GET,PUT,UNLOCK,LOCK,OPTIONS"  path ="*.doc,*.xml"  type ="Webdav.WebdavProtocolHandler, Webdav " />

这个配置允许
WebdavProtocolHandler处理所有对doc和xml的请求处理,为什么要允许xml呢,因为office2003之后,支持xml格式,可以直接在
数据库重以xml的格式存放office文件。

接着,我们要确认我们的数据存储结构,即,office文件在数据库中时如何存放的。

我们有一个附件表:Document
CREATE   TABLE   [ dbo ] . [ Document ]  (
    
[ DocumentId ]   [ int ]   IDENTITY  ( 1 1 NOT   NULL  ,
    
[ Name ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
    
[ Description ]   [ text ]  COLLATE Chinese_PRC_CI_AS  NULL  ,
    
[ CreateTime ]   [ datetime ]   NULL  ,
    
[ Size ]   [ int ]   NULL  ,
    
[ CreatorId ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
    
[ CreatorName ]   [ char ]  ( 10 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
    
[ CreateYear ]   [ int ]   NULL  ,
    
[ ContentType ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
    
[ DeptId ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
    
[ DeptName ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
    
[ Content ]   [ image ]   NULL  ,
    
[ ModifyTime ]   [ datetime ]   NULL  ,
    
[ OwnerType ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
    
[ TemplateAble ]   [ bit ]   NULL  
ON   [ PRIMARY ]  TEXTIMAGE_ON  [ PRIMARY ]
GO
设计一个文裆实体:
    [Serializable]
   
public   class  Document
   {
       
public  Document()
       { }

       
static   public  Document FromPostFile(System.Web.HttpPostedFile file , User user )
       {
           Document doc 
=   new  Document(file);
           doc.CreateTime 
=  DateTime.Now;
           doc.CreatorId 
=  user.Id;
           doc.CreatorName 
=  user.Name; 
           doc.DeptId 
=  user.OrgId;
           doc.DeptName 
=  user.OrgName;
           
return  doc;
       }

       
public  Document(System.Web.HttpPostedFile file)
        {
            
string [] strs  =  file.FileName.Split(  ' \\ '  );
            
this .Name  =  strs[strs.Length  -   1 ];
            Size 
=  file.ContentLength;
            
// 读取文件的数据
             this .Content  =   new   byte [Size];
            Stream fileDataStream 
=  file.InputStream;
            fileDataStream.Read( 
this .Content ,  0 , Size );
            ContentType 
=  file.ContentType;
        }

      
private   int  _DocumentId;
      
///   <summary>
      
///    任务名
      
///   </summary>
       private   string  _Name;
      
///   <summary>
      
///    任务描述
      
///   </summary>
       private   string  _Description;
      
///   <summary>
      
///    报表创建时间
      
///   </summary>
       private  DateTime _CreateTime  =  DateTime.Now ;
      
private   int  _Size  =   0  ;
       
private   byte [] _Data;
      
///   <summary>
      
///    创建人Id
      
///   </summary>
       private   string  _CreatorId;
      
///   <summary>
      
///    创建人名
      
///   </summary>
       private   string  _CreatorName;

      
private   int  _CreateYear;
      
private   string  _ContentType;
      
///   <summary>
      
///    部门ID(便于统计)
      
///   </summary>
       private   string  _DeptId;
      
///   <summary>
      
///    部门名
      
///   </summary>
       private   string  _DeptName;   
      
//  Property DocumentId
      
public   int  DocumentId   
      {
         
get
         {
            
return  _DocumentId;
         }
         
set
         {

           
this ._DocumentId  =  value;
         }
      }      
      
//  Property Name
       public   string  Name   
      {
         
get
         {
            
return  _Name;
         }
         
set
         {

            
this ._Name  =  value;
         }
      }      
      
//  Property Description
       public   string  Description   
      {
         
get
         {
            
return  _Description;
         }
         
set
         {

            
this ._Description  =  value;
         }
      }      
      
//  Property CreateTime
       public  DateTime CreateTime   
      {
         
get
         {
            
return  _CreateTime;
         }
         
set
         {

           
this ._CreateTime  =  value;
         }
      }
       
private  DateTime _ModifyTime  =  DateTime.Now;
       
public  DateTime ModifyTime
       {
           
get
           {
               
return  _ModifyTime;
           }
           
set
           {

              
this ._ModifyTime  =  value;
           }
       }      
      
//  Property Size
       public   int  Size   
      {
         
get
         {
            
return  _Size;
         }
         
set
         {

           
this ._Size  =  value;
         }
      }      
      
//  Property Data
       public   byte [] Content   
      {
         
get
         {
            
return  _Data;
         }
         
set
         {

           
this ._Data  =  value;
         }
      }      
      
//  Property CreatorId
       public   string  CreatorId   
      {
         
get
         {
            
return  _CreatorId;
         }
         
set
         {

           
this ._CreatorId  =  value;
         }
      }

      
//  Property CreatorName
       public   string  CreatorName
      {
          
get
          {
              
return  _CreatorName;
          }
          
set
          {

             
this ._CreatorName  =  value;
          }
      }      
      
//  Property CreateYear
       public   int  CreateYear   
      {
         
get
         {
            
return  _CreateYear;
         }
         
set
         {

           
this ._CreateYear  =  value;
         }
      }      
      
//  Property ContentType
      
// application/msword
      
// text/plain
       public   string  ContentType   
      {
         
get
         {
            
return  _ContentType;
         }
         
set
         {

            
this ._ContentType  =  value;
         }
      }

      
//  Property DeptId
       public   string  DeptId
      {
          
get
          {
              
return  _DeptId;
          }
          
set
          {
              
if  ( this ._DeptId  !=  value)
                  
this ._DeptId  =  value;
          }
      }
      
//  Property DeptName
       public   string  DeptName
      {
          
get
          {
              
return  _DeptName;
          }
          
set
          {

             
this ._DeptName  =  value;
          }
      }

       
private   string  _Type;
       
public   string  OwnerType
       {
           
get
           {
               
return  _Type;
           }
           
set
           {

              
this ._Type  =  value;
           }
       }
       
private   bool  _TemplateAble;
      
///   <summary>
      
///  是否可以作为模版
      
///   </summary>
        public   bool  Templateable
       {
           
get
           {
               
return  _TemplateAble;
           }
           
set
           {

              
this ._TemplateAble  =  value;
           }
       }
       
public   override   string  ToString()
       {
           
return  Encoding.UTF8.GetString( this .Content);
       }        

       
public   static  Document FromString( string  s, User user)
       {
           Document doc 
=   new  Document();
           doc.CreateTime 
=  DateTime.Now;
           doc.CreatorId 
=  user.Id;
           doc.CreatorName 
=  user.Name;
           doc.DeptId 
=  user.OrgId;
           doc.DeptName 
=  user.OrgName;
           doc.Content 
=  Encoding.UTF8.GetBytes(s);
           doc.Size 
=  doc.Content.Length;
           doc.ContentType 
=   " text/plain " ;
           
return  doc;
       }
      
public   static   string  ByteToString(  byte [] bytes )
      {
          
return  Encoding.UTF8.GetString( bytes );
      }
       
public   static   byte [] StringToByte( string  s)
       {
           
return  Encoding.UTF8.GetBytes(s); 
       }
       
public   string  GetExtendName()
       {
           
string [] arr  =   this .Name.Split(  ' . '  );

           
if  (arr.Length  <   1 return   "" ;
           
else   return  arr[ arr.Length  -   1  ];
       }   
   }

考虑到数据操作逻辑的可变性,不同的项目里面附件表设计的不同,这里引入一个数据操作接口:
public   interface  IWebdavDocumentHandler
{
        Document GetDocument(
int  id);//获取文档数据
        
void  ModifyDocContent( int  docId,  byte [] data);//修改文档内容
}

具体的实现这里就不写了。

好了,我们的数据访问逻辑已经有了,那么首先看get动作处理的实现:
     class  GetHandler : IVerbHandler
    {
        
#region  IVerbHandler 成员
        
public   void  Process(System.Web.HttpContext context)
        {
            
int  id  =  WebdavProtocolHandler.GetDocumentId( context ); //获取到主键

           
IWebdavDocumentHandler docSvr  = new DefaultWebdavDocumentHandler() ; //修改此处代码,实现不同的数据操作逻辑,可引入工厂模式
            Document doc 
=  docSvr.GetDocument(id);

            
if  (doc  ==   null )
            {
                context.Response.Write(
" 文档不存在! " );
                
return ;
            }

            context.Response.Clear();
            context.Response.ContentType 
=  doc.ContentType;
            
// 下载文件名限制32字符 16 汉字
             int  maxlength  =   15 ;
            
string  fileName  =  doc.Name;  // att.FileName ;
             if  (fileName.Length  >  maxlength)
            {
                fileName 
=   " - "   +  fileName.Substring(fileName.Length  -  maxlength, maxlength);
            }

            fileName 
=  HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);  // 必须编码,不然文件名会出现乱码
            context.Response.AppendHeader( " Content-Disposition " " attachment;filename= "   +  fileName  +   "" );          

            
if  (doc.Content  !=   null   &&  doc.Content.Length  >   0 )
                context.Response.BinaryWrite(doc.Content);

            context.Response.End();
        }
        
#endregion
    }
很简单吧,跟我们普通实现文档下载的代码一样。

put动作的实现:
  class  PutHandler : IVerbHandler
    {
        
#region  IVerbHandler 成员

        
public   void  Process(System.Web.HttpContext context)
        {
            
int  docId  =  WebdavProtocolHandler.GetDocumentId(context);

            Document doc 
=  GetDocFromInput(context.Request);

            doc.DocumentId 
=  docId;

            
IWebdavDocumentHandler docSvr  = new DefaultWebdavDocumentHandler() ; //修改此处代码,实现不同的数据操作逻辑,可引入工厂模式

            docSvr.ModifyDocContent( doc.DocumentId , doc.Content );
        }

        
private  Document GetDocFromInput(System.Web.HttpRequest request )
        {
            Document doc 
=   new  Document();
            
// 读取文件的数据
            doc.Content  =   new   byte [ request.ContentLength ];
            doc.Size 
=  request.ContentLength;
            Stream fileDataStream 
=  request.InputStream;
            fileDataStream.Read( doc.Content , 
0 , doc.Size );
            doc.ContentType 
=  request.ContentType;
            
return  doc;
        }
        
#endregion
    }

OK,主要的动作都实现了,下面,我们需要WebdavProtocolHandler将各命令处理对象整合到一起:
     public   class  WebdavProtocolHandler : IHttpHandler
    {
        
public   static   int  GetDocumentId( HttpContext context )//按照前面确定的主键策略返回主键
        {
            
string  url  =  context.Request.Url.ToString();
            
string [] arr  =  url.Split(  ' / '  );
            
string  id  =  arr[arr.Length  -   2 ];
            
return  Convert.ToInt32( id );
        }
        
public   void  ProcessRequest(HttpContext context)
        {
            HttpRequest Request 
=  context.Request;
            context.Response.AppendHeader(
" OpenWebDavServer " " 1.0 " );
            
string  verb  =  Request.HttpMethod;
            
// Log.Write(verb);
            IVerbHandler vh 
=  GetVerbHandler( verb );

            
if ( vh  ==   null  )
                
return  ;

            vh.Process(context);      
        }

        
private  IVerbHandler GetVerbHandler( string  verb)
        {
            
switch  (verb)
            {
                
case   " LOCK "  :
                    
return   new  LockHandler();
                
case   " UNLOCK " :
                    
return   new  UnLockHandler();
                
case   " GET " :
                    
return   new  GetHandler();
                
case   " PUT " :
                    
return   new  PutHandler();               
                
case   " OPTIONS " :
                    
return   new  OptionsHandler();
                
default  :
                    
return   null ;
            }
        }    

        
public   bool  IsReusable
        {
            
get  {  return   false ; }
        }

    }

到这里呢,已经基本上算game over了,基于以上代码设计,可以完全实现office文档的在线编辑。若要通过链接直接打开编辑,可以
采用 Office文档在线编辑的实现之一Document_Edit2函数触发office编辑。

哦,IIS还需要做一点小配置:
1)将.doc , .xml 加入到站点虚拟目录的isapi映射, 不要选中 "确认文件是否存在",动作要选全部动作,
2)禁用IIS本身的Webdav扩展,
3)删除虚拟目录HTTP头中的自定义HTTP头: MicrosoftOfficeWebServer,如果有的话。


this is the real end.
目录
相关文章
|
3月前
|
Web App开发 JavaScript 前端开发
网页VUE纯前端在线预览编辑Office,支持doc/docx、xls/xlsx、ppt/pptx、pdf等格式
随着互联网技术的不断发展,越来越多的企业开始采用在线办公模式,微软Office Word 是最好用的文档编辑工具,然而doc、docx、xls、xlsx、ppt、pptx等格式的Office文档是无法直接在浏览器中直接打开的,如果可以实现Web在线预览编辑OffIce,肯定会还带来了更高效、便捷的办公体验,为我们的工作带来了更多可能性。
493 0
|
9月前
|
前端开发 C#
C# 基于NPOI+Office COM组件 实现20行代码在线预览文档(word,excel,pdf,txt,png)
C# 基于NPOI+Office COM组件 实现20行代码在线预览文档(word,excel,pdf,txt,png)
|
1月前
|
Web App开发 JavaScript 前端开发
2024年纯前端VUE在线编辑微软Office/金山WPS的Word/Excel文档
现在,随着数字化进程渗透到到各行各业,数据安全已经成为了数字化革命中的重要组成部分,而在线Office成在OA、ERP、文档系统中得到了广泛的应用,为我国的信息化事业也做出了巨大贡献。随着操作系统、浏览器及Office软件的不断升级和更新换代,加上国家对信息化、数字化系统要求的不断提升,一些厂家的WebOffice控件产品不断被淘汰出局,而现存的几个产品也存在以下几个问题:
411 1
2024年纯前端VUE在线编辑微软Office/金山WPS的Word/Excel文档
|
3月前
|
前端开发
招投标系统是Electron的纯内网编辑Office Word,可以设置部分区域可编辑,其他的地方不能编辑吗?
我们是招投标系统的开发公司,框架是用的Electron,需要在纯内网的环境下编辑Office Word,可以设置部分区域可编辑,其他的地方不能编辑吗(如下红框位置)?并且在用户忘记填写一些区域的时候做提醒。
49 1
|
3月前
|
Web App开发 安全 前端开发
新一代WebOffice高版本谷歌Chrome打开、编辑、保存微软Office/金山WPS解决方案大盘点
随着互联网技术的不断发展,越来越多的企业开始采用在线办公模式,微软Office Word 是最好用的文档编辑工具,然而doc、docx、xls、xlsx、ppt、pptx等格式的Office文档是无法直接在浏览器中直接打开的,如果可以实现Web在线预览编辑OffIce,肯定会还带来了更高效、便捷的办公体验,为我们的工作带来了更多可能性。
390 2
|
9月前
|
Web App开发 存储 C#
C# 10分钟入门基于WebOffice实现在线编辑文档,实时保存到服务器(所有office,兼容WPS)
C# 10分钟入门基于WebOffice实现在线编辑文档,实时保存到服务器(所有office,兼容WPS)
|
4月前
|
安全
猿大师办公助手网页在线安全浏览 Office Word 文档,只读打开 / 禁止编辑 / 禁止复制 / 禁止另存 / 禁止打印 / 禁止截屏
在企业 OA 系统或者在线协作办公场景中,有一些合同公文或者客户数据等重要文档需要我们在线共享给其他人,但是我们只希望其他人只能预览阅读文档,不能随便编辑修改文档,也禁止复制共享 Word 文档的内容到其他文档或者编辑器,不能将共享文件另存为本地文件夹,并且禁止用户打印该 Word 文档,那么该如何实现呢?
73 0
|
4月前
|
JSON 安全 数据安全/隐私保护
WebOffice 网页版在线 Office 的 Word 文档权限控制,限制编辑,只读、修订模式、禁止复制等
在一些在线 Office 文档中,有很多重要的文件需要保密控制,比如:报价单、客户资料等数据,只能给公司成员查看,但是不能编辑,并且不能拷贝,防止重要资料外泄。可以通过猿大师办公助手的在线 Office 的文档编辑权限来解决这些问题!
80 1
|
4月前
网页编辑Office Word文档,开启修订功能,启用留痕、显示留痕并接受留痕
在日常办公环境场景下,有时候会遇到帮助他人修改文档或者为文档提供修改意见,如果我们在文档中直接修改,其他人很不容易看到我们修改了哪个部分,如果一旦你的修改意见不被采纳,原作者还需要恢复原来的文档,这样为别人带来了更多的工作。 如果用猿大师办公助手在网页中编辑Office Word文档,开启修订功能,启用留痕、显示留痕并接受留痕,就可以很好的来解决此问题。
356 1
|
7月前
|
存储 数据安全/隐私保护 对象存储
接入OnlyOffice,支持协同编辑Office文档,可私有化部署的企业知识库 zyplayer-doc 2.2.1 发布啦
zyplayer-doc是一款适合企业和个人私有化部署使用的WIKI知识库管理系统,提供在线化的知识库管理功能,专为私有化部署而设计,最大程度上保证企业或个人的数据安全,您可以完全以内网的方式来部署使用它。
335 0