什麼是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
感謝大家關注
首先特别感謝大家對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/ng-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>
<app-ad-search [request]="request" [columns]="columns" (submited)="search($event)"></app-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/ng-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>
<app-ad-search [request]="request" [columns]="columns" (submited)="search($event)"></app-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子產品來對每個子產品的設計思路,技術細節進行詳細的解說。