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

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

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

暂时无法加载帖子列表

回复

使用道具 举报

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

使用道具 举报

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

    本版积分规则

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

    GMT+8, 2026-10-1 02:23 , Processed in 0.037144 second(s), 25 queries , Gzip On.

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

    Powered by Discuz! X3.5

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