大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: 框架&架构 修改讲义
注册/登录/发布/修改……等简单(增删改查)功能以外,还有一些稍微“挠头”一点的复杂功能:

消息

演示:消息列表页面各种消息(作为邀请人被注册,博客被评论,……

静态 vs 动态

这些消息是:

  1. 动态生成的,即:每次只是利用其他“非Message数据”生成一条消息,

    比如:

    文章(19)讲课这些天DK的新评论:点点滴滴

    对应的底层数据就是:

    生成的消息不予以持久化的保存。
  2. 静态保存的?在消息事件发生时,就生成的消息并予以存储,以后消息接收人只需简单查询专门的消息数据

动态生成节约存储空间,便于控制样式;但是(如果消息种类繁多)每次生成消息都需要大量的扫描/运算,复杂度高,性能消耗大,而且不好解决已读/未读/删除(不能真删)的问题……

所以“一起帮”采用了静态保存的方式,会有一个专门的

entity:Message

一般可保存以下内容:

  • Body:内容,必有
  • Receiver:接收人,必有
  • Sender:发送人,可有可无,比如消息是系统生成,就可以没有Sender
  • Kind:种类,可有可无
  • HasRead/ReadTime:是否已读/阅读时间,在存储空间不是问题的时候,一般建议存储时间(信息量更大)
  • MarkDeleted:标记删除

在“事件触发”(比如:使用邀请人进行注册)时生成……

            Message message = new Message
            {
                Receiver = newPost.Blog.Author,
                Content = $"你的博客(id={newPost..Id})被用户(id={newPost.Author.Id})评论……"
            };

两种选择

在哪里生成这个Message?

按DDD充血模式的原则,应该是在entity的方法(比如:Comment.Publish()/User.Register())中。

但新生成的message怎么持久化呢?按我们的架构要求,entity不能引用repository,所以Message对象只能作为关联对象,依赖于“主entity”持久化而持久化:

Receiver.Messages.Add(this);

Bingo,完美!只是要注意:不要在Receiver.Messages的时候,数据库中加载全部的Messages,这是没有必要的(Hibernate能Inverse()绕开,但EF目前还不行)!

所以有些架构会在Service层中new出Message对象,调用repository持久化
 public void Comment(int id, Post newPost)
        {  //先存Post
            newPost.Author = currentUser;
            newPost.Blog = _blogRepository.Get(id).SingleOrDefault();
            newPost.Publish();
            _postRepository.Save(newPost);

            //再存Message

            _postRepository.Save(message);

            //以后还存其他entity
            //......
        }

这实际上就是把Entity应该做的事情,挪到了Service层,最后会形成了一个臃肿的Service层和一个萎缩的(贫血的)Entity层,还不如Entity和Repository混用呢!

PS:充血的entity应尽可能的“厚”,Service尽快能的“薄”


积分

可以有两种方式:

  1. 只记录一个总数
  2. 记录每一次积分变化:适用于“高价值”积分,便于核实

方式2的做法和Message类似,但有两个不同:

统计

用户现有的积分,如果每次都SUM所有记录汇总的话,计算量还是太大了。

所以可以借鉴银行存折记录格式:演示

日期 增减 金额 余额 备注
2022年10月17日 + 10 10 注册奖励
2022年10月18日 + 5 15 签到
2022年10月19日 + 20 35 发布评论
2022年10月19日 - 30 5 垃圾广告

每次变化都记录一个“当前结余(balance)”数量,

public class Credit
{
    public int Balance { get; set; }

这样通过最近一次记录就能马上拿到用户的现有总积分。

但是,这最近一次记录,究竟是:

  1. 通过数据库查询获得呢?还是
  2. 就记录在User当中?
    public class User : BaseEntity
    {
        public Credit Latest { get; set; }


仍然是两难,¯\_(ツ)_/¯


冗余和校对

很多时候,我们拿到User就要使用它的积分。如果每次都要到数据库中查询的话,很不方便的(尤其是在entity中)。

同时为了些许性能提升,我们可以在User中记录其最后一次Credit变更记录:


清理

按上述方式记录积分,实际上就产生了数据冗余。

要考虑到实际项目运行中,有可能出现“误算”的情形



HtmlTemplate

因为按我们的架构要求,Entity不能使用Repository

架构职责

Email的发送应该是哪一个层的职责?

实际开发中,Email中的正文内容更加复杂,比如通常会添加一个url连接,比如:

    Body = $"点击<a href='http://{host}/Email/Validate?uid={id}&code={code}'>完成注册</a>" ,
看到有html和url,你可能觉得应该是UI层的事;


但是,uid(用户Id)和code(验证码)不都应该考虑:

  • Email验证码应在BLL层生成
  • Email的Id需要在Email被Repository存储之后才能生成
  • url格式(使用的http协议、域名、路径、url参数名等)均在UI层生成



邮箱验证

我们在很多时候(比如重置密码),都需要用户的Email。但在使用之前,需要保证用户输入的Email是:

  • 真实有效,且
  • 属于当前用户的

惯常的办法就是对其进行验证/激活复习:Email发送

策略

如图所示:

  1. 用户:输入他的Email地址,提交给服务器
  2. 服务器:为该Email生成一个验证码code(为了安全期间,还可以有一个有限截止时间),将email和code一起保存到数据库
  3. 服务器:将验证码code发送到该Email中。为了方便用户操作,通常是发送一个链接,链接中包含该code和Email的Id
  4. 用户:从Email邮箱中获得code(或者直接点击发送过来的链接),到服务器激活
  5. 服务器:将用户输入的code和数据库中的存储的值进行比对,如果无误的话,激活该email(email.IsValid=true)

整个流程的关键就在于:用户只能通过他留下的email获取激活code。


文件上传

安全/限制

注意:病从口入

限制文件上传大小:不能几十个G的文件啪啪啪的网上传!

判断文件类型:(复习:文件/操作系统/IO

  • 后缀名:防君子,后缀名仅供windows便捷的确定文件类型,自动呈现图标、确定调用软件等
  • 文件头:防小人,操作系统按文件头确定文件的读写方式
  • 杀毒查木马:防恶人

不存储在数据库

因为:
  • 避免恶意文件“混入”数据库
  • 数据库更贵:早年习惯……
  • 使用CDN(复习)不方便

方案:

  • 文件本身自己存(操作系统管理的)磁盘
  • 数据库存(相对于网站根目录)路径,比如:/article/834/示意图.jpg

路径/名称规划

当需要上传的文件/图片很多的时候,一般我们要考虑以下几个因素:

1、重名和覆盖

如果使用客户端上传的文件名,就有可能:后上传的同名文件覆盖/不能覆盖之前保存的同名文件。

我们要考虑用户的意图:

  • 就是要上传一个新文件覆盖之前旧文件呢,
  • 还是上传的另外一个新文件?

如果要覆盖,我们也有两种方式:

  1. 真正的用同名新文件覆盖旧文件
  2. 只是在数据库中记录新的文件路径

所以会衍生出一个常见的套路:由服务器端重新生成文件名,比如:

  • 用户Id:一个用户只有唯一的一个,常用于用户的头像文件,即使用户上传时使用的是不同的文件,也可以实现方式1的同名文件真正覆盖
  • 随机数/GUID/时间戳等不重名的文件名:保护用户错误的覆盖(尤其是其他用户)之前上传的文件

当然,在能由路径确保操作的一定是当前用户自己的文件时,我们有可以使用用户自己原有的文件名,实现方式1的同名文件真正覆盖。这就需要规划好文件路径,比如,路径中包含:

  • 用户id:/user/12/帅帅的飞哥.jpg和/user/86/帅帅的飞哥.jpg不能相互覆盖
  • 文章id:/article/39/文件上传.jpg和/article/56/文件上传.jpg
  • 合用:/user-12/article-39/文件上传.jpg
  • ……

2、检索性能

规划好目录树,合理配置文件夹和文件,能够大幅提高文件的检索效率。

  • 一个文件夹下文件太多,或者
  • 文件的目录结构过深

都会导致检索文件困难,耗时过多。

飞哥推荐按年/月/日(月日可选)组织,比如:

  • 2019
    • 11
    • 12
  • 2020
    • 1
    • 2
      • 1
      • 2
      • 3
      • 4
      • ...
      • 28
    • 3
  • 2021

这样实现起来很简单,而且分布较为均匀。文件的名称可能是:~/images/2020/2/4/5293132738.jpg

动态文件输出

静态的文件输出(如:图片呈现、文件下载)是HTML的内容。

但有时候我们会在后台动态的生成一些文件,比如统计报表excel文件、辨别机器人的captcha图片等,这时候要理解:

  • 不需要先把文件存到硬盘上再输出
  • 前端(浏览器)无法区分也不会区分我接收到的是一个“磁盘上的、静态的”文件,还是一个“动态的”文件,就像无法区分静态/动态页面一样
  • 文件本质就是“流(stream)”,无论它是在网络还是在磁盘

消息推送



即时聊天

注意:通常来说,触发消息生成的用户,不是消息的接收人。比如:张三 使用 李四 作为邀请人进行注册,张三的行为触发了消息的生成,但

如何获取?

有两种方式:推送(push) vs 拉取(pull)?

  • Web项目,因为(目前)HTTP协议的单向性,且消息接收人不一定在线,所以通常不会采用“推送”机制,而是
  • 当消息接收人登录上线之后,通过主动查询,获取他的消息

短轮询:在客服端通过SetTimeout()/SetInterval()定时发送Ajax请求(又称之为定时轮询

长轮询:在服务器端维持住一个HTTP请求,可以简单的理解为在服务器端SetTimeout()/SetInterval()

WebSocket:一种全新的Web双向通信协议,允许服务端主动向客户端推送数据


错误处理

错误,可以细分为两种:bug和exception复习

  • bug应该尽可能的修复(fix)
  • exception一般只能记录或者恢复

无论哪种,都应该

尽可能早的暴露

前端的bug开发人员其实很难发现的,比如HTML上图片缺失、样式不对、JavaScript不能正常运行……这种bug基本上只有靠测试和用户。尤其是用户,给用户一个简洁方便的反馈渠道非常重要!

后端的错误,如果是

  • 逻辑性的(bug),比如取最大值时漏了一位、本来应该发送给张三的消息给了李四,我们一般也不容易发现,处理方式也只能同前端bug一样,只能依靠测试和用户
  • 异常性质的,被抛出来了,我们就可以发现它,然后对其进行处理!

#体会:为什么要防御式编程?尽可能的将逻辑性bug暴露出来#

错误页面

Web项目中,对异常的处理,除了catch…log和finally…恢复以外,还涉及到:要给用户一个说法,究竟发生了啥事?体现为:

  • 状态码(status code):比如404、500……(复习)
  • 用户友好的提示
    • 传统的、服务端页面项目,通常就是要转到一个错误页面,比如/article/9876543,一般都是30X重定向实现
    • Ajax驱动的、前后端分离的项目,通常都是给一个弹出框提示

这里面一定要注意的是:

  • 一般会有一个全局控制:处理所有未处理(unhandled)的异常
  • 不要暴露(调试用)堆栈信息,@想一想@:为什么?
  • 报错页面不要再有错误(最好是静态页面),否则就会行成死循环

日志

给你的飞机装上黑匣子。

让你的代码不再是玩具。





学习笔记
源栈学历
大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 框架&架构 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码