SpringMVC:业务逻辑:register() / 帮帮币 / 消息 / HtmlTemplate / email激活 / 交易 /

更多
2021年06月19日 22点47分 作者:叶飞 修改

同学们有没有觉得之前的代码都是面向数据库的?仍然没有什么“业务逻辑”?

我们以注册为例,说一说什么是业务逻辑:

  • 新注册用户会:
    • 生成一个随机的邀请码
    • 获得系统赠予的积分:10个帮帮点
  • 系统会在新用户注册后30分钟内某一随机时刻掉落若干帮帮币
  • 邀请人会收到一个消息:xxx使用你的邀请码注册成功……
  • 系统会发送一封确认邮件到用户注册时填写的email,等待用户验证激活

这些逻辑,都应该放到User.Register()方法中。


简单赋值

引入相应字段:

private int bCredit;
private String inviteCode;	
@ManyToOne
private User invitedBy;
邀请人(invitedBy),在对象生成后立即设置。比如在UserService.Register()中:
public int Register(RegisterModel model) {		
	User user = userConvert.toUser(model);		
	User invitedBy = userRepository.GetByName(model.getInvitedByName()).get(0);
	user.setInvitedBy(invitedBy);

其他在注册时直接赋值:

public void Register() {
	inviteCode = String.valueOf(Math.round((1+Math.random())*10000-10000));
	bCredit += 10;
}

邀请码还可以设置成只读,因为之后就不会再变化。

dbFactory

#体会#:当Register()中有了业务逻辑之后,就更能体现出dbFactory的作用

public class UserFactory {
	static User fg, atai;
	public static void create(EntityManager em) {
		fg = register("fg", null, em);
		atai = register("atai", fg, em);
	}
private static User register(String username, User invitedBy, EntityManager em) {


帮帮币

相对于帮帮点,帮帮币(BMoney)价值更高一些(详见功能索引),所以我们应记录其每一次的变动(明细,包括发生时间、金额、事由……),以备查验。

所以我们需要一个新的entity:

public class BMoney extends Entity{
@想一想@:它应该有哪些属性,和User直接是什么关联关系?
@javax.persistence.Entity
public class BMoney extends Entity{
	private int amount;
	private String comment;
	@ManyToOne
	private User owner;	

所有这些字段都可以封装成只读属性!

说明:稍微简化一下,将帮帮币的掉落改成直接奖励。

我们可以在Register()方法里面生成一个帮帮币的明细对象,但怎么将这个对象持久化呢?这就需要用到双向引用和casacade(复习)了:

@OneToMany(mappedBy = "owner", cascade = CascadeType.PERSIST)
private List<BMoney> bMoneys;
bMoneys = new ArrayList<>();
bMoneys.add(new BMoney(5, "注册奖励", this));		

结余balance

很多时候,我们都要知道某用户现有多少帮帮币,怎么办呢,这样吗?(方案一)

public int GetBMoney() {
	return bMoneys.stream()
			.collect(Collectors.summingInt(b -> b.getAmount()))
			.intValue();
}

但每次都这样统计的话,如果用户的帮帮币明细数量较大,是不是就会有性能问题?

为了提高性能(用空间换时间),我们有意识的使用了冗余(方案二)

private int bMoneyBalance;
在User中直接记录用户现有的结余。

但这样的话,又会带来另外一个问题:该用户任何一个地方(比如发布文章/设置求助悬赏/交易……)的帮帮币变动,都要更新结余,

private void refreshBMoney() {
	bMoneyBalance = bMoneys.stream()
bMoneys.add(new BMoney(5, "注册奖励", this));
refreshBMoney();
这不仅麻烦,而且容易疏漏!

所以最后的解决方案是在每一个帮帮币明细项中记录其当前结余(同时满足UI的呈现需求):

public class BMoney extends Entity{
	private int balance;

但这个balance的值从哪里来?还是不能靠repository查询,只能是关联entity!所以在User中添加一个:

@OneToOne
private BMoney latestBMoney;
这样,在BMoney的构造函数中,通过owner的最新BMoney明细记录计算出当下的结余:
//省略null值判断
this.balance = this.owner.getLatestBMoney().balance + this.amount;
不要忘了更新owner的最新BMoney明细记录:
this.owner.setLatestBMoney(this);
然后要获取用户当前帮帮币数量,只需要找到最近的一次帮帮币明细,查看其中的结余即可:
System.out.println(UserFactory.fg.getLatestBMoney().getAmount());

帮帮币消耗

发布文章,就会消耗一个帮帮币,怎么实现?很简单,Article的Publish()方法中:

author.addBMoney(new BMoney(-1, "文章发布", author));
addBMoney是因为User中没有暴露bMoneys集合(避免滥用),声明的方法:
public void addBMoney(BMoney money) {
	bMoneys.add(money);
}

最后,不要忘了在ArticleService中调用article.Publish()方法:

articleRepository.Save(article);		
article.Publish();

登录状态栏显示

用户现有帮帮币。

首先,LoginStatusModel中添加balance字段/属性,便于页面呈现:

public class LoginStatusModel {
	private int balance;
<span>源栈欢迎你, <a href="/User/${loginStatus.id}">${loginStatus.username}</a>
	(${loginStatus.balance}) 
</span>
因为在LoginStatusModel是和entity User进行映射,而User没有balance属性(只有latestBMoney),我也喜欢字符串格式的express映射:
@Mapping(target = "balance", expression = "java(...)")
LoginStatusModel toLoginStatusModel(User entity);
所以就在UserService中用代码赋值:
model.setBalance(currentUser.getLatestBMoney().getBalance());


消息

当新用户注册时,要发送一个消息给邀请人……怎么发?

演示:我的消息

@想一想@:邀请人怎么就能知道谁谁谁用他/她做邀请人?

动态 vs 静态?

所谓“动态”生成,就是每次都利用现有数据(比如博客评论)生成消息,生成的消息不予以保存。

但这有两个问题:

  1. 运算量大,容易遗漏。消息并不仅仅只有一种,而是各式各样可能涉及到很多张表很多关系……
  2. 如何解决已读和删除的问题?

所以我们宁愿有一定的冗余:在消息事件发生(比如注册)时,就生成的消息(message)并予以存储,以后消息接收人(receiver)只需简单查询就能得到ta的消息。

具体实现:

  • 声明作为entity的Message类
    @javax.persistence.Entity
    public class Message extends Entity {
    	private String body;
    	@ManyToOne
    	private User receiver;
    	private String kind;	
    	private boolean hasRead;
  • User中一样添加Message集合(为了演示此处使用Set)
    @OneToMany(mappedBy = "receiver", cascade = CascadeType.PERSIST)
    private Set<Message> messages = new HashSet<>();
    
  • 生成一条message:
    if (invitedBy != null) {
    	invitedBy.messages.add(
    		new Message(this.getUsername() + "使用你的邀请码注册成功", invitedBy, "邀请")
    	);
    }//else nothing		

订阅

用户可以根据种类订阅/退订消息。

首先,如何记录用户的订阅?

在Java语言层面,

  • 订阅种类可以用字符串(或枚举)表示,我们将其作为常量声明在一个专门的类MessageKind中:
    public class MessageKind {
    	public final static String RegisterInvitedBy = "注册邀请";
    	public final static String BeCommentd = "被评论";
  • 某个用户的订阅可以用一个集合或字符串数组表示
    private String[] descriptions;

关键是在数据库层面怎么办?

  • 可以专门建一个只有UserId和MessageKind两列的表,好处是可以很方便的查某种Message有哪些人订阅,坏处是查询有额外性能损失;所以我们
  • 可以使用一种“反范式”的做法:将用户订阅转成字符串后,作为User表的一列存储
    //注意不是数组了!
    private String descriptions = "";

所以就需要一个“转换”:

  • 定义一个分隔符:
    private static final String splitor = ",";
  • 在setter和getter中自定义逻辑,拆分(split)和合并(join):
    public String[] getDescriptions() {
    	return descriptions.split(splitor);
    }
    
    public void setDescriptions(String[] descriptions) {
    	this.descriptions = String.join(splitor, descriptions);
    }

最后,利用上述工具:

if (Arrays.asList(invitedBy.getDescriptions()).contains(MessageKind.RegisterInvitedBy)) {

职责分配

在很多地方都要生成消息,我们都这样复制粘贴的实现么?肯定不行,@想一想@:怎么办呢?

应该有一个方法,类似于SendMessage()啥的,但这个方法放在哪里呢?User中么?user.Send(messge)……

不对,更好的实现应该是:message.Send()(复习:类的职责

public class Message extends Entity {
	public void Send() {
		if (Arrays.asList(receiver.getDescriptions())
				.contains(MessageKind.RegisterInvitedBy)) {
			receiver.getMessages().add(this);
		} // else nothing
	}

判断收件人是否为null值,这里又有两种选择,写在send()方法

  • 里面
    public void Send() {
    	if (receiver != null && 
    			Arrays.asList(receiver.getDescriptions())
    
  • 外面
    public void Register() {
    	if (invitedBy != null) {
            new Message().Send();    
    

@想一想@:哪一个更好?为什么?

外面更好,而且Send()方法内部,还有做检查(复习:防御式编程):

public void Send() {
	if (receiver == null ) {
		throw new IllegalArgumentException("收件人不能为null……");
	}	


HtmlTemplate

帮帮币明细的备注、用户消息的内容(比如“谁谁谁”使用了你的邀请码……中的“谁谁谁”)在entity中生成,但其格式(样式/链接等)应由UI决定,如何解决这个问题呢?

UI把消息内容制作成模板(template),放到entity和ui都能够访问的glb中,

package glb;

public class HtmlTemplate {
	public static String UserRegister(int userId, String username) {
		return String.format("<a href='/User/%s'>%s</a>使用你的邀请码注册成功!", 
				userId, username);
entity调用填充:
new Message(HtmlTemplate.UserRegister(id, username), invitedBy, MessageKind.RegisterInvitedBy)
注意:这就是为什么之前强调的,entity总是要先save()再调用其方法的原因,没有save()就没有id!
userRepository.Save(user);		
user.Register();


激活Email

策略

首先,在数据库中为每个email保存一个校验码(随机数)

然后,将这个校验码发送到该email邮箱中。

这样,就只有email的所有人能得到这个校验码,用于激活邮箱(即让系统确认该email可用,且属于该用户)

为了方便,一般是直接直接发送一个链接,url参数中带着校验码,比如:

点击 <a href='http://localhost:8080/17bang/email/validate?id=1&token=4936'>激活</a>你的email……

实现

实现以上功能,至少需要记录email地址、校验码,以及是否激活三个信息;为了演示效果,我们将其封装成一个component:

@Embeddable
public class EmailStatus {
	private String address;
	private String token;
	private Boolean validated;
public class User extends Entity {
	@Embedded
	private EmailStatus email;
为了有效封装token可用设置成只读,并在构造函数中赋值:
public EmailStatus(String address) {
	super();
	this.token = StringHelper.GetRandom();
PS:GetRandom()是我们自己封装的,实现同邀请码……

在User.Register中就可以调用:

EmailHelper.Send(email.getAddress(), "激活email", 
	HtmlTemplate.EmailValid(getId(), email.getToken()));
public static String EmailValid(int userId, String token) {
	return String.format(
			"点击 <a href='http://localhost:8080/17bang/email/validate?id=%s&token=%s'>激活</a>你的email", 
			userId, token);
}

验证激活

最后,再添加对应的页面:

@RequestMapping(URLMapping.Email.Base)
public class EmailController extends BaseController {
	
	@RequestMapping(URLMapping.Email.Validate)
	public String validate(Model model, int userId, String token) {
		model.addAttribute("result", userService.valid(userId, token));
		return URLMapping.Email.Base + URLMapping.Email.Validate;
在UserService(也可以单独声明一个EmailService)中注意
public boolean valid(int userId, String token) {		
	User user = userRepository.Get(User.class, userId);
	//调用user方法,而不是直接改user值
	return user.ValidEmail(token);
在User中:
public boolean ValidEmail(String token) {
	boolean passed = getEmail().getToken().equals(token);
	if (passed) {
		getEmail().setValidated(true);
	}//else nothing
	return passed;


作业

  1. 发布求助的时候用户会设置悬赏,于是系统会“冻结”用户悬赏数额的帮帮币(本题可由DbFactory完成)
    • 被冻结的帮帮币不可再用,即应减少balance结余
    • 后面如果求助
  2. 帮帮币明细列表页面,加载时合计计算,检查结余有无错误:
    • 正确无误:图标打√
    • 如有错误:图标为×
  3. 我的消息页面:
    1. 未读消息排在已读消息前面,粗体,默认勾选
    2. 可以“删除”或“标记为已读”
  4. 通过email重置密码 (考虑全面一点
  5. 帮帮豆(BBean):每次用户登录,系统会检查该用户的上次帮帮豆发放时间;如果已超过24小时,就为其重新发放一次,数量随机,之前的剩余清零。










log4j

<context-param>
        <param-name>log4jConfiguration</param-name>
        <param-value>/WEB-INF/log4j2.xml</param-value>
    </context-param>


我们自己也可以使用log4j记录各种信息:

import org.jboss.logging.Logger;
private Logger logger = Logger.getLogger(RegisterControler.class);

        logger.info("This is an info log entry");
        logger.error("This is an error log entry");



Java项目 业务层 实战
赞: 0 踩: 0

打赏
已收到打赏的 帮帮币

你的 打赏 非常重要!
为了保证文章的质量,每一篇文章的发布,都已经消耗了作者 1 枚 帮帮币
没有“帮帮币”,作者无法发布新的文章。

全系列阅读
评论 / 0

后台开发


其他:WebForm和WebApi

其他ASP.NET框架,如WebForm、WebApi……

RazorPages(Core)

微软推荐的、最新的、基于Razor页面和.NET core的新一代Web项目开发技术,包括Razor Tag Helper、Model绑定和Validation、Session/Cookie、内置依赖注入等……

MVC(Framework)

过去两年间最流行的、基于.NET Framework和MVC模式的ASP.NET MVC框架,主要用于讲解安全、性能、架构和各种实战功能演示……

C#语法

从入门的变量赋值、分支循环、到面向对象,以及更先进的语言特性,如:泛型、Lambda、Linq、异步方法等…………

Java语法

面向过程的变量赋值、分支循环和函数封装;面向对象的封装、继承和多态;以及更高阶的常用类库(集合/IO/多线程……)、lambda等

Java Web开发

分层架构和综合实战

J&C

Java和C#共有的语法

全部
关键字



帮助

反馈