找回密码
 加入怎通
查看: 175|回复: 1

SimpleAdmin手摸手教学之:项目架构设计_项目框架结构图

[复制链接]
我来看看 发表于 2023-03-27 12:04:33 | 显示全部楼层 |阅读模式
, 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
回复

使用道具 举报

李建唐 发表于 2026-01-12 16:57:57 | 显示全部楼层
完全赞同,我也是这么认为的,英雄所见略同~
回复

使用道具 举报

    您需要登录后才可以回帖 登录 | 加入怎通

    本版积分规则

    QQ|手机版|小黑屋|网站地图|真牛社区 ( 苏ICP备2023040716号-2 )

    GMT+8, 2026-7-7 08:14 , Processed in 0.048601 second(s), 24 queries , Gzip On.

    免责声明:本站信息来自互联网,本站不对其内容真实性负责,如有侵权等情况请联系420897364#qq.com(把#换成@)删除。

    Powered by Discuz! X3.5

    快速回复 返回顶部 返回列表