什么是OSharp
OSharpNS全稱OSharp Framework with .NetStandard2.0,是一個基於.NetStandard2.0
開發的一個.NetCore
快速開發框架。這個框架使用最新穩定版的.NetCore SDK
(當前是.NET Core 2.2),對 AspNetCore 的配置、依賴注入、日志、緩存、實體框架、Mvc(WebApi)、身份認證、權限授權等模塊進行更高一級的自動化封裝,並規范了一套業務實現的代碼結構與操作流程,使 .Net Core 框架更易於應用到實際項目開發中。
- 開源地址:https://github.com/i66soft/osharp
- 官方示例:https://www.osharp.org
- 文檔中心:https://docs.osharp.org
- VS 插件:https://marketplace.visualstudio.com/items?itemName=LiuliuSoft.osharp
- 系列示例:https://github.com/i66soft/osharp-docs-samples
感謝大家關注
首先特別感謝大家對OSharp快速開發框架的關注,這個系列每一篇都收到了比較多園友的關注,也在博客園首頁開啟了 霸屏模式
同時演示網站的用戶注冊數量也在持續上漲
項目 Star 數也增長了幾百,歡迎沒點 Star 的也來關注下 OSharp快速開發框架
再次感謝
概述
前后端分離的系統中,前端和后端只有必要的數據通信交互,前端相當於一個完整的客戶端應用程序,需要包含如下幾個方面:
- 各個模塊的布局組合
- 各個頁面的路由連接
- 業務功能的數據展現和操作流程體現
- 操作界面的菜單/按鈕權限控制
OSharp的Angular前端是基於 NG-ALAIN 框架的,這個框架基於阿里的 NG-ZORRO 封裝了很多方便實用的組件,讓我們很方便的實現自己需要的前端界面布局。
前端業務模塊代碼布局
在Angular應用程序中,存在着模塊module
的組織形式,一個后端的模塊正好可以對應着前端的一個module
。
博客模塊涉及的代碼文件布局如下:
src 源代碼文件夾
└─app APP文件夾
└─routes 路由文件夾
└─blogs 博客模塊文件夾
├─blogs.module.ts 博客模塊文件
├─blogs.routing.ts 博客模塊路由文件
├─blog 博客組件文件夾
│ ├─blog.component.html 博客組件模板文件
│ └─blog.component.ts 博客組件文件
└─post 文章組件文件夾
├─post.component.html 文章組件模板文件
└─post.component.ts 文章組件文件
業務組件
組件Component
是Angular應用程序的最小組織單元,是完成數據展現和業務操作的基本場所。
一個組件通常包含 組件類
和 組件模板
兩個部分,如需要,還可包含 組件樣式
。
STComponentBase
為方便實現各個數據實體的通用管理列表,OSharp定義了一個通用列表組件基類 STComponentBase
,基於這個基類,只需要傳入幾個關鍵的配置信息,即可很方便的實現一個后台管理的實體列表信息。STComponentBase
主要特點如下:
- 使用了 NG-ALAIN 的 STComponent 實現數據表格
- 使用 SFComponent + NzModalComponent 實現數據的
添加/編輯
操作 - 封裝了一個通用的高級查詢組件
AdSearchComponent
,可以很方便實現數據的多條件/條件組無級嵌套數據查詢功能 - 對列表組件進行統一的界面布局,使各列表風格一致
- 提供了對列表數據的
讀取/添加/編輯/刪除
的默認實現 - 極易擴展其他表格功能
STComponentBase 代碼實現如下:
export abstract class STComponentBase {
moduleName: string;
// URL
readUrl: string;
createUrl: string;
updateUrl: string;
deleteUrl: string;
// 表格屬性
columns: STColumn[];
request: PageRequest;
req: STReq;
res: STRes;
page: STPage;
@ViewChild('st') st: STComponent;
// 編輯屬性
schema: SFSchema;
ui: SFUISchema;
editRow: STData;
editTitle = '編輯';
@ViewChild('modal') editModal: NzModalComponent;
osharp: OsharpService;
alain: AlainService;
selecteds: STData[] = [];
public get http(): _HttpClient {
return this.osharp.http;
}
constructor(injector: Injector) {
this.osharp = injector.get(OsharpService);
this.alain = injector.get(AlainService);
}
protected InitBase() {
this.readUrl = `api/admin/${this.moduleName}/read`;
this.createUrl = `api/admin/${this.moduleName}/create`;
this.updateUrl = `api/admin/${this.moduleName}/update`;
this.deleteUrl = `api/admin/${this.moduleName}/delete`;
this.request = new PageRequest();
this.columns = this.GetSTColumns();
this.req = this.GetSTReq(this.request);
this.res = this.GetSTRes();
this.page = this.GetSTPage();
this.schema = this.GetSFSchema();
this.ui = this.GetSFUISchema();
}
// #region 表格
/**
* 重寫以獲取表格的列設置Columns
*/
protected abstract GetSTColumns(): OsharpSTColumn[];
protected GetSTReq(request: PageRequest): STReq {
let req: STReq = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: request,
allInBody: true,
process: opt => this.RequestProcess(opt),
};
return req;
}
protected GetSTRes(): STRes {
let res: STRes = {
reName: { list: 'Rows', total: 'Total' },
process: data => this.ResponseDataProcess(data),
};
return res;
}
protected GetSTPage(): STPage {
let page: STPage = {
showSize: true,
showQuickJumper: true,
toTop: true,
toTopOffset: 0,
};
return page;
}
protected RequestProcess(opt: STRequestOptions): STRequestOptions {
if (opt.body.PageCondition) {
let page: PageCondition = opt.body.PageCondition;
page.PageIndex = opt.body.pi;
page.PageSize = opt.body.ps;
if (opt.body.sort) {
page.SortConditions = [];
let sorts = opt.body.sort.split('-');
for (const item of sorts) {
let sort = new SortCondition();
let num = item.lastIndexOf('.');
let field = item.substr(0, num);
field = this.ReplaceFieldName(field);
sort.SortField = field;
sort.ListSortDirection =
item.substr(num + 1) === 'ascend'
? ListSortDirection.Ascending
: ListSortDirection.Descending;
page.SortConditions.push(sort);
}
} else {
page.SortConditions = [];
}
}
return opt;
}
protected ResponseDataProcess(data: STData[]): STData[] {
return data;
}
protected ReplaceFieldName(field: string): string {
return field;
}
search(request: PageRequest) {
if (!request) {
return;
}
this.req.body = request;
this.st.reload();
}
change(value: STChange) {
if (value.type === 'checkbox') {
this.selecteds = value.checkbox;
} else if (value.type === 'radio') {
this.selecteds = [value.radio];
}
}
error(value: STError) {
console.log(value);
}
// #endregion
// #region 編輯
/**
* 默認由列配置 `STColumn[]` 來生成SFSchema,不需要可以重寫定義自己的SFSchema
*/
protected GetSFSchema(): SFSchema {
let schema: SFSchema = { properties: this.ColumnsToSchemas(this.columns) };
return schema;
}
protected ColumnsToSchemas(
columns: OsharpSTColumn[],
): { [key: string]: SFSchema } {
let properties: { [key: string]: SFSchema } = {};
for (const column of columns) {
if (!column.index || !column.editable || column.buttons) {
continue;
}
let schema: SFSchema = this.alain.ToSFSchema(column);
properties[column.index as string] = schema;
}
return properties;
}
protected GetSFUISchema(): SFUISchema {
let ui: SFUISchema = {};
return ui;
}
protected toEnum(items: { id: number; text: string }[]): SFSchemaEnumType[] {
return items.map(item => {
let e: SFSchemaEnumType = { value: item.id, label: item.text };
return e;
});
}
create() {
if (!this.editModal) return;
this.schema = this.GetSFSchema();
this.ui = this.GetSFUISchema();
this.editRow = {};
this.editTitle = '新增';
this.editModal.open();
}
edit(row: STData) {
if (!row || !this.editModal) {
return;
}
this.schema = this.GetSFSchema();
this.ui = this.GetSFUISchema();
this.editRow = row;
this.editTitle = '編輯';
this.editModal.open();
}
close() {
if (!this.editModal) return;
console.log(this.editModal);
this.editModal.destroy();
}
save(value: STData) {
let url = value.Id ? this.updateUrl : this.createUrl;
this.http.post<AjaxResult>(url, [value]).subscribe(result => {
this.osharp.ajaxResult(result, () => {
this.st.reload();
this.editModal.destroy();
});
});
}
delete(value: STData) {
if (!value) {
return;
}
this.http.post<AjaxResult>(this.deleteUrl, [value.Id]).subscribe(result => {
this.osharp.ajaxResult(result, () => {
this.st.reload();
});
});
}
// #endregion
}
STComponentBase
基類的使用很簡單,只需重寫關鍵的 GetSTColumns
方法傳入實體的列選項,即可完成一個管理列表的數據讀取,查詢,更新,刪除等操作。
博客模塊的組件實現
博客-Blog
- 博客組件
blog.component.ts
import { Component, OnInit, Injector } from '@angular/core';
import { SFUISchema } from '@delon/form';
import { OsharpSTColumn } from '@shared/osharp/services/alain.types';
import { STComponentBase, } from '@shared/osharp/components/st-component-base';
import { STData } from '@delon/abc';
import { AjaxResult } from '@shared/osharp/osharp.model';
@Component({
selector: 'app-blog',
templateUrl: './blog.component.html',
styles: []
})
export class BlogComponent extends STComponentBase implements OnInit {
constructor(injector: Injector) {
super(injector);
this.moduleName = 'blog';
}
ngOnInit() {
super.InitBase();
this.createUrl = `api/admin/${this.moduleName}/apply`;
}
protected GetSTColumns(): OsharpSTColumn[] {
let columns: OsharpSTColumn[] = [
{
title: '操作', fixed: 'left', width: 65, buttons: [{
text: '操作', children: [
{ text: '審核', icon: 'flag', acl: 'Root.Admin.Blogs.Blog.Verify', iif: row => !row.IsEnabled, click: row => this.verify(row) },
{ text: '編輯', icon: 'edit', acl: 'Root.Admin.Blogs.Blog.Update', iif: row => row.Updatable, click: row => this.edit(row) },
]
}]
},
{ title: '編號', index: 'Id', sort: true, readOnly: true, editable: true, filterable: true, ftype: 'number' },
{ title: '博客地址', index: 'Url', sort: true, editable: true, filterable: true, ftype: 'string' },
{ title: '顯示名稱', index: 'Display', sort: true, editable: true, filterable: true, ftype: 'string' },
{ title: '已開通', index: 'IsEnabled', sort: true, filterable: true, type: 'yn' },
{ title: '作者編號', index: 'UserId', type: 'number' },
{ title: '創建時間', index: 'CreatedTime', sort: true, filterable: true, type: 'date' },
];
return columns;
}
protected GetSFUISchema(): SFUISchema {
let ui: SFUISchema = {
'*': { spanLabelFixed: 100, grid: { span: 12 } },
$Url: { grid: { span: 24 } },
$Display: { grid: { span: 24 } },
};
return ui;
}
create() {
if (!this.editModal) {
return;
}
this.schema = this.GetSFSchema();
this.ui = this.GetSFUISchema();
this.editRow = {};
this.editTitle = "申請博客";
this.editModal.open();
}
save(value: STData) {
// 申請博客
if (!value.Id) {
this.http.post<AjaxResult>(this.createUrl, value).subscribe(result => {
this.osharp.ajaxResult(result, () => {
this.st.reload();
this.editModal.destroy();
});
});
return;
}
// 審核博客
if (value.Reason) {
let url = 'api/admin/blog/verify';
this.http.post<AjaxResult>(url, value).subscribe(result => {
this.osharp.ajaxResult(result, () => {
this.st.reload();
this.editModal.destroy();
});
});
return;
}
super.save(value);
}
verify(value: STData) {
if (!value || !this.editModal) return;
this.schema = {
properties: {
Id: { title: '編號', type: 'number', readOnly: true, default: value.Id },
Name: { title: '博客名', type: 'string', readOnly: true, default: value.Display },
IsEnabled: { title: '是否開通', type: 'boolean' },
Reason: { title: '審核理由', type: 'string' }
},
required: ['Reason']
};
this.ui = {
'*': { spanLabelFixed: 100, grid: { span: 12 } },
$Id: { widget: 'text' },
$Name: { widget: 'text', grid: { span: 24 } },
$Reason: { widget: 'textarea', grid: { span: 24 } }
};
this.editRow = value;
this.editTitle = "審核博客";
this.editModal.open();
}
}
- 博客組件模板
blog.component.html
<nz-card>
<div>
<button nz-button (click)="st.reload()"><i nz-icon nzType="reload" nzTheme="outline"></i>刷新</button>
<button nz-button (click)="create()" acl="Root.Admin.Blogs.Blog.Apply" *ngIf="data.length == 0"><i nz-icon type="plus-circle" theme="outline"></i>申請</button>
<osharp-ad-search [request]="request" [columns]="columns" (submited)="search($event)"></osharp-ad-search>
</div>
<st #st [data]="readUrl" [columns]="columns" [req]="req" [res]="res" [(pi)]="request.PageCondition.PageIndex" [(ps)]="request.PageCondition.PageSize" [page]="page" size="small" [scroll]="{x:'800px'}" multiSort
(change)="change($event)" (error)="error($event)"></st>
</nz-card>
<nz-modal #modal [nzVisible]="false" [nzTitle]="editTitle" [nzClosable]="false" [nzFooter]="null">
<sf #sf mode="edit" [schema]="schema" [ui]="ui" [formData]="editRow" button="none">
<div class="modal-footer">
<button nz-button type="button" (click)="close()">關閉</button>
<button nz-button type="submit" [nzType]="'primary'" (click)="save(sf.value)" [disabled]="!sf.valid" [nzLoading]="http.loading" [acl]="'Root.Admin.Blogs.Blog.Update'">保存</button>
</div>
</sf>
</nz-modal>
文章-Post
- 文章組件
post.component.ts
import { Component, OnInit, Injector } from '@angular/core';
import { SFUISchema } from '@delon/form';
import { OsharpSTColumn } from '@shared/osharp/services/alain.types';
import { STComponentBase, } from '@shared/osharp/components/st-component-base';
@Component({
selector: 'app-post',
templateUrl: './post.component.html',
styles: []
})
export class PostComponent extends STComponentBase implements OnInit {
constructor(injector: Injector) {
super(injector);
this.moduleName = 'post';
}
ngOnInit() {
super.InitBase();
}
protected GetSTColumns(): OsharpSTColumn[] {
let columns: OsharpSTColumn[] = [
{
title: '操作', fixed: 'left', width: 65, buttons: [{
text: '操作', children: [
{ text: '編輯', icon: 'edit', acl: 'Root.Admin.Blogs.Post.Update', iif: row => row.Updatable, click: row => this.edit(row) },
{ text: '刪除', icon: 'delete', type: 'del', acl: 'Root.Admin.Blogs.Post.Delete', iif: row => row.Deletable, click: row => this.delete(row) },
]
}]
},
{ title: '編號', index: 'Id', sort: true, readOnly: true, editable: true, filterable: true, ftype: 'number' },
{ title: '文章標題', index: 'Title', sort: true, editable: true, filterable: true, ftype: 'string' },
{ title: '文章內容', index: 'Content', sort: true, editable: true, filterable: true, ftype: 'string' },
{ title: '博客編號', index: 'BlogId', readOnly: true, sort: true, filterable: true, type: 'number' },
{ title: '作者編號', index: 'UserId', readOnly: true, sort: true, filterable: true, type: 'number' },
{ title: '創建時間', index: 'CreatedTime', sort: true, filterable: true, type: 'date' },
];
return columns;
}
protected GetSFUISchema(): SFUISchema {
let ui: SFUISchema = {
'*': { spanLabelFixed: 100, grid: { span: 12 } },
$Title: { grid: { span: 24 } },
$Content: { widget: 'textarea', grid: { span: 24 } }
};
return ui;
}
}
- 文章組件模板
post.component.html
<nz-card>
<div>
<button nz-button (click)="st.reload()"><i nz-icon nzType="reload" nzTheme="outline"></i>刷新</button>
<button nz-button (click)="create()" acl="Root.Admin.Blogs.Post.Create"><i nz-icon type="plus-circle" theme="outline"></i>新增</button>
<osharp-ad-search [request]="request" [columns]="columns" (submited)="search($event)"></osharp-ad-search>
</div>
<st #st [data]="readUrl" [columns]="columns" [req]="req" [res]="res" [(pi)]="request.PageCondition.PageIndex" [(ps)]="request.PageCondition.PageSize" [page]="page" size="small"
[scroll]="{x:'900px'}" multiSort (change)="change($event)" (error)="error($event)"></st>
</nz-card>
<nz-modal #modal [nzVisible]="false" [nzTitle]="editTitle" [nzClosable]="false" [nzFooter]="null">
<sf #sf mode="edit" [schema]="schema" [ui]="ui" [formData]="editRow" button="none">
<div class="modal-footer">
<button nz-button type="button" (click)="close()">關閉</button>
<button nz-button type="submit" [nzType]="'primary'" (click)="save(sf.value)" [disabled]="!sf.valid" [nzLoading]="http.loading" [acl]="'Root.Admin.Blogs.Post.Update'">保存</button>
</div>
</sf>
</nz-modal>
模塊路由 blogs.routing.ts
前端路由負責前端頁面的連接導航,一個模塊中的路由很簡單,只要將組件導航起來即可。
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ACLGuard } from '@delon/acl';
import { BlogComponent } from './blog/blog.component';
import { PostComponent } from './post/post.component';
const routes: Routes = [
{ path: 'blog', component: BlogComponent, canActivate: [ACLGuard], data: { title: '博客管理', reuse: true, guard: 'Root.Admin.Blogs.Blog.Read' } },
{ path: 'post', component: PostComponent, canActivate: [ACLGuard], data: { title: '文章管理', reuse: true, guard: 'Root.Admin.Blogs.Post.Read' } },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class BlogsRoutingModule { }
此外,還需要在根路由配置 routes.routing.ts 上注冊當前模塊的路由,並使用延遲加載特性
{ path: 'blogs', loadChildren: './blogs/blogs.module#BlogsModule', canActivateChild: [ACLGuard], data: { guard: 'Root.Admin.Blogs' } },
模塊入口 blogs.module.ts
模塊入口聲明一個Angular模塊,負責引入其他的公開模塊,並聲明自己的組件/服務
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared';
import { BlogsRoutingModule } from './blogs.routing';
import { BlogComponent } from './blog/blog.component';
import { PostComponent } from './post/post.component';
@NgModule({
imports: [
CommonModule,
SharedModule,
BlogsRoutingModule
],
declarations: [
BlogComponent,
PostComponent,
]
})
export class BlogsModule { }
菜單數據
菜單數據指的是后台管理界面左側導航菜單,在 assets/osharp/app-data.json 文件中進行配置。
{
"app": {
"name": "OSharp Framework",
"description": "一個開源的基於 .NETCORE 的快速開發框架"
},
"menu": [{
"text": "導航菜單",
"i18n": "menu.nav",
"group": true,
"hideInBreadcrumb": true,
"children": [{
"text": "主頁",
"i18n": "menu.nav.home",
"icon": "anticon-dashboard",
"link": "/dashboard",
"acl": "Root.Admin.Dashboard"
}]
}, {
"text": "業務模塊",
"i18n": "menu.nav.business",
"group": true,
"hideInBreadcrumb": true,
"children": [{
"text": "博客模塊",
"group": "true",
"icon": "anticon-border",
"acl": "Root.Admin.Blogs",
"children": [{
"text": "博客管理",
"link": "/blogs/blog",
"acl": "Root.Admin.Blogs.Blog"
}, {
"text": "文章管理",
"link": "/blogs/post",
"acl": "Root.Admin.Blogs.Post"
}]
}]
}, {
"text": "權限模塊",
// ...
}]
}
前端權限控制
OSharp的Angular前端項目的權限控制,是基於 NG-ALAIN 的 ACL 功能來實現的。ACL 全稱叫訪問控制列表(Access Control List),是一種非常簡單的基於角色權限控制方式。
前端權限控制流程
- 代碼實現時,基於ACL功能,給需要權限控制的節點配置需要的功能點字符串。配置原則為:執行當前功能主要需要涉及后端的哪個功能點,就在ACL設置哪個功能點的字符串
- 用戶登錄時,緩存用戶的所有可用功能點集合
- 前端頁面初始化或刷新時(前端路由跳轉是無刷新的,只有主動F5或瀏覽器刷新時,才會刷新),從后端獲取當前用戶的可用功能點集合
- 將功能點集合緩存到 ACLService 中,作為ACL權限判斷的數據源,然后一切權限判斷的事就交給ACL了
- ACL 根據 數據源中是否包含設置的ACL功能點 來決定是否顯示/隱藏菜單項或按鈕,從而達到前端權限控制的目的
NG-ALAIN 的 ACL 模塊的權限控制判斷依賴可為 角色 或 功能點,默認的設置中,角色數據類型是字符串,功能點數據類型是數值。OSharp的功能點是形如 Root.Admin.Blogs.Post
的字符串形式,要應用上 ACL,需要進行如下配置:
src/app/delon.module.ts 文件的 fnDelonACLConfig() 函數中進行配置
export function fnDelonACLConfig(): DelonACLConfig {
return {
guard_url: '/exception/403',
preCan: (roleOrAbility: ACLCanType) => {
function isAbility(val: string) {
return val && val.startsWith('Root.');
}
// 單個字符串,可能是角色也可能是功能點
if (typeof roleOrAbility === 'string') {
return isAbility(roleOrAbility) ? { ability: [roleOrAbility] } : { role: [roleOrAbility] };
}
// 字符串集合,每項可能是角色或是功能點,逐個處理每項
if (Array.isArray(roleOrAbility) && roleOrAbility.length > 0 && typeof roleOrAbility[0] === 'string') {
let abilities: string[] = [], roles: string[] = [];
let type: ACLType = {};
(roleOrAbility as string[]).forEach((val: string) => {
if (isAbility(val)) abilities.push(val);
else roles.push(val);
});
type.role = roles.length > 0 ? roles : null;
type.ability = abilities.length > 0 ? abilities : null;
return type;
}
return roleOrAbility;
}
} as DelonACLConfig;
}
組件權限控制
組件中的權限控制
組件中的權限通常是按鈕權限,例如:
- 列表行操作按鈕:
通過acl
控制功能權限,iif
控制數據權限,共同決定一個按鈕是否可用。
{ text: '編輯', icon: 'edit', {==acl: 'Root.Admin.Blogs.Post.Update'==}, {==iif: row => row.Updatable==}, click: row => this.edit(row) },
組件模板的權限控制
組件模板中各個 html 元素,都可以進行權限控制:
- 按鈕權限:
<button nz-button (click)="create()" {==acl="Root.Admin.Blogs.Post.Create"==}><i nz-icon type="plus-circle" theme="outline"></i>新增</button>
路由權限控制
路由的權限控制,通過 守衛路由 來實現,如果當前用戶沒有權限訪問指定的路由鏈接,將會被攔截,未登錄的用戶將跳轉到登錄頁,已登錄的用戶將跳轉到 403 頁面。
配置路由權限控制很簡單,需要使用守衛路由 [ACLGuard]
,然后在路由的 data
中配置 guard
為需要的功能點字符串:
{ path: 'blog', component: BlogComponent, {==canActivate: [ACLGuard]==}, data: { title: '博客管理', reuse: true, {==guard: 'Root.Admin.Blogs.Blog.Read'==} } },
菜單權限控制
菜單數據上也可以配置ACL權限控制,沒權限的菜單不會顯示
{
"text": "博客模塊",
"group": "true",
"icon": "anticon-border",
"acl": "Root.Admin.Blogs",
"children": [{
"text": "博客管理",
"link": "/blogs/blog",
"acl": "Root.Admin.Blogs.Blog"
}, {
"text": "文章管理",
"link": "/blogs/post",
"acl": "Root.Admin.Blogs.Post"
}]
}
權限控制效果演示
博客信息
根據博客模塊需求分析的設定,博客管理員 和 博主 兩個角色對 博客 的權限分別如下:
-- | 博客管理員 | 博主 |
---|---|---|
查看 | 是 | 是 |
申請 | 否 | 是 |
審核 | 是 | 否 |
修改 | 是 | 是 |
博主-博客
博主只能查看自己的博客數據,能申請博客,不能審核博客,申請成功之后,申請按鈕隱藏。
博客管理員-博客
博客管理員不能申請博客,可以審核新增的博客,博客審核通過之后不能再次審核。
文章信息
根據博客模塊需求分析的設定,博客管理員 和 博主 兩個角色對 文章 的權限分別如下:
-- | 博客管理員 | 博主 |
---|---|---|
查看 | 是 | 是 |
新增 | 否 | 是 |
修改 | 是 | 是 |
刪除 | 是 | 是 |
博主-文章
博主能新增文章,只能查看、更新、刪除自己的文章
博客管理員-文章
博客管理員不能新增文章,能查看、更新、刪除所有文章
步步為營教程總結
本系列教程為OSharp入門初級教程,通過一個 博客模塊 實例來演示了使用OSharp框架進行業務開發所涉及到的項目分層,代碼布局組織,業務代碼實現規范,以及業務實現過程中常用的框架基礎設施。讓開發人員對使用OSharp框架進行項目開發的過程、使用難度等方面有一個初步的認識。
這只是一個簡單的業務演示,限於篇幅,不可能對框架的技術細節進行很詳細的講解,后邊,我們將會分Pack模塊來對每個模塊的設計思路,技術細節進行詳細的解說。