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

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

[复制链接]
我来看看 发表于 2023-03-27 12:04:33 | 显示全部楼层 |阅读模式
# G! a* P/ `9 Z! x; k$ Z

一、说明本章主要介绍的是SimpleAdmin后端架构设计,作为一个系统的基石,一个好的架构设计可以让开发者在开发中少走很多弯路在写SimpleAdmin这个系统之前,也用过一些其他的admin系统,在实际开发中发现一些问题,项目分层不清晰导致依赖严重,耦合度过高,将API和Service都写在服务层,如果想拓展一个api项目或者workservice项目做其他服务,非常麻烦。

3 m# N; J5 B* S% f! u) D+ x+ v5 w

本人虽然不是架构师,但是通过汲取开发过程中的一些经验,加上站在Furion这个巨人的肩膀上,根据Furion脚手架设计了这么一个项目架构,设计目的就是简单,高效,易扩展,利于二次开发,如果有更好的设计,欢迎提出和探讨。

* h; @- J" c% K! U3 w; c3 B: z

二、项目结构整体项目结构分为三大块分别为架构核心、业务模块和应用服务。如图所示:

% x# \3 G! j( |3 H: Z& M* `

三、分层说明3.1 架构核心SimpleAdmin.Core->核心层核心层,存放实体,公共组件,常量,枚举等其他核心代码,可以被任何项目引用,真正做到了无依赖│ Core.Development.json 。

& B% ]* S8 ~ N

--> 开发环境配置 - C6 d5 b5 N t9 M │ Core.Production.json --> 生产环境配置 ' b7 L3 b) ?0 I# A │ Startup.cs --> 启动类% m: A/ L, D) s- r8 X0 \; U ├─Aop --> Aop功能0 A0 G' B. }, F( b, k3 g; y ├─Attributes

" K$ w; f' [/ V% k" y0 f4 w8 z( b

--> 特性 6 t5 U% B5 a- Y: b: d ├─BaseInput --> 共用输入参数(分页,ID传参等) + a+ B; e$ f! `4 r( K% K ├─Components --> 公共组件 J/ [4 p1 t9 T6 m9 S- {7 X1 g ├─Const --> 常量" E3 d6 U4 Z' X ├─Dto --> 数据类# Z7 H: O/ {9 B) x0 Y6 l# l ├─Entity

! e& a7 Q! k2 o* L* h% [. x

--> 数据库实体3 T0 u! Z6 R; u0 {# Y5 g ├─Extension --> 拓展 5 h- D7 L J1 ?/ R ├─ExtJson --> 数据库ExtJson字段对应实体 , ?' [/ }' E6 k, ^ ├─Options --> 配置文件转实体8 V- ?" ]9 k# Q) n p& i ├─Sqlsugar

/ y3 Y1 H1 K. O2 x

--> ORM配置 5 n! F: ~7 D& h9 t. R ^ ├─UnifyResult --> 统一返回结果 D1 l2 l7 ^/ Z( E+ I0 c* A └─Utils --> 工具类(验证码,图片处理,种子数据处理等)3.2 业务模块SimpleAdmin.System->系统应用层

( o4 U! }( r% q. w$ u, P$ y

系统应用层,主要是提供系统应用服务给Api接口层调用,SimpleAdmin的主要功能都由该层实现│ Startup.cs --> 启动类+ p) y0 A/ [/ V7 i# V8 `+ }( M0 { │ System.Development.json --> 开发环境配置

5 M. r3 Y5 ?$ |

3 k0 Z) o( g8 P │ System.Production.json --> 生产环境配置 % t% a5 C! T& A9 v ├─EventSubscriber --> 事件总线( q; w- D1 f* |; f ├─Oss --> 对象存储 8 }3 y1 Q9 N1 I! q( {) T& W9 B# p ├─SeedData --> 种子数据

8 l1 `$ o# M" Y( O

9 w5 S7 `. I9 W& i I7 V ├─Services --> 服务(系统功能接口加实现) ' f& k% H9 p1 h% w+ ]) P3 X ├─SignalR --> 即时通讯 5 }! S8 q/ X' b; m └─UserManager --> 用户中心(获取当前请求用户信息)SimpleAdmin.Application->业务应用层

# B0 M2 P9 ?1 t6 \

业务应用层,主要是业务代码的编写,可以将自己的业务写在该层,当然也可以自己新建一层写本系统该层主要是用作数据权限示例│ Application.Development.json--> 开发环境配置, K W% y9 q1 [6 `% N! v' j │ 。

4 _9 i3 K8 a" N- o6 v4 e

Application.Production.json--> 生产环境配置* O: S1 C+ O/ T2 ? │ Startup.cs--> 启动类" L8 G; A/ u& C! @: ~ └─Service--> 服务(业务功能实现)3.3 应用系统3.3.1 WebSimpleAdmin.Web.Entry->启动层

6 i! w& {% }' [+ I

Web 入口层,主要作用就是作为程序入口,没有什么实际业务,没啥好讲的,主要是一些全局的设置,详情见appsettings.json│--appsettings.json--> 启动层配置文件 ; l% l5 Q1 g6 {2 r: J% x) ` │--ip2region

- M4 v7 @' d2 W

.db--> 解析ip用的数据库文件 # V& C) W$ ?' A& R+ J │--Program.cs--> 启动类SimpleAdmin.Web.Core->WebApi接口层Api接口层,存放web应用所需要用到的代码,如组件,控制器,中间件,过滤器等。

+ n- Z/ P$ o7 _' h& D) F; S" R' [( ~4 x

│ Startup.cs --> 启动类 ! O A1 k" z) d+ @ │ Web.Development.json --> 开发环境配置 - `* g5 i. @) ]) M* q3 ~! p9 V8 ] │ Web.Production.json --> 生产环境配置5 U( K+ {, E" p: `' U/ c- n+ h ├─Components

8 H8 @8 B; [* W

--> 存放Web组件 5 @5 B$ W) x& o ├─Controllers --> 存放控制器 : V6 s9 W; ]! C1 e/ F │ ├─Application --> 业务功能控制器 / E, {0 ~1 L" }7 U- J │ └─System --> 系统功能控制器7 h$ O( |8 k0 \ ├─Filter

+ t0 a0 q! f5 q4 ^8 P$ W# }6 S

--> 过滤器 % t+ ? U$ l( z, ?/ d; A% ?6 } ├─Handlers --> 处理器 5 ?6 V+ J) C- ?# r [ └─Logging --> 操作日志功能3.3.2 后台服务SimpleAdmin.Background->后台服务层后台服务层,作为定时任务,MQTT或其他服务载体常驻于后台,不依赖于Web,不会因web服务升级而停止。

( j6 K: ]/ Z: V3 I9 N4 h2 u& H

这样做的好处就是不会被iis内存回收,也不会因为web服务升级而停止工作│ Background.Development.json--> 开发环境配置 : D* G! \% s1 W% F/ j+ g │ Background.Production

0 \5 e4 d0 e, @9 m. L

.json--> 生产环境配置/ W( K& S1 x, @ │ MqttWorker.cs--> mqtt后台任务 5 k' e/ `+ P/ H2 B& Z- S* a2 b/ w │ Program.cs--> 启动类# T4 v& ?. f1 k( u3 A& ] ├─Dto--> 数据转换类四、关于动态API和Service分离

8 |! X8 N7 |3 `6 C

答案只有一个,那就是解耦Furion的动态API确实非常的方便,可以将Service类直接转换为Api接口类,在小项目开发中确实很方便,但是随着本人在实际工作中的使用,发现这种方式有着非常多的弊端,而且在Furion脚手架中,虽然也用了动态API,但是也是和service是分开的,说明API和Sevice还是不能混在一起,下面我会详细说说分开的好处。

, `- m* B" e* Y7 {' d

4.1 解决臃肿问题在动态API中,一个方法代表一个API接口,所有的方法都在一个类中,如果我有多个接口调用的都是一个方法,只是其中一个方法的参数不一样,那么我就要在Service中写多个接口类和一个方法类,这就导致在一个类中,我有N个方法,有点大杂烩的感觉。

* P; W/ T1 [& @" f5 D

如果我们将动态api和service分离开来,那么代码结构就非常清晰,不管我在控制器类中写多少个接口,都有可能指向一个方法,也可能指向不通的Service类的不同方法,能很直观的知道某个接口引用的哪个服务的哪个方法。

$ B! `+ Y3 l2 c1 |2 ?

可能开发前期看不出来,但是随着工程越来越大,功能越来越多,将服务和动态API分离的好处越明显4.2 更好的实现仓储本系统采用的是Sqlsugar单例模式+仓储模式开发,实现仓储非常简单,只需要在服务类继承仓储类即可。

. ?: g% S' `! o2 S' k0 K; P

publicclassConfigService : DbRepository, IConfigService如果将该类继承动态API接口IDynamicApiController

8 r, I( B2 c: ^1 p$ \# T

,该服务的基类也就是仓储类也会被Swagger解析,导致解析失败,启动报错,虽然可以通过在构造函数中引入仓储,但是还是那句话,动态API做API的事,服务做服务的事如果这个例子不能很好的证明分开的必要,那么下面这个场景会很好的解释为什么要分开。

$ l# \* q5 B& e5 D: v

4.2 更好的拓展假设我们现在是API和Service未分离的模式,并且系统已经开发完毕,现有以下业务场景:在当前系统的基础上增加一个OpenAPI开放接口平台,开放系统中的部分接口供第三方调用,第三方通过系统分配的ApiKey和ApiSercet获取token,然后携带token访问接口。

$ C2 V+ c$ [; l) S% _3 N

根据上述业务场景,设计思路为:新建WebApi项目->引用接口对应服务所在层 ,只需在新的web项目中增加鉴权操作返回token,然后对方直接调用对应的接口就行但是就在我们新建了一个API项目并引用了服务层之后,就会发现一个大问题:所有的接口的引入过来了,因为现在是API和Service未分离的模式,我只要引用了Service,那么所有的接口都会暴露出来,然而需求是只开放部分接口,这可如何是好,通过furion的隐藏api接口功能?那么原来系统的中的接口也将隐藏掉,有点自欺欺人了。

$ w- c% h# s) a9 m' \

还有一个问题,接口路由地址也改不了,比如原来我的接口地址是 api/a/b ,现在我的开放平台要换成 api/c/b 。看来想要实现以上需求,只有一个办法->拆。

& N5 F+ Q. ?0 b4 |1 i

如果采用控制器和服务分离的模式,则不会有这种问题。直接新建API项目。

; e8 q9 ?( |" w% D

引用System层。

( @: R g2 r5 T5 w+ W- I

引用furion。

* u: U2 u2 w# [ a0 W2 l. R9 h, n" r

启动项目,默认浏览器应该是localhost:5133/weatherforecast,直接删除weatherforecast然后回车,可以看到swagger已经启动,并且只有一个系统内置接口。

& D, ]" W! \) O) H4 {' F" T

那么想要实现调用Serivce层方法,也非常简单,只需要创建一个控制器,然后注入想要的服务,调用方法就行////// 测试控制器/// % x7 v d3 \0 y2 H6 v1 j# d" s [ApiDescriptionSettings(Tag = 。

9 t1 j3 T) J; @* x

"测试接口")] # H: }' ]; G/ l& x7 R9 ? [Route("api/[controller]")] 0 ?. d" i* A7 }# a- n9 t7 s publicclassTestController; }' H% Y/ b7 q { v! E2 |/ g- ^1 E/ o9 Z 2 F8 S! d; T, j' c- `; c+ | privatereadonly

- g$ J5 N* W; ]% d+ P. Z

ISysUserService _sysUserService;) d- k/ n) s% U, H% J & j( T- J; |) G, ]' }+ {( n; V publicTestController(ISysUserService sysUserService)& m% f. t( _5 I' Y: f, J {; R5 m* P& G2 k

$ e; a, D& j6 D

this._sysUserService = sysUserService;. e! M6 t( O5 G* Z2 \$ U: O } \8 y, |3 e6 T7 H 3 k' F4 O2 s3 T) O ////// 用户分页查询//////

, `0 I+ K7 h; j2 Z1 ~+ P) O

/// . q& w. U& S/ R$ X' j4 Z: b [HttpGet("page")]( [5 J$ z5 }( ~7 _1 Y publicasync Task Page([FromQuery] UserPageInput input

! u9 k' m; r7 N4 ] I# ?

) ' b' F) Q3 s5 Q- E y1 `1 n* s g {3 l1 C( X( P4 `9 E+ t: e) m returnawait _sysUserService.Page(input);' Y8 Q. l# q( Z1 S/ M1 z }2 U7 S F$ l/ h3 p- k0 l/ `$ y 9 V- @; L+ F, k! X- Q }通过swagger可以看到已经新增了一个接口,并且可以成功调用。

9 C3 E' R, ? @+ ]" R% ?

接下来,想开放哪个接口就直接在控制器里加就行了,是不是非常的方便,虽然相比API和Service在一起的模式多写了一些代码,但是对于项目的可拓展性有了很大的提示,这也是为什么要API和Service分离的原因。

1 Z! v$ ]$ S Q# n + |* V- s$ X- ?- t. h 1 t9 x% x9 k4 U1 A4 x1 I& v 9 _6 H* ?2 ?( v6 k ! X* R$ Z* h& h3 C+ o
回复

使用道具 举报

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

使用道具 举报

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

    本版积分规则

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

    GMT+8, 2026-7-7 09:17 , Processed in 0.034427 second(s), 25 queries , Gzip On.

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

    Powered by Discuz! X3.5

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