分类 其它 下的文章

由于工作需要,最近在弄数据库相关的项目,对于很多地方不甚了解,特别是一些概念性的东西,知其然而不知其所以然,这里列出一些基本知识,做个印记,也和读者共享。

数据仓库

数据仓库(
Data Warehouse
)是一个面向主题的(
Subject Oriented
)、集成的(
Integrate
)、相对稳定的(
Non-Volatile
)、反映历史变化(
Time Variant
)的数据集合,用于支持管理决策。

对于数据仓库的概念我们可以从两个层次予以理解,首先,数据仓库用于支持决策,面向分析型数据处理,它不同于企业现有的操作型数据库;其次,数据仓库是对多个异构的数据源有效集成,集成后按照主题进行了重组,并包含历史数据,而且存放在数据仓库中的数据一般不再修改。

根据数据仓库概念的含义,数据仓库拥有以下四个特点:

1

、面向主题。

数据仓库中的数据是按照一定的主题域进行组织。

主题是一个抽象的概念,是指用户使用数据仓库进行决策时所关心的重点方面,一个主题通常与多个操作型信息系统相关。

而操作型数据库的数据组织面向事务处理任务,各个业务系统之间各自分离。

2
、集成的。

数据仓库中的数据是在对原有分散的数据库数据抽取、清理的基础上经过系统加工、汇总和整理得到的,必须消除源数据中的不一致性,以保证数据仓库内的信息是关于整个企业的一致的全局信息。

面向事务处理的操作型数据库通常与某些特定的应用相关,数据库之间相互独立,并且往往是异构的。

3
、相对稳定的。

数据仓库的数据主要供企业决策分析之用,所涉及的数据操作主要是数据查询,一旦某个数据进入数据仓库以后,一般情况下将被长期保留,也就是数据仓库中一般有大量的查询操作,但修改和删除操作很少,通常只需要定期的加载、刷新。


操作型数据库中的数据通常实时更新,数据根据需要及时发生变化。

4
、反映历史变化。

数据仓库中的数据通常包含历史信息,系统记录了企业从过去某一时点
(
如开始应用数据仓库的时点
)
到目前的各个阶段的信息,通过这些信息,可以对企业的发展历程和未来趋势做出定量分析和预测。


而操作型数据库主要关心当前某一个时间段内的数据。

数据仓库的存储模式

数据仓库存储的两个基本的元素是维度表和事实表。(维是看问题的角度,比如时间,部门,维度表放的就是这些东西的定义,事实表里放着要查询的数据,同时有维的
ID

事实表:是反映业务核心的表,表中存储了与该业务相关的关键数据,我们称其为“度量值”,是今后用来计算及统计的主要字段。除此之外,事实表中还存储着与该业务相关的所有表的关联信息,也就是数据库中外键。能够与其他表建立起联系,从而获得所需要的所有业务信息。而被关联的其他表就是维度表。

维度表:存储与业务相关的非核心信息的表。

事实表与维度表之间通过主外键关联,结构有两种:星型结构和雪花型结构。

星型结构:如图,是指一个事实表连接一个或多个维度表构成的结构,维度表不再关联其他维度表。

雪花型结构:是指一个事实表关联一个或多个维度表,并且维度表还关联了其他的维度表,构成多层级的结构,称为雪花型。

联机分析处理OLAP
:

OLAP(联机分析处理On-Line Analytical Processing)也叫多维DBMS。

OLAP是数据仓库系统的主要应用,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。

OLAP的目标是满足决策支持或者满足在多维环境下特定的查询和报表需求,它的技术核心是"维"这个概念。

“维”是人们观察客观世界的角度,是一种高层次的类型划分。“维”一般包含着层次关系,这种层次关系有时会相当复杂。通过把一个实体的多项重要的属性定义为多个维(dimension),使用户能对不同维上的数据进行比较。因此OLAP也可以说是多维数据分析工具的集合。也叫做多维数据集。一般一个多维数据集可以用一个立方体的方式进行描述。

多维数据集是联机分析处理 (OLAP) 中的主要对象,是一项可对数据仓库中的数据进行快速访问的技术。多维数据集是一个数据集合,通常从数据仓库的子集构造,并组织和汇总成一个由一组维度和度量值定义的多维结构。

每个多维数据集都有一个架构,架构是数据仓库中已联接的各表的集合,多维数据集从数据仓库提取其源数据。架构中的核心表是事实数据表,事实数据表是多维数据集度量值的源。


OLAP
的基本多维分析操作有钻取(
roll up

drill down
)、切片(
slice
)和切块(
dice
)、以及旋转(
pivot
)、
drill
across


drill through
等。

·
钻取是改变维的层次,变换分析的粒度。它包括向上钻取(
roll up
)和向下钻取(
drill down
)。
roll up
是在某一维上将低层次的细节数据概括到高层次的汇总数据,或者减少维数;而
drill
down

则相反,它从汇总数据深入到细节数据进行观察或增加新维。

·
切片和切块是在一部分维上选定值后,关心度量数据在剩余维上的分布。如果剩余的维只有两个,则是切片;如果有三个,则是切块。

·
旋转是变换维的方向,即在表格中重新安排维的放置(例如行列互换)。

ETL介绍:

ETL(Extract-Transform-Load的缩写,即数据抽取、转换、装载的过程)作为BI/DW(Business Intelligence)的核心和灵魂,能够按照统一的规则集成并提高数据的价值,是负责完成数据从数据源向目标数据仓库转化的过程,是实施数据仓库的一重要组成部分 。

企业使用ETL工具后,利用起了已存在的数据资源,避免大量的联机事务处理。ETL工具的典型代表有:OWB、微软DTS, Informatica,Datastage 等

ETL的质量问题具体表现为:正确性、完整性、一致性、完备性、有效性、时效性和可获取性。

不同时期系统业务过程有变化,遗留系统和新业务,遗留系统模块在运营、人事、财务、办公系统等相关信息的不一致,ETL转换的过程主要包含空值处理、规范化数据格式、拆分数据、验证数据正确性及数据替换。.


DTS介绍:

DTS (Data Transformation Services的缩写)能处理数据导入、分析操作过程中与数据转换有关的步骤 .进行校验,清理等.可以自动或交互的从多个异构数据源向数据仓库或数据集市装入数据的技术。

大多数机构都有数据的多种存储格式和多个存储位置。为了支持决策制定、改善系统性能或更新现有系统,数据经常必须从一个数据存储位置移动到另一个存储位置,都可由DTS来做。DTS还允许用户定期导入或变换数据,以实现数据转换的自动化。

可以将 DTS 解决方案创建为一个或多个包。每个包都可能包含一组用来定义要执行工作的经过组织的任务、对数据和对象的转换、用来定义任务执行的工作流约束以及与数据源和目标的连接。DTS 包还提供了一些服务,例如记录包执行详细信息、控制事务和处理全局变量。DTS提供一组工具,可以从不同的源将数据抽取、转换和合并到一个或多个目标位置。借助于DTS工具,您可以创建适合于您的组织特定需要的自定义移动解决方案。

数据仓库与ETL

如下图所示,ETL服务于数据仓库,将数据迁移至仓库中。

ETL功能包括:数据抽取、数据传输、数据转换、数据装载、配置维护。如果说数据仓库的模型设计是一座大厦的设计蓝图,数据是砖

瓦的话,那么ETL就是建设大厦的过程。


E:  Extract ,连接异构数据源平台,提取数据


T:  Transform,过程一般都是批量操作,也是ETL的核心


L:  Load ,由普通关系库导进数据仓库

ETL正式运行特点:


一是数据同步,按照固定周期运行


二是数据量一般都是巨大的,所以会拆分成E,T,L几个过程

ETL正式运行要求:


增量与自动定时运行 ,相应的加载策略、更新策略(周期)、汇总策略(替换变化的数据记录,或新增汇总记录)、维护策略。

ETL与DTS关系

为数据仓库提供导入,清洗,装载的系统解决方案的工具叫ETL,DTS是微软提供的实现ETL工具的一个解决方案,通过DTS组件的提供的功能,可定制自己的ETL运行方案。

导入:配置好数据源连接由数据转换任务从数据源提取数据(DTS的数据源Connections 11个,任务(task)19个,工作流(workflow)3个)

清洗:定制好转换规则,在字段级上处理数据

装载:清洗后处理的数据结果,批量存入数据库中

在一般的检索界面中,基于界面易用和美观方便的考虑,我们往往只提供一些常用的条件查询进行列表数据的查询,但是有时候一些业务表字段很多,一些不常见的条件可能在某些场景下也需要用到。因此我们在通用的查询条件之外,一般可以考虑增加 一个高级查询的模块来管理这些不常见条件的查询处理。本篇随笔基于这个需求,综合ABP框架的特点,整合了高级查询模块功能的处理。

1、高级查询模块的回顾

我们知道,在界面布局中,一般常见的查询条件不能太多,否则会显得臃肿而且占用太多空间,非常不美观,因此常见的查询都是提供寥寥几个的输出条件进行列表记录的查询的。

又或者一些更多内容的界面,我们也是仅仅提供多几个条件,其他的想办法通过高级查询界面进行查询管理。

在早期博客里面《
Winform开发框架之通用高级查询模块
》,我曾经介绍过一款通用的高级查询界面处理,用在Winform框架里面,可以对数据表更多的字段进行统一的查询处理。

对于内容较多的查询,我们可以在主界面增加一个高级查询按钮入口,如上图所示,单击后,显示一个所有字段的列表,如下界面。

一般来说,查询条件分为文本输入,如姓名,邮件,名称等这些。

日期类型条件输入界面:

数字类型条件输入界面:

输入以上几种条件后,高级查询界面里面会显示友好的条件内容,确保用户能够看懂输入的条件,如下所示是输入几个不同类型的条件的显示内容。

以上是高级查询模块的思路,整体界面和处理逻辑虽然可以采用,但是在ABP框架模式下,以前的处理方式有所不同了,下面详细介绍一下如何在ABP框架模块下整合这个高级查询模块的内容。

2、ABP框架模块下的高级查询处理

我们先来了解一下最终在ABP框架下整合的高级查询模块界面如下所示。

可以设置一些模糊查询条件,以及一些区间的查询值,如下所示。

这个模块是以ABP框架的Web API获取数据,并通过Winform界面进行调用,从而形成了一个ABP+Winform的框架体系。

前面ABP框架系列介绍过,我们一般使用GetAll和分页条件DTO进行数据的检索,如下是产品分页DTO的定义

    /// <summary>
    ///用于根据条件分页查询,DTO对象/// </summary>
    public class ProductPagedDto : PagedAndSortedInputDto

而PagedAndSortedInputDto也是自定义的类,它主要用来承载一些分页和排序的信息,如下所示

    /// <summary>
    ///带有排序对象的分页基类/// </summary>
    public classPagedAndSortedInputDto : PagedInputDto, ISortedResultRequest
{
/// <summary> ///排序信息/// </summary> public string Sorting { get; set; }

其中的PagedInputDto也是自定义类,主要承载分页信息。

    /// <summary>
    ///分页对象/// </summary>
    public classPagedInputDto : IPagedResultRequest
{
[Range(
1, int.MaxValue)]public int MaxResultCount { get; set; }

[Range(
0, int.MaxValue)]public int SkipCount { get; set; }publicPagedInputDto()
{
MaxResultCount
= int.MaxValue;
}
}

这样的构建,我们可以传递分页和排序信息,因此在GetAll函数里面,就可以根据这些条件进行数据查询了。

而我们通过重写过滤条件和排序处理,就可以实现数据的分页查询了。对于产品信息的过滤处理和排序处理,我们重写函数如下所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override IQueryable<Product>CreateFilteredQuery(ProductPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.ProductNo.IsNullOrWhiteSpace(), t => t.ProductNo.Contains(input.ProductNo)) //如需要精确匹配则用Equals .WhereIf(!input.BarCode.IsNullOrWhiteSpace(), t => t.BarCode.Contains(input.BarCode)) //如需要精确匹配则用Equals .WhereIf(!input.MaterialCode.IsNullOrWhiteSpace(), t => t.MaterialCode.Contains(input.MaterialCode)) //如需要精确匹配则用Equals .WhereIf(!input.ProductType.IsNullOrWhiteSpace(), t => t.ProductType.Contains(input.ProductType)) //如需要精确匹配则用Equals .WhereIf(!input.ProductName.IsNullOrWhiteSpace(), t => t.ProductName.Contains(input.ProductName)) //如需要精确匹配则用Equals .WhereIf(!input.Unit.IsNullOrWhiteSpace(), t => t.Unit.Contains(input.Unit)) //如需要精确匹配则用Equals .WhereIf(!input.Note.IsNullOrWhiteSpace(), t => t.Note.Contains(input.Note)) //如需要精确匹配则用Equals .WhereIf(!input.Description.IsNullOrWhiteSpace(), t => t.Description.Contains(input.Description)) //如需要精确匹配则用Equals//状态 .WhereIf(input.Status.HasValue, t => t.Status==input.Status)//成本价区间查询 .WhereIf(input.PriceStart.HasValue, s => s.Price >=input.PriceStart.Value)
.WhereIf(input.PriceEnd.HasValue, s
=> s.Price <=input.PriceEnd.Value)//销售价区间查询 .WhereIf(input.SalePriceStart.HasValue, s => s.SalePrice >=input.SalePriceStart.Value)
.WhereIf(input.SalePriceEnd.HasValue, s
=> s.SalePrice <=input.SalePriceEnd.Value)//特价区间查询 .WhereIf(input.SpecialPriceStart.HasValue, s => s.SpecialPrice >=input.SpecialPriceStart.Value)
.WhereIf(input.SpecialPriceEnd.HasValue, s
=> s.SpecialPrice <=input.SpecialPriceEnd.Value)
.WhereIf(input.IsUseSpecial.HasValue, t
=> t.IsUseSpecial == input.IsUseSpecial) //如需要精确匹配则用Equals//最低折扣区间查询 .WhereIf(input.LowestDiscountStart.HasValue, s => s.LowestDiscount >=input.LowestDiscountStart.Value)
.WhereIf(input.LowestDiscountEnd.HasValue, s
=> s.LowestDiscount <=input.LowestDiscountEnd.Value)//创建日期区间查询 .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value);
}
/// <summary> ///自定义排序处理/// </summary> /// <param name="query">可查询LINQ</param> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override IQueryable<Product> ApplySorting(IQueryable<Product>query, ProductPagedDto input)
{
//按创建时间倒序排序 return base.ApplySorting(query, input).OrderByDescending(s => s.CreationTime);//时间降序 }

虽然我们一般在界面上不会放置所有的条件,但是高级查询模块倒是可以把分页条件DTO里面的条件全部摆上去的。

高级查询模块的条件如下所示。

我们高级查询里面的条件还是以GetAll里面的对象分页查询Dto里面的属性,我们需要根据这些条件进行构建,也需要以这些属性的类型进行一个控件的选择。

因此我们需要一个属性的名称说明,以及在高级查询模块的列表界面中对显示那些字段进行控制,如下代码所示。

        privateFrmAdvanceSearch dlg;/// <summary>
        ///高级查询的操作/// </summary>        
        private async voidAdvanceSearch()
{
if (dlg == null)
{
dlg
= newFrmAdvanceSearch();
dlg.SetFieldTypeList
<ProductPagedDto>();//通过分页对象获取查询属性和类型 dlg.ColumnNameAlias = awaitProductApiCaller.Instance.GetColumnNameAlias();
dlg.DisplayColumns
= "ProductNo,BarCode,MaterialCode,ProductType,ProductName,Unit,Price,SalePrice,SpecialPrice,IsUseSpecial,LowestDiscount,Note,Description,Status,CreatorUserId,CreationTime";

通过 SetFieldTypeList<ProductPagedDto> 的处理,我们把分页对象的查询属性和类型赋值给了高级查询模块,让它根据类型来创建不同的输入显示,如常规的字符串、数值区段、日期区段,下拉列表等等。

对于下拉列表,我们需要绑定它的数据源,如下代码所示。

 dlg.AddColumnListItem("ProductType", await DictItemUtil.GetDictListItemByDictType("产品类型"));//字典列表
 dlg.AddColumnListItem("Status", await DictItemUtil.GetDictListItemByDictType("产品状态"));//字典列表

而对于一些常规的固定列表,也可以以类似的方式加入下拉列表

    //固定转义的列表
    var specialList = new List<CListItem>() { new CListItem("特价", "True"), new CListItem("一般", "False") };
dlg.AddColumnListItem(
"IsUseSpecial", specialList);

或者

    dlg.AddColumnListItem("Sex", "男,女");//固定列表

因此整个调用高级查询模块的代码如下所示

    privateFrmAdvanceSearch dlg;/// <summary>
    ///高级查询的操作/// </summary>        
    private async voidAdvanceSearch()
{
if (dlg == null)
{
dlg
= newFrmAdvanceSearch();
dlg.SetFieldTypeList
<ProductPagedDto>();//通过分页对象获取查询属性和类型 dlg.ColumnNameAlias = awaitProductApiCaller.Instance.GetColumnNameAlias();
dlg.DisplayColumns
= "ProductNo,BarCode,MaterialCode,ProductType,ProductName,Unit,Price,SalePrice,SpecialPrice,IsUseSpecial,LowestDiscount,Note,Description,Status,CreatorUserId,CreationTime";#region 下拉列表数据dlg.AddColumnListItem("ProductType", await DictItemUtil.GetDictListItemByDictType("产品类型"));//字典列表 dlg.AddColumnListItem("Status", await DictItemUtil.GetDictListItemByDictType("产品状态"));//字典列表//固定转义的列表 var specialList = new List<CListItem>() { new CListItem("特价", "True"), new CListItem("一般", "False") };
dlg.AddColumnListItem(
"IsUseSpecial", specialList);//dlg.AddColumnListItem("Sex", "男,女");//固定列表//dlg.AddColumnListItem("Credit", await ProductApiCaller.Instance.GetFieldList("Credit"));//动态列表 #endregiondlg.ConditionChanged+= newFrmAdvanceSearch.ConditionChangedEventHandler(dlg_ConditionChanged);
}
dlg.ShowDialog();
}

在处理获取数据GetData函数的时候,我们需要根据高级查询进行一定的切换,以便显示正确的过滤条件,如下代码所示是获取数据的处理。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<ProductDto>>GetData()
{
ProductPagedDto pagerDto
= null;if (advanceCondition != null)
{
pagerDto
= new ProductPagedDto(this.winGridViewPager1.PagerInfo);
pagerDto
=dlg.GetPagedResult(pagerDto);
}
else{//构建分页的条件和查询条件 pagerDto = new ProductPagedDto(this.winGridViewPager1.PagerInfo)
{
//添加所需条件 ProductNo = this.txtProductNo.Text.Trim(),
BarCode
= this.txtBarCode.Text.Trim(),
MaterialCode
= this.txtMaterialCode.Text.Trim(),
ProductType
= this.txtProductType.Text.Trim(),
ProductName
= this.txtProductName.Text.Trim(),
Description
= this.txtDescription.Text.Trim(),
};
//日期和数值范围定义//创建时间,需在ProductPagedDto中添加DateTime?类型字段CreationTimeStart和CreationTimeEnd var CreationTime = new TimeRange(this.txtCreationTime1.Text, this.txtCreationTime2.Text); //日期类型 pagerDto.CreationTimeStart =CreationTime.Start;
pagerDto.CreationTimeEnd
=CreationTime.End;

}
var result = awaitProductApiCaller.Instance.GetAll(pagerDto);returnresult;
}

在高级查询的处理方式下,我们是传入一个列表的分页对象属性,然后传入一个分页DTO对象,就可以构建出我们需要的分页查询条件,传递给Web API端获取对应条件的数据了。

    pagerDto = new ProductPagedDto(this.winGridViewPager1.PagerInfo);
pagerDto
= dlg.GetPagedResult(pagerDto);

而高级查询模块,所需要处理的逻辑就是需要根据不同的属性类型,赋值常规的属性值或者区段属性值,从而构建出分页对应的属性条件即可。

如果是区段(包括日期或者数值)的,我们分页查询条件里面,会有一个ABCStart,ABCEnd的对象属性,依照这个规则,获取到对应的用户输入,采用反射方式赋值DTO对象即可。

我们在很多情况下,可能都是某种组织的会员,如健身、游泳馆、超市、美容店等其他连锁店,这些针对会员的管理和消费管理,从而提供给会员更多的优惠,一般通过积分的方式实现。本文主要从一个开发者的角度,对会员系统进行的设计开发进行剖析,希望能与大家一起探讨,实现更多的思想碰撞。

如果系统是在一个店铺使用的,那么使用单机版本的操作模式即可,如可以使用Winform + SQLite/Access方式,实现数据的访问,并且方便软件复制和备份工作,如果需要性能好一点或者数据更加安全一点,可以采用独立的数据库方式,如采用一个独立的机器部署SqlServer数据库或者Mysql数据库,Oracle数据库就没太大必要了。

如果系统是在一系列连锁店中使用的,那么可以采用Winform+WCF服务方式,实现数据的分布式访问方式,这样数据就不会保存在本地,和B/S通过浏览器的方式很类似,但是Winform客户端能提供更丰富的界面体验效果。当然,我们每一家的连锁店就需要能够上网,随时进行数据的交换处理。

还有一种方式,是离线式的服务,就是弥补第二种方式在断开网络的时候不能工作的缺点,这种方式即使在网络断开,也能照常运营,网络通畅的时候,通过手工进行数据的提交就可以了。由于现在网络一般比较方便,所以这种方式一般采用的不多,只在特殊情况下采用。

1、系统用例的设计

我们知道,会员管理的主要目的就是以会员为中心,实现相关数据的管理。会员管理包括有会员本身的信息管理、会员收费管理、积分管理(积分增减、积分兑换、积分转账)、挂失管理、换卡管理、余额转账、商品管理、消费管理等等,围绕着会员管理展开,通过多个职能操作,实现相关数据的录入和管理。

2、系统数据库设计

数据库的设计,也主要是围绕着会员信息进行的,会员信息是作为所有会员相关记录的外键引用。为了避免数据库表的阅读困难,会员管理的相关表,使用“MS_”前缀声明。

除了以下的表外,还包括了会员的打折设置信息,积分奖励设置,以及用于会员消费的商品信息,及会员消费的记录信息(包括消费主表和明细表记录)。

为了篇幅的介绍,我主要列出会员的主表信息作为讨论参考。

表主要使用字符型的ID作为表的主键,保存的时候,ID自动使用GUID作为数据存储,由于考虑了可能多个连锁店的情况,因此,我们需要增加一个Creator, CreateTime, Editor,EditTime, Dept_ID, Company_ID的通用字段,方便存储用户的相关表记录信息,这样我们在数据过滤以及报表查询的时候,会方便很多。

3、系统模块化设计

当然会员的信息还可以扩展更多,我们一般是以一个通用的会员管理来实现这个模块,从而可以在整个大系统中进行整合和使用。而一般我们都有自己的平台模块积累,在业务层只需要整合现有的一些底层模块作为支持,业务系统我们独立开发即可,大概的构造如下所示。

当然,我们随着系统的开发,我们可能需要整合两个以上的系统(或者底层业务模块)到一个大系统里面,这种要求就需要我们所有的
系统
模块,都可以通过松耦合、插件化整合的方式实现使用的。

本文主要介绍一个会员系统开发的整体思路和设计,随着开发的深入,可能会继续分享一些相关的开发心得。

在上篇《
淘宝API开发系列--开篇概述
》介绍了下淘宝API平台的一些基本知识,由于一直有事情忙,就没有及时跟进随笔的更新,本篇继续讨论淘宝API的开发知识,主要介绍商家的绑定操作。上篇我们说过,
淘宝
就是基于应用程序键来控制用户的访问频率和流量的,另外可以通过应用程序键,让使用者登陆确认,获取到相关的授权码,然后获取SessionKey,作为访问使用者淘宝资源(如买入卖出等私人记录的信息)

我们再看看SessionKey是如何获取的(下面是淘宝关于正式环境下SessionKey的说明):

正式环境下获取SessionKey


注意:web插件平台应用和web其它应用在正式环境下是同样的获取方法

1、WEB应用

例如回调URL为:http://localhost

访问 http://container.open.taobao.com/container?appkey={appkey},页面会跳转到回调URL,地址类似如下:

http://localhost/?top_appkey={appkey} &top_parameters=xxx&top_session=xxx&top_sign=xxx

回调url上的top_session参数即为SessionKey
2、客户端应用

访问 http://auth.open.taobao.com/?appkey={appkey},即可获得授权码

通过http方式访问 http://container.open.taobao.com/container?authcode={授权码},会得到类似如下的字符串

top_appkey=1142&top_parameters=xxx&top_session=xxx&top_sign=xxx

字符串里面的top_session值即为SessionKey。

由于本篇文章主要是介绍C/S客户的应用,因此客户端的应用就不能通过回调Url方式获得用户的验证,我们可以通过在Winform中的WebBrowser控件,显示一个登陆验证及访问确认的操作界面给客户,当客户确认的时候并返回Session Key的内容界面的时候,我们取出Session Key保存并关闭浏览器窗口即可,今后把该SessionKey作为参数来访问相关需要Session Key的API即可。

另外,由于SessionKey的间隔时间比较短,如果API调用间隔时间比较长,那么SessionKey有可能失效的,但是我们注意到,如果API调用的时候,SesionKey过期 那么会抛出TopException(其中ErrorCode为26或者27是SessionKey过期),里面有关于与TopException的部分说明如下:

26 Missing Session 缺少SessionKey参数
27 Invalid Session 无效的SessionKey参数

我们先看看具体实现的界面,然后分析其中的实现逻辑吧。

1、首次需要登录的时候,使用一个Winform嵌套一个WebBrowser控件,实现网页登录。

2、商家用户输入账号密码后,确认是否授权程序访问相关资源。

3、确认后生成SessionKey,这个Key正是我们的程序需要的关键内容,因此需要自动获取出来。

4、程序拿到该Session Key后,把它作为参数来访问淘宝API获取相关的信息,这里获取交易API的购买信息,需要SessionKey的。

以上就是使用SessionKey的API工作流程界面,我们下面介绍一下相关的实现代码。

1) 主窗体主要的操作代码:



代码


public

partial

class
Form1 : Form
{

private
TopJsonRestClient jsonClient;

private
TopContext context;


private

void
Form1_Load(
object
sender, EventArgs e)
{

this
.winGridView1.ProgressBar
=

this
.toolStripProgressBar1.ProgressBar;

this
.winGridView1.AppendedMenu
=

this
.contextMenuStrip1;

jsonClient

=

new
TopJsonRestClient(
"
http://gw.api.taobao.com/router/rest
"
,
"
12033411
"
,
"
你的密钥
"
);

client

=
GetProductTopClient(
"
json
"
);
xmlClient

=

new
TopXmlRestClient(
"
http://gw.api.taobao.com/router/rest
"
,
"
12033411
"
,
"
你的密钥
""
);


}


///

<summary>


///
判断是否顺利获取SessionKey

///

</summary>


///

<returns></returns>



private

bool
GetAuthorizeCode()
{

string
authorizeCode
=

""
;
FrmAuthorized dlg

=

new
FrmAuthorized();

if
(dlg.ShowDialog()
==
DialogResult.OK)
{
authorizeCode

=
dlg.AuthrizeCode;
}

if
(
string
.IsNullOrEmpty(authorizeCode))
return

false
;

context

=
SysUtils.GetTopContext(authorizeCode);

if
(context
==

null
)
return

false
;


return

true
;
}


private

void
BindData()
{

if
(context
==

null
)
{

bool
flag
=
GetAuthorizeCode();

if
(
!
flag)
return
;
}


string
sessionKey
=
context.SessionKey;


///
/获取用户信息



//
UserGetRequest request = new UserGetRequest();

//
request.Fields = "user_id,nick,sex,created,location,alipay_account,birthday";

//
request.Nick = "wuhuacong";

//
User user = client.Execute(request, new UserJsonParser());

//
MessageBox.Show(ReflectionUtil.GetProperties(user));




try

{

//
买入交易


TradesBoughtGetRequest req
=

new
TradesBoughtGetRequest();
req.Fields

=

"
tid,title,price,type,iid,seller_nick,buyer_nick,status,orders
"
;
req.PageNo

=

1
;
req.PageSize

=

10
;
ResponseList

<
Trade
>
rsp
=
jsonClient.GetBoughtTrades(req, sessionKey);

this
.winGridView1.DataSource
=
rsp.Content;
MessageBox.Show(rsp.Content.Count.ToString());


//
卖出交易


TradesSoldGetRequest soldReq
=

new
TradesSoldGetRequest();
soldReq.Fields

=

"
tid,title,price,type,iid,seller_nick,buyer_nick,status,orders
"
;
soldReq.PageNo

=

1
;
soldReq.PageSize

=

10
;
ResponseList

<
Trade
>
soldRsp
=
jsonClient.GetSoldTrades(soldReq, sessionKey);

this
.winGridView1.DataSource
=
soldRsp.Content;
MessageBox.Show(soldRsp.Content.Count.ToString());
}

catch
(TopException ex)
{

if
(ex.ErrorCode
==

26

||
ex.ErrorCode
==

27
)
{

if
(MessageUtil.ShowYesNoAndError(
"
SessionKey过期,您是否需要重新认证
"
)
==
DialogResult.Yes)
{

bool
flag
=
GetAuthorizeCode();

if
(
!
flag)
return
;

BindData();

//
重新刷新


}

else

{

return
;
}
}
}
}


private

void
btnTest_Click(
object
sender, EventArgs e)
{
BindData();
}

2、用户登陆的窗体,就是一个form窗体加上一个WebBrowser控件,窗体代码如下:



代码


public

partial

class
FrmAuthorized : Form
{

///

<summary>


///
授权码

///

</summary>



public

string
AuthrizeCode
=

""
;

private

string
url
=

"
http://open.taobao.com/authorize/?appkey=12033411
"
;


public
FrmAuthorized()
{
InitializeComponent();
}


///

<summary>


///
获取HTML页面内制定Key的Value内容

///

</summary>


///

<param name="html"></param>


///

<param name="key"></param>


///

<returns></returns>



public

string
GetHiddenKeyValue(
string
html,
string
key)
{

string
str
=
html.Substring(html.IndexOf(key));
str

=
str.Substring(str.IndexOf(
"
value
"
)
+

7
);

int
eindex1
=
str.IndexOf(
"
'
"
);

int
eindex2
=
str.IndexOf(
"
\
""
);



int
eindex
=
eindex2;

if
(eindex1
>=

0

&&
eindex1
<
eindex2)
{
eindex

=
eindex1;
}

return
str.Substring(
0
, eindex);
}


private

void
webBrowser1_DocumentCompleted(
object
sender, WebBrowserDocumentCompletedEventArgs e)
{

if
(e.Url.AbsoluteUri
==
url)
{
AuthrizeCode

=
GetHiddenKeyValue(
this
.webBrowser1.DocumentText,
"
autoInput
"
);

if
(
!
string
.IsNullOrEmpty(AuthrizeCode)
&&
AuthrizeCode.IndexOf(
"
TOP-
"
)
>=

0
)
{

this
.DialogResult
=
DialogResult.OK;

this
.Close();
}
}
}


private

void
FrmAuthorized_Load(
object
sender, EventArgs e)
{
webBrowser1.Navigate(url);
}
}

这样我们就可以在首次使用API或者SessionKey失效的时候,让商家用户输入账号密码并确认即可,其他使用即可顺利无阻。

是不是有点意思呢,赶快试试吧,说不定带来一些意想不到的收获及创意哦。

一般来说,一个系统或多或少都会涉及到一些系统参数或者用户信息的配置,而ABP框架也提供了一套配置信息的管理模块,ABP框架的配置信息,必须提前定义好配置的各项内容,然后才能在系统中初始化或者通过接口查询来使用,本篇随笔引入了另外一种配置信息的定义,实现更加简化的处理,本篇随笔着重介绍两者之间的差异和不同的地方。

1、ABP框架的配置管理

如下面是邮件配置信息,配置信息一般先继承自SettingProvider,初始化定义后,才能被系统所使用。

EmailSettingProvider
:继承自SettingProvider, 将
SMTP的各项设置封装成
SettingDefinition
,并以数组形式返回

配置的管理类,主要通过接口ISettingManager来进行统一管理的,底层协同了SettingStore配置存储和SetttingDefinitionMananger的配置定义管理两个部分。

这种方式的配置信息,糅合了配置项的定义(强制性),以及多语言特性的处理,根据不同的语言返回不同的配置名称,同时也整合了缓存信息的处理,以减少系统的一些消耗。

不过从上面的图示我们也可以看到,整个配置模块由于引入这些内容,导致处理起来必须按部就班的创建配置管理类,定义配置信息,重新编译系统后,然后才能进行信息的调用,因此这些配置信息必须预定义。而且管理起来协同这些类的处理,也略显得有点复杂化。

在ABP核心模块的启动过程中,会预先初始化这些配置管理类,如下代码所示

然后在AddSettingProviders中加入预先定义好的配置类。

接着在完成初始化过程中,有配置定义类统一根据这些配置对象,进行定义的初始化,这样才能在系统中进行使用。

配置定义的管理类接口,可以用下面这个图示进行说明。

以上就是在ABP框架中,基于配置模块的管理过程。

一般情况下,如果我们需要在Web API端中对这些接口进行调用管理,如对用户或者系统Email配置信息的获取和修改,那么我们需要定义一个配置接口服务(默认下载的ABP框架中没有公布这个接口定义和实现)。

如下我们定义一个SettingsAppService和他的接口

然后我们可以实现它的获取信息和修改信息的接口,如下所示是对系统级别的邮件参数进行配置管理。

        /// <summary>
        ///获取应用程序级别的邮件配置(系统邮件配置)/// </summary>
        /// <returns></returns>
        public async Task<EmailSettingsEditDto>GetEmailSettingsForApplication()
{
var smtpPassword = awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.Password);return newEmailSettingsEditDto
{
DefaultFromAddress
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.DefaultFromAddress),
DefaultFromDisplayName
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.DefaultFromDisplayName),
SmtpHost
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.Host),
SmtpPort
= await SettingManager.GetSettingValueForApplicationAsync<int>(EmailSettingNames.Smtp.Port),
SmtpUserName
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.UserName),
SmtpPassword
=SimpleStringCipher.Instance.Decrypt(smtpPassword),
SmtpDomain
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.Domain),
SmtpEnableSsl
= await SettingManager.GetSettingValueForApplicationAsync<bool>(EmailSettingNames.Smtp.EnableSsl),
SmtpUseDefaultCredentials
= await SettingManager.GetSettingValueForApplicationAsync<bool>(EmailSettingNames.Smtp.UseDefaultCredentials)
};
}
/// <summary> ///更新应用程序级别的邮件配置(系统邮件配置)/// </summary> /// <returns></returns> public asyncTask UpdateEmailSettingsForApplication(EmailSettingsEditDto input)
{
awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.DefaultFromAddress, input.DefaultFromAddress);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.DefaultFromDisplayName, input.DefaultFromDisplayName);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Host, input.SmtpHost);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Port, input.SmtpPort.ToString(CultureInfo.InvariantCulture));awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.UserName, input.SmtpUserName);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Password, SimpleStringCipher.Instance.Encrypt(input.SmtpPassword));awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Domain, input.SmtpDomain);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.EnableSsl, input.SmtpEnableSsl.ToString().ToLowerInvariant());awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.UseDefaultCredentials, input.SmtpUseDefaultCredentials.ToString().ToLowerInvariant());
}

2、使用自定义的参数配置管理

我在较早的随笔《
Winform开发框架之参数配置管理功能实现-基于SettingsProvider.net的构建
》中介绍过对配置信息的管理实现,这种配置参数方式一直很好的应用在我的各个框架上,定义和使用都相对比较简单,能够满足绝大多数的应用场景,相对ABP框架的配置模块来说,简单易用。

首先我们定义一个用来存储通用配置信息的表,如下所示。

这个配置表的主要特点也是以键为操作对象,然后内容是JSON序列化后的内容,可以存储用户自定义的类的序列号字符串,这个是它的灵魂所在。和ABP框架仅仅存储简单类型的值有所不同。

和其他模块的定义一样,我们可以先根据常规表的方式,使用代码快速生成类的结构,如下所示。

    /// <summary>
    ///用户参数配置,应用层服务接口实现/// </summary>
[AbpAuthorize]public class UserParameterAppService : MyAsyncServiceBase<UserParameter, UserParameterDto, string, UserParameterPagedDto, CreateUserParameterDto, UserParameterDto>, IUserParameterAppService
{
private readonly IRepository<UserParameter, string>_repository;public UserParameterAppService(IRepository<UserParameter, string> repository) : base(repository)
{
_repository
=repository;
}

然后定义几个用于用户级别和系统程序级别的接口实现,如获取信息,修改信息等。

然后,在生成的Caller层类里面,增加以上的Web API接口调用的实现代码,如下所示

    /// <summary>
    ///用户参数配置的Web API调用处理/// </summary>
    public class UserParameterApiCaller : AsyncCrudApiCaller<UserParameterDto, string, UserParameterPagedDto, CreateUserParameterDto, UserParameterDto>, IUserParameterAppService
{
/// <summary> ///提供单件对象使用/// </summary> public staticUserParameterApiCaller Instance
{
get{return Singleton<UserParameterApiCaller>.Instance;
}
}
/// <summary> ///默认构造函数/// </summary> publicUserParameterApiCaller()
{
this.DomainName = "UserParameter";//指定域对象名称,用于组装接口地址 }public async Task<UserParameterDto>GetSettingForUser(NameInputDto input)
{
return await DoActionAsync<UserParameterDto>(MethodBase.GetCurrentMethod(), input);
}
public asyncTask ChangeSettingForUser(NameValueDto input)
{
awaitDoActionAsync(MethodBase.GetCurrentMethod(), input);
}
public async Task<UserParameterDto>GetSettingForApplication(NameInputDto input)
{
return await DoActionAsync<UserParameterDto>(MethodBase.GetCurrentMethod(), input);
}
public asyncTask ChangeSettingForApplication(NameValueDto input)
{
awaitDoActionAsync(MethodBase.GetCurrentMethod(), input);
}
}

如果对于上面的DoActionAsyn的处理有疑问,可以参考之前随笔《
ABP开发框架前后端开发系列---(10)Web API调用类的简化处理
》进行了解。

我在之前介绍过的配置模块里面,结合过FireFoxDialog界面效果,实现较好的参数配置管理功能,如下界面所示。

我们本次使用这两个不同的配置模块,也希望使用这个来展现一下,以便更好的理解。

由于整合了SettingsProvider.net组件,我们只需要封装一下对数据库的存储获取方式就可以了。

    /// <summary>
    ///数据库参数存储设置/// </summary>
    public classDatabaseStorage : JsonSettingsStoreBase
{
/// <summary> ///配置级别/// </summary> public SettingScopes Scope { get; set; }/// <summary> ///构造函数/// </summary> publicDatabaseStorage()
{
this.Scope =SettingScopes.User;
}
/// <summary> ///参数构造函数/// </summary> /// <param name="scope">配置级别</param> publicDatabaseStorage(SettingScopes scope)
{
this.Scope =scope;
}
/// <summary> ///保存到数据库/// </summary> /// <param name="filename">文件名称(类型名称)</param> /// <param name="fileContents">参数内容</param> protected override void WriteTextFile(string filename, stringfileContents)
{
var info = newNameValueDto(filename, fileContents);if (this.Scope ==SettingScopes.Application)
{
AsyncContext.Run(()
=>UserParameterApiCaller.Instance.ChangeSettingForApplication(info));
}
else{
AsyncContext.Run(()
=>UserParameterApiCaller.Instance.ChangeSettingForUser(info));
}
}
/// <summary> ///从数据库读取/// </summary> /// <param name="filename">文件名称(类型名称)</param> /// <returns></returns> protected override string ReadTextFile(stringfilename)
{
var info = newNameInputDto(filename);

UserParameterDto result
= null;if (this.Scope ==SettingScopes.Application)
{
result
= AsyncContext.Run(() =>UserParameterApiCaller.Instance.GetSettingForApplication(info));
}
else{
result
= AsyncContext.Run(() =>UserParameterApiCaller.Instance.GetSettingForUser(info));
}
return result != null ? result.Content : null;
}
}

有了这个实现,这样在操作上,就不用管理这些内容如何获取和更新了,和之前的使用配置管理方式一致了。可以处理各种不同的配置对象信息。

先来看看默认ABP的配置处理方式,管理界面如下所示。

这里的配置存储咋ABP的AbpSettings表里面,如下所示,每项内容是以字符串方式独立存储的。

它的调用主要就是SettingsApiCaller的内容了,注意这个邮件配置,必须在EmailSettingProvider中提前定义好对象的信息。

        privateEmailSettingsEditDto GetParameter()
{
EmailSettingsEditDto param
= AsyncContext.Run(() =>SettingsApiCaller.Instance.GetEmailSettingsForApplication());if(param == null)
{
param
= newEmailSettingsEditDto();
}
returnparam;
}
public override voidOnInit()
{
var parameter =GetParameter();if (parameter != null)
{
this.txtEmail.Text =parameter.DefaultFromAddress;this.txtLoginId.Text =parameter.SmtpUserName;this.txtPassword.Text =parameter.SmtpPassword;this.txtPassword.Tag =parameter.SmtpPassword;this.txtSmtpPort.Value =parameter.SmtpPort;this.txtSmtpServer.Text =parameter.SmtpHost;this.txtUseSSL.Checked =parameter.SmtpEnableSsl;
}
}

下面我们再来看看自定义的配置管理方式。如下是自定义配置模块获取显示的内容。

这个配置是系统级别的,它的获取方式如下所示。

    public partial classPageEmailApplication : PropertyPage
{
privateSettingsProvider settings;privateISettingsStorage store;publicPageEmailApplication()
{
InitializeComponent();
if (!this.DesignMode)
{
store
= newDatabaseStorage(SettingScopes.Application);
settings
= newSettingsProvider(store);
}
}
public override voidOnInit()
{
EmailParameter parameter
= settings.GetSettings<EmailParameter>();if (parameter != null)
{
this.txtEmail.Text =parameter.Email;this.txtLoginId.Text =parameter.LoginId;this.txtPassword.Text =parameter.Password;this.txtPassword.Tag =parameter.Password;this.txtPop3Port.Value =parameter.Pop3Port;this.txtPop3Server.Text =parameter.Pop3Server;this.txtSmtpPort.Value =parameter.SmtpPort;this.txtSmtpServer.Text =parameter.SmtpServer;this.txtUseSSL.Checked =parameter.UseSSL;
}
}

以上是标准的SettingsProvider.net的组件调用方式,我们不用知道具体的数据存储,只需要把内容直接GetSetting方式获取出来即可。

而保存内容,直接通过使用SaveSettings保存即可。

                EmailParameter parameter = settings.GetSettings<EmailParameter>();if (parameter != null)
{
parameter.Email
= this.txtEmail.Text;
parameter.LoginId
= this.txtLoginId.Text;
parameter.Password
= this.txtPassword.Text;
parameter.Pop3Port
= Convert.ToInt32(this.txtPop3Port.Value);
parameter.Pop3Server
= this.txtPop3Server.Text;
parameter.SmtpPort
= Convert.ToInt32(this.txtSmtpPort.Value);
parameter.SmtpServer
= this.txtSmtpServer.Text;
parameter.UseSSL
= this.txtUseSSL.Checked;

settings.SaveSettings
<EmailParameter>(parameter);
}

其中 EmailParameter 类是我们定义的一个类,用来承载相关的配置信息,如下所示。它支持默认值,加密处理等设置。

    /// <summary>
    ///邮箱设置/// </summary>
    public classEmailParameter
{
/// <summary> ///邮件账号/// </summary> //[DefaultValue("wuhuacong@163.com")] public string Email { get; set; }/// <summary> ///POP3服务器/// </summary> [DefaultValue("pop.163.com")]public string Pop3Server { get; set; }/// <summary> ///POP3端口/// </summary> [DefaultValue(110)]public int Pop3Port { get; set; }/// <summary> ///SMTP服务器/// </summary> [DefaultValue("smtp.163.com")]public string SmtpServer { get; set; }/// <summary> ///SMTP端口/// </summary> [DefaultValue(25)]public int SmtpPort { get; set; }/// <summary> ///登陆账号/// </summary> public string LoginId { get; set; }/// <summary> ///登陆密码/// </summary> [ProtectedString]public string Password { get; set; }/// <summary> ///使用SSL加密/// </summary> [DefaultValue(false)]public bool UseSSL { get; set; }
}

由于SettingsProvider.net组件的支持,我们还可以把配置信息当成本地文件存储起来,对于一些需要存为文件的方式的配置,非常不错。

    public partial classPageReport : PropertyPage
{
privateSettingsProvider settings;privateISettingsStorage store;publicPageReport()
{
InitializeComponent();
if (!this.DesignMode)
{
//PortableStorage: 在运行程序目录创建一个setting的文件记录参数数据 store = newPortableStorage();
settings
= newSettingsProvider(store);
}
}

以上就是介绍了ABP配置管理模块的实现原理和客户端的调用,以及使用自定义配置管理模块的方式进行处理更加动态化或者灵活一点的配置信息,使用自定义配置信息管理服务,整合了SettingProvider.net的支持,可以实现更好的参数配置管理体验。