|
, J9 x' W! o' t* _ 一、说明本章主要介绍的是SimpleAdmin后端架构设计,作为一个系统的基石,一个好的架构设计可以让开发者在开发中少走很多弯路在写SimpleAdmin这个系统之前,也用过一些其他的admin系统,在实际开发中发现一些问题,项目分层不清晰导致依赖严重,耦合度过高,将API和Service都写在服务层,如果想拓展一个api项目或者workservice项目做其他服务,非常麻烦。
- @! ?( U$ o! p 本人虽然不是架构师,但是通过汲取开发过程中的一些经验,加上站在Furion这个巨人的肩膀上,根据Furion脚手架设计了这么一个项目架构,设计目的就是简单,高效,易扩展,利于二次开发,如果有更好的设计,欢迎提出和探讨。
$ O1 ]+ f2 C9 d& j6 ? 二、项目结构整体项目结构分为三大块分别为架构核心、业务模块和应用服务。如图所示:
. Z/ x. S- C. S! s* ?0 X 三、分层说明3.1 架构核心SimpleAdmin.Core->核心层核心层,存放实体,公共组件,常量,枚举等其他核心代码,可以被任何项目引用,真正做到了无依赖│ Core.Development.json 。 % H: ~9 D& a* t- q
--> 开发环境配置
* A% X2 E6 M/ r │ Core.Production.json --> 生产环境配置1 r1 U3 R4 \2 a7 T: [; t
│ Startup.cs --> 启动类
6 I' f+ [- O6 l, H; G* J9 f ├─Aop --> Aop功能
/ f( }' V& y& P' s ├─Attributes
# O' a- b4 U, q --> 特性 y/ l5 d4 [2 `0 i, w
├─BaseInput --> 共用输入参数(分页,ID传参等)8 S p: K' j, C0 [1 ]5 i
├─Components --> 公共组件6 Y$ F( w' o$ v5 P/ w S
├─Const --> 常量2 M1 p) P. M! G" L* C; { y9 w
├─Dto --> 数据类# n6 v7 z3 f4 F$ E$ m
├─Entity 2 e2 a0 b% Y/ Q7 }) @
--> 数据库实体
% l$ @; Y1 _' } ├─Extension --> 拓展# t% y5 o4 x$ I/ k
├─ExtJson --> 数据库ExtJson字段对应实体* M) k {' ^6 H: {
├─Options --> 配置文件转实体
: X$ m/ J- i9 t$ t$ l: s7 e/ S ├─Sqlsugar ( u) d F6 {* s# {% g, V0 v
--> ORM配置' s/ @7 v6 M4 q9 w9 ?5 a0 ^1 W
├─UnifyResult --> 统一返回结果/ w$ o0 c; M% u7 E# P
└─Utils --> 工具类(验证码,图片处理,种子数据处理等)3.2 业务模块SimpleAdmin.System->系统应用层 ; B/ g) w$ f1 Y3 j# T; {$ I
系统应用层,主要是提供系统应用服务给Api接口层调用,SimpleAdmin的主要功能都由该层实现│ Startup.cs --> 启动类
: {7 Z$ p/ ~2 C% n! W │ System.Development.json --> 开发环境配置 ! O E" P7 x) C6 V( x/ ?# u8 c% J; J K
0 |0 X; f1 s& O R p
│ System.Production.json --> 生产环境配置
2 v. K: r" e6 V* Q( W) E3 i, K ├─EventSubscriber --> 事件总线- c0 }) v: V* c* C# s+ h
├─Oss --> 对象存储
, P n6 M( f( U. X1 | ├─SeedData --> 种子数据
7 s6 Y4 `1 \* ~$ R, h; c
$ L2 W$ b4 q% e( T1 R ├─Services --> 服务(系统功能接口加实现)
7 \, S* s# K3 B* D ├─SignalR --> 即时通讯4 ?- J4 {" Z$ A
└─UserManager --> 用户中心(获取当前请求用户信息)SimpleAdmin.Application->业务应用层 6 G/ b* b1 L& J( u
业务应用层,主要是业务代码的编写,可以将自己的业务写在该层,当然也可以自己新建一层写本系统该层主要是用作数据权限示例│ Application.Development.json--> 开发环境配置: _0 n4 F# C- N2 D" D
│ 。 " h" V1 X4 U: f# r a- ^. g
Application.Production.json--> 生产环境配置/ G) c, K& _7 c" x2 T
│ Startup.cs--> 启动类
1 m( `" F% i9 m' n9 {; G └─Service--> 服务(业务功能实现)3.3 应用系统3.3.1 WebSimpleAdmin.Web.Entry->启动层 & n3 `6 q% r: B4 A: x' l# w
Web 入口层,主要作用就是作为程序入口,没有什么实际业务,没啥好讲的,主要是一些全局的设置,详情见appsettings.json│--appsettings.json--> 启动层配置文件
! G2 E1 S6 g" ^/ | c4 ^ │--ip2region + b7 I" v8 O$ X
.db--> 解析ip用的数据库文件 J& y3 Z7 V0 v6 N! w
│--Program.cs--> 启动类SimpleAdmin.Web.Core->WebApi接口层Api接口层,存放web应用所需要用到的代码,如组件,控制器,中间件,过滤器等。 4 m" K6 ?4 j' _# \1 O
│ Startup.cs --> 启动类- T! W/ L4 O) K8 W
│ Web.Development.json --> 开发环境配置! m4 D4 W2 w+ R* B2 ?1 C
│ Web.Production.json --> 生产环境配置
- l% j$ q0 U. o! ~+ n ├─Components . N: T! Y9 m2 I( n% |
--> 存放Web组件/ }9 S& z( y, W. V
├─Controllers --> 存放控制器, `2 T" |2 q/ x# K
│ ├─Application --> 业务功能控制器
4 E- R5 {" ?; O │ └─System --> 系统功能控制器
4 S! }% ^1 |% i# ` ├─Filter
8 e- r- N/ h9 j$ ]9 p0 X2 ?' k --> 过滤器# [4 f; |3 Q, o, s
├─Handlers --> 处理器" Z3 c* K) x6 ~5 A! c" G
└─Logging --> 操作日志功能3.3.2 后台服务SimpleAdmin.Background->后台服务层后台服务层,作为定时任务,MQTT或其他服务载体常驻于后台,不依赖于Web,不会因web服务升级而停止。 : Z2 s( i; v( X6 G( ^! T4 m
这样做的好处就是不会被iis内存回收,也不会因为web服务升级而停止工作│ Background.Development.json--> 开发环境配置+ {' x+ \- ^8 {0 H! J
│ Background.Production 3 B5 R( p& T" Q7 p
.json--> 生产环境配置
! R* A/ p \4 ]! s; K7 k1 c │ MqttWorker.cs--> mqtt后台任务# m" s7 }6 y3 K* P; W
│ Program.cs--> 启动类
$ [& d% ^9 }$ F3 O; ~0 a- R0 f" F ├─Dto--> 数据转换类四、关于动态API和Service分离
& _- Y8 t: u: v% s% c 答案只有一个,那就是解耦Furion的动态API确实非常的方便,可以将Service类直接转换为Api接口类,在小项目开发中确实很方便,但是随着本人在实际工作中的使用,发现这种方式有着非常多的弊端,而且在Furion脚手架中,虽然也用了动态API,但是也是和service是分开的,说明API和Sevice还是不能混在一起,下面我会详细说说分开的好处。 9 C0 F) m7 v5 ?8 l, g. R2 |3 l
4.1 解决臃肿问题在动态API中,一个方法代表一个API接口,所有的方法都在一个类中,如果我有多个接口调用的都是一个方法,只是其中一个方法的参数不一样,那么我就要在Service中写多个接口类和一个方法类,这就导致在一个类中,我有N个方法,有点大杂烩的感觉。
2 @% E2 G6 P' [4 [1 ] 如果我们将动态api和service分离开来,那么代码结构就非常清晰,不管我在控制器类中写多少个接口,都有可能指向一个方法,也可能指向不通的Service类的不同方法,能很直观的知道某个接口引用的哪个服务的哪个方法。
5 d* Z% ?8 a0 f( C 可能开发前期看不出来,但是随着工程越来越大,功能越来越多,将服务和动态API分离的好处越明显4.2 更好的实现仓储本系统采用的是Sqlsugar单例模式+仓储模式开发,实现仓储非常简单,只需要在服务类继承仓储类即可。
6 z# z, U, Q0 U( [1 i: } publicclassConfigService : DbRepository, IConfigService如果将该类继承动态API接口IDynamicApiController
A2 g. x0 ?# u3 R' B; i+ f5 f ,该服务的基类也就是仓储类也会被Swagger解析,导致解析失败,启动报错,虽然可以通过在构造函数中引入仓储,但是还是那句话,动态API做API的事,服务做服务的事如果这个例子不能很好的证明分开的必要,那么下面这个场景会很好的解释为什么要分开。
( f" Y8 d) Y `2 H& p( i3 u 4.2 更好的拓展假设我们现在是API和Service未分离的模式,并且系统已经开发完毕,现有以下业务场景:在当前系统的基础上增加一个OpenAPI开放接口平台,开放系统中的部分接口供第三方调用,第三方通过系统分配的ApiKey和ApiSercet获取token,然后携带token访问接口。
+ ?* a# w1 ]* R0 X7 z! v2 q 根据上述业务场景,设计思路为:新建WebApi项目->引用接口对应服务所在层 ,只需在新的web项目中增加鉴权操作返回token,然后对方直接调用对应的接口就行但是就在我们新建了一个API项目并引用了服务层之后,就会发现一个大问题:所有的接口的引入过来了,因为现在是API和Service未分离的模式,我只要引用了Service,那么所有的接口都会暴露出来,然而需求是只开放部分接口,这可如何是好,通过furion的隐藏api接口功能?那么原来系统的中的接口也将隐藏掉,有点自欺欺人了。 ( p/ _$ N* P0 N) J% s, r9 ?
还有一个问题,接口路由地址也改不了,比如原来我的接口地址是 api/a/b ,现在我的开放平台要换成 api/c/b 。看来想要实现以上需求,只有一个办法->拆。 4 `! B+ j, I+ U9 R# s8 x# e& W
如果采用控制器和服务分离的模式,则不会有这种问题。直接新建API项目。 " G8 W* Z4 }% ^( A
引用System层。
7 a) L. o. i' y" B# Y 引用furion。
- @4 T' N3 S# h6 w/ [! S" Y 启动项目,默认浏览器应该是localhost:5133/weatherforecast,直接删除weatherforecast然后回车,可以看到swagger已经启动,并且只有一个系统内置接口。
/ k4 x- B2 R6 P& r! T; _, R: ^ 那么想要实现调用Serivce层方法,也非常简单,只需要创建一个控制器,然后注入想要的服务,调用方法就行////// 测试控制器///
7 m8 L5 \# w! l& D7 @$ Y0 O8 s7 } [ApiDescriptionSettings(Tag = 。 ( R5 \ e5 y7 j: M- H: U1 G
"测试接口")]3 o; a) [# H' \' f) B
[Route("api/[controller]")]
: i8 s, f# q& { |; n) ^# ?! Z# ? publicclassTestController
! a2 ^- S/ l% i+ M- o# x9 p {
# O' K' U; K; m! C9 }( P* E( ?; B
privatereadonly
. f8 q! p" n; ?' O( b" ` ISysUserService _sysUserService;. z. S p B$ O, P. N
3 Q9 R: j" y) L, `
publicTestController(ISysUserService sysUserService)
& f5 Z/ t) z- t {
$ q9 A4 A% b) R7 j; ?' p
, `/ O3 X; Y( Y( X, [3 M this._sysUserService = sysUserService;0 U. m) x K( n1 w
}) d* e5 A5 D" ^8 Y+ W5 [6 ^' ~5 F @
* J6 }& \' y" H: p7 e, A Y2 { ////// 用户分页查询////// * y: x% Y* \, x' Z! ?& P/ E
///* p V0 A. u1 R& u, C O" x
[HttpGet("page")]4 q0 @; M0 {1 X$ f4 t0 u
publicasync Task Page([FromQuery] UserPageInput input % B& ?' ]% l9 u' j/ ^8 i
)
- ]! n# l7 `) U4 N {
7 B& o% \) L3 X ~7 X/ x" ]% S' ` returnawait _sysUserService.Page(input);
5 R( e. O; Y' H* M i }
! y& j3 |# B ?7 n8 O1 r/ e9 v8 V
# w2 ^/ P- @7 I$ s; v }通过swagger可以看到已经新增了一个接口,并且可以成功调用。 
4 ^% v1 K2 f. j: n 接下来,想开放哪个接口就直接在控制器里加就行了,是不是非常的方便,虽然相比API和Service在一起的模式多写了一些代码,但是对于项目的可拓展性有了很大的提示,这也是为什么要API和Service分离的原因。
9 M# i9 T. Y1 o6 \
4 v$ F& e# {" L# `! l9 A1 ]9 G0 x% \( z, C1 r
1 Z' H1 j! A( C2 h) u# h' x5 B, c
% R1 h& Y0 m" P |