首页 / 资讯中心 / 文章详情

C# ASP.NET Core 文档管理系统:在线编辑、版本回滚与权限控制

C# ASP.NET Core 文档管理系统:在线编辑、版本回滚与权限控制 ★ FEATURED ARTICLE
简介一套基于 C# 与 ASP.NET 构建的 Web 文档管理系统面向课程设计、毕业设计及中小型团队文档协作场景重点解决文档在线创建、编辑、存储、共享与权限控制等问题适合具备一定 C# 基础、希望了解 ASP.NET Web 应用完整实现流程的学习者参考。压缩包共 98 个文件约 1.31MB包含 32 个 cs 服务端源码、14 个 aspx 页面、7 个 js 与 4 个 css 前端资源以及 SQL Server 的 mdf/ldf 数据库文件、sln 解决方案和 csproj 工程文件另有使用手册、测试分析报告及 jpg/gif 界面素材便于还原系统结构与运行环境。已有 558 人学习下载。项目将文档管理、在线编辑、角色权限、版本控制和操作日志等能力落在可参考的代码与数据库设计中适合用来理解 ASP.NET 页面交互、C# 业务逻辑与 SQL Server 存储检索的配合方式也可作为二次开发或答辩演示的基础工程对理解系统分层与模块职责较有帮助。1. 从“存文档”到“改文档”这套系统真正要解决的那八成工作量有人拿着课程设计题目来问文档管理系统不就是建文件夹、上传、下载三件事吗。真动手做才发现目录树和上传下载加起来只占两成工作量剩下八成全砸在在线编辑上——两个人同时改一份方案怎么不打架、改到一半浏览器崩了内容还在不在、上周的版本能不能一键回滚、谁能改哪一份文档。这套用 C# 与 ASP.NET Core 搭起来的文档管理系统重心不在文件存在哪儿而在“改”这件事怎么被安全地记录下来、怎么被追溯回去。它适合要交一份完整毕设或课程设计的人也适合要给团队内部做一套轻量文档库的开发者下面从需求拆解一路写到验收清单中间该给的代码和配置都给到位。2. 需求拆解与技术选型三类角色、四组用例与一套能直接建起来的项目骨架动手写第一行代码之前我会强制自己先干一件很枯燥但极省事的事把角色、用例、页面路由、数据表四者对齐成一张纸。这一步不做后面必然出现“功能写完了但不知道谁在用”的情况答辩时被问一句“这个按钮是给谁点的”就哑火。这一章先把角色和用例拆干净再定技术栈最后落一套能直接dotnet new出来照着建的骨架。2.1 先把角色和用例钉死再谈功能清单这套系统的角色只有三类不用设计得更细细了反而每个角色都不好用。普通成员的动线是登录后进入自己可见的目录树能新建、上传、在线编辑自己有写权限的文档能查看历史版本、能把自己的文档共享给指定的人部门管理员在此基础上多出目录维护、成员授权、回收站还原与彻底删除系统管理员只管三件事——账号生命周期、存储配额、全量审计日志他不应该能看到文档正文。四组用例决定了整个系统的骨架。文档生命周期组负责新建、上传、重命名、移动、删除、还原对应的是文档表和文件夹表在线编辑与协作组负责打开编辑器、自动保存、版本对比、回滚对应的是文档表和版本表检索与定位组负责按标题、创建人、时间范围、内容关键字找到文档对应的是索引字段和查询接口审计与回收组负责记录谁在什么时候对哪份文档做了什么对应审计表。每组用例在页面里都要有明确入口在数据库里都要有明确落点凡是落不到表的用例就是需求本身没想清楚。砍需求比加需求更重要。全文检索引擎不自建用数据库的模糊匹配加索引字段先撑住文档量上万再考虑接入外部检索组件实时协同编辑不做改为“进入编辑加锁提示和保存前版本校验”这个折中在毕设场景里完全够用而且讲得清工作流审批不做把精力留给版本与权限。我一般会把“不做的三件事”也写进设计文档评审时反而加分因为它说明你权衡过而不是漏掉了。2.2 技术栈定版为什么是 C# 加 ASP.NET Core 这一套关注点选择理由运行时与框架.NET 当前 LTS 版本 ASP.NET Core长期支持、内置依赖注入与中间件管线、异步 IO 完整数据访问EF Core 关系型数据库SQL Server 或 PostgreSQL迁移脚本可版本化LINQ 查询可读性高认证授权框架自带的身份体系 策略式授权不自己造密码哈希与会话管理文件存储抽象出存储接口默认存本地磁盘毕设环境不必上云接口留好将来换对象存储的余地文档转换Open XML 处理库 后台托管服务解析 docx 结构转换任务异步化前端服务端渲染页面 原生 JS 编辑模块减少构建链答辩现场不依赖网络装包选 C# 这条线的核心理由不是语言本身而是 ASP.NET Core 把几个最容易写歪的东西标准化了中间件顺序、依赖注入生命周期、授权策略、后台任务宿主。文档系统里“上传要限流、下载要鉴权、转换要排队”这三件事用框架现成的机制写出来只有几十行自己搭一套反而容易在细节上出问题。EF Core 的迁移能力对毕设尤其友好从第一次建表到后期加字段全部有脚本可查答辩时把迁移记录一摆数据库演进过程一目了然。2.3 分层结构与项目目录一套可以直接照建的骨架分层不做成教条只分四层Web 层管路由、模型绑定和授权应用层管用例编排一个用例一个服务方法领域层放实体和领域规则比如“版本号必须连续递增”基础设施层放数据库上下文、存储实现、转换任务。层与层之间只允许上层调下层跨层调用一次都不要留否则一个月后自己都理不清。# 解决方案目录结构按职能划分而不是按文件类型划分 DocManager.sln src/ DocManager.Web/ # 控制器、视图、中间件、前端静态资源 Controllers/ Views/ wwwroot/js/editor/ # 在线编辑前端模块 DocManager.Application/ # 用例服务文档、版本、权限、审计 Documents/ Versions/ Permissions/ DocManager.Domain/ # 实体、枚举、领域异常 DocManager.Infrastructure/ # EF Core 上下文、存储实现、转换任务 Persistence/ Storage/ Conversion/ tests/ DocManager.Tests/ # 版本合并、权限判定的单元测试启动入口把所有服务一次性注册清楚注册顺序和生命周期是最容易出错的地方写的时候顺手加注释后面改起来不用猜。// Program.cs服务注册与中间件顺序 var builder WebApplication.CreateBuilder(args); builder.Services.AddDbContextDocDbContext(opt opt.UseSqlServer(builder.Configuration.GetConnectionString(DocDb))); // 存储抽象默认本地磁盘将来换对象存储只改这一行 builder.Services.AddSingletonIStorageProvider, LocalDiskStorageProvider(); // 授权文档级 ACL 走自定义 handler不散落在控制器里 builder.Services.AddScopedIAuthorizationHandler, DocumentPermissionHandler(); builder.Services.AddAuthorization(); // 转换任务跑在后台宿主里避免上传请求被转换耗时拖住 builder.Services.AddHostedServiceConversionWorker(); builder.Services.AddControllersWithViews(); var app builder.Build(); app.UseStaticFiles(); // 静态资源要在鉴权之前 app.UseRouting(); app.UseAuthentication(); // 顺序固定先认证 app.UseAuthorization(); // 再授权 app.MapDefaultControllerRoute(); app.Run();这段代码里三个参数值得说明AddDbContext默认作用域生命周期别改成单例否则并发请求会共用同一个上下文AddSingleton用在无状态的存储实现上是安全的如果存储实现里带了缓存字典就要换成作用域并考虑并发UseAuthentication必须排在UseAuthorization前面反过来写会出现“永远判不出当前用户”的经典问题排查时日志里一片 401却怎么都找不到原因。中间件顺序在 ASP.NET Core 里是硬约束不是风格偏好。3. 数据模型与权限设计文档、版本、权限三张核心表怎么撑住整个系统数据模型定错了后面所有功能都是在补窟窿。文档系统最容易犯的错是只建一张文档表把内容、版本、权限全塞进去写到第三个功能就发现字段互相打架。这一章把核心表结构、版本策略和权限判定三件事拆开讲每张表都给出可执行的建表语句和取舍理由。3.1 核心表结构与字段取舍文件夹用物化路径存父路径加自身标识拼成一条字符串查子树用前缀匹配一条语句搞定比递归查询好维护。文档表只存当前内容与指针历史内容全部推到版本表。版本表额外存内容哈希用来做去重和“内容没变就不落版本”的判断。权限表用主体类型加主体标识的写法既能授权给人也能授权给部门。-- 文件夹物化路径方便一次查出整棵子树 CREATE TABLE Folders ( Id BIGINT PRIMARY KEY IDENTITY, ParentId BIGINT NULL, Name NVARCHAR(200) NOT NULL, Path NVARCHAR(900) NOT NULL, -- 形如 /1/7/23/ OwnerId BIGINT NOT NULL, CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME() ); CREATE INDEX IX_Folders_Path ON Folders(Path); -- 文档主表只放当前版本内容与并发控制字段 CREATE TABLE Documents ( Id BIGINT PRIMARY KEY IDENTITY, FolderId BIGINT NOT NULL, Title NVARCHAR(300) NOT NULL, ContentJson NVARCHAR(MAX) NULL, -- 编辑器中间模型 CurrentVersion INT NOT NULL DEFAULT 0, RowVersion ROWVERSION, -- 乐观锁保存时比对 SizeBytes BIGINT NOT NULL DEFAULT 0, Status TINYINT NOT NULL DEFAULT 1, -- 0草稿 1正常 2回收站 OwnerId BIGINT NOT NULL, UpdatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME() ); CREATE INDEX IX_Documents_Folder_Status ON Documents(FolderId, Status); -- 版本表哈希去重 快照标记避免自动保存撑爆表 CREATE TABLE DocumentVersions ( Id BIGINT PRIMARY KEY IDENTITY, DocumentId BIGINT NOT NULL, VersionNo INT NOT NULL, ContentJson NVARCHAR(MAX) NOT NULL, ContentHash CHAR(64) NOT NULL, -- SHA-256判断内容是否真的变了 IsSnapshot BIT NOT NULL DEFAULT 0, -- 快照版本不可被合并清理 CreatedBy BIGINT NOT NULL, CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME() ); CREATE UNIQUE INDEX UX_Versions_Doc_No ON DocumentVersions(DocumentId, VersionNo); -- 权限表主体可以是人也可以是部门Level 越高权限越大 CREATE TABLE DocumentPermissions ( Id BIGINT PRIMARY KEY IDENTITY, DocumentId BIGINT NULL, -- 为空表示这是文件夹级授权 FolderId BIGINT NULL, SubjectType TINYINT NOT NULL, -- 1用户 2部门 SubjectId BIGINT NOT NULL, Level TINYINT NOT NULL, -- 1只读 2可编辑 3可管理 Inherited BIT NOT NULL DEFAULT 1 -- 是否允许向下继承 );字段取舍上有两处是经验换来的。Documents表里放ContentJson会让单行变宽但它换来的是“打开文档零额外查询”在文档系统这种读多写多的场景里划算如果文档经常超过几兆就把内容挪到独立的DocumentContents表用一对一关联主表保持窄行。RowVersion是数据库自动维护的并发令牌不要用时间戳代替毫秒级精度在并发保存下会撞车。3.2 版本模型不做全量快照也不做真正的增量很多人一听版本管理就想到存 diff。文档编辑器产生的是结构化 JSON块顺序频繁变动做真正的文本级增量既复杂又容易在回滚时出错。我一般采用的折中方案是每次保存都算内容哈希哈希和上一版相同就直接返回不落新版本哈希不同则落一条普通版本每隔固定次数或固定时间间隔额外落一条IsSnapshot 1的快照版本。回滚时从最近的快照加后续版本重放清理历史时只清非快照版本。// 版本落库先比哈希再决定是否写入避免自动保存刷出上千条记录 public async Taskint SaveVersionAsync(long docId, string contentJson, long userId) { var hash ComputeSha256(contentJson); // 归一化之后再做哈希 var last await _db.DocumentVersions .Where(v v.DocumentId docId) .OrderByDescending(v v.VersionNo) .FirstOrDefaultAsync(); if (last is not null last.ContentHash hash) return last.VersionNo; // 内容没变直接复用版本号 var nextNo (last?.VersionNo ?? 0) 1; var isSnapshot nextNo % 20 0; // 每 20 版落一个快照 _db.DocumentVersions.Add(new DocumentVersion { DocumentId docId, VersionNo nextNo, ContentJson contentJson, ContentHash hash, IsSnapshot isSnapshot, CreatedBy userId }); await _db.SaveChangesAsync(); return nextNo; }ComputeSha256之前一定要先做归一化把 JSON 的键排序、去掉空白、把编辑器插入的临时节点剔除。不做归一化同一份内容因为键顺序不同就会算出两个哈希去重等于没做。快照间隔的 20 是个可调参数文档改动频繁就调小存储紧张就调大一般 10 到 30 之间都合理。3.3 权限从角色到文档级 ACL 的判定链路只用角色做权限在文档系统里一定会不够用因为总有人要“把这一份文档单独共享给某个同事”。做法是在角色之上加一层资源级授权判定时从文档自身向上沿文件夹路径逐级查找显式授权最近的一级优先路径上出现显式拒绝就直接拒绝。// 计算用户在某个文档上的最终权限级别 public async TaskPermissionLevel GetEffectiveLevelAsync(long docId, long userId) { var doc await _db.Documents.FindAsync(docId); var folderPath await GetFolderPathAsync(doc.FolderId); // 形如 /1/7/23/ // 1. 文档级显式授权优先级最高 var docLevel await _db.DocumentPermissions .Where(p p.DocumentId docId p.SubjectType 1 p.SubjectId userId) .Select(p (PermissionLevel?)p.Level).FirstOrDefaultAsync(); if (docLevel.HasValue) return docLevel.Value; // 2. 沿文件夹路径自近向远查找最近一级生效 foreach (var folderId in ParsePathReversed(folderPath)) { var level await _db.DocumentPermissions .Where(p p.FolderId folderId p.SubjectType 1 p.SubjectId userId) .Select(p (PermissionLevel?)p.Level).FirstOrDefaultAsync(); if (level.HasValue) return level.Value; } // 3. 都没有命中落到角色默认权限本人创建的可编辑其余只读 return doc.OwnerId userId ? PermissionLevel.Edit : PermissionLevel.Read; }这段逻辑的关键在于“最近一级生效”和“本人创建兜底”这两条规则。实际写的时候容易漏掉部门授权主体类型等于 2 时要先查出用户所属部门再匹配一次否则共享给整个部门的功能形同虚设。权限判定不要在控制器里 if-else 拼全部收进一个IAuthorizationHandler下载、预览、编辑、删除四个入口复用同一份判定越权漏洞基本就堵住了。4. 在线编辑模块落地文档中间模型、自动保存与冲突处理在线编辑是这套系统的重心也是最容易做成“看起来能打字、实际不能用”的模块。很多课程设计里的编辑器就是一个大textarea提交时把纯文本塞进数据库导出之后排版全丢。要做得能拿得出手得先把文档中间模型定下来再把自动保存和冲突处理这两条链路打通。4.1 文档中间模型把文档拆成可存储、可校验的块不直接存 HTML是因为 HTML 里样式和结构混在一起做版本对比几乎不可能做权限控制也无从下手。折中方案是存一份块模型 JSON文档由若干块组成每块有类型和属性正文内容作为块的内容字段。段落、标题、列表、表格、图片、引用这六种块足够覆盖绝大多数办公文档。{ version: 1, blocks: [ { id: b1, type: heading, level: 2, text: 季度工作说明 }, { id: b2, type: paragraph, text: 本季度重点推进三件事。, marks: [] }, { id: b3, type: list, ordered: false, items: [完成文档迁移, 补齐权限配置, 整理历史版本] }, { id: b4, type: image, fileId: f_2024_a1, width: 640 } ] }这份模型带来三个直接好处渲染时服务端和前端各写一个渲染器输出结果一致版本对比按块 id 做差异能精确定位“哪一段被改了”导出 docx 时块类型可以直接映射到文档元素不用反向解析 HTML。块的 id 在创建时生成编辑过程中不变这是后面做冲突提示的基础。4.2 自动保存与乐观锁一次保存请求的完整链路前端在停止输入后延迟三秒触发保存请求里带上当前文档的版本号作为基准。服务端用RowVersion做条件更新影响行数为零就说明期间有人先保存了返回 409 让前端提示用户。// 保存接口条件更新实现乐观锁冲突时返回 409 而不是覆盖 [HttpPost(/api/documents/{id:long}/save)] public async TaskIActionResult Save(long id, [FromBody] SaveRequest req) { var affected await _db.Documents .Where(d d.Id id d.RowVersion req.BaseRowVersion) .ExecuteUpdateAsync(s s .SetProperty(d d.ContentJson, req.ContentJson) .SetProperty(d d.UpdatedAt, DateTime.UtcNow)); if (affected 0) return Conflict(new { code VERSION_CONFLICT, latestVersion await GetLatestAsync(id) }); var versionNo await _versions.SaveVersionAsync(id, req.ContentJson, CurrentUserId); return Ok(new { version versionNo }); }BaseRowVersion是前端打开文档时拿到的并发令牌从加载接口返回不要用前端自己维护的计数代替。ExecuteUpdateAsync直接生成一条带条件的 UPDATE不存在“先查后改”的竞态窗口比自己写比较逻辑可靠。返回 409 之后前端不要简单地弹个框就完事正确做法是把服务端最新内容拉回来做一次块级差异比对把有冲突的块标红让用户手动选这个体验差别在答辩演示时非常明显。// 前端自动保存防抖 冲突后重载差异 let saveTimer null; let baseRowVersion null; function scheduleSave(contentJson) { clearTimeout(saveTimer); saveTimer setTimeout(() doSave(contentJson), 3000); } async function doSave(contentJson) { const resp await fetch(/api/documents/${docId}/save, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ contentJson, baseRowVersion }) }); if (resp.status 409) { const data await resp.json(); renderConflictDiff(data.latestVersion); // 标记冲突块等待用户选择保留哪一版 return; } const ok await resp.json(); baseRowVersion ok.rowVersion; // 更新令牌下一次保存继续用 }这里有个细节值得提前想清楚防抖时间设成三秒不是随便定的设成一秒会让网络请求数量翻好几倍设成十秒又容易在切页面时丢内容。三到五秒是经验区间同时再补一个“页面可见性变化时立即保存”的监听浏览器切后台或关闭前把最后一份内容送出去这条兜底比调参数更管用。4.3 附件与在线预览转换任务怎么排进后台上传的 docx 想在浏览器里直接看需要转换成 HTML。转换是 CPU 密集且耗时的操作绝不能放在上传请求里同步做否则一个几十兆的文档就能把请求线程占住。做法是上传完成后把文档标识写入任务队列表由后台托管服务轮询消费。// 后台转换任务取任务、转换、写回结果失败标记可重试 public class ConversionWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken token) { while (!token.IsCancellationRequested) { using var scope _scopeFactory.CreateScope(); // 后台任务必须自建作用域 var queue scope.ServiceProvider.GetRequiredServiceIConversionQueue(); var job await queue.DequeueAsync(token); if (job is null) { await Task.Delay(2000, token); continue; } try { var html await _converter.ToHtmlAsync(job.FilePath); await queue.MarkDoneAsync(job.Id, html); } catch (Exception ex) { await queue.MarkFailedAsync(job.Id, ex.Message, retryAfter: 3); } } } }后台服务里必须自己创建作用域这是踩过一次才记住的点BackgroundService是单例直接注入作用域的数据库上下文会在第一次迭代之后一直复用同一个实例跑久了就出现跟踪实体异常。重试次数给三次超过就标记为永久失败并把原始文件保留让用户能下载原件这比反复重试把队列堵死要体面得多。5. 避坑与排查五个最容易在上线前翻车的点前面几章写的是“应该怎么做”这一章写“做完了为什么还是不对”。这五条都是我在类似系统里真实遇到过的按现象、原因、解决三段说清楚照着排查基本能覆盖八成故障。5.1 上传稍大的文档就报 413 或者进程内存暴涨现象是本地传 5MB 的文档没事传 50MB 直接返回 413或者服务器内存曲线陡增之后接口超时。原因通常有两层框架默认对请求体大小有上限超过就直接拒绝同时代码里习惯性地把上传流全部读进MemoryStream再写盘一个并发上传就能吃掉几百兆。解决分两步先在配置里显式放开请求体限制并把单次请求上限控制住再把写盘改成流式复制。// 放开请求体限制同时用流式写入避免整块进内存 [HttpPost(/api/files/upload)] [RequestSizeLimit(200 * 1024 * 1024)] // 单请求上限 200MB [RequestFormLimits(MultipartBodyLengthLimit 200 * 1024 * 1024)] public async TaskIActionResult Upload(IFormFile file) { var target _storage.ResolvePath(file.FileName); await using var input file.OpenReadStream(); // 不读进内存 await using var output System.IO.File.Create(target); await input.CopyToAsync(output); // 流式落盘 return Ok(new { fileId await _storage.RegisterAsync(target, file.Length) }); }RequestSizeLimit和RequestFormLimits两个都要加前者管整体请求体后者管表单分段只加一个仍然会在某些客户端上报 413。如果前置了反向代理还要在代理层同步调整上限否则请求根本到不了应用。5.2 编辑一小时后版本表里多了上千条记录现象是打开版本历史面板卡住数据库体积涨得离谱。原因是每次自动保存无条件插一条版本用户敲一个字就存一次一小时下来几百上千条很正常。解决思路是三层过滤内容哈希相同直接跳过五秒内的连续保存合并成一条按固定间隔落快照清理时只清非快照版本。第一层在服务端做第二层在保存接口入口做用文档标识加时间窗判断第三层就是 3.2 里那段快照逻辑。清理任务要放在后台别在用户操作路径上跑删除。5.3 两个人同时编辑后保存的把先保存的覆盖了现象很隐蔽用户不会立刻发现等看到内容少了才来报。原因是没有并发控制两个请求都执行了无条件 UPDATE后到的覆盖先到的。解决就是把版本号或行版本令牌带进 WHERE 条件影响行数为零返回 409前端拉最新内容做块级比对。这里有个反直觉的点光加锁不够锁只能拦住同时提交拦不住“A 打开后去开会B 改完A 回来继续改”这种长事务场景所以冲突提示一定要做成能合并的界面而不是简单弹一个“保存失败”。5.4 权限判断写在控制器里越写越乱最后漏判现象是某个下载接口忘了加判断用户改一下地址栏的文档 ID 就能拿到别人的文件。原因是权限逻辑散落在各个 action 里加一个入口就要记一次早晚会漏。解决是把判定收进统一的授权处理器所有涉及文档的入口走同一套策略下载链接用短时效的签名令牌而不是直接暴露物理路径。// 下载走一次性签名令牌避免直接暴露文件路径 public string BuildDownloadToken(long docId, long userId, TimeSpan ttl) { var payload ${docId}:{userId}:{DateTimeOffset.UtcNow.Add(ttl).ToUnixTimeSeconds()}; var sig Convert.ToBase64String(_hmac.ComputeHash(Encoding.UTF8.GetBytes(payload))); return ${Convert.ToBase64String(Encoding.UTF8.GetBytes(payload))}.{sig}; }令牌有效期给五分钟足够签名用服务端密钥校验时先验签再验过期两步都不能省。5.5 文档内容原样输出粘贴进来的脚本被执行了现象是某份从别处粘贴过来的文档一打开就弹窗或者页面错乱。原因是富文本内容在保存时没做过滤渲染时又直接输出到页面。解决分两道服务端保存前对块模型做白名单校验只允许预定义的块类型和有限的样式属性渲染层统一走自己写的渲染器不使用原样插入 HTML 的方式同时在响应头加一条内容安全策略兜底。白名单要写死成常量表别用黑名单黑名单永远补不全。6. 验收与答辩用一份十二项清单把系统讲成一套完整方案到了这一步功能写完了真正决定评价的是你能不能在三十分钟里证明它跑得通。我的习惯是准备一份固定的验收清单从空数据库开始一条一条走走不通的那条就是答辩会被追问的那条。清单不要写“测试权限功能”这种模糊项每一项都要能落到具体操作和具体结果。序号操作通过标准1注册新账号并登录登录态保持刷新页面不掉线2新建三级目录目录树正确展示路径字段正确3上传 30MB 文档上传成功内存无明显上涨进度可见4打开在线编辑器修改三段文字三秒内自动保存版本号加一5断网后继续编辑再恢复内容不丢恢复后正常保存6两个浏览器同时编辑同一文档后保存方收到冲突提示并能合并7查看版本历史并回滚到上一版内容准确还原回滚本身也产生新版本8把文档共享给另一个账号只读对方能看不能改编辑按钮不可用9直接拼接他人文档 ID 访问下载接口返回 403不泄露文件内容10上传含脚本的内容片段保存后被过滤页面无异常执行11删除文档后从回收站还原原目录位置恢复权限不丢12查看审计日志上述每步操作都有记录包含操作人与时间这十二项覆盖了存储、编辑、并发、权限、安全、回收、审计七条主线正好对应前面几章的设计。演示顺序建议按表走不要临时挑功能点因为评审最想看的恰恰是“边界情况你怎么处理”第 5、6、9、10 四项是加分项主动演示比被问出来强得多。答辩陈述控制在八分钟两分钟讲需求和为什么不做全文检索、不做实时协同三分钟讲数据模型把文档表、版本表、权限表画在白板上讲清关系两分钟现场跑清单里的第 4、6、9 项剩下一分钟讲你知道的不足和下一步怎么改。主动说不足比被问出来体面比如“并发冲突目前是块级提示还没做到字符级合并”这句话说出来评审通常会点头而不是继续追问。从那以后我每次带人做这类系统演示前都强制自己清空数据库、换个没登录过的浏览器按清单从注册开始重走一遍不看代码、不连开发环境只当自己是第一次用的用户。走不通的那一步往往就是评审第一个会点的地方。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站