目录标题
写在前面
这个部分对于我而言是一个重要的学习分水岭,学习了JavaWeb知识的之后,自己动手写一个demo的话可以更好的有助于之后阶段的学习。(应该)框架固然可以简化我们的开发流程,但如基本的JavaWeb都不熟练,又怎么会体验到框架的快捷性呢?
(快递e站本来是有微信端和后台两部分组成的,我微信端忘了好多,就不写了。)
本demo实现了以下功能:
项目成功运行视频如下
后台视频
一,项目介绍
1.1项目背景
真实的:开课吧课程学习需要,培训进度到了。
虚假的:
随着互联网和通信技术的高速发展,使电子商务迅速普及,网购成为人们购物的重要手段之一。据统计2019年双十一,11月11日~16日,受电商平台集中促销影响,业务量达全年高峰,6天内共处理邮(快)件23.09亿件。
疫情期间配送快递成了一件难事。疫情防控已经趋于常态化,很多小区出台了新的治安管理条例,开始禁止快递人员进入。这给民众收发快件造成了极大的不便,因进不去小区,快递人员只能在小区门口周边摆摊设点,也影响了小区周边环境。
除了快递,外卖也一样面临相同的问题。除了小区,写字楼、校园也面临着相同的问题。正是基于对疫情期间配送最后一公里深刻的认识,我们探索出一条解决之道,那就是社区快递e栈(类似快递柜)。
1.2项目目的
1、锻炼编码能力
2、融会贯通JavaWeb技术
3、体验项目开发流程
4、做出上线级别的微信端+JavaWeb后端项目(我微信端做的有点水)
5、为框架阶段打下前后端交互的基础
1.3项目涉及技术点
html、css、js、Jquery、bootstrap、layui、ajax、mysql、tomcat、servlet、filter、jsp,Javaweb相关知识点。
1.4需求分析
该快递项目按照用户分为三层,管理员,普通用户和快递员,管理员可以对用户和快递员的数据进行增删改查操作,以及对快递的增删改查。管理员可以对快递进行增删改查,用户可以取件,也可以查看自己的快递情况。
按照功能模块分为登录模块,用户模块,快递员模块,快递模块,控制台模块,本系统主要是面向管理员端的后台关联系统。
二,搭建项目前准备
2.1编写MVC框架
服务器启动时,会加载application.properties配置文件,配置文件中有多个类,配置文件是由HandlerMapping来加载。
HandlerMapping根据配置文件中的每个类,获取到每个类的方法,获取到方法之后再循环遍历这些方法,再找每个带有ResponseBody或ResponseView的方法,再把这些方法都存放在HandlerMapping中,之后用于处理给用户响应,所以服务器启动之后HandlerMapping中会存一堆方法。
当用户去请求DispatcherServlet时,首先去HandlerMapping中找有没有处理这个请求的方法,没有的话抛404异常。请求servlet的路径是:*.do
2.1.1编写web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>DispatherServlet</servlet-name>
<servlet-class>com.kaikeba.mvc.DispatherServlet</servlet-class>
<!--初始化配置文件-->
<init-param>
<param-name>contentConfigLocation</param-name>
<param-value>application.properties</param-value>
</init-param>
<!--服务器启动的时候就加载这个servlet-->
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
2.1.2定义两个注解
package com.kaikeba.mvc;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
/**
* 注解的作用:
* 被此注解添加的方法, 会被用于处理请求。
* 方法返回的内容,会以文字形式返回到客户端
*/
public @interface ResponseBody {
String value();
}
package com.kaikeba.mvc;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
/**
* 注解的作用:
* 被此注解添加的方法, 会被用于处理请求。
* 方法返回的内容,会直接重定向
*/
public @interface ResponseView {
String value();
}
再定义枚举类对应上面的两个注解:
package com.kaikeba.mvc;
/**
* 枚举:用于描述响应的类型:文字;ResponseBody,视图;ResponseView
*/
public enum ResponseType {
TEXT,VIEW;
}
2.1.3 编写映射器 HandlerMapping
package com.kaikeba.mvc;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 映射器 包含了大量的网址与方法的对应关系)
*/
public class HandlerMapping {
// 静态的map集合,字符串加上mvc映射对象
private static Map<String,MVCMapping> data = new HashMap<>();
// 获取map集合中的对应方法
public static MVCMapping get(String uri){
return data.get(uri);
}
public static void load(InputStream is ){
// 常规方法加载配置文件
Properties ppt = new Properties();
try {
ppt.load(is);
} catch (IOException e) {
e.printStackTrace();
}
//获取配置文件中描述的一个个的类
Collection<Object> values = ppt.values();
for (Object cla:values) {
// 遍历配置文件中的所有类
String className = (String) cla;
try {
//加载配置文件中描述的每一个类
Class c = Class.forName(className);
//创建这个类的对象
Object obj = c.getConstructor().newInstance();
//获取这个类的所有方法
Method[] methods = c.getMethods();
for (Method m:methods) {
// 遍历一个类中的所有方法
Annotation[] as = m.getAnnotations(); // 获取方法里的所有注解
if(as != null){
for(Annotation annotation:as){
// 遍历一个方法中的所有注解
if(annotation instanceof ResponseBody){
// 如果注解的类型是ResponseBody
//说明此方法,用于返回字符串给客户端
MVCMapping mapping = new MVCMapping(obj,m,ResponseType.TEXT);
Object o = data.put(((ResponseBody) annotation).value(),mapping);
if(o != null){
//存在了重复的请求地址
throw new RuntimeException("请求地址重复:"+((ResponseBody) annotation).value());
}
}else if(annotation instanceof ResponseView){
// 如果注解的类型是ResponseView
//说明此方法,用于返回界面给客户端
MVCMapping mapping = new MVCMapping(obj,m,ResponseType.VIEW);
Object o = data.put(((ResponseView) annotation).value(),mapping);
if(o != null){
//存在了重复的请求地址
throw new RuntimeException("请求地址重复:"+((ResponseView) annotation).value());
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 映射对象,每一个对象封装了一个方法,用于处理请求
*/
public static class MVCMapping{
private Object obj; // 对象
private Method method; // 方法
private ResponseType type; // 泛型
public MVCMapping() {
}
public MVCMapping(Object obj, Method method, ResponseType type) {
this.obj = obj;
this.method = method;
this.type = type;
}
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public ResponseType getType() {
return type;
}
public void setType(ResponseType type) {
this.type = type;
}
}
}
2.1.4 定义分发Servlet
package com.kaikeba.mvc;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;
public class DispatherServlet extends javax.servlet.http.HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
// 读取配置文件的文件名称
String path = config.getInitParameter("contentConfigLocation");
// 加载配置文件
InputStream is = DispatherServlet.class.getClassLoader().getResourceAsStream(path);
// 常规方法加载
/* //Properties可以用来保存属性集,主要用于读取Java的配置文件
Properties ppt = new Properties();
try {
ppt.load(is);
} catch (IOException e) {
e.printStackTrace();
}*/
// 使用映射器加载
HandlerMapping.load(is);
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1. 获取用户请求的uri /xx.do
String uri = req.getRequestURI();
// 根据url得到MVCMapping映射对象
HandlerMapping.MVCMapping mapping = HandlerMapping.get(uri);
if( mapping == null){
resp.sendError(404,"MVC:映射地址不存在:"+uri);
return;
}
Object obj = mapping.getObj();
Method method = mapping.getMethod();
Object result = null;
try {
// 参数1.obj对象要执行method方法
// 参数2.调用方法时传递的参数 req, resp 请求,响应对象
result = method.invoke(obj, req, resp);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
switch (mapping.getType()){
case TEXT:
resp.getWriter().write((String)result);
break;
case VIEW:
resp.sendRedirect((String)result);
break;
}
}
}
2.2layui介绍
2.2.1布局部分
与BootStrap很像,就是展示一些界面效果。
2.2.2 弹出层
步骤:
1.下载layer , 并将解压后的layer文件夹 移动到项目中
2.引入jquery.js
3.引入layer.js
layer - msg函数
用于弹出信息提示框
格式1:
layer.msg("文本");
格式2 抖动显示:
layer.msg("文本",function(){
//弹窗结束后会执行
});
layer - load函数
格式1
弹出loading:
var index = layer.load(数字0-2);
// 参数表示 loading的图表
//loading窗口在弹出时, 不允许进行操作.
关闭loading:
layer.close(index);
格式2
超时自动关闭的loading
var index = layer.load(数字0-2,{
time:毫秒数字})
//在指定的毫秒后 ,如果没有使用layer.close关闭, 则自动关闭
layer - msg函数(load效果)
格式:
弹出的格式:
var index = layer.msg("文本",{
icon:16,shade:0.01})
//因为是msg函数, 所以此窗口会自动消失
关闭的格式:
layer.close(index);
layer - alert函数 信息提示窗
格式:
layer.alert("文本内容",{
icon:图片编号});
//图片编号: 0-16之间
layer - tips函数 提示层
格式:
layer.tips("文本内容","选择器",{
tipsMore:true,tips:数字});
参数:
参数: tipsMore : 是否允许同时存在多个弹出的tips
参数: tips : 取值为数字1-4 , 分别表示弹出在元素的 上/右/下/左 . 默认弹出在右边
layer 所有弹出层的关闭
layer.closeAll();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/jquery2.1.4.js"></script>
<script src="layer/layer.js"></script>
<script>
function msgTest() {
// layer.msg("提示的文字");
layer.msg("提示的文字", function () {
// 窗口关系时执行,和上面的区别还有会添加一个抖动
});
}
function loadTest() {
// 返回弹出层的id
var windowId = layer.load(0); // 0 - 16的数字,每一个代表不同效果
// 通过窗口id关闭窗口
setTimeout(function () {
layer.close(windowId);
}, 2000);
}
function msg_loadTest() {
// icon不同的值对应不同的图标
layer.msg("提示的文字", {
icon:16, shade:0.01});
}
function alertTest() {
layer.alert("文字内容", {
icon : 10});
}
function tipsTest() {
// tipsMore表示
layer.tips("这里有秘密", "#s1", {
tipsMore:true, tips:2});
}
</script>
</head>
<body>
<button onclick="msgTest()">msg函数(*)</button><br>
<button onclick="loadTest()">load函数(*)</button><br>
<button onclick="msg_loadTest()">load函数(load效果)</button><br>
<button onclick="alertTest()">alert函数</button><br>
<button onclick="tipsTest()">tips函数</button><br>
<p>
锄禾日<span id="s1">当午</span>,汗滴禾下土
</p>
</body>
</html>
2.3工具类介绍
2.3.1DruidUtil
进行数据库连接池的工具类
package com.kaikeba.util;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class DruidUtil {
private static DataSource ds;
static{
try {
Properties ppt = new Properties();
ppt.load(DruidUtil.class.getClassLoader().getResourceAsStream("druid.properties"));
ds = DruidDataSourceFactory.createDataSource(ppt);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 从连接池中取出一个连接给用户
* @return
*/
public static Connection getConnection(){
try {
return ds.getConnection();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
return null;
}
public static void close(Connection conn, Statement state, ResultSet rs){
try {
if(rs!=null)
rs.close();
} catch (Exception throwables) {
throwables.printStackTrace();
}
try {
if(state!=null)
state.close();
} catch (Exception throwables) {
throwables.printStackTrace();
}
try {
if(conn!=null)
conn.close();
} catch (Exception throwables) {
throwables.printStackTrace();
}
}
}
2.3.2DateFormatUtil
时间格式化工具类
package com.kaikeba.util;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 时间日期格式化工具
*/
public class DataFormatUtil {
private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //字符串转日期类型
public static String format(Date date){
// 把传入的Date类型转换为我们定义的Date类型
return format.format(date);
}
}
2.3.3随机数工具类
package com.kaikeba.util;
import java.util.Random;
/**
* @Description : 生成随机取件码
* @Author : Administrator
* @Date : 2021/11/17 7:56
* @Version : 1.0
**/
public class RandomUtil {
private static Random r = new Random();
public static int getCode(){
return r.nextInt(900000)+100000;
};
}
2.3.4Json操作类
package com.kaikeba.util;
import com.google.gson.Gson;
/**
* @Description : 为了更好的把数据返回给用户,返回Gson类型的数据给用户
* @Author : Administrator
* @Date : 2021/11/15 10:24
* @Version : 1.0
**/
public class JSONUtil {
private static Gson g = new Gson();
public static String toJSON(Object obj){
// 把对象变为json字符串
return g.toJson(obj);
}
}
2.3.5用户的工具类
package com.kaikeba.util;
import com.kaikeba.bean.User;
import javax.servlet.http.HttpSession;
/**
* @Description : 用户的工具类,
* @Author : Administrator
* @Date : 2021/11/17 22:05
* @Version : 1.0
**/
public class UserUtil {
public static String getUserName(HttpSession session){
// 根据session获取当前登录的用户名
return (String) session.getAttribute("adminUserName");
}
public static String getUserPhone(HttpSession session){
// 根据session获取当前登录的用户手机号
// TODO : 还没有编写柜子端,未存储任何的录入人信息
return "18888888888";
}
}
2.3.6前后端用于通信的消息类
package com.kaikeba.bean;
/**
* @Description : 专门一个发消息的类
* @Author : Administrator
* @Date : 2021/11/15 10:13
* @Version : 1.0
**/
public class Message {
//状态码: 0成功,-1表示失败
private int status;
//消息内容
private String result;
//消息所携带的一组数据
private Object data;
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public Message(int status, String result) {
this.status = status;
this.result = result;
}
public Message(String result) {
this.result = result;
}
public Message(int status) {
this.status = status;
}
public Message() {
}
public Message(int status, String result, Object data) {
this.status = status;
this.result = result;
this.data = data;
}
}
2.3.7包装分页数据的类
package com.kaikeba.bean;
import java.util.ArrayList;
import java.util.List;
/**
* @Description : 包装分页数据的一种格式
* @Author : Administrator
* @Date : 2021/11/17 16:20
* @Version : 1.0
**/
public class ResultData<T> {
//每次查询的数据集合(存储分页的数据)
private List<T> rows = new ArrayList<>();
//总数量(长度)
private int total;
public List<T> getRows() {
return rows;
}
public void setRows(List<T> rows) {
this.rows = rows;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
}
2.3.8自定义的异常类
取件码重复异常:
package com.kaikeba.exception;
/**
* @Description : 异常类:取件码重复异常
* @Author : Administrator
* @Date : 2021/11/17 7:20
* @Version : 1.0
**/
public class DuplicateCodeException extends Exception{
/**
* Constructs a new exception with {@code null} as its detail message.
* The cause is not initialized, and may subsequently be initialized by a
* call to {@link #initCause}.
*/
public DuplicateCodeException() {
}
/**
* Constructs a new exception with the specified detail message. The
* cause is not initialized, and may subsequently be initialized by
* a call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
*/
public DuplicateCodeException(String message) {
super(message);
}
}
2.4数据库设计+实体类编写
2.4.1数据库分析
之前的需求分析中有说到。要实现登录模块,用户模块,快递员模块,快递模块,控制台模块。那么就要创建自少四张表。管理员表,用户表,快递员表,快递员表。
管理员表
用户表
快递员表
快递表
其实单看这几张表的话,也能感觉到这是一个很简单的练手项目。甚至还不如他的前置项目表复杂。基本都是单表操作。具体的表内容就大家自己创建了。
2.4.2实体类
根据所创建的sql创建对应的类
类名=表名,属性名=列名
1,管理员类不用创建对应的实体类。
2,再创建时间类型的属性时,把他定义为String类型的,方便用对应的工具类显示
User:
package com.kaikeba.bean;
import java.sql.Timestamp;
/**
* @Description : 同样也是用户实体类,不过这里用了字符类型来结束时间参数
* @Author : Administrator
* @Date : 2021/11/18 16:55
* @Version : 1.0
**/
public class BootStrapTableUser {
private int id;
private String username;
private String userphone;
private String idcard;
private String userpassword;
private String registeradtime;
private String login_time;
private int status;
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public BootStrapTableUser() {
}
public BootStrapTableUser(int id, String username, String userphone, String idcard, String userpassword, String registeradtime, String login_time, int status) {
this.id = id;
this.username = username;
this.userphone = userphone;
this.idcard = idcard;
this.userpassword = userpassword;
this.registeradtime = registeradtime;
this.login_time = login_time;
this.status = status;
}
public BootStrapTableUser(String username, String userphone, String idcard, String userpassword, int operation) {
this.username = username;
this.userphone = userphone;
this.idcard = idcard;
this.userpassword = userpassword;
}
public BootStrapTableUser(int id, String username, String userphone, String idcard, String userpassword, String registeradtime, String login_time) {
}
@Override
public String toString() {
return "BootStrapTableUser{" +
"id=" + id +
", username='" + username + '\'' +
", userphone='" + userphone + '\'' +
", idcard='" + idcard + '\'' +
", userpassword='" + userpassword + '\'' +
", registeradtime='" + registeradtime + '\'' +
", login_time='" + login_time + '\'' +
", status=" + status +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getUserphone() {
return userphone;
}
public void setUserphone(String userphone) {
this.userphone = userphone;
}
public String getIdcard() {
return idcard;
}
public void setIdcard(String idcard) {
this.idcard = idcard;
}
public String getUserpassword() {
return userpassword;
}
public void setUserpassword(String userpassword) {
this.userpassword = userpassword;
}
public String getRegisteradtime() {
return registeradtime;
}
public void setRegisteradtime(String registeradtime) {
this.registeradtime = registeradtime;
}
public String getLogin_time() {
return login_time;
}
public void setLogin_time(String login_time) {
this.login_time = login_time;
}
}
Express:
package com.kaikeba.bean;
/**
* @Description : 同样也是快递实体类,不过这里用了字符类型来结束时间参数
* @Author : Administrator
* @Date : 2021/11/17 16:20
* @Version : 1.0
**/
public class BootStrapTableExpress {
private int id;
private String number;
private String username;
private String userPhone;
private String company;
private String code;
private String inTime;
private String outTime;
private String status;
private String sysPhone;
private Courier Courier;
public BootStrapTableExpress() {
}
public BootStrapTableExpress(int id, String number, String username, String userPhone, String company, String code, String inTime, String outTime, String status, String sysPhone) {
this.id = id;
this.number = number;
this.username = username;
this.userPhone = userPhone;
this.company = company;
this.code = code;
this.inTime = inTime;
this.outTime = outTime;
this.status = status;
this.sysPhone = sysPhone;
}
public BootStrapTableExpress(int id, String number, String username, String userPhone, String company, String code, String inTime, String outTime, String status, String sysPhone, com.kaikeba.bean.Courier courier) {
this.id = id;
this.number = number;
this.username = username;
this.userPhone = userPhone;
this.company = company;
this.code = code;
this.inTime = inTime;
this.outTime = outTime;
this.status = status;
this.sysPhone = sysPhone;
Courier = courier;
}
@Override
public String toString() {
return "BootStrapTableExpress{" +
"id=" + id +
", number='" + number + '\'' +
", username='" + username + '\'' +
", userPhone='" + userPhone + '\'' +
", company='" + company + '\'' +
", code='" + code + '\'' +
", inTime='" + inTime + '\'' +
", outTime='" + outTime + '\'' +
", status='" + status + '\'' +
", sysPhone='" + sysPhone + '\'' +
", Courier=" + Courier +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {