2019年2月20日 By mikel 分类: 架构设计
http://baa.im/847971

1 主流深度学习框架对比 当今的软件开发基本都是分层化和模块化的,应用层开发会基于框架层。比如开发Linux Driver会基于Linux kernel,开发Android app会基于Android

来源: Tensorflow源码解析1 — 内核架构和源码结构 – 阿里云云栖社区 – 博客园

1 主流深度学习框架对比

当今的软件开发基本都是分层化和模块化的,应用层开发会基于框架层。比如开发Linux Driver会基于Linux kernel,开发Android app会基于Android Framework。深度学习也不例外,框架层为上层模型开发提供了强大的多语言接口、稳定的运行时、高效的算子,以及完备的通信层和设备层管理层。因此,各大公司早早的就开始了深度学习框架的研发,以便能占领市场。当前的框架有数十种之多,主流的如下(截止到2018年11月)

显然TensorFlow是独一无二的王者。第二名Keras,它是对TensorFlow或Theano接口的二次封装,严格意义上并不是一个独立的深度学习框架。TensorFlow目前也已经集成了Keras,使得安装了TensorFlow的用户就可以直接使用Keras了。

TensorFlow之所以能够从数十种框架中脱颖而出,主要优点有

  1. 出身高贵,是谷歌出品的。但其他很多框架出身也不差,例如PyTorch之于Facebook,MXNET之于Amazon
  2. 2015年就开源了,比较早的俘获了一大批开发者。这个确实是tf的一大先发优势,但PyTorch的前身Caffe,以及MXNET开源时间都不晚,而且Caffe流行时间比tf早,后来才被赶超的。更有Theano这样的绝对老前辈。由此可见,软件开源是多么重要。目前流行的深度学习框架也基本都开源了。
  3. 支持的开发语言多,支持Python Java Go C++等多种流行语言。相比某些框架,确实是优势很大。相比MXNET则小巫见大巫了。MXNET早期发展的一个主要方向就是前端多语言的支持,连MATLAB R Julia等语言都支持了。
  4. 运行效率高。早期的时候,其实tf的运行效率比很多框架都要低一些的。
  5. 安装容易,用户上手快,文档齐全,社区活跃。这个是tf的一个较大优势,特别是社区方面,也就是我们常说的生态优势。互联网头部集中效应十分明显,体现在开源软件上也是一样。这也是我认为最大的一个优势。

总结起来,TensorFlow虽然每个方面都不是绝对领先的优势,但贵在每个方面都做的不错,因此最终能够一骑绝尘,独领风骚。

学习Tensorflow框架内核,可以理解前端接口语言的支持,session生命周期,graph的构建、分裂和执行,operation的注册和运行,模块间数据通信,本地运行和分布式运行模式,以及CPU GPU TPU等异构设备的封装支持等。学习这些,对于模型的压缩 加速 优化等都是大有裨益的。

2 TensorFlow系统架构

TensorFlow设计十分精巧,基于分层和模块化的设计思想进行开发的。框架如下图

整个框架以C API为界,分为前端和后端两大部分。

  1. 前端:提供编程模型,多语言的接口支持,比如Python Java C++等。通过C API建立前后端的连接,后面详细讲解。
  2. 后端:提供运行环境,完成计算图的执行。进一步分为4层
    1. 运行时:分为分布式运行时和本地运行时,负责计算图的接收,构造,编排等。
    2. 计算层:提供各op算子的内核实现,例如conv2d, relu等
    3. 通信层:实现组件间数据通信,基于GRPC和RDMA两种通信方式
    4. 设备层:提供多种异构设备的支持,如CPU GPU TPU FPGA等

模型构造和执行流程

TensorFlow的一大特点是,图的构造和执行相分离。用户添加完算子,构建好整图后,才开始进行训练和执行,也就是图的执行。大体流程如下

  1. 图构建:用户在client中基于TensorFlow的多语言编程接口,添加算子,完成计算图的构造。
  2. 图传递:client开启session,通过它建立和master之间的连接。执行session.run()时,将构造好的graph序列化为graphDef后,以protobuf的格式传递给master。
  3. 图剪枝:master根据session.run()传递的fetches和feeds列表,反向遍历全图full graph,实施剪枝,得到最小依赖子图
  4. 图分裂:master将最小子图分裂为多个Graph Partition,并注册到多个worker上。一个worker对应一个Graph Partition。
  5. 图二次分裂:worker根据当前可用硬件资源,如CPU GPU,将Graph Partition按照op算子设备约束规范(例如tf.device(‘/cpu:0’),二次分裂到不同设备上。每个计算设备对应一个Graph Partition。
  6. 图运行:对于每一个计算设备,worker依照op在kernel中的实现,完成op的运算。设备间数据通信可以使用send/recv节点,而worker间通信,则使用GRPC或RDMA协议。

3 前端多语言实现 – swig包装器

TensorFlow提供了很多种语言的前端接口,使得用户可以通过多种语言来完成模型的训练和推断。其中Python支持得最好。这也是TensorFlow之所以受欢迎的一大原因。前端多语言是怎么实现的呢?这要归功于swig包装器。

swig是个帮助使用C或者C++编写的软件能与其它各种高级编程语言进行嵌入联接的开发工具。在TensorFlow使用bazel编译时,swig会生成两个wrapper文件

  1. pywrap_tensorflow_internal.py:对接上层Python调用
  2. pywrap_tensorflow_internal.cc:对接底层C API调用。

pywrap_tensorflow_internal.py 模块被导入时,会加载_pywrap_tensorflow_internal.so动态链接库,它里面包含了所有运行时接口的符号。而pywrap_tensorflow_internal.cc中,则注册了一个函数符号表,实现Python接口和C接口的映射。运行时,就可以通过映射表,找到Python接口在C层的实现了。

4 tensorflow 源码结构

TensorFlow源码基本也是按照框架分层来组织文件的。如下

其中core为tf的核心,它的源码结构如下

5 总结

TensorFlow框架设计精巧,代码量也很大,我们可以从以下部分逐步学习

    1. TensorFlow内核架构和源码结构。先从全局上对框架进行理解。
    2. 前后端连接的桥梁–Session,重点理解session的生命周期,并通过相关源码可以加深理解Python前端如何调用底层C实现。
    3. TensorFlow核心对象—Graph。图graph是TensorFlow最核心的对象,基本都是围绕着它来进行的。graph的节点为算子operation,边为数据tensor。
    4. TensorFlow图的节点 — Operation。operation是图graph的节点,承载了计算算子。
    5. TensorFlow图的边 — Tensor。Tensor是图graph的边,承载了计算的数据。
    6. TensorFlow本地运行时。
    7. TensorFlow分布式运行时。和本地运行时有一些共用的接口,但区别也很大。
    8. TensorFlow设备层。主要了解设备层的定义规范,以及实现。
    9. TensorFlow队列和并行运算。
    10. TensorFlow断点检查checkpoint,模型保存Saver,以及可视化tensorboard。这三个为TensorFlow主要的工具。

原文链接
本文为云栖社区原创内容,未经允许不得转载。

Tensorflow源码解析1 — 内核架构和源码结构 – 阿里云云栖社区 – 博客园已关闭评论
2019年2月20日 By mikel 分类: 架构设计
http://baa.im/847971

现代管理教育对供应链的定义为“供应链是围绕核心企业,通过对商流,信息流,物流,资金流的控制,从采购原材料开始,制成中间产品以及最终产品,最后由销售网络把产品送到消费者手中的将供应商,制造商,分销商,零

来源: 供应链金融&区块链应用 – 莱布尼茨 – 博客园

现代管理教育对供应链的定义为“供应链是围绕核心企业,通过对商流,信息流,物流,资金流的控制,从采购原材料开始,制成中间产品以及最终产品,最后由销售网络把产品送到消费者手中的将供应商,制造商,分销商,零售商,直到最终用户连成一个整体的功能网链结构。那什么是核心企业呢?

当供应链中某一企业在整个供应链中占据主导地位,对其他成员具有很强的辐射能力和吸引能力,通常称该企业为核心企业或主导企业。核心企业是供应链的物流中心、信息中心和资金周转中心。在供应链竞争中,核心企业承担供应链组织者和管理者的职能。 核心企业的关键在技术,而在技术更高一层的是管理,例如精益管理的鼻祖丰田汽车,它就是以自身的生产为核心,提供定单,围绕这个企业,形成了一二三个批次的供应链条,成百上千个中小企业为之服务。


供应链金融

在传统供应链金融业务模式下,业务主体包括供应商(上游企业)、经销商(下游企业)、核心企业、银行、仓储机构以及物流公司等,业务模式主要包括三种,分别为应收类(应收账款融资模式)、预付类(保兑仓融资模式)以及存货类(融通仓融资模式)(可参看 传统供应链金融业务模式:应收账款融资、保兑仓融融资、融通仓融资)。其中应收账款融资与保理业务关系密切。

保理

全称保付代理,又称托收保付,卖方将其现在或将来的基于其与买方订立的货物销售/服务合同所产生的应收账款转让给保理商(提供保理服务的金融机构),由保理商向其提供资金融通、买方资信评估、销售账户管理、信用风险担保、账款催收等一系列服务的综合金融服务方式。

国际上对保理的定义五花八门,我们只要掌握精髓就好。

由来:保理业务是从出口代理交易方式演变而来的,起源于14世纪英国毛纺工业。当时英国毛纺织品是在寄售基础上委托专业代理商代销的。这些代理商向国外买主出售商品,同时向出口商担保买主的商业信用。当时由于交通不便,外贸业务往来活动比较缓慢。如果在国外没有可靠的代理人进行协助,任何出口企业很难取得成功。到18世纪,美国的一些代理商逐步以其高度效率和雄厚资金掌握了为扩大其国内市场所需要的代贷管理工作。他们的地位也逐步由以前被委托的代理人身分演变为独立的经济实体——保理商。保理商根据保理合同专门为有关商业企业提供信贷和信用管理服务。经过不断地发展,现代保理商已能提供一揽子服务,包括向卖方提供买方资信调查,100%货款商业风险担保,应收账款管理和资金融通等。

盈利模式:保理业务的盈利模式一般为向客户收取利息及保理费,不仅能带来丰厚的利润,更为重要的是可为保理公司创造可观的中间业务收入。一般来说,销售方保理商除了可以获得发票金额一定比例的佣金外,还可以通过向销售商提供融资服务获得融资利息;而作为购买方保理商,由于承担买方信用,收取的佣金比例更高。

坏账担保(full protection against bad debts)保理商为自己核准的应收账款提供100%的担保,有效消除因买方信用给出口方造成的坏账风险的担保服务。

扩展阅读:五分钟教你看懂供应链金融(文尾抛出的问题可看读者评论)

融资租赁

融资租赁和供应链金融是不同概念,前者是一种融资形式,它可以融入到供应链金融的保兑仓融资融通仓融资这两种模式中,当然它是独立于具体金融应用领域的。

融资租赁分为直租和售后回租两种模式。直租类似于分期付款,可以降低客户购买设备的门槛,但在企业角度看,它规避了分期付款的缺点,可以使企业实现一次性回款,减少了企业的应收账款。售后回租也类似于分期付款,只不过商品原本是承租人自己的,转卖给融资租赁公司,融资租赁公司再租售给承租人而已,绕这么一大弯,承租人手里的可用资金却多了。

保兑仓&融通仓

要了解两个概念仓单质押银行承兑汇票

仓单质押

仓单:是指仓储公司签发给存储人或货物所有权人的记载仓储货物所有权的唯一合法的物权凭证,仓单持有人随时可以凭仓单直接向仓储方提取仓储货物。仓单质押指银行与借款人(出质人)、保管人(仓储公司)签订合作协议,以保管人签发的借款人自有或第三方持有的存货仓单作为质押物向借款人办理贷款的信贷业务。

扩展阅读:贷款按照有无担保一般分为信用贷款和担保贷款,其中担保贷款又分为保证贷款、抵押贷款和质押贷款。一般来说,银行认可的抵押物主要是不动产如房屋、厂房、机器设备等,质押物主要是银行存单、国债等有价证券。尽管生产企业或商业企业的存货也具有一定的价值,理论上也可以进行抵押,但是由于银行难以对存货进行有效的监管,同时缺乏对存货市场价值的评估,一般银行不愿意接受存货抵押借款的方式。这样,对于那些缺乏合适抵押品的企业,尽管其拥有大量的存货,却难以从银行获得贷款支持。大多数中小企业都存在这种尴尬的局面。仓单融资实质是一种存货抵押融资方式,通过银行、仓储公司和企业的三方协议,引入专仓储公司在融资过程中发挥监督保管抵押物、对抵押物进行价值评估、担保等作用,实现以企业存货仓单为抵押的融资方式。仓单融资适用于流通性较高的大宗货物,特别是具有一定国际市场规模的初级产品,如有金属及原料、黑色金属及原料、煤炭、焦碳、橡胶、纸浆以及大豆、玉米等农产品。任何特制的商品、专业机械设备、纺织服装,家电等产品,一般难以取得银行仓单融资的机会(这就给了其它融资机构提供服务的机会)。

银行承兑汇票

这是一个银行、供应方、采购方三方共赢的商业模式,可参看 张虎成讲票据(三)终于有人把银行承兑汇票彻底讲清楚了。目下很多平台也提供票据结算业务,比如找钢网,应该是与企业低贴息结算(相对银行贴息率较低),然后等着票据到期去银行取钱,赚这一部分贴息率。

保兑仓:企业分批赎货。融通仓:融资企业拿这批货抵押贷款,核心企业提供担保和[可能的]回购。

传统供应链金融存在的问题

  1. 由于银行依赖核心企业信用,出于风控的考虑,银行仅愿意对核心企业有直接应付账款义务的上游供应商(即一级供应商)提供保理业务,或对其一级经销商提供预付款或者存货融资。这就导致了有巨大融资需求的二级、三级等供应商/经销商的资金需求得不到满足,无法充分挖掘整个供应链的潜力。
    资金来自于银行等金融机构,银行在放款上具有强有力的话语权,企业在向银行提出融资申请后,按照它自己的标准来对企业进行评估,看它是否具有偿还能力。例如,有些中小企业虽然基于真实的交易背景具有偿还能力,可是并没能从账面财务信息表达,特别是当这些企业没有有效的、可供抵押的固定价值资产时,银行只能拒绝给企业融资。在这种情况下,中小企业只能以更高的价格,转向其他中小金融机构(如保理公司、民间借贷等)进行融资。如何将真实的交易背景层层穿透至供应链的末端,成为了最大的难题。
  2. 由于牵涉到多方以及多环节,各方须签署复杂的纸质合同,另外还涉及到合同的不同版本,因此维护和管理难度较大。在办理业务时,常需要找到特定版本的原始文档,及指定专人前来签署(如在实际金融操作中,银行非常关注应收账款债权“转让通知”的法律效应,如果核心企业无法签回,银行不会愿意授信。据了解,银行对于签署这个债权“转让通知”的法律效应很谨慎,甚至要求核心企业的法人代表去银行当面签署)。
  3. 信息不对称、不透明,金融方需要花费人力物力去鉴别交易真伪和核实线下抵押品,这反过来提高了融资方的融资成本。

国内市场前景

国家统计局数据显示,2016年末,我国规模以上工业企业应收账款12.6万亿元,同比增长10% ,这其中产生了企业巨大的融资需求。而相比于巨大的应收账款,2015年我国年商业保理量仅在2000亿元左右,可以看出,还有大量供应链需求没有被满足,因而供应链金融行业发展空间巨大。网贷天眼发布的《2016互联网+供应链金融研究报告》,预测2020年我国供应链金融市场规模将达到15万亿左右,这是一个非常巨大的市场。


区块链应用

笔者认为,虽然区块链脱胎于数字货币这种纯虚拟产物,但供应链金融反而是更能培养她成长的合适土壤。这是因为首先,供应链金融这个场景具有万亿级别的市场规模,天花板足够高;其次,这个场景天然需要多方合作,却又没有一个传统中心化的机构在治理,需要用区块链来建立信任;同时,在技术上这个场景并不需要高并发,目前的区块链技术能够满足。

与业务相关的核心功能就是基于真实共享的上链交易数据自动产生数字票据,该票据可以在公开透明、多方见证的情况下进行随意的拆分、汇总和转移。相当于把整个供应链中的信用变得可传导、可追溯,为大量原本无法融资的中小企业提供了融资机会,极大地提高票据的流转效率和灵活性,降低中小企业的资金成本。

如何将实体资产映射为数字资产,并实时同步状态,是个较为麻烦的事情。以目前的技术来说,通过RFID、NFC等射频方案可自动同步信息,然而资产损耗(如丢失、芯片故障)等场外因素仍需要手动盘点。

如何切入

如何切入是个难点,特别是涉及到多方合作的项目,可以从核心企业的影响力强制要求上下游加入 or 提供一个让人无法拒绝的服务:)

  • 联盟链模式——直接与核心企业/平台合作,为其提供区块链底层解决方案,在积累足够多数据之后,通过搭建联盟链,对接资金方提供金融服务。
  • 私有链模式——从提供供应链管理服务入手,比如溯源、追踪、可视化等,将信息流、物流和资金流整合到一起,在此基础之上从事金融服务。

大致可分为以下几步:

  1. 数据上链,将交易数据放到链上,利用区块链的特性使其不可篡改,并提供数据的确权,溯源等服务。
  2. 资产数字化,把仓单、合同、以及可代表融资需求的区块链票据都变成数字资产,且具有唯一、不可篡改、不可复制等特点。
  3. 数字资产的交易,供应链金融平台将转变成一个金融资产交易所,将非标的企业贷款需求转变成标准化的金融产品,进行代币化,对接投融资需求,进行价值交易。

最终,区块链技术将能有效地增强供应链金融资产的流动性,调动新型的融资工具和风控体系帮助覆盖中小企业融资的长尾市场,催生供应链金融即服务。

作用

提高行业透明度:区块链为供应链提供了交易状态实时、可靠的视图,有效提升了交易透明度,这将大大方便中介机构基于常用的发票、库存资产等金融工具进行放款。其中抵押资产的价值将根据现实时间实时更新,最终这将有助于建立一个更可靠和稳定的供应链金融生态系统。

降低金融成本:同样的,由于区块链填补了信任鸿沟,并能实时查阅包括合同条款在内的所有数据,另外还有智能合约的加持,减少了人工核实和反馈的成本,这部分同样会反映到金融服务上。

新的商业模式:新型供应链金融平台,主要的参与者包括平台本身、保理机构、中介金融机构、企业、个人甚至是算法公司。平台提供数据,算法公司可以基于平台提供的API接口,开发金融模型,并出售给第三方金融机构和保理公司。

提升用户体验:由于用户数据(包括交易数据、征信、资产配置等)都已上链(敏感数据可配置为只有特定机构可查询),平台可推送合适的金融方案给到用户,用户选择后资方会自动划拨款项到用户银行账户,而不需要漫长的资料准备、各种复杂的条款、审批等等。

ps:并不是说区块链上的数据都一定是真实的,但至少比传统系统如ERP之类的方便排查的多,且更易于发现。


其它

敞口:一般指的是风险敞口。指在金融活动中存在金融风险的部分以及受金融风险影响的程度。

托盘:钢贸行业的仓单质押,属于保兑仓。

外贸

T/T: 电汇,也就是说客户直接从他的开户银行把钱付到你的开户银行中,或者反之。前T/T就是预先付款,后T/T就是先发货后付款。前T/T当然就对卖家有保障对买家不利了。
L/C是指信用证。一般都要求买家开不可撤销信用证。意思是买家把钱存在银行,然后委托银行按照一定的外贸流程,预先设定好受益人,交易方式,所需单据等要求,卖方根据信用证要求出货并把单据寄到对方银行,单据都没错银行就直接给你放款,不受买家的要求影响。对卖方买方都有保障。对单据要求比较高,而且手续费高。

长尾

长尾效应,英文名称Long Tail Effect。“头”(head)和“尾”(tail)是两个统计学名词。正态曲线中间的突起部分叫“头”;两边相对平缓的部分叫“尾”。从人们需求的角度来看,大多数的需求会集中在头部,而这部分我们可以称之为流行,而分布在尾部的需求是个性化的,零散的小量的需求。而这部分差异化的、少量的需求会在需求曲线上面形成一条长长的“尾巴”,而所谓长尾效应就在于它的数量上,将所有非流行的市场累加起来就会形成一个比流行市场还大的市场。

 

参考资料:

供应链金融三类模式的最全对比分析

研究报告:区块链+供应链金融

 

转载请注明本文出处:https://www.cnblogs.com/newton/p/9827715.html

供应链金融&区块链应用 – 莱布尼茨 – 博客园已关闭评论
2019年2月20日 By mikel 分类: ASP.NET, 架构设计
http://baa.im/847971

Docker可以说是现在微服务,DevOps的基础,咱们.Net Core自然也得上Docker。.Net Core发布到Docker容器的教程网上也有不少,但是今天还是想来写一写。 你搜.Net c

来源: .Net Core in Docker – 在容器内编译发布并运行 – Agile.Zhou – 博客园

Docker可以说是现在微服务,DevOps的基础,咱们.Net Core自然也得上Docker。.Net Core发布到Docker容器的教程网上也有不少,但是今天还是想来写一写。
你搜.Net core程序发布到Docker网上一般常见的有两种方案:

  • 1、在本地编译成Dll文件后通过SCP命令或者WinSCP等工具上传到服务器上,然后构建Docker镜像再运行容器。该方案跟传统的发布很像,麻烦的地方是每次都要打开相关工具往服务器上复制文件。
  • 2、在服务端直接通过Git获取最新源代码后编译成Dll然后构建Docker镜像再运行容器。该方案免去了往服务器复制文件这步操作,但是服务器环境需要安装.Net Core SDK 来编译源代码。
    自从用了Docker简直懒的不能自理,我既不想手工复制文件到服务器,也不想在服务器装.Net Core环境。显然只要Docker镜像包含.Net Core SDK环境就可以在Docker内帮我们编译代码然后运行,这样连我们的服务器都不用装啥.Net Core的环境拉。

    在Docker内编译发布.Net Core程序并运行

    新建一个ASP.NET Core MVC项目

    我们使用一个ASP.NET Core MVC程序来演示如何发布到Docker并运行。
    新建项目
    使用vs新建一个ASP.NET core mvc项目

  public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return Content($"Core for docker , {DateTime.Now} , verson 2");
        }
    }

修改HomeController下的index Action,直接输出一段文字

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
            .UseKestrel(op =>
            {
                op.ListenAnyIP(5000);
            })
            .UseStartup<Startup>();

修改Program下的CreateWebHostBuilder方法,让Kestrel监听5000端口


本地运行一下试试

推送源码到代码仓库

把我们的代码推送到对应的Git仓库,方便我们从部署服务器上直接拉取最新的代码。

X:\workspace\CoreForDocker>git remote add origin https://gitee.com/kklldog/CoreForDocker.git

X:\workspace\CoreForDocker>git push -u origin master
Username for 'https://gitee.com': xxx@gmail.com
Password for 'https://xxx@gmail.com@gitee.com':
Counting objects: 88, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (83/83), done.
Writing objects: 100% (88/88), 527.07 KiB | 2.43 MiB/s, done.
Total 88 (delta 7), reused 0 (delta 0)
remote: Powered By Gitee.com
To https://gitee.com/kklldog/CoreForDocker.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

添加Dockerfile文件

在CoreForDocker下新增一个Dockerfile文件,注意没有任何扩展名。我们需要基于microsoft/dotnet:latest这个镜像构建一个新的镜像。并且在构建的过程中直接对源码进行编译并发布。

FROM microsoft/dotnet:latest
WORKDIR /app
COPY /. /app
RUN dotnet restore
RUN dotnet publish -o /out -c Release
EXPOSE 5000
ENTRYPOINT ["dotnet", "/out/CoreForDocker.dll"]

大概解释下Dockerfile的意思:
FROM microsoft/dotnet:latest:使用dotnet的最新镜像,这个镜像其实对应的应该就是2.2-sdk这个镜像,里面包含了dotnet-core 2.2 sdk
WORKDIR /app:指定工作目录为app
COPY /. /app复制宿主机当前目录的内容到容器的app文件夹
RUN dotnet restore:还原nuget包
RUN dotnet publish -o /out -c Release编译并发布程序集到容器的out目录
EXPOSE 5000:暴露5000端口
ENTRYPOINT [“dotnet”, “/out/CoreForDocker.dll”]:容器启动的时候执行dotnet命令,参数为/out/CoreForDocker.dll


Dockerfile的文件属性设置为始终复制
新建好Dockerfile后git push到代码仓库。

在服务器上构建Docker镜像

这里以Ubuntu为例,ssh登录到服务器后使用git clone命令拉取源代码。

git clone https://gitee.com/kklldog/CoreForDocker.git

进入源码目录

cd CodeForDocker\CodeForDocker

使用docker build命令构建新的镜像,注意不要忘记最后一个’.’

docker build -t image_code4docker .

运行容器

如果以上步骤都没有报错,那么恭喜你镜像已经构建成功了,我们可以使用此镜像运行Docker容器了。

docker run -d --name code4docker -p 5000:5000 -v /ect/localtime:/ect/localtime image_core4docker

使用image_core4docker镜像运行一个名为core4docker的容器,绑定宿主机的5000到容器的5000口。其中需要注意的是-v参数映射宿主机的/ect/localtime文件夹到容器的/ect/localtime文件夹,因为经过实践发现容器中的时区有可能跟宿主机不一致,需要映射宿主机的/ect/localtime让容器的时区跟宿主机保持一致。


访问一下服务器的5000端口,发现能够正确返回数据表示我们的Asp.net Core程序在容器中运行成功了

以后当我们对源码进行修改,并提交后,我们只需在服务器上拉取最新的代码然后使用docker build,docker run命令来再次生成镜像并运行容器。但是手工输入docker build,docker run的命令好像也很麻烦,参数又那么多,太烦了。

使用shell脚本简化操作

为了偷懒不想敲那么长的命令,我们可以构建一个脚本,把命令一次性写好,以后只要运行一次脚本就可以了。
使用vim新建一个publish.sh的文件

vim publish.sh

键盘上按i进入编辑模式,输入以下内容

cd CoreForDocker/CoreForDocker
git pull
docker stop core4docker
docker rm core4docker
docker rmi image_core4docker
docker build -t image_core4docker .
docker run --name core4docker -d -p 5000:5000 -v /etc/localtime:/etc/localtime image_core4docker

以上命令,不光有新建镜像跟运行容器的命令,还有移除原来的容器跟镜像的命令
按ecs进入命令模式,退出保存

:wq

让我们模拟修改一下源代码,并提交到代码仓库

    public IActionResult Index()
    {
        return Content($"Core for docker , {DateTime.Now} , version 2");
    }

再次修改homecontroller的index action,输出内容上新增一个version
ssh登录到服务器,运行publish.sh文件

/bin/bash publish.sh


跑完之后我们再次访问下服务器的5000口,数据返回正确,表示服务器上跑的已经是最新的程序了

总结

通过以上演示我们基本了解如何通过git跟docker配合在Ubuntu服务器上不安装.Net Core SDK来发布.Net Core 程序到容器中运行,并且通过shell脚本的方式再次简化发布。但是尽管这样每次发布都需要ssh到服务器上然后运行脚本,特别是开发环境可能经常需要发布,还是觉得麻烦。有没有什么办法让我们push代码后服务器自动就开始部署最新的代码的到容器中运行了呢?
后面我会介绍下如何通过jenkins跟webhook来做CICD。

.Net Core in Docker – 在容器内编译发布并运行 – Agile.Zhou – 博客园已关闭评论
2019年2月19日 By mikel 分类: ASP.NET, C#
http://baa.im/847971

来源: CentOS开发ASP.NET Core入门教程 – 依乐祝 – 博客园

作者:依乐祝
原文地址:https://www.cnblogs.com/yilezhu/p/9891346.html

因为之前一直没怎么玩过CentOS,大多数时间都是使用Win10进行开发,然后程序都部署在Window Server2008或者Window Server2012上!因此想尝试下Linux系统。最后经过选型选了比较流行的CentOS系统。正好,今晚要加班,所以在数据备份的空隙,写了今天这篇关于使用CentOS开发ASP.NET Core的入门教程。干货不多,主要是为了记录自己向Linux迈出第一步的大门。大家将就着看吧!

前言

程序员的江湖一直有这么一个传说,就是入坑的第一个程序都是向世界问好,所以这篇CentOS开发ASP.NET Core入门教程的第一篇也仅仅是搭建环境,然后向世界输出“Hello World!”。

CentOS系统ASP.NET Core开发环境的搭建

这里假设大家已经装了Linux虚拟机或者买了阿里云的Linux服务器。而且在Windows开发机上安装 xshell ,xshell用于SSH连接Linux服务器(当然,你也可以用其他的软件,这么不过多阐述)。下面用xshell连接上你的linux服务。然后开始进入正式的部署吧。

安装.Net Core SDK

要开始构建.Net Core应用程序前,你需要安装.NET Core SDK(软件开发工具包)即可。
具体怎么安装呢?有以下几个步骤:

  1. 添加dotnet 产品Feed(就是为了告诉微软,我们的服务器要使用.net Core sdk了)

    在安装.net sdk之前呢,你需要注册Microsoft密钥,注册产品存储库并安装所需的依赖项。这个步骤每台服务器只需要执行一次既可以了。命令如下所示:

    sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm
  2. 安装.Net Core SDK

    首先要安装可用的产品更新,然后才是安装.Net Core SDK。在命令行,分别运行下面的命令。

    sudo yum update 
    y
    sudo yum install dotnet-sdk-2.1
    y

    大家注意一下,有两个“y”的原因是,命令执行的中途会停顿下人,让你确认下是否进行安装,你要输入“y”确认安装才会执行安装的。

在CentOS上创建你的第一个.Net Core 应用程序

  1. 前面安装好.net core sdk以后,我们输入如下命令来看下我们是否安装成功吧!
dotnet --info

1541070786941

如果出现上面的图说明我们已经安装成功了。上面显示有.Net Core的版本信息。

  1. 接下来我们新建一个文件夹名字叫“netcore”用来存放我们的asp.net core应用程序。然后进入这个文件夹
mkdir netcore
cd netcore
  1. 输入如下的命令来创建第一个ASP.NET Core应用程序
    dotnet new console -o myFirstApp
    cd myFirstApp

    dotnet命令为您创建一个新的控制台应用程序。该-o参数为新的应用程序创建一个名为myFirstApp的目录。该cd myFirstApp命令将切换到这个新的应用程序目录。
    然后输入ls命令可以看到下图所示的三个文件:

    1541071244630

    该myFirstApp文件夹中的主文件是 Program.cs。默认情况下,它已包含了向控制台输入“Hello World!”所需的代码。

  2. 使用如下的命令来运行下这个应用程序吧。
    dotnet run

    1541071468709

    如果不出意外的话,大家可以看到,程序向我们输入了Hello World的!
    至此,我们在Centos上的第一个.Net Core程序就跑起来了!

    总结

    今天也是忙里偷闲,利用加班的间隙写了这篇window向CentOS进击的第一篇入门教程!既然ASP.NET Core已经全面跨平台了,那我们也得学会改变,学着使用Linux系统!不管你愿不愿意,这是一个趋势!金庸的江湖已去,而我们程序员的江湖还在继续!多一种技能在身,总归是好事!

CentOS开发ASP.NET Core入门教程 – 依乐祝 – 博客园已关闭评论
2019年2月19日 By mikel 分类: ASP.NET, C#
http://baa.im/847971

来源: 使用Visual Studio Code开发.NET Core看这篇就够了 – 依乐祝 – 博客园

在本文中,我将带着大家一步一步的通过图文的形式来演示如何在Visual Studio Code中进行.NET Core程序的开发,测试以及调试。尽管Visual Studio Code的部分功能还达不到Visual Studio的水平,但它实际上已经足够强大来满足我们的日常开发。而且其轻量化,插件化以及跨平台的特性则是VS所不具备的。而且Visual Studio Code还可以通过社区来创建一系列的扩展来增强其功能,且社区已经足够活跃。我们可以期待更多很酷的扩展和功能来增强VS Code,这将使在这个轻量级,跨平台编辑器中的开发.NET Core应用程序更加流畅和有趣。赶紧跟着博主一起开始今天的文章吧!

为什么要写这篇文章?

因为上篇文章也说了,.NET Core已经全面跨平台了,而且我们也在尝试使用Linux了,但是上篇CentOS开发ASP.NET Core入门教程 中使用的CLI进行.NET Core开发的话,感觉很不适应。毕竟从.net过度过来的我们已经习惯了使用Microsoft的Visual Studio进行开发。那么有没有一款媲美Visual Studio的开发工具可以让我们能够在Linux系统上进行高效的.NET Core开发呢?答案是肯定的,因为微软已经开发了一个名为Visual Studio Code的跨平台和开源的文本编辑器。Visual Studio Code是如此强大和令人惊叹,因为它提供了内置的智能提醒,调试功能和Git支持。而且Visual Studio Code提供了强大的插件扩展功能。使得你可以在插件扩展库里面找到满足你需求的插件。如果你没有在他们的扩展库中找到它,那么你还可以自己创建一个插件并使用它。很酷,对吗?那就开始吧!

安装

这部分,我们将讲解如何进行Visual Studio Code的安装,配置以便进行.NET Core的开发

准备工作

  1. 安装.NET Core SDK。具体的安装方式大伙可以点击这里进行查看并进行安装。因为微软的东西都比较傻瓜式,所以这里就不演示了。
  2. 安装Visual Studio Code。您可以从此处 然后根据您的操作系统进行选择下载,不同操作系统的安装过程可能会有所不同 您可以在此处 查看Visual Studio Code的安装说明。还是 因为微软的东西都比较傻瓜式,所以这里就不演示了。1541339022729
  3. 在Visual Studio Code 中安装C# 扩展以便让Visual Studio Code 支持C#的开发,当然你也可以安装其他语言的扩展来进行其他编程语言的开发,比如说python,go等等。为了安装C#的扩展,你可以通过Visual Studio Code左侧工具栏中的Extensions图标或使用键盘快捷键Ctrl + Shift + X打开Extensions视图。在搜索框中搜索C#并从列表中安装扩展程序。如下图所示: 1541339435927

    这里需要注意下,安装完成之后,需要重启下Visual Studio Code才能够使用C#扩展功能。1541339538011
    重启之后会出现如下的界面,表示已经安装好了C#扩展1541339685496

    使用Visual Studio Code开发基本的.NET Core程序

    既然环境都已经准备好了,那么现在我们就开始使用Visual Studio Code开发一个.NET Core应用程序吧!

  4. 在电脑上一个位置创建一个名为DotNetCoreSample的空文件夹,然后右键单击该文件夹,从弹出的菜单中选择“使用Visual Studio Code打开”。这将打开Visual Studio Code,并将选定该文件夹作为工作区。当然也可以通过下图所示的步骤来打开这个文件夹,这个按照你的习惯来操作就好。1541340456205
  5. 使用`Ctrl+Shift+“ 快捷键在 Visual Studio Code 中快速打开终端,如下图所示:

    1541590018735

  6. 接下来我们使用dotnet new console --name DotNetCoreSample 命令来在这个打开的终端里面创建一个基础的控制台程序并进行restore。如下图所示

    1541590248007

  7. 接下来我们打开生成的Program.cs 文件,Visual Studio Code会安装OmniSharp插件,然后会在右下角弹出如下图所示的是否需要生成用来构建以及调试的资产文件的询问窗口,这里点击“是”就会帮我们生成“launch.json”以及“task.json”文件,这些文件将有助于使用Visual Studio代码构建和调试应用程序 。

    1541590405069

    1541590667259

  8. 下面我们修改下Program.cs 文件中的内容,添加下面这行代码。然后保存文件,并把鼠标移动到终端,然后终端cd到我们的项目目录cd DotNetCoreSample。输入dotnet run 然后按下Enter键,可以看到如下所示的内容:

    1541591036238

Visual Studio Code中vscode-solution-explorer解决方案管理器插件的使用

可能很多.neter朋友们刚开始使用Visual Studio Code的时候很不适应各种命令行dotnet命令来创建项目以及解决方案。幸运的是,Visual Studio Code扩展中提供了类似于Visual Studio的解决防范资源管理的插件来解决这个问题。下面我们一步一步的看下如何使用此插件吧!

  1. 打开Visual Studio Code扩展,然后输入vscode-solution-explorer,然后如下图所示进行安装。

    1541591973934

  2. 安装后插件后,VS Code Explorer左侧栏中将多了一个显示名为“SOLUTION EXPLORER”的新窗格。

    1541592290514

  3. 接下来我们使用它来创建解决方案,并在解决方案中添加项目吧。我们按下快捷键Ctrl + Shift + P

    然后选择“Create a new empty solution ” VS Code 将提示我们输入一个解决方案的名称。我们输入一个SimpleCalculator 作为解决方案的名称。

    1541592847941

  4. 现在,VS Code将使用我们提供的名称创建一个空的解决方案。在后台,我们安装的扩展将执行dotnet new sln  命令。您可以在“SOLUTION EXPLORER”窗格中看到空白解解决方案。然后此扩展程序将询问你是否创建模板文件夹请参见下图。如果允许,它将在.vscode / solution-explorer 目录中添加一些模板。

    1541593203311

  5. 现在,让我们向这个空白的解决方案中添加类库和控制台应用程序。右键单击解决方案(在Solution Explorer窗格中),然后从上下文菜单中选择Add new project选项。这将列出.NET CLI提供的可用项目类型(请参见下图)。选择“类库”选项。

    1541593412253

    1541593430198

  6. 系统将询问您将使用哪种语言。选择C#,编辑器将提示输入项目名称。 像我们之前给出的那样给出MathOperations的名称。类库已添加到解决方案中。
  7. 重复相同的步骤并添加名为“Calculator ”的控制台应用程序。请记住从项目模板中选择控制台应用程序。
  8. 现在我们需要在控制台应用程序中添加类库项目的引用。右键单击控制台应用程序项目,然后从上下文菜单中选择“添加引用”选项。由于解决方案中只有两个项目,扩展程序将自动添加另一个项目的引用。如果有两个以上的项目,我们需要从列表中选择项目。

    1541593786278

  9. 导航到类库目录MathOperations。将Class1.cs  类文件重命名为MathOperations.cs。在类中添加一个两个数字的简单简单加法的方法,代码如下:
        public static class MathOperation    
        {    
            public static int Add(int num1, int num2) => num1 + num2;
        }  
  10. 修改导航到Calculator控制台程序并在Program.cs文件中使用类库中的方法。这里大家可以使用Shift + Alt + F快捷键格式化代码。如下所示:
    static void Main(string[] args)
            {
                int num1 = 10;
                int num2 = 20;
                int sum = MathOperation.Add(num1, num2); // Method from class library    
                Console.WriteLine($"{num1} + {num2} = {sum}");
                Console.ReadLine();
            }
  11. 现在,右键单击解决方案资源管理器树中的控制台应用程序项目,然后从上下文菜单中选择“运行”选项。您可以看到.NET CLI将在后台运行应用程序。并在Output窗口中输出结果,如下图所示。

    1541594980618

    1541594999015

Visual Studio Code在.NET Core应用程序中运行测试插件

单元测试是软件开发不可或缺的一部分。这里我不打算详细解释单元测试,因为有很多在线资源。我只给大家介绍如何在.NET Core应用程序中包含单元测试以及可用于运行单元测试的Visual Studio Code的扩展。

  1. 首先让我们该写下数学运算的类库方法
 public static class MathOperation
    {
        public static int Add(int num1, int num2) => num1 + num2;
        public static int Subtract(int num1, int num2) => num1 - num2;

        public static int Multiply(int num1, int num2) => num1 * num2;

        public static int Divide(int num1, int num2) => num1 / num2;
    }
  1. 现在,我们需要在解决方案中添加一个单元测试项目。

我们可以使用.NET CLI或上面提到的Solution Explorer扩展来添加单元测试项目。要通过Solution Explorer扩展添加项目,请右键单击解决方案,然后 从上下文菜单中选择“ 添加新项目 ”。从项目模板中选择xUnit Test Project 并命名为 MathOperationTests。创建测试项目后,将MathOperations类库的引用添加到测试项目中。

如果您使用的是.NET CLI,则需要运行以下命令。

dotnet new xunit -n MathOperationTests  
dotnet add MathOperationTests\MathOperationTests.csproj reference MathOperations\MathOperations.csproj  
dotnet sln SimpleCalculator.sln add MathOperationTests\MathOperationTests.csproj
  1. UnitTest1.cs重命名为OperationTests.cs。也要在代码中更改类名。现在我们将为类库方法添加一些测试。
    public class OperationTests
        {
            [Fact]
            public void AddTwoNumbers_ReturnsSum()
            {
                var num1 = 10;
                var num2 = 20;
                var result = MathOperation.Add(num1, num2);
                Assert.Equal(30, result);
            }
    
            [Fact]
            public void SubtractTwoNumbers_ReturnsDifference()
            {
                var num1 = 20;
                var num2 = 10;
                var result = MathOperation.Subtract(num1, num2);
                Assert.Equal(10, result);
            }
    
            [Fact]
            public void MultiplyTwoNumbers_ReturnsProduct()
            {
                var num1 = 10;
                var num2 = 20;
                var result = MathOperation.Multiply(num1, num2);
                Assert.Equal(200, result);
            }
    
            [Fact]
            public void DivideTwoNumbers_ReturnsQuotient()
            {
                var num1 = 20;
                var num2 = 10;
                var result = MathOperation.Divide(num1, num2);
                Assert.Equal(2, result);
            }
        }
  2. 现在,我们需要运行我们创建的测试。我们为此使用.NET CLI。打开终端。导航到MathOperationTests目录。输入dotnet test命令。我们将获得以下输出。

    1541596760373

  3. 如您所见,输出信息量较少。如果我们在Visual Studio中有类似于Test Explorer的东西来执行我们的单元测试并查看结果,那将会很好。好消息是有一个名为.NET Core Test Explorer的Visual Studio Code插件。下面按照下图所示在Visual Studio代码中安装此扩展吧。这里不过多说明了

    1541597219891

  4. 安装扩展程序后,您可以在左侧活动栏中看到一个烧杯图标。单击该图标,您将看到测试的侧栏面板,其中列出了项目中发现的单元测试。测试项目将显示在按命名空间和类分组的树视图中。您还可以看到每个测试的“运行”按钮和顶部的“全部运行”按钮。单击Run All按钮,您可以看到正在执行的所有测试及其结果。

    1541597348658

  5. 我们可以看到所有测试都已通过,并在测试资源管理器窗格中标有绿色勾号。现在让我们让测试失败。我将更改Add方法的逻辑以使测试失败。
     public static int Add(int num1, int num2) => num1 - num2;//这里有bug
  6. 现在再次运行测试。我们可以看到我们对Add方法的测试失败,并在test explorer窗格中用红色符号标记。

    1541597509689

  7. 如果我们导航到我们编写的测试方法,我们可以看到它现在在Assert方法中有一个红色的波浪下划线。如果我们将鼠标悬停在该波浪线上,将显示一个信息框,显示测试的实际值和预期值。VS代码的底部面板(终端所在的面板)的“ 问题”选项卡中显示相同的信息。这可以在下图中看到。

    1541597599699

  8. 修复错误并再次运行测试,以便所有测试都通过,我们可以再次看到绿色标记。

Visual Studio Code中顺畅的调试.NET Core应用程序

在这部分,我们将了解如何在Visual Studio Code中顺畅的调试.NET Core应用程序。为了在Visual Studio Code中调试.NET Core应用程序,我们需要为VS Code安装C#扩展。(上面我们已经安装过了)

  1. 我们首先在Calculator控制台程序的Program.cs文件中加入断点。与Visual Studio类似,我们可以通过单击源代码文件的左边距,或者将光标放在一行代码上并按F9,在源代码中设置行断点。断点在编辑器的左边缘显示为红点。
  2. 要开始调试,请按F5。这将自动将调试器附加到我们的Calculator应用程序来启动应用程序。我们可以看到执行在我们设置的断点处停止,这有助于我们在调试时了解当前的程序状态。

    1541598520046

    这里需要注意下,需要修改launch.json中的对应路径以及项目名称为Calculator。

    1541598690693

  3. 我们可以看到VS Code的Debug视图在编辑器的左侧打开。Debug视图显示与调试相关的所有信息。我们还可以注意到编辑器顶部出现了一个调试工具栏。调试时,调试工具栏可用于代码导航选项。这里调试试图的大部分功能跟vs2017差不多,因此这里不做过多地阐述了。

总结

在本文中,我已经为大家一步一步的通过图文教程解释了如何在Visual Studio Code中进行.NET Core程序的开发,测试以及调试。赶紧下载一个试试吧!你会发现你会越来越喜欢他的!
本文参考:https://www.c-sharpcorner.com/article/create-a-net-core-development-environment-using-visual-studio-code2/

使用Visual Studio Code开发.NET Core看这篇就够了 – 依乐祝 – 博客园已关闭评论
2019年2月18日 By mikel 分类: 架构设计, PHP
http://baa.im/847971

首先了解一个方法: 使用docker exec进入Docker容器 docker在1.3.X版本之后还提供了一个新的命令exec用于进入容器,这种方式相对更简单一些,下面我们来看一下该命令的使用: 接

来源: docker完整配置nginx+php+mysql – 冒雨ing – 博客园

首先了解一个方法:

使用docker exec进入Docker容器

docker在1.3.X版本之后还提供了一个新的命令exec用于进入容器,这种方式相对更简单一些,下面我们来看一下该命令的使用:

sudo docker exec --help

 

接下来我们使用该命令进入一个已经在运行的容器

$ sudo docker ps  
$ sudo docker exec -it 775c7c9ee1e1 /bin/bash

 

一. 配置nginx

查找 Docker Hub 上的 nginx 镜像

runoob@runoob:~/nginx$ docker search nginx
NAME                      DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
nginx                     Official build of Nginx.                        3260      [OK]       
jwilder/nginx-proxy       Automated Nginx reverse proxy for docker c...   674                  [OK]
richarvey/nginx-php-fpm   Container running Nginx + PHP-FPM capable ...   207                  [OK]
million12/nginx-php       Nginx + PHP-FPM 5.5, 5.6, 7.0 (NG), CentOS...   67                   [OK]
maxexcloo/nginx-php       Docker framework container with Nginx and ...   57                   [OK]
webdevops/php-nginx       Nginx with PHP-FPM                              39                   [OK]
h3nrik/nginx-ldap         NGINX web server with LDAP/AD, SSL and pro...   27                   [OK]
bitnami/nginx             Bitnami nginx Docker Image                      19                   [OK]
maxexcloo/nginx           Docker framework container with Nginx inst...   7                    [OK]
...

这里我们拉取官方的镜像

runoob@runoob:~/nginx$ docker pull nginx

等待下载完成后,我们就可以在本地镜像列表里查到 REPOSITORY 为 nginx 的镜像。

runoob@runoob:~/nginx$ docker images nginx
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              555bbd91e13c        3 days ago          182.8 MB

创建并运行容器:
docker run --name mynginx -p 80:80 -v /var/www:/var/www -v /usr/local/nginx/conf/conf.d:/etc/nginx/conf.d -d nginx

注意:

-v 添加文件映射关系,这样在宿主机上更改的文件可以直接映射到容器中。这里的目录根据自己实际情况进行映射。

创建并运行容器后,docker内的nginx即启动成功,无需进入docker内部再次启动nginx, 否则会提示80等端口被占用,因为nginx已经启动。

这时候便可以访问nginx配置的域名验证了。

我这里映射的conf.d主要包含nginx的配置文件,php的配置信息为:

复制代码
# php
server {
    charset utf-8;
    client_max_body_size 128M;

    listen 80; ## listen for ipv4
    #listen [::]:80 default_server ipv6only=on; ## listen for ipv6

    server_name www.baidu.com;
    root        /var/www;
    index       index.php;

    location / {
        #-e表示只要filename存在,则为真
        if (!-e $request_filename){
            rewrite  ^(.*)$  /index.php?s=$1  last;
            break;
        }
        # Redirect everything that isn't a real file to index.php
        try_files $uri $uri/ /index.php$is_args$args;
    }

    # uncomment to avoid processing of calls to non-existing static files by Yii
    #location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
    #    try_files $uri =404;
    #}
    #error_page 404 /404.html;

    # deny accessing php files for the /assets directory
    location ~ ^/assets/.*\.php$ {
        deny all;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass 172.17.0.3:9000;
        #fastcgi_pass unix:/var/run/php5-fpm.sock;
        try_files $uri =404;
    }

    location ~* /\. {
        deny all;
    }
}
复制代码

注意最后面的fastcgi_pass的ip地址,在php配置常见问题有详细介绍。

 

二. php配置

查找Docker Hub上的php镜像

runoob@runoob:~/php-fpm$ docker search php
NAME                      DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
php                       While designed for web development, the PH...   1232      [OK]       
richarvey/nginx-php-fpm   Container running Nginx + PHP-FPM capable ...   207                  [OK]
phpmyadmin/phpmyadmin     A web interface for MySQL and MariaDB.          123                  [OK]
eboraas/apache-php        PHP5 on Apache (with SSL support), built o...   69                   [OK]
php-zendserver            Zend Server - the integrated PHP applicati...   69        [OK]       
million12/nginx-php       Nginx + PHP-FPM 5.5, 5.6, 7.0 (NG), CentOS...   67                   [OK]
webdevops/php-nginx       Nginx with PHP-FPM                              39                   [OK]
webdevops/php-apache      Apache with PHP-FPM (based on webdevops/php)    14                   [OK]
phpunit/phpunit           PHPUnit is a programmer-oriented testing f...   14                   [OK]
tetraweb/php              PHP 5.3, 5.4, 5.5, 5.6, 7.0 for CI and run...   12                   [OK]
webdevops/php             PHP (FPM and CLI) service container             10                   [OK]
...

这里我们拉取官方的镜像,标签为5.6-fpm

runoob@runoob:~/php-fpm$ docker pull php:5.6-fpm

等待下载完成后,我们就可以在本地镜像列表里查到REPOSITORY为php,标签为5.6-fpm的镜像。

runoob@runoob:~/php-fpm$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
php                 5.6-fpm             025041cd3aa5        6 days ago          456.3 MB

创建并运行php容器:

docker run -p 9000:9000 --name  phpfpm -v /var/www:/var/www -d php:5.6-fpm

注意这里一定要创建文件映射,或者php容器内有对应的php代码。上一步nginx的文件映射,在这里是找不到的。所以如果没有文件映射,127.0.0.1:9000 在此容器内就找不到文件 。

 

常见问题:

启动php容器后,如果访问nginx为:502 Bad Gateway

尝试以下方法:

查看php镜像的ip地址

docker inspect --format='{{.NetworkSettings.IPAddress}}' phpfpm

如:192.168.4.202

那么修改nginx的conf配置文件,使fastcgi_pass的值为 192.168.4.202:9000
vim /docker/nginx/conf.d/default.conf
fastcgi_pass 192.168.4.202:9000;

重启nginx容器后,就可以正常访问。

 

三. mySQL配置

查找Docker Hub上的mySQL镜像

runoob@runoob:/mysql$ docker search mysql
NAME                     DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
mysql                    MySQL is a widely used, open-source relati...   2529      [OK]       
mysql/mysql-server       Optimized MySQL Server Docker images. Crea...   161                  [OK]
centurylink/mysql        Image containing mysql. Optimized to be li...   45                   [OK]
sameersbn/mysql                                                          36                   [OK]
google/mysql             MySQL server for Google Compute Engine          16                   [OK]
appcontainers/mysql      Centos/Debian Based Customizable MySQL Con...   8                    [OK]
marvambass/mysql         MySQL Server based on Ubuntu 14.04              6                    [OK]
drupaldocker/mysql       MySQL for Drupal                                2                    [OK]
azukiapp/mysql           Docker image to run MySQL by Azuki - http:...   2                    [OK]
...

这里我们拉取官方的镜像,标签为5.6

runoob@runoob:~/mysql$ docker pull mysql:5.6

等待下载完成后,我们就可以在本地镜像列表里查到REPOSITORY为mysql,标签为5.6的镜像。

runoob@runoob:~/mysql$ docker images |grep mysql
mysql               5.6                 2c0964ec182a        3 weeks ago         329 MB

创建并运行MySQL容器:

docker run -p 3306:3306 --name mysql -v /usr/local/mysql:/etc/mysql/sqlinit -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.6

这里的文件映射主要目的是把宿主机的sql数据库数据文件映射到docker mysql容器,方便导入,注意这里mysql容器的目录不能是已有的目录,否则会覆盖。

注意:

这里创建容易已经有了my.cnf,无需自己添加。

 

 拓展

使用外部工具navicat连接docker 内mysql

mysql的host 填写docker内的IP,获取方式为:

1 docker inspect --format='{{.NetworkSettings.IPAddress}}' mysql

填写ssh连接信息:

即可连接成功!

 

注意:

docker的容器启动顺序问题会导致容器的IP地址不一致,如果在连接数据库和fastcgi处有用到容器的IP,要注意容器的启动顺序。

重启容器:docker restart 容器名/容器ID

关闭容器:docker stop xxx

开启容器:docker start xxx

查看正在运行的容器:docker ps

查看所有容器(包括未运行的容器): docker ps -a

创建并运行容器: docker run

 

—————————————

 

常见报错:

1.  thinkphp报错 Undefined class constant ‘MYSQL_ATTR_INIT_COMMAND’

缺少pdo_mysql扩展,链接数据库失败

找到php.ini,docker中在/usr/local/etc/php中,复制一份php.ini,增加 extension=pdo_mysql.so  ,重启phpfpm。

如果还不行,访问phpinfo页面,查看是否有pdo_mysql

如果没有,说名扩展不存在,需要编译。

编译方法如下:

可以通过两种方式实现
方式一(未验证):

pecl pdo_msql

方式二(已验证可行):

到docker的php容器中,在php文件夹下:

docker-php-ext-install pdo pdo_mysql

如果报 /usr/local/bin/docker-php-ext-enable: cannot create /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini: Directory nonexistent
解决方案:
直接在/usr/local/etc/php目录下面新建 conf.d目录和对应的docker-php-ext-pdo_msql.ini文件
其中docker-php-ext-pdo_msql.ini的内容为:
extension=pdo_mysql.so

 

2. thinkphp 报错 _STORAGE_WRITE_ERROR_:./Application/Runtime/Cache/Home/4e64ea6a2012f26b832b14cbc2152b28.php

是因为服务器缓存文件夹的操作权限不够,即Runtime没有权限,把缓存文件全部删除,再给Runtime777权限就行了

sudo chmod 777 Runtime 或者直接对代码库最外层设置777权限

 

3. thinkphp验证码图片显示不出来

缺少gd扩展,安装:

docker-php-ext-install gd

可能以下报错:

If configure fails try --with-webp-dir=<DIR>
If configure fails try --with-jpeg-dir=<DIR>
configure: error: png.h not found.

安装:

apt-get install libpng-dev libjpeg-dev

再次执行:

// 增加freetype配置
docker-php-ext-configure gd --enable-gd-native-ttf --with-freetype-dir=/usr/include/freetype2 --with-png-dir=/usr/include --with-jpeg-dir=/usr/include

// 安装
docker-php-ext-install gd

php.ini增加php_gd2.so

phpinfo中显示gd库

注意如果phpinfo的gd库中没有freetype的支持,验证码依然显示不出来, 会报错:

Call to undefined function Think\imagettftext()

 

如果gd库中没有freeType,则按照以下步骤进行:

docker-php-ext-configure gd --enable-gd-native-ttf --with-freetype-dir=/usr/include/freetype2 --with-png-dir=/usr/include

重新编译:
docker-php-ext-install gd

如果报错:

configure: error: freetype-config not found.

运行: apt-get -y install libfreetype6-dev ,然后再继续运行上面的命令。

gd库中有了freetype,则验证码显示正常了:

docker完整配置nginx+php+mysql – 冒雨ing – 博客园已关闭评论
2019年2月17日 By mikel 分类: 架构设计
http://baa.im/847971

随着现在数据量的不断增加,很多大数量的问题随之而来,就得需要我们想办法解决,我找了一些问题并首先思考,然后找到方法,在这里记录一下,未来有需要的同学可以拿走去用。 1. 在海量日志数据里,提取某天访问

来源: 海量数据处理方法整理记录 – 黄青石 – 博客园

随着现在数据量的不断增加,很多大数量的问题随之而来,就得需要我们想办法解决,我找了一些问题并首先思考,然后找到方法,在这里记录一下,未来有需要的同学可以拿走去用。

1. 在海量日志数据里,提取某天访问量最多的IP。

一般处理海量的思路都是分治处理,就是现将数据进行拆分,然后进行处理,排序等。这个例子也不例外,IPV4的地址一共32位,最大值为2^32也就是总数大约4G左右,如果放到内存里边,以目前的内存容量也是可以处理的,但是咱们可以为自己设置一些条件,比如目前没有那么多内存。

a) 首先分治,将这个文件按照IP的HASH分成1024份(如果想要均匀的分的算法需要使用一致性Hash算法),这样每个文件大约4M左右并且存放到磁盘上去。

b) 构建一个需要以IP为Key,出现次数为Value的TreeMap。读取每个文件,将IP和出现次数放入有序的TreeMap。

c) 这样就可以得到出现次数最多的IP,前N个出现次数多的IP都可以获取到了。

这种问题一般是TOP K的问题,思路都可以按照这样的思路去解决。当然这种场景比较合适的就是Map Reduce莫属了。另外,关于TOP K的这种排序的话可以采用最小堆排序(即根节点是最小的),它的时间复杂度为n*mlogm,n即为一共多少数据,m为取出前m个数据。关于这种结构不知道的同学可以进行谷歌搜索。分治的作用就是为了减少使用系统的资源,比如系统内容。

2. 上个问题是统计重复出现的个数,那么如何统计不重复的个数。比如:有个电话本,里边记录的电话号码都是8位数字,统计电话本里边有多少电话号码?这个里边肯定也是有一些局限的,比如内存限制。再比如再2.5亿整数中找到不重复的整数的个数,当然,内存中不能够存储着2.5亿数据。这种解决的思路一般是位图算法(bitMap)解决。

以电话号码为例:

a)电话号码是8位数字,也就是出现的数字应该为11111111-99999999,总数为99999999,咱们采用位图法(因为最省内存)。

b)一个bit位代表一个数字,那么这些数字共需要99999999个bit,占用内存为 99999999/8/1024/1024约等于11.92M,即如果这个数字所在的位有数据,那么这个bit位就设置为1,否则设置为0。

这样只需要12M的内存就可以统计这些数据了。当然2.5亿整数同理,在内存中所有整数的个数为2^32,一个数对应一个bit,大概需要512M内存就可以了,如果给的内存还不够的话,则需要再次进行拆分。

3. 还有一些与上边类似的,但是不太相同的,因为有重复的数(1、2、2、3、3、4,排好序的数并且偶数个的话,中位数是[2+3]/2=2.5 奇数个的话正好是中间的),比如在5亿int数中找到中位数。这个问题的解决思路其实采用双层桶划分思路。注意一个int占4个Byte,整数的最大位数为32位,那么我们将每个数转换为二进制,然后截取前多少位,要看内存大小。解决思路:

a) 把整数转为二进制数,然后截取前5位,那么总共分出2^5=32个区间,如果分出文件来共分出32个文件,如果内存不够的话,那么再继续截取(比如16位,这里举例)。比如:file_00000, file_00001等。

b) 如果截取完了,所有文件一共32个文件,因为都是二进制,所以文件是按照有序排好的。统计每个文件的个数,然后计算中位数所在的文件里。

c) 如果文件还是比较大,假设文件在最后一个文件,即前边2.5亿,最后一个文件2.5亿,文件名字为file_11111,那么再继续按照上边的方法继续拆分(比如再5位 文件名:file_11111_00000 等),知道内存中可以装下整个文件。

d) 可以装下整个文件下的话再进行排序,排好序之后,找到中间的数就是中位数。

4. 两个文件,各存放50亿条URL,每个URL占64字节。内存限制是4G,找出两个文件中相同的URL。这个问题有一个内存限制,那么肯定需要分治法。

方法一(分治+hash+hashset):

a) 50亿个64Byte= 5G*64Byte = 320G,内存4个G,肯定是不可以的。那么咱们将每个URL进行hash,然后放到1024个文件中,也就是每个文件为320G/1024=320M左右。以hash值作为文件名,第一个文件hash出来的文件命名为(hash[URL]%1024)a1…..a1024,第二个文件hash出来的文件命名为b1…..b1024。

b)1024个文件生成了,那么相同的URL肯定在hash命名文件的后缀中,比如a1 vs b1,这样依次读取文件的内容放入到hashset中,如果存在的话记录并且追加放到文件中。

c)  最后文件中就是所有URL即为相同的URL。

方法二(Bloom Filter布隆过滤器)

a) 先说一下布隆过滤器,主要将需要内容进行hash,然后对应到相应的bit上,即Bit Map位图法,但是这个里边有一个问题就是hash会碰撞,即不同的结果可能会hash成相同的值,这样就会出错。如果可以接受错误率,当然错误率较低,那么可以采用这种方式。4G内存=2^32 * 8 约等于 40亿Byte * 8 大约等于340亿。先遍历第一个文件,然后再遍历第二个,这样会错误率。

5. 有40亿个不重复的unsigned int的整数,没排过序,现在给一个数,如何快速判断这个数是否在这40亿个数当中。这个如果直接放到内存里边的话得需要2^32*4Byte(int 4Byte) = 4G *4 = 16G. 显然内存比较大了。

a) 这个也采用位图法,所需要的内存为  2*32Byte / 8 = 500M 内存,所以仅仅需要500M内存就可以放下这些数字了,然后查找就可以了。

6. 给定一个文件,里面最多含有n个不重复的正整数(也就是说可能含有少于n个不重复正整数),且其中每个数都小于等于n,n=10^7。输出:得到按从小到大升序排列的包含所有输入的整数的列表。条件:最多有大约1MB的内存空间可用,但磁盘空间足够。且要求运行时间在5分钟以下,10秒为最佳结果。

如果采用位图法的话需要为10^7 / 8 /1024/1024 大约等于1.19M,大于题目的1M,显然位图法不太合适,那么咱们考虑一下多路归并排序。

a)  首先将这个文件分批次读取拆分,比如一次读取256K,然后进行memory sort 在内存排序,写到文件中。假如文件大小是10M的大小,则需要循环40次,写入40个文件当中。

b)  然后将文件进行merge sort合并排序,创建一个数组40个长度,依次读取最小的文件,然后找到数组中最小的写入到文件当中,然后继续读取文件并且继续排序,将最小的再次写入文件即可。

6. 有10个文件,每个文件1G,每个文件的每一行都存放的是用户的搜索的关键字,每个文件的搜索的关键字都可能重复。找出热度高的前1000个搜索关键字。(提示分治+hash+trie树+最小堆)

看到这种问题的话,首先得考虑是否机器资源足够使用,如果足够使用的话,就直接加入内存,但是如果不够的话需要考虑分治。解决思路。

a) 将每个文件按关键字进行hash,然后拆分成100个文件,然后每个文件大概100M左右。(分治+hash)。

b) 读取每个小文件,并且将读取的关键字形成Trie树字典树,这样会达到去重的效果。Trie树的插入和查询复杂度是O(k), k为最长字符串的长度。然后建立长度为1000的小根堆,将遍历每个关键字的出现的次数放到小根堆里。

c) 以上一遍就可以得出第一个1G文件的结果,然后按照相同的原理继续以上步骤。

 

总结一下:

如果是大量数据不重复的,而且需要内存占用比较少的需要找出出现的内容的话,适合使用BitMap位图法进行处理。

还有就是一般的TOP K问题,就是找出前多少位的这种,一般内存容量都不是很大,采用的方式是 分治+hash+最小(大)堆排序。当然分布式的适合处理方式为MapReduce处理。

排序可以有很多种,按照不同的方式进行不同的排序,比如快排,最小堆排序,归并排序。如果大文件需要排序,并且严格要求内存的话,分治成小文件,然后采用归并排序很合适。

如果涉及到单词的类型处理的话,需要使用Trie树进行,因为这个非常合适处理,并且复杂度为O(k)。

 

如果有不对的地方,欢迎指正。

海量数据处理方法整理记录 – 黄青石 – 博客园已关闭评论
2019年2月15日 By mikel 分类: 架构设计
http://baa.im/847971

在文章开始之前首先要思考的问题是为什么要建立对象池。这和.NET垃圾回收机制有关,正如下面引用所说,内存不是无限的,垃圾回收器最终要回收对象,释放内存。尽管.NET为垃圾回收已经进行了大量优化,例如将

来源: Object Pooling(对象池)实现 – Zhang_Xiang – 博客园

在文章开始之前首先要思考的问题是为什么要建立对象池。这和.NET垃圾回收机制有关,正如下面引用所说,内存不是无限的,垃圾回收器最终要回收对象,释放内存。尽管.NET为垃圾回收已经进行了大量优化,例如将托管堆划分为 3 Generations(代)并设定新建的对象回收的最快,新建的短生命周期对象将进入 Gen 0(新建对象大于或等于 85,000 字节将被看作大对象,直接进入 Gen 2),而 Gen 0 通常情况下分配比较小的内存,因此Gen 0 将回收的非常快。而高频率进行垃圾回收导致 CPU 使用率过高,当 Gen 2 包含大量对象时,回收垃圾也将产生性能问题。

.NET 的垃圾回收器管理应用程序的内存分配和释放。 每当有对象新建时,公共语言运行时都会从托管堆为对象分配内存。 只要托管堆中有地址空间,运行时就会继续为新对象分配空间。 不过,内存并不是无限的。 垃圾回收器最终必须执行垃圾回收来释放一些内存。 垃圾回收器的优化引擎会根据所执行的分配来确定执行回收的最佳时机。 执行回收时,垃圾回收器会在托管堆中检查应用程序不再使用的对象,然后执行必要的操作来回收其内存。参考

构造对象池

.Net Core 在(Base Class Library)基础类型中添加了 ArrayPool,但 ArrayPool 只适用于数组。针对自定义对象,参考MSDN有一个实现,但没有初始化池大小,且从池里取对象的方式比较粗糙,完整的对象池应该包含:

  • 池大小
  • 初始化委托
  • 实例存取方式(FIFO、LIFO 等自定义方式,根据个人需求实现获取实例方式)
  • 获取实例策略

1. 定义对象存取接口,以实现多种存取策略,例如 FIFO、LIFO

/// <summary>
/// 对象存取方式
/// </summary>
public interface IAccessMode<T>
{
    /// <summary>
    /// 租用对象
    /// </summary>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    T Rent();
    
    /// <summary>
    /// 返回实例
    /// </summary>
    /// <param name="item"></param>
    void Return(T item);
}

2. 实现存取策略

FIFO

FIFO通过Queue实现,参考

public sealed class FIFOAccessMode<T> : Queue<T>, IAccessMode<T>
{
    private readonly int _capacity;
    private readonly Func<T> _func;
    private int _count;

    public FIFOAccessMode(int capacity, Func<T> func) : base(capacity)
    {
        _capacity = capacity;
        _func = func;
        InitialQueue();
    }

    public T Rent()
    {
        Interlocked.Increment(ref _count);
        return _capacity < _count ? _func.Invoke() : Dequeue();
    }

    public void Return(T item)
    {
        if (_count > _capacity)
        {
            var disposable = (IDisposable)item;
            disposable.Dispose();
        }
        else
        {
            Enqueue(item);
        }
        Interlocked.Decrement(ref _count);
    }

    private void InitialQueue()
    {
        for (var i = 0; i < _capacity; i++)
        {
            Enqueue(_func.Invoke());
        }
    }
}
LIFO

在LIFO中借助Stack特性实现进栈出栈,因此该策略继承自Stack,参考

public sealed class LIFOAccessModel<T> : Stack<T>, IAccessMode<T>
{
    private readonly int _capacity;
    private readonly Func<T> _func;
    private int _count;

    public LIFOAccessModel(int capacity, Func<T> func) : base(capacity)
    {
        _capacity = capacity;
        _func = func;
        InitialStack();
    }

    public T Rent()
    {
        Interlocked.Increment(ref _count);
        return _capacity < _count ? _func.Invoke() : Pop();
    }

    public void Return(T item)
    {
        if (_count > _capacity)
        {
            var disposable = (IDisposable)item;
            disposable.Dispose();
        }
        else
        {
            Push(item);
        }
        Interlocked.Decrement(ref _count);
    }

    private void InitialStack()
    {
        for (var i = 0; i < _capacity; i++)
        {
            Push(_func.Invoke());
        }
    }
}

注意:以上两个实现都遵循池容量不变原则,但租用的实例可以超过对象池大小,返还时还将检测该实例直接释放还是进入池中。而如何控制池大小和并发将在下面说明。

3.Pool实现

public class Pool<T> : IDisposable where T : IDisposable
{
    private int _capacity;
    private IAccessMode<T> _accessMode;
    private readonly object _locker = new object();
    private readonly Semaphore _semaphore;

    public Pool(AccessModel accessModel, int capacity, Func<T> func)
    {
        _capacity = capacity;
        _semaphore = new Semaphore(capacity, capacity);
        InitialAccessMode(accessModel, capacity, func);
    }

    private void InitialAccessMode(AccessModel accessModel, int capacity, Func<T> func)
    {
        switch (accessModel)
        {
            case AccessModel.FIFO:
                _accessMode = new FIFOAccessMode<T>(capacity, func);
                break;
            case AccessModel.LIFO:
                _accessMode = new LIFOAccessModel<T>(capacity, func);
                break;
            default:
                throw new NotImplementedException();
        }
    }

    public T Rent()
    {
        _semaphore.WaitOne();
        return _accessMode.Rent();
    }

    public void Return(T item)
    {
        _accessMode.Return(item);
        _semaphore.Release();
    }

    public void Dispose()
    {
        if (!typeof(IDisposable).IsAssignableFrom(typeof(T))) return;

        lock (_locker)
        {
            while (_capacity > 0)
            {
                var disposable = (IDisposable)_accessMode.Rent();
                _capacity--;
                disposable.Dispose();
            }

            _semaphore.Dispose();
        }
    }
}

在Pool中如何控制程序池并发,这里我们引入了 Semaphore 以控制并发,这里将严格控制程序池大小,避免内存溢出。

4.使用

Student 类用作测试

public class Student : IDisposable
{
    public string Name { get; set; }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private bool _disposed;

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            Name = null;
             //Free any other managed objects here.
        }

        _disposed = true;
    }
}
public void TestPool()
{
    Func<Student> func = NewStudent;
    var pool = new Pool<Student>(AccessModel.FIFO, 2, func);
    for (var i = 0; i < 3; i++)
    {
        Student temp = pool.Rent();
        //todo:Some operations
        pool.Return(temp);
    }

    Student temp1 = pool.Rent();

    pool.Return(temp1);

    pool.Dispose();
}

public Student NewStudent()
{
    return new Student();
}

总结:至此,一个完整的对象池建立完毕。

Object Pooling(对象池)实现 – Zhang_Xiang – 博客园已关闭评论
2019年2月14日 By mikel 分类: 架构设计
http://baa.im/847971

来源: ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(一) 基本模型以及数据库的建立 – NanaseRuri – 博客园

前言:

本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。

本系列文章主要参考资料:

微软文档:https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/?view=aspnetcore-2.1&tabs=windows

《Pro ASP.NET MVC 5》、《锋利的 JQuery

 

当此系列文章写完后会在一周内推出修正版。

 

此系列皆使用 VS2017+C# 作为开发环境。如果有什么问题或者意见欢迎在留言区进行留言。

项目 github 地址:https://github.com/NanaseRuri/LibraryDemo

 

 

本章内容:对图书馆系统组成的简要分析。以及对域模型以及相应数据库的建立。

知识点:Code First、EF 基本使用方法、ASP.NET Core 使用 EF Core 的配置方法、EF 多对多关系的建立。

 

 

一、对图书馆系统域模型的分析

一个图书馆系统需要有管理员、 学生、书架以及书籍

 

域模型,即用来存储数据的模型。

在此域模型可以用以下结构创建:

 

 

 

 二、项目结构

然后就可以开始建立该项目了:

 

 

 

三、建立域模型

学位枚举:

复制代码
1     public enum Degrees
2     {
3         [Display(Name = "本科生")]
4         CollegeStudent,
5         [Display(Name = "研究生")]
6         Postgraduate,
7         [Display(Name = "博士生")]
8         DoctorateDegree
9     }
复制代码

 

图书借阅状态枚举:

复制代码
 1     public enum BookState
 2     {
 3         /// <summary>
 4         /// 可借阅
 5         /// </summary>
 6         [Display(Name = "正常")]
 7         Normal,
 8 
 9         /// <summary>
10         /// 馆内阅览
11         /// </summary>
12         [Display(Name = "馆内阅览")]
13         Readonly,
14 
15         /// <summary>
16         /// 已借出
17         /// </summary>
18         [Display(Name = "已借出")]
19         Borrowed,
20 
21         /// <summary>
22         /// 被续借
23         /// </summary>
24         [Display(Name = "被续借")]
25         ReBorrowed,
26 
27         /// <summary>
28         /// 被预约
29         /// </summary>
30         [Display(Name = "被预约")]
31         Appointed,
32 
33         [Display(Name = "过期")]
34         Expired
35     }
复制代码

 

该项目准备使用一个数据库存储学生账户信息,另一个则用于存储学生借书信息:

学生账户信息:

复制代码
 1     public class Student : IdentityUser
 2     {
 3         /// <summary>
 4         /// 学号
 5         /// </summary>
 6         [ProtectedPersonalData]
 7         [RegularExpression("[UIA]\\d{9}")]
 8         [Display(Name = "学号")]
 9         public override string UserName { get; set; }
10 
11         [Display(Name = "手机号")]
12         [StringLength(14, MinimumLength = 11)]
13         public override string PhoneNumber { get; set; }
14 
15         [Display(Name = "姓名")]
16         public string Name { get; set; }
17         [Display(Name = "学历")]
18         public Degrees Degree { get; set; }
19         [Display(Name = "最大借书数目")]
20         public int MaxBooksNumber { get; set; }
21     }
复制代码

 

书籍信息:

复制代码
 1     public class Book
 2     {                                
 3         /// <summary>
 4         /// 二维码
 5         /// </summary>
 6         [Key]
 7         [Display(Name = "二维码")]
 8         [Required(ErrorMessage = "未填写二维码")]
 9         public string BarCode { get; set; }
10 
11         public string ISBN { get; set; }
12 
13         /// <summary>
14         /// 书名
15         /// </summary>
16         [Display(Name = "书名")]
17         public string Name { get; set; }
18 
19         /// <summary>
20         /// 取书号
21         /// </summary>
22         [Display(Name = "取书号")]
23         public string FetchBookNumber { get; set; }
24 
25         /// <summary>
26         /// 所在书架
27         /// </summary>
28         public Bookshelf Bookshelf { get; set; }
29 
30         [Display(Name = "书架号")]
31         public int BookshelfId { get; set; }
32 
33         /// <summary>
34         /// 借出时间
35         /// </summary>
36         [Display(Name = "借出时间")]
37         public DateTime? BorrowTime { get; set; }
38 
39         /// <summary>
40         /// 到期时间
41         /// </summary>
42         [Display(Name = "到期时间")]
43         public DateTime? MatureTime { get; set; }
44 
45         /// <summary>
46         /// 预约最晚借书日期
47         /// </summary>
48         [Display(Name = "预约取书时间")]
49         public DateTime? AppointedLatestTime { get; set; }
50 
51         /// <summary>
52         /// 借阅状态
53         /// </summary>
54         [Display(Name = "书籍状态")]
55         public BookState State { get; set; }
56 
57         /// <summary>
58         /// 持有者,指定外键
59         /// </summary>
60         public StudentInfo Keeper { get; set; }
61         [Display(Name = "持有者学号")]
62         public string KeeperId{ get; set; }
63 
64         [Display(Name = "位置")]
65         public string Location { get; set; }
66 
67         [Display(Name = "分类")]
68         public string Sort { get; set; }
69 
70         public ICollection<AppointmentOrLending> Appointments { get; set; }
71     }
复制代码

 

书架信息:

复制代码
 1     public class Bookshelf
 2     {
 3         /// <summary>
 4         /// 书架ID
 5         /// </summary>
 6         [Key]
 7         //不自动增长
 8         [DatabaseGenerated(DatabaseGeneratedOption.None)] 
 9         public int BookshelfId { get; set; }
10 
11         /// <summary>
12         /// 书架的书籍类别
13         /// </summary>
14 
15         [Required]
16         public string Sort { get; set; }               
17         /// <summary>
18         /// 最小取书号
19         /// </summary>
20         [Required]
21         public string MinFetchNumber { get; set; }
22         [Required]
23         public string MaxFetchNumber { get; set; }
24 
25         /// <summary>
26         /// 书架位置
27         /// </summary>
28         [Required]
29         public string Location { get; set; }
30 
31         /// <summary>
32         /// 全部藏书
33         /// </summary>
34         public ICollection<Book> Books { get; set; }
35     }
复制代码

 

 

 

由于一个学生可以借阅多本书籍,一本书籍可被多人预约,因此书籍和学生具有多对多的关系,在此引入中间类:

其中的 AppointingDateTime 用来区分中间类包含的书籍是借阅书籍还是预约书籍:

复制代码
1     public class AppointmentOrLending
2     {
3         public Book Book { get; set; }
4         public string BookId { get; set; }
5         public StudentInfo Student { get; set; }
6         public string StudentId { get; set; }
7         public DateTime? AppointingDateTime { get; set; }
8     }
复制代码

 

学生借书信息:

在 EF 中多对多关系实际上是两个多对一关系。此处 ICollection 的属性成为导航属性,用来提示 EF  StudentInfo 和 AppointmentOrLending 之间存在着多对一的关系。

复制代码
 1     public class StudentInfo
 2     {
 3         [Key]
 4         public string UserName { get; set; }
 5 
 6         [Required]
 7         public string Name { get; set; }
 8 
 9         /// <summary>
10         /// 学位,用来限制借书数目
11         /// </summary>
12         [Required]
13         public Degrees Degree { get; set; }
14 
15         /// <summary>
16         /// 最大借书数目
17         /// </summary>
18         [Required]
19         public int MaxBooksNumber { get; set; }
20 
21         /// <summary>
22         /// 已借图书
23         /// </summary>
24         public ICollection<AppointmentOrLending> KeepingBooks { get; set; }
25 
26         public string AppointingBookBarCode { get; set; }
27 
28         [StringLength(14, MinimumLength = 11)]
29         public string PhoneNumber { get; set; }
30 
31         /// <summary>
32         /// 罚款
33         /// </summary>
34         public decimal Fine { get; set; }               
35     }
复制代码

 

外借/阅览书籍信息:

在约定中,若不指定主键,则 EF 会使用 (类名)+ID 的方式指定或创建主键,在此使用 [Key] 指定主键,使用 [Required] 指定字段为必须,这种可以为属性添加在数据库中的约束或者在视图中的约束的修饰称为 DataAnnotations 。

此处 ICollection 的属性成为导航属性,用来提示 EF  Book 和 AppointmentOrLending 之间存在着多对一的关系。

复制代码
 1     public class Book
 2     {                                
 3         /// <summary>
 4         /// 二维码
 5         /// </summary>
 6         [Key]
 7         [Display(Name = "二维码")]
 8         [Required(ErrorMessage = "未填写二维码")]
 9         public string BarCode { get; set; }
10 
11         public string ISBN { get; set; }
12 
13         /// <summary>
14         /// 书名
15         /// </summary>
16         [Display(Name = "书名")]
17         public string Name { get; set; }
18 
19         /// <summary>
20         /// 取书号
21         /// </summary>
22         [Display(Name = "取书号")]
23         public string FetchBookNumber { get; set; }
24 
25         /// <summary>
26         /// 所在书架
27         /// </summary>
28         public Bookshelf Bookshelf { get; set; }
29 
30         [Display(Name = "书架号")]
31         public int BookshelfId { get; set; }
32 
33         /// <summary>
34         /// 借出时间
35         /// </summary>
36         [Display(Name = "借出时间")]
37         public DateTime? BorrowTime { get; set; }
38 
39         /// <summary>
40         /// 到期时间
41         /// </summary>
42         [Display(Name = "到期时间")]
43         public DateTime? MatureTime { get; set; }
44 
45         /// <summary>
46         /// 预约最晚借书日期
47         /// </summary>
48         [Display(Name = "预约取书时间")]
49         public DateTime? AppointedLatestTime { get; set; }
50 
51         /// <summary>
52         /// 借阅状态
53         /// </summary>
54         [Display(Name = "书籍状态")]
55         public BookState State { get; set; }
56 
57         /// <summary>
58         /// 持有者,指定外键
59         /// </summary>
60         public StudentInfo Keeper { get; set; }
61         [Display(Name = "持有者学号")]
62         public string KeeperId{ get; set; }
63 
64         [Display(Name = "位置")]
65         public string Location { get; set; }
66 
67         [Display(Name = "分类")]
68         public string Sort { get; set; }
69 
70         public ICollection<AppointmentOrLending> Appointments { get; set; }
71     }
复制代码

 

 

 

四、创建 DbContext 

学生账户信息数据库:

1     public class StudentIdentityDbContext:IdentityDbContext<Student>
2     {
3         public StudentIdentityDbContext(DbContextOptions<StudentIdentityDbContext> options) : base(options)
4         {
5         }
6     }

 

借阅信息数据库:

为了使 StudentInfo 类的 UserName 和 Book 的 BarCode 共同作为 AppointmentOrLending 中间类的主键,需覆写 OnModelCreating 方法:

至此 StudentInfo 和 Book 的多对多关系正式确立。

复制代码
 1     public class LendingInfoDbContext:DbContext
 2     {
 3         public LendingInfoDbContext(DbContextOptions<LendingInfoDbContext> options) : base(options)
 4         {
 5         }
 6 
 7         public DbSet<Book> Books { get; set; }
 8         public DbSet<BookDetails> BooksDetail { get; set; }
 9         public DbSet<Bookshelf> Bookshelves { get; set; }
10         public DbSet<RecommendedBook> RecommendedBooks { get; set; }
11         public DbSet<StudentInfo> Students { get; set; }
12         public DbSet<AppointmentOrLending> AppointmentOrLendings { get; set; }
13 
14         protected override void OnModelCreating(ModelBuilder modelBuilder)
15         {
16             base.OnModelCreating(modelBuilder);
17             modelBuilder.Entity<AppointmentOrLending>()
18                 .HasKey(c => new { c.BookId, c.StudentId });
19         }
20     }
复制代码

于是 Book 和 StudentInfo 之间的多对多关系确立完成。

 

 

 

五、根据约定配置数据库,进行依赖注入

在  appsettings.json 中添加数据库连接字符串。

复制代码
 1 {
 2   "ConnectionStrings": {
 3     "LendingInfoDbContext": "Server=(localdb)\\mssqllocaldb;Database=LendingInfoDbContext;Trusted_Connection=True;MultipleActiveResultSets=true",
 4     "StudentIdentityDbContext": "Server=(localdb)\\mssqllocaldb;Database=StudentIdentityDbContext;Trusted_Connection=True;MultipleActiveResultSets=true"
 5   },
 6   "Logging": {
 7     "LogLevel": {
 8       "Default": "Warning"
 9     }
10   },
11   "AllowedHosts": "*"
12 }
复制代码

 

在 Startup.cs 中的 ConfigureServices 方法中对数据库进行配置:

复制代码
1             services.AddDbContext<LendingInfoDbContext>(options =>
2             {
3                 options.UseSqlServer(Configuration.GetConnectionString("LendingInfoDbContext"));
4             });
5             services.AddDbContext<StudentIdentityDbContext>(options =>
6             {
7                 options.UseSqlServer(Configuration.GetConnectionString("StudentIdentityDbContext"));
8             });
复制代码

 

 

 

六、数据库的迁移、创建及更新

然后在 pm控制台 中添加迁移:

添加迁移的语法为 add-migration <迁移类名> -c <具体 DbContext 名>

1       cd LibraryDemo
2       add-migration LendingInfo -c LibraryDemo.Data.LendingInfoDbContext
3       add-migration StudentIdentity -c LibraryDemo.Data.StudentIdentityDbContext

 

运行 add-migration 命令会创建 Migrations 文件夹以及相应的迁移快照:

 

显示的类名为 <创建时间>_<迁移类名>,而实际的类名为 add-migration 后的第一个参数名。

 

在创建迁移时,EF 会自动为我们创建或更新对应 DbContext 的快照,即其中后缀为 Snapshot 的类。其中会包含当前对应的 DbCOntext 的结构,并会以代码保留相应的约束,如 LendingInfoDbContextModelSnapshot 类:

 

生成的迁移类 LendingInfo 和 Account 类则有两个方法—— 用于更新数据库的 Up 方法和用以回溯数据库的 Down 方法,可以在这两个方法或者在快照的 BuildModel 方法中使用 Fluent API 对数据库做进一步的改动,并且通过对 Fluent API 的使用可以使我们的类少用 DataAnnotations 以保证类的整洁。

需要注意的是,生成的迁移类中的 Up 和 Down 方法是根据生成迁移之前的数据库快照生成的,如我在之后为 LendingInfoDbContext 添加 DbSet<RecommendedBook> 时,在以上的基础上运行了 add-migration AddRecommendedBook -c LibraryDemo.Data.LendingInfoDbContext ,生成的 Up 方法只包括添加表 RecommendedBooks 的行为,而 Down 方法只包括删除表 RecommendedBooks 的行为。

 

 

随后在 pm控制台 执行以下创建或更新数据库:

1      update-database -c LibraryDemo.Data.LendingInfoDbContext
2      update-database -c LibraryDemo.Data.StudentIdentityDbContext

 

 

最后在 SQL server对象管理器 中可以看见创建的数据库以及对应的表:

 

 

至此域模型创建工作完成。

 

 

 

 

补充:

使用命令行对数据库进行迁移及更新有两种方式:

1  dotnet ef migrations migrationName -c TargetContext
2  dotnet ef database update -c TargetContext

 

1  add-migration migrationName -c TargetContext
2  update-Database -c TargetContext

 

windows 命令行命令不区分大小写,其中 migrationName 为迁移类名,最好提供有意义的命名;而 TargetContext 为目标 DbContext 类名,需要使用带有命名空间的完全命名。

如果需要删除数据库则使用 drop 方法

drop-database -c TargetContext

 

而为 update 方法指定迁移类则可以回溯数据库。

Update-Database LendingInfoDbContext -TargetMigration:"20181127081115_LendingInfo.cs"
ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(一) 基本模型以及数据库的建立 – NanaseRuri – 博客园已关闭评论
2019年2月14日 By mikel 分类: ASP.NET, 架构设计
http://baa.im/847971

基本登录页面以及授权逻辑的建立

来源: ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(二)用户数据库初始化、基本登录页面以及授权逻辑的建立 – NanaseRuri – 博客园

前言:

本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。

本系列文章主要参考资料:

微软文档:https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/?view=aspnetcore-2.1&tabs=windows

《Pro ASP.NET MVC 5》、《锋利的 JQuery

 

当此系列文章写完后会在一周内推出修正版。

 

此系列皆使用 VS2017+C# 作为开发环境。如果有什么问题或者意见欢迎在留言区进行留言。

项目 github 地址:https://github.com/NanaseRuri/LibraryDemo

 

 

本章内容:Identity 框架的配置、对账户进行授权的配置、数据库的初始化方法、自定义 TagHelper

 

 

 一到四为对 Student 即 Identity框架的使用,第五节为对 Admin 用户的配置

 

 

一、自定义账号和密码的限制

在 Startup.cs 的 ConfigureServices 方法中可以对 Identity 的账号和密码进行限制:

复制代码
 1             services.AddIdentity<Student, IdentityRole>(opts =>
 2             {
 3 
 4                 opts.User.RequireUniqueEmail = true;
 5                 opts.User.AllowedUserNameCharacters = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789";
 6                 opts.Password.RequiredLength = 6;
 7                 opts.Password.RequireNonAlphanumeric = false;
 8                 opts.Password.RequireLowercase = false;
 9                 opts.Password.RequireUppercase = false;
10                 opts.Password.RequireDigit = false;
11             }).AddEntityFrameworkStores<StudentIdentityDbContext>()
12                 .AddDefaultTokenProviders();
复制代码

RequireUniqueEmail 限制每个邮箱只能用于一个账号。

此处 AllowedUserNameCharacters 方法限制用户名能够使用的字符,需要单独输入每个字符。

剩下的设置分别为限制密码必须有符号 / 包含小写字母 / 包含大写字母 / 包含数字。

 

 

 

二、对数据库进行初始化

在此创建一个 StudentInitiator 用以对数据库进行初始化:

复制代码
 1     public class StudentInitiator
 2     {
 3         public static async Task Initial(IServiceProvider serviceProvider)
 4         {
 5             UserManager<Student> userManager = serviceProvider.GetRequiredService<UserManager<Student>>();
 6             if (userManager.Users.Any())
 7             {
 8                 return;
 9             }
10             IEnumerable<Student> initialStudents = new[]
11             {
12                 new Student()
13                 {
14                     UserName = "U201600001",
15                     Name = "Nanase",
16                     Email = "Nanase@cnblog.com",
17                     PhoneNumber = "12345678910",
18                     Degree = Degrees.CollegeStudent,
19                     MaxBooksNumber = 10,
20                 },
21                 new Student()
22                 {
23                     UserName = "U201600002",
24                     Name = "Ruri",
25                     Email = "NanaseRuri@cnblog.com",
26                     PhoneNumber = "12345678911",
27                     Degree = Degrees.DoctorateDegree,
28                     MaxBooksNumber = 15
29                 },
30             };
31 
32             foreach (var student in initialStudents)
33             {
34                 await userManager.CreateAsync(student, student.UserName.Substring(student.UserName.Length - 6,6));
35             }
36         } 
37        }
复制代码

 

为确保能够进行初始化,在 Startup.cs 的 Configure 方法中调用该静态方法:

复制代码
1             app.UseMvc(routes =>
2             {
3                 routes.MapRoute(
4                     name: "default",
5                     template: "{controller=Home}/{action=Index}/{id?}");
6             });
7             DatabaseInitiator.Initial(app.ApplicationServices).Wait();
复制代码

 

Initial 方法中 serviceProvider 参数将在传入 ConfigureServices 方法调用后的 ServiceProvider,此时在 Initial 方法中初始化的数据也会使用 ConfigureServices 中对账号和密码的限制。

此处我们使用账号的后六位作为密码。启动网页后查看数据库的数据:

 

 

 

 

三、建立验证所用的控制器以及视图

 

首先创建一个视图模型用于存储账号的信息,为了方便实现多种登录方式,此处创建一个 LoginType 枚举:

[UIHint] 特性构造函数传入一个字符串用来告知对应属性在使用 Html.EditorFor() 时用什么模板来展示数据。

复制代码
 1     public enum LoginType
 2     {
 3         UserName,
 4         Email,
 5         Phone
 6     }
 7 
 8     public class LoginModel
 9     {
10         [Required(ErrorMessage = "请输入您的学号 / 邮箱 / 手机号码")]
11         [Display(Name = "学号 / 邮箱 / 手机号码")]
12         public string Account { get; set; }
13 
14         [Required(ErrorMessage = "请输入您的密码")]
15         [UIHint("password")]
16         [Display(Name = "密码")]
17         public string Password { get; set; }
18 
19         [Required]
20         public LoginType LoginType { get; set; }
21     }
复制代码

 

   使用支架特性创建一个 StudentAccountController

 

StudentAccount 控制器:

第 5 行判断是否授权以避免多余的授权:

复制代码
 1      public class StudentAccountController : Controller
 2      {
 3         public IActionResult Login(string returnUrl)
 4         {
 5             if (HttpContext.User.Identity.IsAuthenticated)
 6             {
 7                 return RedirectToAction("AccountInfo");
 8             }
 9 
10             LoginModel loginInfo = new LoginModel();
11             ViewBag.returnUrl = returnUrl;
12             return View(loginInfo);
13         }
14     }
复制代码

 

在在 Login 视图中添加多种登录方式,并使视图更加清晰,创建了一个 LoginTypeTagHelper ,TagHelper 可制定自定义 HTML 标记并在最终生成视图时转换成标准的 HTML 标记。

复制代码
 1     [HtmlTargetElement("LoginType")]
 2     public class LoginTypeTagHelper:TagHelper
 3     {
 4         public string[] LoginType { get; set; }
 5 
 6         public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
 7         {
 8             foreach (var loginType in LoginType)
 9             {
10                 switch (loginType)
11                 {
12                     case "UserName": output.Content.AppendHtml($"<option selected=\"selected/\" value=\"{loginType}\">学号</option>");
13                         break;
14                     case "Email": output.Content.AppendHtml(GetOption(loginType, "邮箱"));
15                         break;
16                     case "Phone": output.Content.AppendHtml(GetOption(loginType, "手机号码"));
17                         break;
18                     default: break;
19                 }                
20             }            
21             return Task.CompletedTask;
22         }
23 
24         private static string GetOption(string loginType,string innerText)
25         {
26             return $"<option value=\"{loginType}\">{innerText}</option>";
27         }
28     }
复制代码

 

Login 视图:

25 行中使用了刚建立的 LoginTypeTagHelper:

复制代码
 1 @model LoginModel
 2 
 3 @{
 4     ViewData["Title"] = "Login";
 5 }
 6 
 7 <h2>Login</h2>
 8 <br/>
 9 <div class="text-danger" asp-validation-summary="All"></div>
10 <br/>
11 <form asp-action="Login" method="post">
12     <input type="hidden" name="returnUrl" value="@ViewBag.returnUrl"/>
13     <div class="form-group">   
14         <label asp-for="Account"></label>
15         <input asp-for="Account" class="form-control" placeholder="请输入你的学号 / 邮箱 / 手机号"/>
16     </div>
17     <div class="form-group">   
18         <label asp-for="Password"></label>
19         <input asp-for="Password" class="form-control" placeholder="请输入你的密码"/>
20     </div>
21     <div class="form-group">
22         <label>登录方式</label>
23         <select asp-for="LoginType">
24             <option disabled value="">登录方式</option>
25             <LoginType login-type="@Enum.GetNames(typeof(LoginType))"></LoginType>
26         </select>
27     </div>
28     <input type="submit" class="btn btn-primary"/>
29 </form>
复制代码

 

 

然后创建一个用于对信息进行验证的动作方法。

 

为了获取数据库的数据以及对数据进行验证授权,需要通过 DI(依赖注入) 获取对应的 UserManager 和 SignInManager 对象,在此针对 StudentAccountController 的构造函数进行更新。

StudentAccountController 整体:

复制代码
 1     [Authorize]
 2     public class StudentAccountController : Controller
 3     {
 4         private UserManager<Student> _userManager;
 5         private SignInManager<Student> _signInManager;
 6 
 7         public StudentAccountController(UserManager<Student> studentManager, SignInManager<Student> signInManager)
 8         {
 9             _userManager = studentManager;
10             _signInManager = signInManager;
11         }
12 
13         [AllowAnonymous]
14         public IActionResult Login(string returnUrl)
15         {
16             if (HttpContext.User.Identity.IsAuthenticated)
17             {
18                 return RedirectToAction("AccountInfo");
19             }
20 
21             LoginModel loginInfo = new LoginModel();
22             ViewBag.returnUrl = returnUrl;
23             return View(loginInfo);
24         }
25 
26         [HttpPost]
27         [ValidateAntiForgeryToken]
28         [AllowAnonymous]
29         public async Task<IActionResult> Login(LoginModel loginInfo, string returnUrl)
30         {
31             if (ModelState.IsValid)
32             {
33                 Student student =await GetStudentByLoginModel(loginInfo);
34 
35                 if (student == null)
36                 {
37                     return View(loginInfo);
38                 }
39                 SignInResult signInResult = await _signInManager.PasswordSignInAsync(student, loginInfo.Password, false, false);
40 
41                 if (signInResult.Succeeded)
42                 {
43                     return Redirect(returnUrl ?? "/StudentAccount/"+nameof(AccountInfo));
44                 }
45 
46                 ModelState.AddModelError("", "账号或密码错误");
47                             
48             }
49 
50             return View(loginInfo);
51         }
52 
53         public IActionResult AccountInfo()
54         {
55             return View(CurrentAccountData());
56         }
57 
58         Dictionary<string, object> CurrentAccountData()
59         {
60             var userName = HttpContext.User.Identity.Name;
61             var user = _userManager.FindByNameAsync(userName).Result;
62 
63             return new Dictionary<string, object>()
64             {
65                 ["学号"]=userName,
66                 ["姓名"]=user.Name,
67                 ["邮箱"]=user.Email,
68                 ["手机号"]=user.PhoneNumber,
69             };
70         }
71     }
复制代码

_userManager 以及  _signInManager 将通过 DI 获得实例;[ValidateAntiForgeryToken] 特性用于防止 XSRF 攻击;returnUrl 参数用于接收或返回之前正在访问的页面,在此处若 returnUrl 为空则返回 AccountInfo 页面;[Authorize] 特性用于确保只有已授权的用户才能访问对应动作方法;CurrentAccountData 方法用于获取当前用户的信息以在 AccountInfo 视图中呈现。

 

由于未进行授权,在此直接访问 AccountInfo 方法默认会返回 /Account/Login 页面请求验证,可通过在 Startup.cs 的 ConfigureServices 方法进行配置以覆盖这一行为,让页面默认返回 /StudentAccount/Login :

1             services.ConfigureApplicationCookie(opts =>
2             {
3                 opts.LoginPath = "/StudentAccount/Login";
4             }

 

为了使 [Authorize] 特性能够正常工作,需要在 Configure 方法中使用 Authentication 中间件,如果没有调用 app.UseAuthentication(),则访问带有 [Authorize] 的方法会再度要求进行验证。中间件的顺序很重要:

1             app.UseAuthentication();
2             app.UseHttpsRedirection();
3             app.UseStaticFiles();
4             app.UseCookiePolicy();

 

直接访问 AccountInfo 页面:

 

输入账号密码进行验证:

 

验证之后返回 /StudentAccount/AccountInfo 页面:

 

 

 

四、创建登出网页

简单地调用 SignOutAsync 用以清除当前 Cookie 中的授权信息。

复制代码
 1         public async Task<IActionResult> Logout(string returnUrl)
 2         {
 3             await _signInManager.SignOutAsync();
 4             if (returnUrl == null)
 5             {
 6                 return View("Login");
 7             }
 8 
 9             return Redirect(returnUrl);
10         }
复制代码

 

同时在 AccountInfo 添加登出按钮:

复制代码
 1     @model Dictionary<string, object>
 2     @{
 3         ViewData["Title"] = "AccountInfo";
 4     }
 5     <h2>账户信息</h2>
 6     <ul>
 7         @foreach (var info in Model)
 8         {
 9             <li>@info.Key: @Model[info.Key]</li>
10         }
11     </ul>
12     <br />
13     <a class="btn btn-danger" asp-action="Logout">登出</a>
复制代码

 

 

 

登出后返回 Login 页面,同时 AccountInfo 页面需要重新进行验证。

 

附加使用邮箱以及手机号验证的测试:

 

 

 

 

 

五、基于 Role 的 Identity 授权

修改 StudentInitial 类,添加名为 admin 的学生数组并使用 AddToRoleAsync 为用户添加身份。在添加 Role 之前需要在 RoleManager 对象中使用 Create 方法为 Role 数据库添加特定的 Role 字段:

复制代码
 1     public class StudentInitiator
 2     {
 3         public static async Task InitialStudents(IServiceProvider serviceProvider)
 4         {
 5             UserManager<Student> userManager = serviceProvider.GetRequiredService<UserManager<Student>>();
 6             RoleManager<IdentityRole> roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
 7             if (userManager.Users.Any())
 8             {
 9                 return;
10             }
11 
12             if (await roleManager.FindByNameAsync("Admin")==null)
13             {
14                 await roleManager.CreateAsync(new IdentityRole("Admin"));
15             }
16 
17             if (await roleManager.FindByNameAsync("Student")==null)
18             {
19                 await roleManager.CreateAsync(new IdentityRole("Student"));
20             }
21 
22             IEnumerable<Student> initialStudents = new[]
23             {
24                 new Student()
25                 {
26                     UserName = "U201600001",
27                     Name = "Nanase",
28                     Email = "Nanase@cnblog.com",
29                     PhoneNumber = "12345678910",
30                     Degree = Degrees.CollegeStudent,
31                     MaxBooksNumber = 10,
32                 },
33                 new Student()
34                 {
35                     UserName = "U201600002",
36                     Name = "Ruri",
37                     Email = "NanaseRuri@cnblog.com",
38                     PhoneNumber = "12345678911",
39                     Degree = Degrees.DoctorateDegree,
40                     MaxBooksNumber = 15
41                 }
42             };
43 
44             IEnumerable<Student> initialAdmins = new[]
45             {
46                 new Student()
47                 {
48                     UserName = "A000000000",
49                     Name="Admin0000",
50                     Email = "Admin@cnblog.com",
51                     PhoneNumber = "12345678912",
52                     Degree = Degrees.CollegeStudent,
53                     MaxBooksNumber = 20
54                 },
55                 new Student()
56                 {
57                     UserName = "A000000001",
58                     Name = "Admin0001",
59                     Email = "123456789@qq.com",
60                     PhoneNumber = "12345678910",
61                     Degree = Degrees.CollegeStudent,
62                     MaxBooksNumber = 20
63                 },
64             };
65             foreach (var student in initialStudents)
66             {
67                 await userManager.CreateAsync(student, student.UserName.Substring(student.UserName.Length - 6, 6));
68             }
69             foreach (var admin in initialAdmins)
70             {
71                 await userManager.CreateAsync(admin, "zxcZXC!123");
72                 await userManager.AddToRoleAsync(admin, "Admin");
73             }
74         }
75     }
复制代码

 

对 ConfigureServices 作进一步配置,添加 Cookie 的过期时间和不满足 Authorize 条件时返回的 Url:

复制代码
1             services.ConfigureApplicationCookie(opts =>
2             {
3                 opts.Cookie.HttpOnly = true;
4                 opts.LoginPath = "/StudentAccount/Login";
5                 opts.AccessDeniedPath = "/StudentAccount/Login";
6                 opts.ExpireTimeSpan=TimeSpan.FromMinutes(5);
7             });
复制代码

则当 Role 不为 Admin 时将返回 /StudentAccount/Login 而非默认的 /Account/AccessDeny。

 

 

然后新建一个用以管理学生信息的 AdminAccount 控制器,设置 [Authorize] 特性并指定 Role 属性,使带有特定 Role 的身份才可以访问该控制器。

复制代码
 1     [Authorize(Roles = "Admin")]
 2     public class AdminAccountController : Controller
 3     {
 4         private UserManager<Student> _userManager;
 5 
 6         public AdminAccountController(UserManager<Student> userManager)
 7         {
 8             _userManager = userManager;
 9         }
10 
11         public IActionResult Index()
12         {
13             ICollection<Student> students = _userManager.Users.ToList();
14             return View(students);
15         }
16     }
复制代码

 

Index 视图:

复制代码
  1 @using LibraryDemo.Models.DomainModels
  2 @model IEnumerable<LibraryDemo.Models.DomainModels.Student>
  3 @{
  4     ViewData["Title"] = "AccountInfo";
  5     Student stu = new Student();
  6 }
  7 <link rel="stylesheet" href="~/css/BookInfo.css" />
  8 
  9 <script>
 10     function confirmDelete() {
 11         var userNames = document.getElementsByName("userNames");
 12         var message = "确认删除";
 13         var values = [];
 14         for (i in userNames) {
 15             if (userNames[i].checked) {
 16                 message = message + userNames[i].value+",";
 17                 values.push(userNames[i].value);
 18             }
 19         }
 20         message = message + "?";
 21         if (confirm(message)) {
 22             $.ajax({
 23                 url: "@Url.Action("RemoveStudent")",
 24                 contentType: "application/json",
 25                 method: "POST",
 26                 data: JSON.stringify(values),
 27                 success: function(students) {
 28                     updateTable(students);
 29                 }
 30             });
 31         }
 32     }
 33 
 34     function updateTable(data) {
 35         var body = $("#studentList");
 36         body.empty();
 37         for (var i = 0; i < data.length; i++) {
 38             var person = data[i];
 39             body.append(`<tr><td><input type="checkbox" name="userNames" value="${person.userName}" /></td>
 40             <td>${person.userName}</td><td>${person.name}</td><td>${person.degree}</td>
 41             <td>${person.phoneNumber}</td><td>${person.email}</td><td>${person.maxBooksNumber}</td></tr>`);
 42         }
 43     };
 44 
 45     function addStudent() {
 46         var studentList = $("#studentList");
 47         if (!document.getElementById("studentInfo")) {
 48             studentList.append('<tr id="studentInfo">' +
 49                 '<td></td>' +
 50                 '<td><input type="text" name="UserName" id="UserName" /></td>' +
 51                 '<td><input type="text" name="Name" id="Name" /></td>' +
 52                 '<td><input type="text" name="Degree" id="Degree" /></td>' +
 53                 '<td><input type="text" name="PhoneNumber" id="PhoneNumber" /></td>' +
 54                 '<td><input type="text" name="Email" id="Email" /></td>' +
 55                 '<td><input type="text" name="MaxBooksNumber" id="MaxBooksNumber" /></td>' +
 56                 '<td><button type="submit" onclick="return postAddStudent()">添加</button></td>' +
 57                 '</tr>');
 58         }
 59     }
 60     
 61     function postAddStudent() {
 62         $.ajax({
 63             url: "@Url.Action("AddStudent")",
 64             contentType: "application/json",
 65             method: "POST",
 66             data: JSON.stringify({
 67                 UserName: $("#UserName").val(),
 68                 Name: $("#Name").val(),
 69                 Degree:$("#Degree").val(),
 70                 PhoneNumber: $("#PhoneNumber").val(),
 71                 Email: $("#Email").val(),
 72                 MaxBooksNumber: $("#MaxBooksNumber").val()
 73             }),
 74             success: function (student) {
 75                 addStudentToTable(student);
 76             }
 77         });
 78     }
 79 
 80     function addStudentToTable(student) {
 81         var studentList = document.getElementById("studentList");
 82         var studentInfo = document.getElementById("studentInfo");
 83         studentList.removeChild(studentInfo);
 84 
 85         $("#studentList").append(`<tr>` +
 86             `<td><input type="checkbox" name="userNames" value="${student.userName}" /></td>` +
 87             `<td>${student.userName}</td>` +
 88             `<td>${student.name}</td>`+
 89             `<td>${student.degree}</td>` +
 90             `<td>${student.phoneNumber}</td>` +
 91             `<td>${student.email}</td>` +
 92             `<td>${student.maxBooksNumber}</td >` +
 93             `</tr>`);
 94     }
 95 </script>
 96 
 97 <h2>学生信息</h2>
 98 
 99 <div id="buttonGroup">
100     <button class="btn btn-primary" onclick="return addStudent()">添加学生</button>
101     <button class="btn btn-danger" onclick="return confirmDelete()">删除学生</button>
102 </div>
103 
104 
105 <br />
106 <table>
107     <thead>
108         <tr>
109             <th></th>
110             <th>@Html.LabelFor(m => stu.UserName)</th>
111             <th>@Html.LabelFor(m => stu.Name)</th>
112             <th>@Html.LabelFor(m => stu.Degree)</th>
113             <th>@Html.LabelFor(m => stu.PhoneNumber)</th>
114             <th>@Html.LabelFor(m => stu.Email)</th>
115             <th>@Html.LabelFor(m => stu.MaxBooksNumber)</th>
116         </tr>
117     </thead>
118     <tbody id="studentList">
119 
120         @if (!@Model.Any())
121         {
122             <tr><td colspan="6">未有学生信息</td></tr>
123         }
124         else
125         {
126             foreach (var student in Model)
127             {
128                 <tr>
129                     <td><input type="checkbox" name="userNames" value="@student.UserName" /></td>
130                     <td>@student.UserName</td>
131                     <td>@student.Name</td>
132                     <td>@Html.DisplayFor(m => student.Degree)</td>
133                     <td>@student.PhoneNumber</td>
134                     <td>@student.Email</td>
135                     <td>@student.MaxBooksNumber</td>
136                 </tr>
137             }
138         }
139     </tbody>
140 </table>
复制代码

 

使用 Role 不是 Admin 的账户登录:

 

 

使用 Role 为 Admin 的账户登录:

ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(二)用户数据库初始化、基本登录页面以及授权逻辑的建立 – NanaseRuri – 博客园已关闭评论
备案信息冀ICP 0007948