草庐IT

创业之路的故事|如何设计一个用户系统

轻风博客 2023-03-28 原文

前言

本文从最原始的需求出发,结合需求变化,推导出用户系统的演进。
(如果有理解偏差之处,还请谅解,欢迎讨论)
(故事纯属虚构)

一、创业

老王是程序员,出去创业了,做当下最火的web3.0,需要做app和web,第一步需要一个用户登录功能,这对老王来说简单。
用户可以拿邮箱或者手机注册,填一些基本信息(如名称、性别、年龄等),初始化密码,提交后给用户生成平台唯一id。用户可以拿手机或者邮箱登录,设计如图,搞定了。

二、新增需求-三方登录

 

产品经理提出为了方便用户快速注册,借助当前用户量最大的平台,实现授权登录,授权登录后经过用户授权从大平台那里拿用户基本信息(手机号,邮箱等等),避免用户注册耗时原因导致用户放弃注册的流失。
安全同学也提出了,有可能用户邮箱泄露,黑客拿着邮箱来调用平台的登录服务暴力破解密码,需要限制验密错误次数以及冻结功能等。
老王想了想,简单,在模型中扩充一些字段就可以解决。把市面上比较火的平台id都预留一下,免得后面一个一个加;同时在把验密失败次数或验密失败时间记录下来,一旦达到一定次数,就不允许调用登录服务。

三、新增需求-用户类型区分

 

由于业务的发展,用户不在局限于个人用户,一些公司也需要入驻进来,显然,年龄、性别等信息就不再适合给公司用户使用了,到这里,模型必须要拆分了。
老王希望把模型拆成继承关系,用户表作为父类,可以延伸出公司用户和个人用户,如下图所示。

四、业务扩张了

 

老王的创业公司发展迅速,开始融资了,不仅nft搞得如火如荼,还要搞某币交易。
某币交易涉及到金融(我们假设老王创业的地方监管环境允许),那么需要进行实人(实体)认证,需要知道用户在法律意义上到底是谁。这里的需求,已经不是纯用户角度了,而是知道你的客户是谁,已经开始向客户系统演进。
另外由于涉及到某币账户操作,因此需要搞一个交易密码,现在很多设备都支持指纹和人脸了,产品希望指纹和人脸也能支持。
老王接到了以上需求,他开始思考以下几个问题:
  1. 密码:现在密码种类变多,从登录密码扩展了交易密码;另外交易密码可以支持普通密码、指纹密码、人脸等子类型。
  2. 登录方式:之前的设计太粗暴了,扩展性很差,之后如果要支持身份证登录,或者营业执照号登录,都需要再加字段。
  3. 实人(实体)认证:真实世界的人或者企业,与之前系统模型中的用户是一个概念吗?假如没有监管的要求,之前用户模型里边的信息,就可以支撑用户在平台上的各种使用诉求(登录、信息显示等等)。

4.1 登录方式

 

我们思考一下为什么会有登录方式,登录方式是用来确定系统中的哪个用户正在进行操作(这里先不考虑账号被盗的情况)。只要登录进来,就能在系统中确定是某一个userid(系统内部唯一标识),登录后的所有操作,都可以通过userid来留痕。

登录方式总共有多少种?能穷举吗?目前行业里的登录方式实现基本包含以下几种:

  • 电子联系方式:手机号、邮箱,用这类方式有个很好的收益,就是密码管理,如果出现被盗或者忘记密码,可以多因子认证(验证码)找回在平台上的账号。
  • 证件号码:身份证、营业执照号等,这类登录方式在政务系统上比较常见,政务系统一般希望直接服务社会上的实人或者组织。
  • 系统生成的id:系统中的唯一id,QQ是最典型的例子,注册QQ会生成一个qq号,需要用户记住直接拿QQ号作为登录名。
  • 三方登录:通过oauth协议,在客户授权的前提下,获取用户在第三方平台的部分信息,比如头像、昵称、唯一id,进行授权登录。
总结一下上述的多种登录方式,除了系统生成的id登录,其他三种都是通过外部系统唯一标识登录,登录系统实际就是维护一个用户记忆的系统外部唯一标识 ——> 系统内部唯一标识的关联关系。而外部系统唯一标识可以有千千万万种,并不能穷举,那么就不能像userV2版本中通过字段的模式,每次新增字段解决。
通过以上的分析,老王从模型层面讲登录方式从用户模型中拆了出来,如下图所示。loginType可以是邮箱、手机号、支付宝、身份证、甚至银行卡号,这些都是能够定义注册人是谁的外部唯一标识(这些标识不可能同时被两个人拥有)。这样,任何外部唯一标识都可以作为系统的登录方式,找到系统中的用户,模型做到了高度可扩展。

4.2 密码

 

基于上述需求,密码首先需要定义分类,现在有了登录密码和交易密码的区别,同时交易密码要支持普通密码、指纹密码、人脸密码三种密码,因此再定义一个subtype来标识,如下图所示。
那么密码和登录方式的关系是啥?是不是每一个登录方式都要定义一个登录密码?显然不是,用户在平台有多个登录方式(登录名:手机、邮箱等),但是不可能让用户每个登录方式都维护一个密码,用户记密码、维护密码都累死了。那么,显然登录方式和登录密码的关系是N:1的。
但是,交易密码怎么实现呢?交易密码是用户维度在交易时的验证,因此,每个用户只需要一个交易密码,那么,我们将模型设计成如下这样,每个用户可以有多种密码,但是在登录的主类型(type)下,只有一个密码。在交易的主类型(type)下,有三个密码,靠子类型(subtype)区分开(数字、指纹、人脸)。

总结一下密码这么设计的好处:

  1. 不同登录方式之间可以共享登录密码,用户不用维护多个登录密码;

  2. 密码根据类型、子类型可以支持多种密码同时存在;

好像登录和密码这么设计很完美了,想一想有没有什么问题。

4.3 实人认证

 

大部分互联网应用,平台并不用知道注册人的真实信息,互联网精神就是自由开放,有一句上个世纪流行语,你永远不知道网络的另一边是人还是什么。
但是也有一些业务必须知道注册人的真实合法的信息,才能完成业务,比如金融业务,监管要求必须实人认证还有附加的限制规则。比如一些toB的CRM业务,会维护潜客、客户的真实信息,甚至是客户公司的经营情况,这里就涉及到了真实世界是否存在的认定。
因此,我们需要在系统中记录真实世界的合法标识,对于个人有身份证、护照,对于企业有营业执照。那么是不是直接在用户模型中添加一些字段就搞定了?可以这么做,但是有个问题,一个人完全可以在平台中注册多个账户,如果在用户模型维度来承载实人信息,那么每个注册的账户都要维护认证状态,显然是不合理的,如果能够做到一个真实世界的实体(人、组织),在系统中只维护一份,针对这一份认证相关的数据进行维护,无论这个人注册了多少个账号,都关联到这份实体信息上,那么实体是认证过的,所有账号都是认证过的。
那么很显然,由于真实的人和用户在系统中是1:N的关系,在这里抽象了新的模型,我们可以把它命名为客户,模型扩展后如下图所示。客户和用户的设计类似,是一体两面的。一个人的真实信息属于客户这个领域,而一个人在平台里边可以有多个账号登录,使用平台的服务,因此系统中为了登录和现实信息相关的数据,都属于用户这个领域。没有客户域的数据,是可以在系统中运行的(使用系统服务),这就是用户领域的职责。那么客户领域的职责是什么?我认为是维护客户关系,要维护客户关系第一步是知道你的客户是谁,是不是真实的,在这之后,就可以进行营销相关的操作,将客户从潜客发展成资深客户,同时不断完善客户信息,包括不限于真实信息、喜好、消费习惯等等。所以,实人就是客户信息建设的第一步。

4.4 其他问题

 

到这里模型好像已经很强大了,对于老王的创业团队来说可以支撑很多情况,我们在回顾下整个模型,客户是真实世界存在的实体的反映,登录方式是断定注册人是谁的外部唯一标识,这两个概念互相之间貌似是有关系的。也就是说,登录方式里边的手机号、邮箱都是真实世界的实体所拥有的唯一标识。同一个手机号不可能同时被两个人拥有,并且用作平台的登录名。
那么在4.3提到,一个人在平台上可以有多个账户,比如张三,刚开始作为nft的用户用手机号注册了一个账号,过了一段时间他开了一家公司,想在平台上开店,张三希望还是用这个手机号来登录(这个例子举得并不恰当,大家不要当真)。这个场景下,登录方式和用户变成了1:2的关系。

手机号作为我的唯一标识,我希望在一个平台只需要手机号就能登陆,同时我在平台可能有多个身份,因此登录方式需要升级一下。无论有多少登录方式,都是为了识别出真实世界的人是谁在登录,因此抽象实体外标并分配一个id,就代表了这个人在登录,登录成功之后,可以选择具体的身份,比如个人用户或者企业用户。
密码模型不再和userId强关联,抽象出targetEntity字段可以对登录、用户甚至客户等维度做密码验证。当前登录密码场景直接通过实体外标的id关联,交易密码场景通过userid关联。
到这里,整个模型比较完备了。

五、总结

 

总结一下前边的模型,简化下来就是下图的关系,登录名就是标识,系统用来判定实际是谁在注册,用登录名登录后,可以选择一个身份在系统中操作,可以是卖家(经营店铺),也可以是买家(逛店+购物),还可以是客服(负责与买家沟通,处理异常订单)。
该模型中每一个抽象都充分考虑了业务场景的需求,非常灵活,可以支撑大用户量级的各种需求。
作者|徐浩(銆然)

有关创业之路的故事|如何设计一个用户系统的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  6. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  7. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  8. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  9. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  10. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

随机推荐