|
4 S1 @8 c! y4 h2 E; _* w2 s
一、说明本章主要介绍的是SimpleAdmin后端架构设计,作为一个系统的基石,一个好的架构设计可以让开发者在开发中少走很多弯路在写SimpleAdmin这个系统之前,也用过一些其他的admin系统,在实际开发中发现一些问题,项目分层不清晰导致依赖严重,耦合度过高,将API和Service都写在服务层,如果想拓展一个api项目或者workservice项目做其他服务,非常麻烦。 / Q7 v. F" U; a2 [# i) W. [
本人虽然不是架构师,但是通过汲取开发过程中的一些经验,加上站在Furion这个巨人的肩膀上,根据Furion脚手架设计了这么一个项目架构,设计目的就是简单,高效,易扩展,利于二次开发,如果有更好的设计,欢迎提出和探讨。 / a: P8 {: q/ ^8 D! T! T: X
二、项目结构整体项目结构分为三大块分别为架构核心、业务模块和应用服务。如图所示:
, m2 E8 l+ S. F/ r- G 三、分层说明3.1 架构核心SimpleAdmin.Core->核心层核心层,存放实体,公共组件,常量,枚举等其他核心代码,可以被任何项目引用,真正做到了无依赖│ Core.Development.json 。
/ ?3 R/ \: d4 u, Y4 S1 U --> 开发环境配置
$ g$ F' r. i* l, K( S' s │ Core.Production.json --> 生产环境配置
3 ^/ x: u# D! ]/ A0 z2 ~2 } │ Startup.cs --> 启动类' Y6 }7 I5 b4 d" z/ s
├─Aop --> Aop功能
% |6 c; v& J' N! |) ? ├─Attributes
) M- M) ^. F. z* j* b. c/ w" i8 j& G --> 特性" t$ N+ F [3 U6 ]
├─BaseInput --> 共用输入参数(分页,ID传参等)8 {% J: O' {7 n# n
├─Components --> 公共组件
( i! g0 K2 }* A1 i5 V9 u. W ├─Const --> 常量
T2 e* C# i0 j+ h. [7 o0 s ├─Dto --> 数据类
$ S( q" f- A% |1 p, I1 r8 r ├─Entity
# d; H3 @1 p! X. u! a --> 数据库实体
! h* D. V ?. O! t/ a% u ├─Extension --> 拓展
8 t& O3 g- m) j, R y9 t ├─ExtJson --> 数据库ExtJson字段对应实体
0 \3 g! U1 Z' P" L ├─Options --> 配置文件转实体
8 L: n: u; y7 X( J7 f ├─Sqlsugar 8 u; u7 E+ ^! D6 K% k( m
--> ORM配置
" ?* W. D6 Z# E* V7 u; b* { ├─UnifyResult --> 统一返回结果
- @, F" E) _: f/ Z' u+ E( ^ └─Utils --> 工具类(验证码,图片处理,种子数据处理等)3.2 业务模块SimpleAdmin.System->系统应用层 : Z ^, c, H0 r0 h7 P9 Q, E6 i
系统应用层,主要是提供系统应用服务给Api接口层调用,SimpleAdmin的主要功能都由该层实现│ Startup.cs --> 启动类
' s: k0 }& _% W │ System.Development.json --> 开发环境配置 9 H( ^% r: R4 a6 Q5 V* Z
; b8 n \# p9 M# Z" t8 v │ System.Production.json --> 生产环境配置; C6 r8 D. t7 h" O2 O3 H* T
├─EventSubscriber --> 事件总线+ K3 D9 h( U3 n( Q
├─Oss --> 对象存储
; I! x3 g- D8 h3 T7 ? ├─SeedData --> 种子数据 & P2 K# G( T4 J5 L; ]3 r: e
1 ?. l/ Q) y1 D- q) ~ @ ├─Services --> 服务(系统功能接口加实现)$ y# T- m6 L: W# k
├─SignalR --> 即时通讯# l7 m# {/ ?8 Q+ O9 [
└─UserManager --> 用户中心(获取当前请求用户信息)SimpleAdmin.Application->业务应用层 $ S! x0 i _( g9 k7 E
业务应用层,主要是业务代码的编写,可以将自己的业务写在该层,当然也可以自己新建一层写本系统该层主要是用作数据权限示例│ Application.Development.json--> 开发环境配置
" C6 D9 y0 P) w6 m │ 。
9 R' @2 {5 ]8 @% j; X Application.Production.json--> 生产环境配置2 `! ]$ E! _& m$ T# J8 w
│ Startup.cs--> 启动类6 m: K: o/ D. n* k& {; O
└─Service--> 服务(业务功能实现)3.3 应用系统3.3.1 WebSimpleAdmin.Web.Entry->启动层
/ U9 C" I+ j& u/ O$ V Web 入口层,主要作用就是作为程序入口,没有什么实际业务,没啥好讲的,主要是一些全局的设置,详情见appsettings.json│--appsettings.json--> 启动层配置文件* k) c3 n+ q( I4 g$ l/ j
│--ip2region ' C& M3 M# P: O6 s7 C: C; h1 h
.db--> 解析ip用的数据库文件
" S' u( F1 s6 X- ^2 u │--Program.cs--> 启动类SimpleAdmin.Web.Core->WebApi接口层Api接口层,存放web应用所需要用到的代码,如组件,控制器,中间件,过滤器等。
7 G9 b: b- v( J3 o │ Startup.cs --> 启动类
: y/ M4 u" ~; G# ^4 H, L* [) Q# s0 c │ Web.Development.json --> 开发环境配置
$ a$ F( i7 E N, { f │ Web.Production.json --> 生产环境配置
7 F7 ]; J; P4 J8 v1 B" v R+ Q ├─Components
! a2 d( r2 p: {' r; E% } --> 存放Web组件6 i2 v$ U- o9 u b
├─Controllers --> 存放控制器
5 X* f3 K& N& l" r, z x │ ├─Application --> 业务功能控制器4 l7 Z3 G; |5 ]7 [1 A
│ └─System --> 系统功能控制器1 `& T# [0 m5 y5 f
├─Filter 2 t8 T. [2 y" s' ^+ k0 V5 p- o
--> 过滤器: m2 \. @& U7 c/ x. I/ N5 t6 K
├─Handlers --> 处理器* V! ^5 s; e! {$ O/ S. s2 V
└─Logging --> 操作日志功能3.3.2 后台服务SimpleAdmin.Background->后台服务层后台服务层,作为定时任务,MQTT或其他服务载体常驻于后台,不依赖于Web,不会因web服务升级而停止。 1 q* y$ {2 b" |- \
这样做的好处就是不会被iis内存回收,也不会因为web服务升级而停止工作│ Background.Development.json--> 开发环境配置
4 W# x! c0 Y, s; O0 Q │ Background.Production ) B# [+ j, m6 ~: ^6 f/ |
.json--> 生产环境配置3 [; J: n3 p, O1 G
│ MqttWorker.cs--> mqtt后台任务 |' v( m6 m1 b2 Q% B% T* t S% _
│ Program.cs--> 启动类4 K! ^3 x$ Q+ v! I/ V. T* d
├─Dto--> 数据转换类四、关于动态API和Service分离 $ M7 R; f9 _# x/ V: F0 D- ?& I
答案只有一个,那就是解耦Furion的动态API确实非常的方便,可以将Service类直接转换为Api接口类,在小项目开发中确实很方便,但是随着本人在实际工作中的使用,发现这种方式有着非常多的弊端,而且在Furion脚手架中,虽然也用了动态API,但是也是和service是分开的,说明API和Sevice还是不能混在一起,下面我会详细说说分开的好处。 ) M* L9 d% R$ B8 |# ?* k7 @
4.1 解决臃肿问题在动态API中,一个方法代表一个API接口,所有的方法都在一个类中,如果我有多个接口调用的都是一个方法,只是其中一个方法的参数不一样,那么我就要在Service中写多个接口类和一个方法类,这就导致在一个类中,我有N个方法,有点大杂烩的感觉。 u' H. w( n0 v2 b/ @' j, z3 t, s
如果我们将动态api和service分离开来,那么代码结构就非常清晰,不管我在控制器类中写多少个接口,都有可能指向一个方法,也可能指向不通的Service类的不同方法,能很直观的知道某个接口引用的哪个服务的哪个方法。 2 b% n0 Y7 {. U8 i0 `0 ?, s1 b
可能开发前期看不出来,但是随着工程越来越大,功能越来越多,将服务和动态API分离的好处越明显4.2 更好的实现仓储本系统采用的是Sqlsugar单例模式+仓储模式开发,实现仓储非常简单,只需要在服务类继承仓储类即可。 4 U3 d9 U4 |5 q7 c! q
publicclassConfigService : DbRepository, IConfigService如果将该类继承动态API接口IDynamicApiController 3 ]3 F6 z* u5 y0 v- u9 \
,该服务的基类也就是仓储类也会被Swagger解析,导致解析失败,启动报错,虽然可以通过在构造函数中引入仓储,但是还是那句话,动态API做API的事,服务做服务的事如果这个例子不能很好的证明分开的必要,那么下面这个场景会很好的解释为什么要分开。
5 s- C5 c* `6 V# [' p8 Z: T& u 4.2 更好的拓展假设我们现在是API和Service未分离的模式,并且系统已经开发完毕,现有以下业务场景:在当前系统的基础上增加一个OpenAPI开放接口平台,开放系统中的部分接口供第三方调用,第三方通过系统分配的ApiKey和ApiSercet获取token,然后携带token访问接口。 * w( z) O$ |- B1 c' K+ X5 \
根据上述业务场景,设计思路为:新建WebApi项目->引用接口对应服务所在层 ,只需在新的web项目中增加鉴权操作返回token,然后对方直接调用对应的接口就行但是就在我们新建了一个API项目并引用了服务层之后,就会发现一个大问题:所有的接口的引入过来了,因为现在是API和Service未分离的模式,我只要引用了Service,那么所有的接口都会暴露出来,然而需求是只开放部分接口,这可如何是好,通过furion的隐藏api接口功能?那么原来系统的中的接口也将隐藏掉,有点自欺欺人了。 1 ~5 \; H8 ]6 T8 K' J4 O
还有一个问题,接口路由地址也改不了,比如原来我的接口地址是 api/a/b ,现在我的开放平台要换成 api/c/b 。看来想要实现以上需求,只有一个办法->拆。
# e: Y& B* D( d% F; l% u2 G, p 如果采用控制器和服务分离的模式,则不会有这种问题。直接新建API项目。 & ]6 P# @0 j7 {
引用System层。 ; V* C$ F3 I7 y6 P4 ~
引用furion。
8 j; c6 }( I. M4 M$ E( g 启动项目,默认浏览器应该是localhost:5133/weatherforecast,直接删除weatherforecast然后回车,可以看到swagger已经启动,并且只有一个系统内置接口。
% C& F: q9 S& _" p3 I. W& h4 i% z4 f 那么想要实现调用Serivce层方法,也非常简单,只需要创建一个控制器,然后注入想要的服务,调用方法就行////// 测试控制器///
; f H& u4 R: I y- }; m. q [ApiDescriptionSettings(Tag = 。
) K3 f$ T( q% ]" K/ n7 `. n "测试接口")]
~8 y- A' Q& F! [% l/ E [Route("api/[controller]")]9 z( B5 @- A+ H% T' }1 D. w
publicclassTestController% G! E! ^: b1 l) d6 |
{
- u# v% e' G/ w k/ `4 ]) s! c2 X9 A
& s( R% k8 h; J/ L/ J4 A privatereadonly
4 |3 K) M1 N+ G! o$ v( m- H$ T ISysUserService _sysUserService;- X1 m" {) ~8 F& \) s8 T+ ^
" ?7 w5 K5 f1 ]# q- B; T4 h: s publicTestController(ISysUserService sysUserService)
w/ [" N: V$ v; m {
, y( v4 x2 q% Q T7 ^$ t# g . M9 l# u0 a3 V5 ?
this._sysUserService = sysUserService;
' l$ S5 Q! Z, x# g4 S. V }
( V& T; l1 e; l! H/ a( h( V' p1 P( U3 U! t: ^9 j& P
////// 用户分页查询////// ( Q8 @1 E* j) X) K* c7 w# s
///
+ \( P6 l' L/ h0 Q$ I3 }8 R [HttpGet("page")]
% I1 {( R: H: _* N( b8 i# S publicasync Task Page([FromQuery] UserPageInput input
6 r3 R; B* S: S3 P! I )& ~7 p6 X5 m" m1 i- o1 T! ^. J
{
2 C9 o7 ~7 X+ r+ u' S2 f: m' k returnawait _sysUserService.Page(input);# I, ~/ M9 f! w' V5 L
}1 y2 a: z; S2 W5 W
+ C a8 |/ T. U& l5 k
}通过swagger可以看到已经新增了一个接口,并且可以成功调用。  # k0 d' p) K, {& ~' l3 n$ a) y
接下来,想开放哪个接口就直接在控制器里加就行了,是不是非常的方便,虽然相比API和Service在一起的模式多写了一些代码,但是对于项目的可拓展性有了很大的提示,这也是为什么要API和Service分离的原因。 - x7 z4 e2 s& v7 ]) {. d
h+ R0 v2 U# h# x. U7 p- ^* I1 ~: C
q6 g" X/ t5 ?# y0 t3 A8 y2 E- \/ k7 S$ O7 X( _9 ^
|