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

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

[复制链接]
我来看看 发表于 2023-03-27 12:04:33 | 显示全部楼层 |阅读模式
1 U e2 L) t9 y9 z" B

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

" e" @3 G) K/ @& n- Q6 w- E

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

- o% H7 \( a& u5 Z% G' K- y: E

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

+ J: t) ?8 {2 J) }9 _

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

6 A$ W9 N% Y# G

--> 开发环境配置. Q, G5 S3 S @; |( H1 K │ Core.Production.json --> 生产环境配置 / E8 M5 ^! K6 ~ │ Startup.cs --> 启动类9 m9 Y- T, Q1 q' M. H ├─Aop --> Aop功能5 i4 @. `0 f; w ├─Attributes

: C& b! V9 o$ E6 z, A

--> 特性1 V( I; ?3 Y0 k. Z f- g ├─BaseInput --> 共用输入参数(分页,ID传参等) 7 j' v" K3 ]0 T4 R; {; t* Z ├─Components --> 公共组件 . _' l) E6 }: i+ j; i3 Q- G+ s2 | ├─Const --> 常量 % v9 F! _! T' o/ S/ ^1 S ├─Dto --> 数据类 . T9 d$ S! f" y# g, t5 Y3 Z: z ├─Entity

: c' v2 c0 A$ R; o

--> 数据库实体 . b7 G) N9 j8 y8 R3 D$ n3 ~ ├─Extension --> 拓展 5 S3 M2 o, P$ t3 u/ n& w ├─ExtJson --> 数据库ExtJson字段对应实体. X0 q1 f) a* N; v3 W9 H& v- c* K" V ├─Options --> 配置文件转实体 * v( Z+ F( }' V7 X* @+ q1 O ├─Sqlsugar

N) g6 }; Z6 s5 R) d

--> ORM配置 / F7 k0 I0 E5 a9 {) R ├─UnifyResult --> 统一返回结果 $ _8 r& }9 T+ w └─Utils --> 工具类(验证码,图片处理,种子数据处理等)3.2 业务模块SimpleAdmin.System->系统应用层

- [ L7 N& b" o" x

系统应用层,主要是提供系统应用服务给Api接口层调用,SimpleAdmin的主要功能都由该层实现│ Startup.cs --> 启动类) H7 N m5 }9 A* S/ T/ _ │ System.Development.json --> 开发环境配置

3 R. e3 W0 e; L2 V

( J [1 o) H b" R { ]# R- D; Y* u │ System.Production.json --> 生产环境配置 j: U; t; }7 L9 O( J6 ]/ K ├─EventSubscriber --> 事件总线7 B! f$ O& P) ?4 y7 O ├─Oss --> 对象存储 u" Q6 \! e) |. A( x ├─SeedData --> 种子数据

. k5 X9 {- @' j& l: N$ O- l$ Z: ^) t

3 s. _4 h2 }5 g: Y" v ├─Services --> 服务(系统功能接口加实现). G/ u+ x9 L. u3 i: X ├─SignalR --> 即时通讯4 ^! v; K. U; m0 H* g" F/ U └─UserManager --> 用户中心(获取当前请求用户信息)SimpleAdmin.Application->业务应用层

* G4 T9 b5 \2 I+ B. v0 j2 d5 R! p

业务应用层,主要是业务代码的编写,可以将自己的业务写在该层,当然也可以自己新建一层写本系统该层主要是用作数据权限示例│ Application.Development.json--> 开发环境配置 ! y: W4 V- p, j# Y! Y# U! n │ 。

8 _& `8 U. v; q; j7 C

Application.Production.json--> 生产环境配置& T1 H$ ^6 c$ c │ Startup.cs--> 启动类8 p* J- c) U+ ~) O2 e └─Service--> 服务(业务功能实现)3.3 应用系统3.3.1 WebSimpleAdmin.Web.Entry->启动层

9 d h- p% e1 x* r8 P

Web 入口层,主要作用就是作为程序入口,没有什么实际业务,没啥好讲的,主要是一些全局的设置,详情见appsettings.json│--appsettings.json--> 启动层配置文件( U- M) c, f+ |; d0 n" M/ I │--ip2region

2 a% `/ P4 n1 ^0 Z8 d

.db--> 解析ip用的数据库文件 ' P8 y* T7 `( w3 |0 a$ s l │--Program.cs--> 启动类SimpleAdmin.Web.Core->WebApi接口层Api接口层,存放web应用所需要用到的代码,如组件,控制器,中间件,过滤器等。

% j2 V2 {( r2 o! b

│ Startup.cs --> 启动类8 I! ]. w2 x6 W2 t- K* D │ Web.Development.json --> 开发环境配置1 B8 j2 d! [0 N │ Web.Production.json --> 生产环境配置 4 m7 I8 S/ k b; k$ |0 l ├─Components

}2 x- M* \2 A' ]2 ~. X

--> 存放Web组件 1 K7 L( `! z$ J5 ~& |. O ├─Controllers --> 存放控制器 2 }* D" G4 E0 g* {) B. ^& a; Y a │ ├─Application --> 业务功能控制器 - ]- F& I$ p! |- K4 j1 s" ]5 d/ f7 j │ └─System --> 系统功能控制器$ b0 v5 H7 Z6 o; f& O ├─Filter

" d' T" q+ }- f( v$ d- `

--> 过滤器/ s1 z4 Y! o# D3 ]1 K$ @ ├─Handlers --> 处理器 8 | z4 U* `2 x3 W3 _, k └─Logging --> 操作日志功能3.3.2 后台服务SimpleAdmin.Background->后台服务层后台服务层,作为定时任务,MQTT或其他服务载体常驻于后台,不依赖于Web,不会因web服务升级而停止。

, c/ n) Y0 ^* t

这样做的好处就是不会被iis内存回收,也不会因为web服务升级而停止工作│ Background.Development.json--> 开发环境配置 " J0 u a7 Z' _( [3 q7 \ │ Background.Production

- h# z& c% n5 L. k! I' K+ V) z% o# Y

.json--> 生产环境配置5 T3 |" B5 A5 j+ ?# F │ MqttWorker.cs--> mqtt后台任务; ]8 y u8 E9 L0 h! d$ V │ Program.cs--> 启动类 : z* a; \3 H- b9 X/ s) m ├─Dto--> 数据转换类四、关于动态API和Service分离

, M* N9 s- b" r3 H$ P/ K- ^

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

7 {$ U9 w1 K) x

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

) p6 R1 [2 K) ]9 w- i1 F0 R2 {

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

: A/ `) C$ i; M

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

% |6 ]# L5 w5 A" K& D1 T6 M' E+ P

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

1 ?. Y5 ~" W! _9 T8 q1 C. c

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

5 W# Y( l" ?5 T3 H6 O

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

- V. H- L" T4 x! u

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

# @7 z9 }5 o4 _, y; z6 j

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

1 Q( ?0 V& M/ \2 ]

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

3 |% I9 q( G- ^: O% m& R

引用System层。

% [4 P2 F4 s7 x

引用furion。

3 A+ @$ Q" ^4 `0 @* K9 W5 U

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

* M" ~/ O$ B/ \% ?

那么想要实现调用Serivce层方法,也非常简单,只需要创建一个控制器,然后注入想要的服务,调用方法就行////// 测试控制器/// 7 |/ P* u3 W) |; A% q [ApiDescriptionSettings(Tag = 。

+ k6 ]3 e6 [) _+ O" e. Q/ z" c

"测试接口")]! Z- M/ o, k8 r, P* k# z! ~% W [Route("api/[controller]")] ) t% h9 Y! v$ Q; E7 C publicclassTestController+ v! f. @5 \$ O9 V { 5 m0 H; ~& F; r4 t3 e% j7 G$ e# P5 Q5 N privatereadonly

: V3 g1 P1 B2 c, x

ISysUserService _sysUserService;" g* u9 k( s4 y0 R- I, Z; o $ g8 t. M, J# k5 F4 x Z publicTestController(ISysUserService sysUserService) 6 }$ E& t: [3 f/ {* j7 Q { " u: R: f: o6 h, b6 V2 @+ Q

6 C/ |4 \; p3 v- l" N" `

this._sysUserService = sysUserService; ( k6 B4 f7 d2 O9 Z" ~: a) P }& m+ o" x: h# }* b' F$ j 2 n2 ^& _7 B6 {0 Y. Q ////// 用户分页查询//////

7 R$ R1 \3 h) p0 \6 Y# E

/// + Z. V. g( ?8 e+ h8 I7 y# s( Z [HttpGet("page")] [9 h: p- a& ^1 u" R publicasync Task Page([FromQuery] UserPageInput input

/ S6 E2 Z2 |5 D+ M% d$ n

)4 ]. [( {! V4 V$ \- U {3 e. H/ ?( K% ?. P8 y* P returnawait _sysUserService.Page(input); 1 y0 |: j R$ H6 Q& G }% A N7 L- g! p* K & ~7 Z, ]6 ?$ O0 e/ } }通过swagger可以看到已经新增了一个接口,并且可以成功调用。

" g6 N# v; J2 t# Y8 o4 z

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

, [5 y* Y3 y& J3 q& m( m; M6 k 8 h* r: d1 y" M4 ]% Y2 D2 G9 w4 D5 ]* R' S) h3 y% E , U" L2 i& L1 o% N, S / f* ~1 P- \5 k
回复

使用道具 举报

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

使用道具 举报

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

    本版积分规则

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

    GMT+8, 2026-4-3 20:33 , Processed in 0.075055 second(s), 22 queries , Gzip On.

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

    Powered by Discuz! X3.5

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