ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

WaterCloud 3.x二次开发实战:框架结构、权限模型与踩坑

WaterCloud 3.x二次开发实战:框架结构、权限模型与踩坑 1. 项目整体认知与模块拆分做.NET后端开发的应该都听过WaterCloud这个名字。它是一套基于ASP.NET Core 3.x的敏捷开发框架主打权限管理、代码生成和快速落地GitHub上Star一直不少很多公司的内部管理系统、中小型SaaS项目都拿它做底子。我最早接触WaterCloud是在一个OA项目里当时团队要在两周内交付一套带审批流和角色权限的后台系统靠原生ASP.NET Core从头搭的话光权限模型就得折腾好几天换成WaterCloud之后基础框架几乎没花时间主力全放在业务模块上那次的交付节奏让我印象很深。这次要聊的是WaterCloud 3.x版本相比2.x它在底层框架和模块组织上做了不少调整。很多人拿到源码之后第一反应是懵——项目太多、依赖关系复杂、不知道从哪看起更别提做二次开发了。这篇文章我会从项目分组、核心机制、二开流程、踩坑经验四个维度拆尽量用做过一遍的人的口吻讲清楚哪些是框架替你做了的哪些是你必须自己动手的以及做的时候容易在哪里翻车。先说项目分组。WaterCloud 3.x的解决方案默认分了好几层常见的结构是这样WaterCloud.Application应用服务层业务逻辑的主要承载地二开时新增业务大多数时候就是往这里加类。WaterCloud.Domain领域层放实体、仓储接口、领域服务相当于业务模型的地基。WaterCloud.Repository仓储实现层基于SqlSugar封装了通用仓储CRUD操作基本不用自己写SQL。WaterCloud.Code公共代码层工具类、缓存封装、公共枚举、常量都在这。WaterCloud.EntityFrameworkCore如果你用的是EF Core版本这层放DbContext和实体映射SqlSugar版本里这层更多是做数据库初始化和种子数据。WaterCloud.Web表现层MVC控制器、视图、静态资源都在这也是日常开发接触最多的一层。各层之间的引用关系是有方向的Domain在最底层Application调用Domain和RepositoryWeb调用Application层与层之间不搞循环引用。这个分层思路本身没什么特别但好处是边界感强——你二开的时候只要遵守“Web不直接碰数据库、Application里不写SQL”这种规则代码就不会烂到哪里去。再说框架内置的能力。3.x把很多通用功能做成了内置模块你在菜单里直接就能看到系统管理用户、角色、菜单、按钮权限、任务调度基于Quartz.NET的作业管理、代码生成器、数据字典、操作日志、多租户管理、文件上传。也就是说权限、日志、定时任务这些几乎所有后台系统都要的东西框架已经给了一版可用的实现你二次开发的核心工作其实是“在这个骨架上长业务肉”而不是重复造轮子。我建议第一次接触WaterCloud的人别急着改代码先把解决方案编译跑起来登录后台把所有内置菜单点一遍感受下权限是怎么控的、代码生成器出来的代码长什么样这个过程比对着文档读十遍都管用。等你对框架能做什么有了体感再去看源码里对应的类和表结构理解成本会低很多。2. 核心机制解析与关键组件选型2.1 数据库访问层SqlSugar还是EF Core怎么选WaterCloud 3.x同时维护了SqlSugar和EF Core两个版本这一点在框架里是少见的“双轨制”。很多刚上手的人会纠结到底用哪个版本做二开我说下我的使用感受。SqlSugar版本更贴合敏捷开发的节奏。它的原生用法就是db.QueryableT().Where(...).ToList()这种链式写法简单直接特别是免掉了大量样板代码。WaterCloud在SqlSugar版本里默认开启了“从实体生成表”的模式——就是你新建一个实体类框架启动时会自动帮你建表这在快速原型阶段非常爽。EF Core版本的优劣势刚好反过来它的状态跟踪和导航属性做复杂查询更顺手但初期的模型配置、迁移流程都更重。我的建议是如果你接手的项目已经是SqlSugar版本那就别中途切EF Core除非你有非常强烈的理由。因为WaterCloud的仓储层和业务逻辑已经在用SqlSugar特性比如[SugarTable]、[SugarColumn]这些特性标注切换意味着大量重构收益不划算。反过来如果你是新项目起步、团队普遍熟悉EF Core选EF Core版本也完全可行框架已经把两者都封装好了日常业务开发层面差异不大。2.2 权限模型从用户到按钮的五层控制WaterCloud的权限设计是典型的RBAC变种控制粒度细化到了按钮级。整个模型的链路是用户→角色→菜单→按钮再叠加一个数据范围的概念。在数据库层面核心表就那么几张SysUser用户、SysRole角色、SysUserRole用户角色关联、SysRoleAuthorize角色权限授权表和SysModule菜单及按钮。SysModule表里通过F_ParentId来维护树形结构菜单和按钮其实都在同一张表里区分靠F_Type字段0目录、1菜单、2按钮标识。权限校验的流程是这样的用户登录时框架会把该用户所有角色对应的授权信息合并生成一份权限缓存存到Redis或内存里。每次请求进控制器时通过HandlerAuthorizeAttribute这个过滤器校验当前用户是否有当前页面或按钮的权限码。你在视图里经常会看到这种写法if (Html.CurrentPermission(WaterCloud.SystemManage.Info.Add)) { button classbtn btn-primary onclickadd()新增/button }这个权限码就是SysModule表里的F_UrlAddress字段你给按钮配置了什么权限码视图里就写什么。这里有一个很容易踩的坑改了角色授权或新增菜单之后记得清一下权限缓存否则用户看到的还是旧的菜单和按钮状态。框架后台的“系统管理→角色管理→授权”页面操作完一般会自动刷新缓存但如果你是直接从数据库改数据那就要去缓存管理里手动清除。2.3 多租户与数据隔离3.x版本对多租户的支持做得比以前完善。表结构里普遍带一个F_TenantId字段逻辑上通过SqlSugar的全局过滤器实现数据自动隔离。简单说框架在查询时会自动往SQL里拼上TenantId 当前租户这个条件你不用每次写查询都手动带条件。但全局过滤器也有副作用如果你在二开时遇到了“明明数据在库里却查不到”的问题先别急着怀疑SQL写错了大概率是租户过滤器在起作用。排查思路是看一下当前登录用户的租户ID再检查你查的那条数据是不是属于其他租户。另外跨租户的数据操作比如管理员后台要查看所有租户的数据需要显式关闭过滤器SqlSugar里可以用db.QueryFilter.Clear()来做但这属于高级操作我后面在避坑章节会详细讲。3. 二次开发实操从建表到页面生成的全流程3.1 数据库表创建与实体编写二开最常规的操作就是新增一张业务表。先用Navicat或SSMS建好表结构或者在代码里新建实体类让框架自动建表我推荐后者因为WaterCloud的设计就是这样能用代码解决的就别用人工去操作数据库。下面是一个业务实体的示例/// summary /// 订单主表 /// /summary [SugarTable(bus_order, 订单管理)] public class OrderEntity : EntityBase { /// summary /// 订单编号 /// /summary [SugarColumn(ColumnName F_OrderNo, ColumnDescription 订单编号, Length 50)] public string OrderNo { get; set; } /// summary /// 客户名称 /// /summary [SugarColumn(ColumnName F_CustomerName, ColumnDescription 客户名称, Length 100)] public string CustomerName { get; set; } /// summary /// 订单金额 /// /summary [SugarColumn(ColumnName F_Amount, ColumnDescription 订单金额, DecimalDigits 2)] public decimal Amount { get; set; } }EntityBase是框架提供的基础实体基类里面已经包含了主键F_Id、创建时间F_CreatorTime、创建人F_CreatorUserId、删除标记F_DeleteMark这些通用字段你不用每个实体都重复定义。实体写好后如果是SqlSugar版本程序启动时会自动同步表结构注意它是“增量同步”也就是缺列会补多余列不会删所以你改字段名时要小心别指望它帮你做数据迁移。3.2 仓储与业务服务层的搭建实体建好后接下来是仓储和服务层。WaterCloud里这块的写法相对固定照着现有模块抄就行。仓储层一般不用单独写接口直接用框架的IRepositoryBaseT跑CRUD就够了。真正要写的是Application层比如新增一个OrderApppublic class OrderApp : BaseApp { private readonly IRepositoryBaseOrderEntity _orderRepository; public OrderApp(IRepositoryBaseOrderEntity orderRepository) { _orderRepository orderRepository; } public async TaskListOrderEntity GetListAsync(Pagination pagination) { return await _orderRepository.FindListAsync(o o.F_DeleteMark false, pagination); } public async TaskOrderEntity GetDetailAsync(string id) { return await _orderRepository.FindEntityAsync(o o.F_Id id); } public async Task AddOrderAsync(OrderEntity entity) { entity.F_Id Guid.NewGuid().ToString(); await _orderRepository.InsertAsync(entity); } }这里有个细节需要注意WaterCloud的分页查询会直接修改传入的Pagination对象把总记录数和总页数回填进去所以你控制器里取完列表后要拿同一个Pagination对象来给视图返回分页信息。这个设计容易让新手困惑——为什么查完列表之后pagination对象里的值变了其实这正是框架的设计意图。3.3 控制器与视图的生成策略到了控制器和视图这一步你有两条路可选。第一条路是用代码生成器。WaterCloud 3.x后台自带代码生成功能你填好表名、实体名、生成路径它会自动生成Controller、ViewModel、视图和菜单SQL。生成的代码质量说不上多优雅但作为初版骨架完全够用然后你再在这个骨架上改业务逻辑效率比手写高得多。第二条路是纯手写。手写的好处是你能完全控制代码风格和业务细节特别是在生成器对复杂表结构支持不太好的情况下比如一对多子表、多表联查的页面手写反而更快。两种方式我都用过我的习惯是第一版用代码生成器快速跑通然后立刻把生成的代码通读一遍该改的改、该删的删。不要迷信生成器把生成的代码当黑盒用是二开的大忌——因为后面但凡出问题你要去调试时连代码都找不到在哪那就非常被动了。3.4 菜单注册与权限绑定新页面做出来后最后一步是把它挂到系统菜单里。在WaterCloud里这个操作通常不是直接往数据库插菜单记录而是通过后台的“系统管理→菜单管理”里手动添加或者执行生成器生成的菜单SQL。菜单表的关键字段是F_ParentId上级菜单、F_UrlAddress访问地址对应控制器/动作、F_PermissionCode权限码。如果你在代码生成器里已经配置了按钮权限那生成SQL里会一并给你生成按钮的权限记录。菜单配置完毕后还要去角色管理里给对应角色勾选菜单权限否则已经登录的用户刷新页面也看不到新菜单。我遇到过不少同事问“为什么我加了菜单别人看不到”十有八九是忘了在角色授权里勾选或者没清缓存。整个流程串起来就是建实体→建服务→建控制器和视图→加菜单→角色授权→清缓存→测试这七步走完一个标准的一页CRUD功能就落地了。4. 常见问题与排查技巧实录4.1 登录失效与Redis缓存不同步WaterCloud 3.x的用户会话和权限信息默认走缓存有些部署环境里用的是Redis。二开时最容易遇到的一类问题是改了角色权限之后用户那边怎么刷新都没变化。这个基本就是缓存问题Redis里的权限数据和数据库里的授权数据没对上。排查顺序我建议这样先去后台“系统管理→缓存管理”里看有没有相关缓存项直接清空整个缓存键然后重新登录。如果问题依旧再看下登录令牌的过期时间——WaterCloud默认的登录过期时间是可以配置的如果你把过期时间设置了非常长那用户旧令牌一直在用自然看不到新权限。有一种情况容易被忽视多个站点共用一个Redis库且缓存键前缀没配置区分A环境改了权限把B环境的缓存也清了这在多环境共用缓存时尤其要小心。4.2 多租户下数据越权或查不到数据前面提到过租户全局过滤器这里展开讲。SqlSugar版本的WaterCloud在多租户设计上是靠过滤器实现的过滤器的作用域是整个请求上下文。二开时如果你的业务逻辑里是多表关联查询且表A有租户字段、表B没有这时候查询可能报错也可能查到跨租户的数据。排查技巧是在出问题的服务方法里临时输出一下SqlSugar生成的SQL看一眼条件里到底拼了什么内容。WaterCloud的仓储基类有个ToSql()方法FindListAsync之前你可以先调一下看生成的SQL带了哪些过滤条件。如果确实是因为租户过滤器导致的多表问题有两种解决办法一是给所有参与查询的表都加上租户字段二是在查询时明确关闭过滤器再手工拼过滤条件。我个人更推荐前者因为关闭全局过滤器属于“拆东墙补西墙”你这次关了下次别人在别的业务里又踩一遍。4.3 代码生成器生成的页面报“对象引用未设置”代码生成器这东西爽是真爽但生成的代码偶尔也会给你埋坑。最常见的报错是“Object reference not set to an instance of an object”出现在列表页或表单页。原因多半是生成器生成的View里某些字段绑定了实体里不存在的属性或者主键字段名不一致。我的处理习惯是代码生成器跑完后第一件事是全局搜索生成代码里所有的F_Id、F_ParentId这些关键字段确保和实际实体字段对的起来。另一个高发点是删除功能——生成器对“物理删除”和“逻辑删除”的判断不一定准如果你的实体继承了EntityBase那默认应该走逻辑删除更新F_DeleteMark但生成器可能生成的是物理删除的SQL语句一旦误点就会把数据彻底删掉。所以上线前建议把删除逻辑统一改用框架内置的DeleteAsync方法这是我从一次数据事故里换来的教训。4.4 定时任务不执行或重复执行的排查WaterCloud内置了基于Quartz.NET的任务调度模块二开时往里加定时任务很方便。但有两个高频问题一是任务不触发二是任务重复执行。任务不触发的通用排查顺序先看任务是否在后台任务管理里启动再看Cron表达式是否正确——建议先用在线Cron工具验证一遍表达式别凭感觉写0 0 2 * * ?和0 0 2 * * *的差异肉眼看不出来但执行完全不同。还要看应用是否重启过Quartz的持久化如果没配置应用重启后任务调度状态会重新初始化。任务重复执行则多半和部署环境有关——多实例部署时每个实例都会启动一套Quartz调度器同一个任务就被多个实例同时触发。WaterCloud的默认方案没做分布式锁所以在多实例部署场景下你要么给任务执行方法加数据库唯一约束或Redis锁要么就保证只有一台实例的定时任务模块是开启的。这个不是WaterCloud特有的毛病几乎所有基于Quartz单机调度的框架都有这问题理解了机制就知道怎么处理了。4.5 配置项与实际环境脱节的经典问题Web.config或appsettings.json里的配置项二开时经常会漏改。比如框架默认的数据库连接串、Redis连接串、文件存储路径很多人拿到源码先改了一个连接串结果日志、缓存、定时任务的连接还是指向默认地址导致页面能登录但验证码加载不出来、日志不写入、消息推送失败。我习惯拿到项目后先把所有配置文件通读一遍把连接串、缓存、队列、文件存储这些基础配置逐个确认再开始动业务代码。另外注意3.x版本里很多框架级配置是通过Options模式绑定的你新增配置项时不仅要写在配置文件里还要在对应的配置绑定类里加属性漏了的话配置读了没反应是常有的事。5. 二开流程规范与团队协作建议5.1 分支管理与模块隔离如果你不是单人开发者而是团队基于WaterCloud做项目一套合适的分支策略比代码技巧更重要。WaterCloud这种“内置功能多、业务侵入性强”的框架最大的问题是多人同时改同一份核心模块时极易冲突。我推荐的核心策略是“业务模块与框架升级隔离”。简单说框架自带的功能系统管理、权限、任务调度等尽量少改如果确实要改也要单独拉分支做并保留完整的改动记录。你们自己的业务代码放到独立的模块目录里和内置模块物理隔离。这样以后想升级WaterCloud版本时升级的只是框架主干业务代码不受影响。代码提交规范上建议按“模块名功能描述操作类型”的格式写提交信息比如“订单模块-新增导出功能-修复导出金额精度丢失”。这种规范看起来老生常谈但在二开项目里确实能救命——因为框架代码和业务代码交错在一起没有清晰的提交历史出问题的时候回溯追踪会非常困难。5.2 数据库变更的同步机制WaterCloud二开中数据库结构的变更管理是比较容易失控的一环。因为SqlSugar版本支持自动同步实体很多开发图省事直接改了实体类就跑导致开发库、测试库、生产库的表结构不一致上线时要么漏了字段要么多出列。我的做法是所有数据库变更都走显式的SQL脚本哪怕框架能自动同步也照样导出脚本并在发布记录里登记。比如加一张表、加一个字段、改一个索引都整理成V1.0.3__order_add_index.sql这种格式的脚本文件集中放到一个db/migration目录。上线时按顺序执行脚本并核对执行结果。这个习惯在二开项目里尤其重要——你面对的不是一个全新的空库框架本身的种子数据和业务数据都还在任何不可逆的数据库操作都要先备份。另外一个补充建议实体类里那些公共字段F_Id、F_CreatorTime这种别轻易改类型或长度。因为它们被基类统一管理一旦改动会影响所有业务表得不偿失。真要改的话评估面会非常大别只盯着当前这一张表。5.3 前后端分离改造的切入方式WaterCloud 3.x默认是MVC视图模式Vue版本在生态里也有但并非主线。如果你团队的前端资源比较弱或者只想做轻量改造我建议保持MVC模式利用框架内置的Layui前端组件日常后台功能足够应付别为了“技术上先进”去强行前后端分离。如果确实要前后端分离我的建议是分三步走第一步只把登录认证改造为JWT接口令牌模式确保接口层可以被非MVC客户端调用第二步按业务模块逐个提供RESTful API视图层逐步替换为前端工程第三步等所有接口替完之后再移除MVC视图代码。不要一次性推倒重来那是拿业务项目的稳定性开玩笑。说实话WaterCloud这种偏传统服务端渲染的框架做纯API改造的技术成本并不低非必要不建议做全量重构把有限的人力放回业务上才更合理。6. 从源码层面提升二开效率的个人总结6.1 必读的源码文件清单WaterCloud的源码量不小但真正值得精读的核心文件其实就那么几个。我筛选下来这几个文件建议二开前都花时间过一遍BaseApp.cs应用服务基类里面封装了通用CRUD、缓存操作、用户上下文获取这些高频方法理解了它你就理解了业务层该怎么跟框架交互。RepositoryBase.cs仓储基类所有数据库操作都从这里过里面的分页、条件构造、事务控制逻辑值得细看。HandlerAuthorizeAttribute.cs权限过滤器权限校验的真实实现就在这追一遍代码你能彻底明白权限码是怎么生效的。OperatorProvider.cs操作者上下文也就是当前登录用户信息存在哪、怎么取二开中几乎所有“当前用户”相关逻辑都依赖它。我见过很多半路接手WaterCloud的开发者遇到报错就顺手双击堆栈里的某个框架内部方法查看看完还是一头雾水其实就是因为这些核心类没系统读过。花一个下午把这四个文件啃完后面二开的疑难杂症大部分都能自己对付。6.2 缓存运用的最佳实践框架里大量运用了缓存内存缓存和Redis都有。二开时对缓存的态度要务实高频读取、更新不频繁、允许短暂不一致的数据优先走缓存强一致性要求的数据别偷懒直接查库。比如数据字典、系统参数这类数据完全适合缓存。但像订单状态、库存数量这种一变就影响核心流程的数据如果你图省事也塞缓存那就要做好缓存更新失败导致的数据不一致风险。WaterCloud内置了ICache接口你可以通过依赖注入在业务服务中直接使用它的接口设计得很简单Get/Set/Remove就三个核心里程碑做业务缓存读写非常方便。顺便说一句二开时尽量依赖ICache接口而不是直接引用具体的Redis或内存实现这样未来切换缓存介质时不用改业务代码。6.3 日志与异常兜底运维层面最容易忽略的是日志配置。WaterCloud用的日志组件是NLog默认配置会写到文件但很多二开者在开发环境压根没注意日志输出上线之后出了问题才想起来翻日志发现日志文件里全是无关紧要的内容关键错误却因为日志级别配置不对而被过滤了。我的建议是二开阶段就把日志级别调整为Debug跑一轮核心流程至少保证关键业务路径上都会有执行轨迹。上线前再把级别调回Info或Warn。另外框架里的全局异常中间件会把未处理异常包装成友好提示返回给前端但记住它只做包装不会自动帮你清掉问题——真正的细节还是要靠异常日志。7. 写在最后的经验做WaterCloud二开这几年我最深的感触是这个框架真正值钱的地方不是代码本身而是它把一套后台系统里七七八八的通用需求提前做完了。你用它的时间越久越会发现它的边界内藏了不少“理应有但没人告诉你会踩”的设计——多租户过滤器只是其中之一定时任务分布式、缓存复用这些点都得靠实际动手才能体会到。最后再分享一个小技巧碰到框架层面解决不了的问题时别急着改框架源码先看它的GitHub Issues和提交记录很多你认为是bug的现象其实官方或其他开发者早就讨论过答案可能就在某次提交说明里。对于二开者来说理解官方设计和社区共识远比靠一次“本地修复”要稳妥得多——因为你改掉的那行代码未来升级时可能就是冲突的来源。这篇文章先写到这里。项目里有一堆业务还在迭代后续等我把前端改造和接口层拆分的实操经验整理出来再来做第二部分。
返回列表