首先讲讲我开发这套系统的初衷,想直接看代码的往下划
在日复一日的上班工作中,我发现生活之余所剩的时间少之又少,而且剩下的这些时间又被大量的闲杂琐事与自身的慵懒占据,下班躺床上玩手机、追剧、看小说,时间眨眼就过去,回首望去,自己的生活抛去工作,似乎没有什么有意义的事物存在。然而工作并不是生活的全部啊。空心病这个词逐渐在身边浮现,毫无在意的事,刻苦的努力一瞬间变得毫无意义。但我觉得生活不该是这样,一定存在着某种有趣的事物只是我们还没有发现,更何况,生而为人,来都来了,不去追求探索岂不可惜。在没有找到我的追求时,我觉得我需要让自己的身体保持一个良好的状态,于是我自己定制了一个健身计划,并不是健身房的那种增肌塑形,只是简简单单的夜跑,夜跑中,我发现类似于我的并不止一个。
很多人都拥有着他们自己的计划,当你迷茫不知方向时,看看其他人在做些什么,或者参与其中,坚持下去,时间久了,你会获得启发从而寻找到自己的追求的。
这就是我设计制作“明日计划”的初衷,它并不是一只闹钟,我希望它能够成为你深处黑夜时的一盏灯,哪怕只有星星点点的光芒。
“明日计划”的重点在于打卡、协作以及自我审视,所以打卡页面需重点设计,我对此的初期构想为,该页面一定要具备操作感且需简洁明了,于是我参考探探app的卡片划动效果,来设计了这部分页面。用户查看自己的历史数据时,一般重点在于回顾自身计划的执行率(完成率),所以历史数据查询这部分需以图表来直观的反应出用户数据。用户注册模块需要最大程度上的简化,这会使用户更加快捷的加入其中。从之前制作网站来看,用户并不喜欢繁琐的账号注册,所以在“明日计划”中,只要用户填写好邮箱后,其他的一切资料,包括密码,均有系统后台自动生成。当然,密码是可以在用户登录后,通过邮箱验证的方式更改的。当然网站设计中还包含着其他方方面面的细节,这里只是简单的说几句,但一定要注意,设计的出发点是用户,而不是作为一个开发者怎么方便怎么来*。 比如:你眼中各种完善的条件查询,往往是用户觉得最最麻烦的地方。
这套系统由Vue+Springboot搭建而成,模式采取前后端分离,容器为tomcat
后端以若依系统为框架基础,前端则采用vant、element-ui两种UI框架
涉及到的技术栈有:
从账号注册开始:
我们能够想到的普通账户流程是这样的
其中注册信息必填项包含账号和密码,有的还会有昵称、年龄、性别等等;
信息验证包括账号内容的合法性以及验证是否为本人操作注册;
比如手机号码为账号注册时会以短信验证、邮箱为账号注册时会以邮件链接验证;
我们这里使用邮箱验证(因为短信验证是需要RMB的)
考虑到用户便捷性,我将账号注册作以简化,用户仅需要提供邮箱信息即可。毕竟用户可不喜欢填写调查问卷。
后台接收到邮箱信息后,校验邮箱格式是否正确、数据库中该账号的唯一性,然后通过名称库自动拼接生成随机昵称,再从数据库的配置数据表中获取默认密码,整合好注册信息后,插入该数据完成账号的注册,最后向前台返回注册结果,前台根据注册结果执行下一步操作,若注册成功,将自动登录。这样,账号注册的流程就被极大的简化。
当然,这种方案下,由于默认密码是公开的,为保证账号安全性,需用户后续自主更改密码。
在首次更改密码流程中,必须通过邮箱邮件的验证,以校验是否为账户本人操作,后续密码更改则无需验证。
controller::
/**
* 注册用户
*/
@Log(title = "用户注册", businessType = BusinessType.INSERT)
@PostMapping("/regist")
public AjaxResult regist(@RequestBody SysUser user){
//关于传参格式的校验我通常会放在controller中,service里主要是放业务逻辑
if(StringUtils.isEmpty(user.getUserName().trim()) || !StringUtils.isEmail(user.getUserName()))
return AjaxResult.error(2,"邮箱格式不正确");
return capacityService.register(user.getUserName());
}
service::
/**
* 注册用户
*/
@Override
public AjaxResult register(String email){
//邮箱、账号查重
SysUser info = userMapper.selectUserByUserName(email);
if(StringUtils.isNotNull(info))
return AjaxResult.error(3,"已存在该账号,无需重复注册");
//设置账户名、邮箱值均为email
SysUser sysUser = new SysUser();
sysUser.setUserName(email);
sysUser.setEmail(email);
//生成随机昵称
sysUser.setNickName(RandomLovelyNameUtils.generateName());
//设置默认密码,直接从配置数据表中拿默认密码,这里是直接用id拿的
SysConfig sysConfig = configService.selectConfigById((long) 2);
//密码要加密,不要遗漏
sysUser.setPassword(SecurityUtils.encryptPassword(sysConfig.getConfigValue()));
//设置账户状态为2-试用性状态,当首次更改密码后,状态会进行相应变更为正式用户的,这个设置很重要。
sysUser.setStatus("2");
// 插入用户信息
int rows = userMapper.insertUser(sysUser);
return rows>0?AjaxResult.success():AjaxResult.error();
}
要注意注册时账号的状态 [试用性-2],后续的密码修改会根据这个状态来处理
/**
* 修改用户密码,试用性账号修改,需校验邮箱
*/
@Override
public AjaxResult editPwd(String oldPwd, String newPwd) throws Exception {
//获取用户信息
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
String userName = loginUser.getUser().getUserName();
SysUser sysUser = userMapper.selectUserByUserName(userName);
//校验oldPwd是否正确
if(!SecurityUtils.matchesPassword(oldPwd,sysUser.getPassword()))
return AjaxResult.error(-2,"原密码错误");
//判断是否为试用性账号,如果为2,需邮件激活
if(TEST_ACCOUNT.equals(sysUser.getStatus())){
return postEmail(sysUser.getEmail(),newPwd);
}else{
// 如果不是,那就进行常规的修改操作
sysUser.setPassword(SecurityUtils.encryptPassword(newPwd));
sysUser.setUpdateTime(new Date());
int i = userMapper.updateUser(sysUser);
return i>0?AjaxResult.success():AjaxResult.error();
}
}
首次修改密码时,向用户发送激活邮件,其中注意邮件内的链接
/**
* 向邮箱中发送确认修改密码的链接,5分钟内有效
*/
public AjaxResult postEmail(String email,String pwd) throws Exception {
String requestUrl = this.getEmailRequestURL(email,pwd);
String subject = "缔曦平台密码修改校验";
String content = "请点击以下链接激活账号
"+requestUrl+"";
try {
MailUtil.send(email, subject, content, true);
}catch(MailException e){
e.printStackTrace();
System.out.println(e.getMessage());
return new AjaxResult(-3,"发送激活邮件失败");
}
return new AjaxResult(1,"已向邮箱发送激活邮件,请于5分钟内完成激活");
}
链接中包含时间戳、账号、新密码、以及校验密匙。
其中肯定要对时间戳和密码加密,简单说一下这个加密的方法。
时间戳a、新密码加密生成b、时间戳拼接新密码形成的新字符串+自定义盐加密生成c,即三个参数 a、b、a+b+盐生成的c。
如果用户破解a与b并作以修改,传到后台时,后台会得到 修改后的a、修改后的b、和 c。此时我们再将a+b+自定义盐加密,生成的就不再是c,以此我们判断参数被修改过,不予继续操作。
其中密码加密用到了一个自定义盐SALT_1,密匙的生成用到了一个自定义盐SALT_2,上面的方法使SALT_1防守失效时还有一道SALT_2的保障,同时也能保证用户无法随意修改时间戳,更改激活链接的时效性。
/**
* 获取邮箱指向的链接请求
*/
private String getEmailRequestURL(String email,String pwd) throws Exception {
//拼接获取邮箱中链接跳转的请求地址路径
HttpServletRequest request = ServletUtils.getRequest();
String servletPath = request.getScheme()+"://"+ request.getServerName() + ":" + request.getServerPort() + request.getContextPath();
SysConfig sysConfig = configService.selectConfigById((long) 4);
String configValue = sysConfig.getConfigValue();
String requestUrl = servletPath + configValue;
//获取时间戳
long time = new Date().getTime();
//拼接参数
String secret = "";
try {
secret = HMACSHA256.generate(time + pwd, SALT_1);
} catch (Exception e) {
throw new Exception("HMACSHA256加密算法错误");
}
//pwd先加密,再utf-8编码
String codePwd = AESUtils.AES_Encrypt(pwd, SALT_2, AESEncryptMode);
String param = "?email="+email+"&pwd="+URLEncoder.encode(codePwd, "UTF-8")+"&time="+time+"&secret="+secret;
String wholeUrl = requestUrl + param;
return wholeUrl;
}
用户点击邮件激活链接时,执行账号的升级及密码的首次修改。
/**
* 通过邮件激活用户,更改status 2试用状态->0正常状态
*/
@Override
public AjaxResult accountLevelUp(String email, String pwd, long time, String secret) throws Exception {
//5分钟校验
long _time = new Date().getTime();
if(_time-time > 1000*60*5)
return AjaxResult.error(-1,"链接已超时");
//pwd 不需要再手动utf-8解码了 , 直接解密就好
String _pwd = AESUtils.AES_Decrypt(pwd, SALT_2, AESEncryptMode);
String _secret = "";
try {
_secret = HMACSHA256.generate(time + _pwd, SALT_1);
} catch (Exception e) {
throw new Exception("HMACSHA256解密算法错误");
}
if(!_secret.equals(secret))
return AjaxResult.error(-2,"加密链接不匹配");
//校验账号是否停封或删除
SysUser sysUser = userMapper.selectUserByUserName(email);
if(UserStatus.DELETED.getCode().equals(sysUser.getDelFlag()))
return AjaxResult.error(-3,"该账号已删除");
if(UserStatus.DISABLE.getCode().equals(sysUser.getStatus()))
return AjaxResult.error(-4,"该账号已停封");
//执行levelup status由2->0
sysUser.setStatus("0");
int i = userMapper.updateUser(sysUser);
//判断是否修改成功
if(i<=0)
return AjaxResult.error(-5,"修改数据状态失败");
//执行密码修改
sysUser.setPassword(SecurityUtils.encryptPassword(_pwd));
sysUser.setUpdateTime(new Date());
int _i = userMapper.updateUser(sysUser);
return _i>0?AjaxResult.success():AjaxResult.error("修改数据密码失败");
}
/**
* 生成随机昵称
*
* @return
*/
public static String generateName() {
int adjLen= adjective.length;
int nLen= noun.length;
StringBuffer sb = new StringBuffer();
Random random = new Random();
sb.append(adjective[random.nextInt(adjLen)]);
sb.append(noun[random.nextInt(nLen)]);
return sb.toString();
}
private static String adjective[] = {
"一样的", "喜欢的", "美丽的", "一定的", "原来的", "美好的", "开心的", "可能的", "可爱的",
"明白的", "所有的", "后来的", "重要的", "经常的", "自然的", "真正的", "害怕的", "空中的",
"红色的", "痛苦的", "干净的", "辛苦的", "精彩的", "欢乐的", "进步的", "影响的", "黄色的",
"亲爱的", "根本的", "完美的", "金黄的", "聪明的", "清新的", "迷人的", "光明的", "共同的",
"直接的", "真实的", "听说的", "用心的", "飞快的", "雪白的", "着急的", "乐观的", "主要的",
"鲜艳的", "冰冷的", "细心的", "奇妙的", "水平的", "动人的", "大量的", "无知的", "礼貌的",
"暖和的", "深情的", "正常的", "平淡的", "光亮的", "落后的", "大方的", "老大的", "刻苦的",
"晴朗的", "专业的", "永久的", "大气的", "知己的", "刚好的", "相对的", "平和的", "友好的",
"广大的", "秀丽的", "日常的", "高级的", "相同的", "笔直的", "安定的", "知足的", "结实的",
"许久的", "听话的", "知名的", "闷热的", "众多的", "拥挤的", "天生的", "迷你的", "老实的",
"友爱的", "原始的", "可笑的", "合格的", "公共的", "大红的", "得力的", "洁净的", "暗淡的",
"鲜红的", "桃红的", "吓人的", "多余的", "秀美的", "繁忙的", "冰凉的", "热心的", "空旷的",
"冷清的", "公开的", "冷淡的", "齐全的", "草绿的", "能干的", "发火的", "可心的", "业余的",
"空心的", "凉快的", "长远的", "土黄的", "和好的", "合法的", "明净的", "过时的", "低下的",
"不快的", "低级的", "中用的", "不定的", "公办的", "用功的", "少许的", "忙乱的", "日用的",
"要紧的", "少见的", "非分的", "怕人的", "大忙的", "幸福的", "特别的", "未来的", "伟大的",
"困难的", "伤心的", "实在的", "现实的", "丰富的", "同样的", "巨大的", "耐心的", "优秀的",
"亲切的", "讨厌的", "严厉的", "积极的", "整齐的", "环保的"};
private static String[] noun = {
"子璇", "淼", "国栋", "夫子", "瑞堂", "甜", "敏", "尚", "国贤", "贺祥", "晨涛",
"昊轩", "易轩", "益辰", "益帆", "益冉", "瑾春", "瑾昆", "春齐", "杨", "文昊",
"东东", "雄霖", "浩晨", "熙涵", "溶溶", "冰枫", "欣欣", "宜豪", "欣慧", "建政",
"美欣", "淑慧", "文轩", "文杰", "欣源", "忠林", "榕润", "欣汝", "慧嘉", "新建",
"建林", "亦菲", "林", "冰洁", "佳欣", "涵涵", "禹辰", "淳美", "泽惠", "伟洋",
"涵越", "润丽", "翔", "淑华", "晶莹", "凌晶", "苒溪", "雨涵", "嘉怡", "佳毅",
"子辰", "佳琪", "紫轩", "瑞辰", "昕蕊", "萌", "明远", "欣宜", "泽远", "欣怡",
"佳怡", "佳惠", "晨茜", "晨璐", "运昊", "汝鑫", "淑君", "晶滢", "润莎", "榕汕",
"佳钰", "佳玉", "晓庆", "一鸣", "语晨", "添池", "添昊", "雨泽", "雅晗", "雅涵",
"清妍", "诗悦", "嘉乐", "晨涵", "天赫", "玥傲", "佳昊", "天昊", "萌萌", "若萌"
};
以上就是账号注册及首次修改密码时账号激活相关的代码
后续我会逐步分享“明日计划”中的一些技术应用以及代码细节。