概述
因为后半学期太多的实验课,以及繁重的学习压力还有期末项目,博客停更了很久.期末考试后睡了两天,是时候重新拿起笔了.跟同学@qdu_neymar 协作,两个人完成了学期项目.考虑到时间紧迫,学业繁忙的现实压力以及我们的人数不足,采用了增量模型的开发模式,以求在最短的时间内开发出能够运行的项目,避免因为可能的失误导致项目完全无法运行的严重后果.从0开始进行设计,到最终成功部署并进行演示,虽然因为时间匆忙,同时面临期末复习等繁重的压力,项目没有做到尽善尽美,但还是很有成就感的.因此决定将项目的开发细节记录在博客中,并将其开源.这个假期,计划将项目重构,将一些没有能来得及实现的想法弥补上.
十分感谢@qdu_neymar对本项目的开发做出的贡献,以及@wangsz12在项目开发中给我的无私的,充满价值的建议.
功能模块划分
本项目是一个招聘网站平台,实现了用户应聘和企业发布岗位等相关的基本业务.具体的业务模块划分参见下图:
这些模块的划分都是在项目开发的最开始设计的,在后续的开发中得到了很好的执行,没有打折扣也基本没有偏离,这一点还是很难得的.
项目解决的核心业务问题是:企业能够在本平台发布岗位,并接收应聘者的简历进行筛选,选择满意的应聘者进行面试,以及最终的录用.用户通过本平台浏览有意向的岗位,并选择岗位进行简历投递,与企业进行交互并等待最终结果.
基础架构
项目数据库采用MySQL开发,使用了Redis实现简单的缓存.使用MyBatis作为ORM框架,Spring Boot+SpringMVC+Spring进行后端开发.前端采用了Vue框架以及Axios开发,实现了前后端的分离.下图可以生动展示项目的层次结构.这不是一篇推销或者自吹自擂的文章,我会倾向于挑选我们项目开发中遇到的问题以及不足来进行介绍,便于在后面的开发中作为自我反思的材料.
数据库设计
由于本项目的后端完全由我一人开发,并且急于赶进度,从设计数据字典到简历MySQL数据库仅用了2天,所以数据库的结构设计仍然是比较幼稚的,不够企业级.所幸的是本项目的业务逻辑并不复杂,没有暴露太多的问题.下图为本项目数据库的结构:
可以看出,这次的数据库设计存在以下问题:1.不够企业级.仍然是数据库初学者的一个实体一个表单的简单思路,没有充分利用OOA的继承等机制,并且没有采用身份表或权限表等方式进行权限管理.存在很多重复的字段,浪费了空间,比如Admin,User,Company都存在的email和name等字段.2.字段不够完善.考虑到开发的重点在于业务功能,数据库中的字段秉承的原则是足够展示即可,没有设计更详细的字段.这当然不影响项目开发的质量,但是会给项目带来廉价感和小作坊感.3.主键设计出现了失误.在后期的开发中,发现项目初期开发的Message表单,采用的联合主键不能符合业务需求的问题.最初设计的联合主键是(用户id,企业邮箱,消息类型),但后来发现这样不足以唯一地标识一条消息,导致调试时频繁地出现了主键重复的错误.于是不得不从数据库层面开始为Message补上了自增主键,消耗了大量的时间用来弥补这个错误.
使用OSS进行文件管理
由于我并不喜欢虚拟路径等方式将文件存储在本地,我最初试图采用的方式是将文件直接以Blob的形式存储在MySQL中.并且在最初的开发中,利用Java中byte[]数组可以与MySQL的Blob类型直接可以对接的特点,项目顺利地推进了下去.但是随着我决定改为采用前后端分离的开发模式,这样的文件存储方式变得不那么可靠了.首先,如何采用JSON将数据库中的Blob类型文件传递到前端,再生成为文件?其次,从数据库到前端,这样做的效率还能得到保证吗?所以我不得不考虑其它的方法.
阿里云OSS(Object Storage Service)是一种非常优秀的文件存储方式,可以允许用户使用ali-oss的API来实现文件的在线存储.在本项目中,对用户的头像,图片,以及简历的存储全部使用了OSS,不仅避免了服务器对文件上传的带宽限制,也直接避开了Blob类型的处理难点。因为采用了OSS,在后端数据库中存储文件时,只需要存储文件的URL即可,在前端可以很容易地将URL转化为具体的文件。下图为我的OSS控制台
下面为Vue前端框架中,用来实现OSS上传文件的工具类:
'use strict'
import { dateFormat } from '@/utils/utils'
var OSS = require('ali-oss')
const url = ''
export default {
/**
* 创建随机字符串
* @param num
* @returns {string}
*/
randomString(num) {
const chars = [
'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
]
let res = ''
for (let i = 0; i < num; i++) {
var id = Math.ceil(Math.random() * 35)
res += chars[id]
}
return res
},
/**
* 创建oss客户端对象
* @returns {*}
*/
createOssClient() {
return new Promise((resolve, reject) => {
const client = new OSS({
region: 'oss-cn-qingdao',
accessKeyId: 'LTAI5tGPHhu18Ns6THrqDn97',
accessKeySecret: 'jwN6RPl8QfzdYt56uBQ7UbEe0NMlDy',
bucket: 'aaronwu-first-bucket',
secure: true // 上传链接返回支持https
})
resolve(client)
})
},
/**
* 文件上传
*/
ossUploadFile(option) {
const file = option.file
const self = this
// var url = '';
return new Promise((resolve, reject) => {
const date = dateFormat(new Date(), 'yyyyMMdd') // 当前时间
const dateTime = dateFormat(new Date(), 'yyyyMMddhhmmss') // 当前时间
const randomStr = self.randomString(4) // 4位随机字符串
const extensionName = file.name.substr(file.name.indexOf('.')) // 文件扩展名
const fileName = 'image/' + date + '/' + dateTime + '_' + randomStr + extensionName // 文件名字(相对于根目录的路径 + 文件名)
// 执行上传
self.createOssClient().then(client => {
// 异步上传,返回数据
resolve({
fileName: file.name,
fileUrl: fileName
})
// 上传处理
// 分片上传文件
client
.multipartUpload(fileName, file, {
progress: function(p) {
const e = {}
e.percent = Math.floor(p * 100)
// console.log('Progress: ' + p)
option.onProgress(e)
}
})
.then(
val => {
window.url = val
console.info('woc', url)
if (val.res.statusCode === 200) {
option.onSuccess(val)
return val
} else {
option.onError('上传失败')
}
},
err => {
option.onError('上传失败')
reject(err)
}
)
})
})
}
}
邮箱验证功能
项目实现了利用邮箱验证进行密码修改的功能,但是在这个方面没有进一步深入探索。
可以看到,这条验证码邮件是纯文本,并不美观,可以设法通过H5的方式设计出较为美观和友好的邮件页面,来带给用户更好的感受。此外,邮箱验证不仅可以用来验证用户身份,在业务流程中,也可以多多使用这个功能。
下面为后端实现发送邮件的工具类,邮箱和授权码已经隐去,不同的邮箱客户端,开启服务的方式不同,这里不多赘述.
/**
* 实现邮箱验证的工具类
*
* @author AaronWu
*/
public class SendMail {
/*发件人的邮箱账号如:xxx@163.com*/
public static String sendEmailAccount = "";
/**发件人的邮箱的授权码(自己在邮箱服务器中开启并设置)*/
public static String sendEmailPassword = "";
/**发件人邮箱的SMTP服务器地址,如:smtp.163.com,我使用的是qq邮箱所以如下:*/
public static String sendEmailSMTPHost = "smtp.qq.com";
/**收件人的邮箱账号*/
public static String receiveMailAccount = "";
/**
* 把发送邮件封装为函数,参数为收件人的邮箱账号和要发送的内容
* @param receiveMailAccount 收邮件的人的邮件
* @param mailContent 邮件内容
*/
public void sendMail(String receiveMailAccount, String mailContent) {
// 创建用于连接邮件服务器的参数配置
Properties props = new Properties();
// 设置使用SMTP协议
props.setProperty("mail.transport.protocol", "smtp");
// 设置发件人的SMTP服务器地址
props.setProperty("mail.smtp.host", sendEmailSMTPHost);
// 设置需要验证
props.setProperty("mail.smtp.auth", "true");
// 根据配置创建会话对象, 用于和邮件服务器交互
Session session = Session.getInstance(props);
// 设置debug模式,便于查看发送过程所产生的日志
session.setDebug(true);
try {
// 创建一封邮件
MimeMessage message = createMimeMessage(session, sendEmailAccount, receiveMailAccount, mailContent);
// 根据 Session 获取邮件传输对象
Transport transport = session.getTransport();
transport.connect(sendEmailAccount, sendEmailPassword);
// 发送邮件, 发到所有的收件地址, 通过message.getAllRecipients() 可以获取到在创建邮件对象时添加的所有收件人
transport.sendMessage(message, message.getAllRecipients());
// 关闭连接
transport.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
* @param session 和服务器交互的会话
* @param sendMail 发件人邮箱
* @param receiveMail 收件人邮箱
* @throws Exception
*/
public static MimeMessage createMimeMessage(Session session, String sendMail, String receiveMail,
String mailContent) throws Exception {
// 创建一封邮件
MimeMessage message = new MimeMessage(session);
// 设置发件人姓名和编码格式
message.setFrom(new InternetAddress(sendMail, "伯乐网-邮箱验证", "UTF-8"));
// 收件人
message.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(receiveMail, "尊敬的用户", "UTF-8"));
// 设置邮件主题
message.setSubject("找回密码提醒", "UTF-8");
// 设置邮件正文
message.setContent(mailContent, "text/html;charset=UTF-8");
// 设置发件时间
message.setSentDate(new Date());
// 保存设置
message.saveChanges();
return message;
}
}
安全问题
平台的安全问题一度给我带来了巨大的困惑.最开始我尝试过使用Redis+Token鉴权的方式,但是出现了跨域失败的问题.然后我使用了Spring Security,但存在多次跨域的问题.只能说第一次跨域,没什么经验.所以最终只能采用了Vue Router的导航卫士,实现纯前端的权限控制.例如在没有登录的情况下,通过修改url强行进入用户端页面,就会出现下图的403错误页面:
原理是在用户登录时,系统会生成用户信息的locaoStorage存储在客户端浏览器,当用户退出登录时删除,从而做到对未登录的用户权限进行限制.然而,在我看来,这种存储在客户端的鉴权方式是始终存在隐患的,对于这个最终解决方案我并不满意.
下面为Vue Router 导航卫士的源码:
// 导入路由表
import router from "./router";
/**
* 只要哈希值更改立即执行此方法,内部是一个三参回调
* to:终点哈希
* from:起点哈希
* next:执行过程
*/
router.beforeEach((to, from, next) => {
/** 拿取用户保存在浏览器中的信息*/
const user = localStorage.getItem("uEmail");
const admin = localStorage.getItem("aEmail");
const company = localStorage.getItem("cEmail");
/*如果目标路路径是登录或者注册模块,不进行拦截*/
if(['/#/', '/#/hr_login', '/#/user_login', '/#/hr_register', '/#/user_register',"/#/hr/home"].includes(to.path)){
console.warn("include------------")
console.warn(to.path)
}
/*登录账户为空但是想访问用户页面,直接拦截到首页去*/
else if(to.path.startsWith("/user/")&&!user){
console.warn("user------------")
console.warn(to.path)
next("/403")
}
else if(to.path.startsWith("/hr/")&&!company){
console.warn("hr------------")
console.warn(to.path)
next("/403")
}
else if(to.path.startsWith("/admin/")&&!admin){
console.warn("admin------------")
console.warn(to.path)
next("/403");
}else{
next();
console.warn("else------------")
console.warn(to.path)
}
});
非常容易理解,无非就是将除了首页和登录页面以外的任何页面跳转添加一条判定,加入localStorage不存在就拒绝访问跳转到403页面。注意,导航卫士的编写过程中非常容易出现跳转的死循环,也就是从一个页面跳转到另一个页面,由于编写逻辑的问题又跳转回去,进入无休止的相互跳转。这种问题的表现就是页面显示异常,在浏览器控制台中出现类似于爆栈的错误。注意编写时理清各个页面的逻辑关系。
项目页面概览
首页
登录页面
用户首页
审批简历
浏览岗位
数据统计(Apache Echarts)
项目代码仓库开源
总结
这次开发经历是我比较用心,同时也比较顺利的一次开发经历。虽然项目的质量不够完美,但在客观的学习压力下,在有限的时间内,我认为还是非常满意的。同时也多亏了与我一起开发的@qdu_neymar足够的用心与专注,以及令人惊叹的前端开发天赋,保证了项目开发的速度与质量。本项目前端的布局,网站Logo的设计以及各种样式的设计,全部来源于他的创意,而不是来自网络上的东拼西凑。
能够将学习过的知识,尽最大的努力转化为有价值的结晶,是一件多么让人快乐的事情。