一个基于Java Web的商城后台管理系统。模仿 Spring Boot 的核心特性,实现了自己的依赖注入容器和 MVC 框架。
1. 系统简介
本购物系统分为管理端和用户端两部分,旨在实现高效的商品管理与用户购物体验。管理端为管理员提供核心操作功能,包括用户管理、商品管理和订单管理。管理员可通过系统对用户信息进行查询、增删改查商品及其库存,并支持条件查询订单。
用户端面向公众和会员,功能丰富。公共模块支持商品分类浏览、商品详情和评价查看以及问题咨询。会员模块进一步提供登录注册、购物车管理、订单查询及评价功能。用户可通过平台轻松完成商品的购买流程,包括加入购物车、一键下单等操作。
整体系统设计注重用户体验,功能划分清晰,操作便捷,为商家和用户提供了高效、友好的购物服务。
项目仓库地址:https://gitee.com/enhead/shop
2. 系统功能模块图
3. 表设计
表1 管理员信息表(admins)
列名 | 数据类型 | 长度 | 是否为空 | 描述 |
---|---|---|---|---|
id | BIGINT | – | 否 | 管理员ID(主键) |
account | VARCHAR | 255 | 否 | 管理员账号(唯一) |
name | VARCHAR | 64 | 否 | 管理员姓名 |
pwd | VARCHAR | 255 | 否 | 管理员密码 |
表2 用户信息表(users)
列名 | 数据类型 | 长度 | 是否为空 | 描述 |
---|---|---|---|---|
id | BIGINT | – | 否 | 用户ID(主键) |
VARCHAR | 64 | 否 | 用户邮箱(唯一) | |
pwd | VARCHAR | 255 | 否 | 用户密码 |
nickname | VARCHAR | 64 | 否 | 用户昵称(唯一) |
sex | INT | – | 否 | 性别(0-保密,1-男性,2-女性) |
recipient | VARCHAR | 64 | 是 | 收件人姓名 |
address | VARCHAR | 500 | 是 | 收货地址 |
phone | VARCHAR | 64 | 是 | 联系电话 |
headimg | VARCHAR | 500 | 否 | 用户头像 |
updatetime | DATETIME | – | 否 | 更新时间 |
createtime | DATETIME | – | 否 | 创建时间 |
表3 商品类别信息表(types)
列名 | 数据类型 | 长度 | 是否为空 | 描述 |
---|---|---|---|---|
id | BIGINT | – | 否 | 类别ID(主键) |
name | VARCHAR | 255 | 否 | 类别名称 |
表4 商品信息表(goods)
列名 | 数据类型 | 长度 | 是否为空 | 描述 |
---|---|---|---|---|
id | BIGINT | – | 否 | 商品ID(主键) |
name | VARCHAR | 500 | 否 | 商品名称 |
typeId | BIGINT | – | 否 | 商品类别ID |
img | VARCHAR | 500 | 是 | 商品图片URL |
desc | TEXT | – | 是 | 商品描述 |
updatetime | DATETIME | – | 否 | 更新时间 |
createtime | DATETIME | – | 否 | 创建时间 |
表5 商品规格信息表(goodsDetails)
列名 | 数据类型 | 长度 | 是否为空 | 描述 |
---|---|---|---|---|
id | BIGINT | – | 否 | 商品详情ID(主键) |
goodsId | BIGINT | – | 否 | 商品ID |
specName | VARCHAR | 500 | 否 | 规格名称 |
stockNum | INT | – | 否 | 库存数量 |
unitPrice | FLOAT | – | 否 | 单价 |
updatetime | DATETIME | – | 否 | 更新时间 |
createtime | DATETIME | – | 否 | 创建时间 |
表6 商品评价信息表(comments)
列名 | 数据类型 | 长度 | 是否为空 | 描述 |
---|---|---|---|---|
id | BIGINT | – | 否 | 评价ID(主键) |
userId | BIGINT | – | 否 | 评价用户ID |
goodsId | BIGINT | – | 否 | 商品ID |
goodsDetailId | BIGINT | – | 否 | 商品规格详情ID |
orderId | BIGINT | – | 否 | 订单ID |
content | VARCHAR | 500 | 是 | 评价内容 |
score | INT | – | 否 | 评分(1-5分) |
createtime | DATETIME | – | 否 | 创建时间 |
表7 留言信息表(messages)
列名 | 数据类型 | 长度 | 是否为空 | 描述 |
---|---|---|---|---|
id | BIGINT | – | 否 | 留言ID(主键) |
userId | BIGINT | – | 否 | 用户ID |
goodsId | BIGINT | – | 否 | 商品ID |
content | VARCHAR | 500 | 否 | 留言内容 |
state | INT | – | 否 | 留言状态(0-未回复,1-已回复) |
createtime | DATETIME | – | 否 | 创建时间 |
表8 订单信息表(orders)
列名 | 数据类型 | 长度 | 是否为空 | 描述 |
---|---|---|---|---|
id | BIGINT | – | 否 | 订单ID(主键) |
userId | BIGINT | – | 否 | 用户ID |
goodsDetailId | BIGINT | – | 否 | 商品规格详情ID |
goodsNum | INT | – | 否 | 商品数量 |
amount | FLOAT | – | 否 | 订单金额 |
state | INT | – | 否 | 订单状态 |
updatetime | DATETIME | – | 否 | 更新时间 |
createtime | DATETIME | – | 否 | 创建时间 |
表9 留言回复信息表(replies)
列名 | 数据类型 | 长度 | 是否为空 | 描述 |
---|---|---|---|---|
id | BIGINT | – | 否 | 回复ID(主键) |
messageId | BIGINT | – | 否 | 留言ID |
content | VARCHAR | 500 | 否 | 回复内容 |
createtime | DATETIME | – | 否 | 创建时间 |
实体关系图:(EDR图)
4. 系统实现
系统实现概述
本商城系统采用前后端分离架构,前端使用了 Vue.js 进行开发,后端基于 Servlet 技术实现,并通过反射机制简化实现了 SpringMVC 的基础注解功能。整个系统遵循 RESTful 规范,前后端通过 HTTP 协议进行交互,确保系统的简洁性与可扩展性。
在前端部分,采用 Vue.js 实现响应式的用户界面,配合 axios 实现与后端的 HTTP 请求交互。系统遵循 RESTful API 设计原则,前端通过 GET、POST 等 HTTP 请求方法与后端交互,操作商品、用户和订单等资源。数据格式主要采用 JSON,以保证数据传输的高效性与兼容性。
后端部分,通过传统的 Servlet 技术处理 HTTP 请求,并通过反射机制实现了简化版的 SpringMVC 控制层注解功能。通过自定义注解,如 @RestController、@RequestMapping和全局异常处理器 等,模拟了 SpringMVC 的请求映射和响应处理机制,使得系统能够更灵活地处理用户请求并返回相应的结果。同时,IOC 容器的实现通过反射扫描组件类并进行依赖注入,进一步简化了代码结构。
在团队合作方面,我们采用了 Git 进行版本控制,并通过 Gitee 进行代码托管和协作开发。API 文档的编写和管理使用了 ApiFox,确保了前后端开发的同步与接口的一致性。此外,团队成员之间通过不断的代码审查和讨论,确保了系统的高效开发和稳定性。
通过这些技术实现,本商城系统具备了高效、可扩展、易维护的特点,能够为用户提供流畅的购物体验。
4.1 技术选型
4.1.1 后端部分
本商城系统的后端基于Java开发,采用传统的 Servlet 和 JSP 技术进行搭建,注重简洁性和高效性,主要技术选型如下:
-
开发工具与环境
-
Maven:作为项目的构建和依赖管理工具,用于管理依赖库、简化项目的编译与打包过程。
-
Java 11:提供现代化语言特性,如模块化系统和改进的垃圾回收机制,提升开发效率和性能。
-
Web 技术
-
Jakarta Servlet API:处理 HTTP 请求和响应,实现核心的服务端逻辑。
-
Jakarta JSP API:用于动态生成 HTML 页面,支持简单的数据展示和用户交互。
-
Commons BeanUtils:提供便捷的 JavaBean 操作能力,提高代码的可读性与可维护性。
-
数据库与连接池
-
MySQL Connector:支持与 MySQL 数据库进行高效交互,提供稳定的数据库访问功能。
-
Druid:阿里巴巴开源的数据库连接池,具备高性能、稳定性和强大的监控功能。
-
Commons DBUtils:封装 JDBC 操作,简化数据库查询与事务处理。
-
数据处理与安全
-
FastJSON:高性能的 JSON 序列化与反序列化工具,用于后端与前端的数据传输。
-
JJWT:用于生成和验证 JWT(JSON Web Token),支持安全的身份认证与授权。
-
JBCrypt:基于 BCrypt 算法的密码加密工具,用于存储用户密码,增强系统安全性。
-
日志与调试
-
Log4j:高效、灵活的日志框架,用于记录系统运行状态,支持多种日志级别和输出方式。
-
Reflections:用于反射操作,便于动态加载类或处理运行时元数据。
4.1.2 前端部分
前端部分是有原型项目的,在此项目的项目做出一点小改动,项目地址:https://gitee.com/enhead/shop-prototype
本系统的前端部分采用了基于 Vue.js 的技术栈进行开发,具体选型如下:
-
核心框架
使用 Vue.js 作为核心框架,提供了响应式的数据绑定与组件化开发模式,能够快速构建高性能的单页应用程序(SPA)。
-
路由管理
采用 vue-router 进行前端路由管理,支持多页面间的路由跳转与参数传递,实现了视图的动态切换。
-
状态管理
借助 Vuex 实现全局状态管理,用于管理购物车、用户信息等共享数据,保证了组件间数据的统一性和可维护性。
-
HTTP请求
使用 axios 作为 HTTP 客户端,用于与后端 API 进行数据交互,支持请求拦截与响应处理,简化了异步请求的管理。
-
模块打包
使用 Webpack 作为打包工具,通过 vue-loader 和 babel 编译工具支持 Vue 单文件组件的开发及 ES6 语法的转译,提升了开发效率和代码兼容性。
-
样式处理
前端样式处理集成了 less 和 less-loader,支持灵活的样式预处理,提高了样式的可复用性和开发效率。
-
辅助工具
- html-webpack-plugin:生成自动引入依赖的 HTML 文件。
- copy-webpack-plugin:用于静态资源的拷贝和处理。
- friendly-errors-webpack-plugin:优化开发过程中的错误提示,提升开发体验。
-
浏览器兼容性
配置了 autoprefixer 和 browserslist,自动添加浏览器前缀,确保兼容大多数主流浏览器。
通过上述技术选型,系统的前端具备了高性能、模块化、易维护和高兼容性的特点,能够满足商城系统的复杂业务需求。
4.1.3 团队开发部分
本商城系统的开发团队使用了现代化的协作工具和版本控制系统,以确保开发过程高效、协同。主要的团队合作开发工具选型如下:
- 版本控制与代码托管
- Git:作为分布式版本控制系统,用于管理代码的版本、分支和合并,确保团队成员能够高效协作,避免代码冲突。
- Gitee:作为代码托管平台,提供 Git 仓库管理、协作开发和版本控制服务。Gitee 支持快速的代码提交与代码审查流程,提升团队协作效率。
- 接口文档与测试
- ApiFox:用于接口文档的编写与管理,支持团队成员共同编写、更新 API 文档,确保接口的一致性与可用性。ApiFox 还具备接口调试功能,帮助开发人员快速测试接口,保证后端与前端的接口对接准确无误。
4.2 系统框架实现
4.2.1 基于servelet、序列化和放射机制实现SpringMVC基础注解
4.2.1.1 功能概述
这段代码的核心可以分为两个部分:IOC容器和AOP请求处理器。
- IOC容器(依赖注入容器):
- 负责管理应用中的所有组件和服务实例。在本代码中,IOC容器通过 ApplicationContext 类提供,负责存储和获取控制器实例及其依赖对象。
- 通过注解扫描 (@RestController 和 @RestControllerAdvice),IOC 容器自动识别并实例化控制器和全局异常处理类,并进行管理。
- 容器提供依赖注入功能,确保控制器和异常处理器能通过反射机制正确地被调用。
- AOP请求处理器:
- 通过注解驱动的方式(如 @GetMapping, @PostMapping 等),AopRequestHandler 负责将 HTTP 请求映射到相应的控制器方法。
- 处理请求时,首先解析请求路径和请求方法,然后通过反射机制调用对应的控制器方法,并将请求参数(通过 @RequestParam 和 @RequestBody 注解)传递给方法。
- 对于异常,AOP 请求处理器会根据配置的全局异常处理器(通过 @ExceptionHandler 注解)捕获并处理,返回 JSON 格式的错误信息。
- 通过 response 对象统一设置响应格式(如 JSON 格式和跨域支持)。
简而言之,IOC容器负责管理应用中的组件实例,而AOP请求处理器通过注解配置和反射机制处理请求、参数解析和异常捕获等逻辑。
4.1.1.2 代码实现
package com.wawu.common.httpHandler; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; //TODO 待优化:请求路径处理冗余了 // 这里我规定/api,还会进行中心代理,返回json @WebServlet("/api/*") public class AopServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 通过AopRequestHandler处理请求 AopRequestHandler.handleRequest(request, response); } }
package com.wawu.common.httpHandler; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.wawu.common.IOC.ApplicationContext; import com.wawu.common.annotation.controller.globalExceptionHandle.ExceptionHandler; import com.wawu.common.annotation.controller.mapping.*; import com.wawu.common.annotation.controller.parameter.RequestBody; import com.wawu.common.annotation.controller.parameter.RequestParam; import com.wawu.common.enumeration.RequestMethodEnum; import com.wawu.common.property.TomcatProperty; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.beanutils.ConvertUtils; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 使用切面编程处理原生的Servlet * 功能: * 1. 路由注册以及控制层实例的映射 * 2. 分配实际的请求相应,并以json格式放回 * * 包含注解(控制层相关注解的简化): * 1. @RestController,@RequestMapping,@请求方式Mapping * 2. 请求方法接受方法相关注解:@RequestBody,@RequestParam */ public class AopRequestHandler { private static final Map routes = new HashMap();//请求的路由映射 private static final Map restControllers = new HashMap(); // 路由对应控制器实例(来自IOC容器中的bean对象) //TODO:待优化: 1.如果需要简化代码:①请求路径以及方法可以放在@RequestMapping,但是这里还需要完成注解(类似`@AliasFor(annotation = Component.class)`用来处理别名);②简单的办法可以定义一个集合来枚举 // 2.支持多路径、多请求方式,这里默认只有一个路径 // 3.当@请求方式Mapping为空时,特判 /** * 初始化路由 * - 注册路由 * - 注册请求控制对应的实例 * @param context:IOC容器 */ public static void initializeRoutes(ApplicationContext context) { //获取所有被@RestController修饰的类及其方法 for (Class restcontrollerClass : context.getRestControllerClazzes()) { Object restController = context.getBean(restcontrollerClass); String basePath = ""; //类上的是父路径 if (restcontrollerClass.isAnnotationPresent(RequestMapping.class)) { basePath = restcontrollerClass.getAnnotation(RequestMapping.class).value()[0]; } //类方法上还有子路径 //合成完成的路由同时进行注册 for (Method method : restcontrollerClass.getDeclaredMethods()) { String methodPath = ""; // 方法的路径,默认空 if (method.isAnnotationPresent(GetMapping.class)) { methodPath = method.getAnnotation(GetMapping.class).value()[0]; registerRoute(basePath, methodPath, RequestMethodEnum.GET, method, restController); } else if (method.isAnnotationPresent(PostMapping.class)) { methodPath = method.getAnnotation(PostMapping.class).value()[0]; registerRoute(basePath, methodPath, RequestMethodEnum.POST, method, restController); } else if (method.isAnnotationPresent(PutMapping.class)) { methodPath = method.getAnnotation(PutMapping.class).value()[0]; registerRoute(basePath, methodPath, RequestMethodEnum.PUT, method, restController); } else if (method.isAnnotationPresent(DeleteMapping.class)) { methodPath = method.getAnnotation(DeleteMapping.class).value()[0]; registerRoute(basePath, methodPath, RequestMethodEnum.DELETE, method, restController); } } } System.out.println("基础请求路由:"+routes); System.out.println("请求控制层:"+restControllers); System.out.println("注意:默认是去掉Tomcat对应的上下文"+ TomcatProperty.CONTEXT_PATH); } /** * 注册请求路径和方法映射 * @param basePath:类上的父路径 * @param methodPath:子类上的子路径 * @param methodEnum:请求方式 * @param method:放射中的方法 * @param controller:后续将要进行调用的实体类 */ private static void registerRoute(String basePath, String methodPath, RequestMethodEnum methodEnum, Method method, Object controller) { String fullPath = (basePath + methodPath).replaceAll("/+", "/"); routes.put(methodEnum.name() + fullPath, method); restControllers.put(methodEnum.name() + fullPath, controller); // 记录控制器实例 } /从这里开始写注册异常处理部分/ //大部分其实就是参照上面 private static final Map parameterType = parameters[0].getType(); if (Throwable.class.isAssignableFrom(parameterType)) { exceptionTypes = new Class[]{(Class type) { //之前有问题,这里如果是long或者是自定义类型咋办? // if (value == null) return null; // if (type == Integer.class || type == int.class) { // return Integer.parseInt(value); // } else if (type == Double.class || type == double.class) { // return Double.parseDouble(value); // } // return value; if (value == null) return null; try { // 使用 ConvertUtils 转换基本类型和常见类型 //TODO 后续有时间添加时间类型 // 这个部分赶时间由GPT生成了 // 处理 LocalDateTime 类型 if (type == LocalDateTime.class) { // 处理常见的日期格式,包括带时间和不带时间的日期 DateTimeFormatter formatter; // 如果是带时间的格式 (例如 yyyy-MM-dd HH:mm:ss) if (value.contains("T")) { formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; } // 如果是日期格式 (例如 yyyy-MM-dd),就加上默认时间 else if (value.matches("\\d{4}-\\d{2}-\\d{2}")) { value += "T00:00:00"; // 加上默认时间 formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; } else { throw new DateTimeParseException("Invalid date time format", value, 0); } return LocalDateTime.parse(value, formatter); } // 处理 java.util.Date 类型 if (type == Date.class) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(value); } return ConvertUtils.convert(value, type); } catch (Exception e) { System.out.println("Type conversion error for value: " + value + ", target type: " + type.getName()); e.printStackTrace(); } return value; } }
package com.wawu.common.IOC; import com.wawu.common.annotation.IOC.Autowired; import com.wawu.common.annotation.IOC.component.Component; import com.wawu.common.annotation.IOC.component.RestController; import com.wawu.common.annotation.controller.globalExceptionHandle.RestControllerAdvice; import com.wawu.common.utils.AnnotationUtils; import lombok.Getter; import org.reflections.Reflections; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * 实现IOC容器(控制反转和依赖注入) * 表现:使用@Component及其相关注解注册bean对象,然后使用@Autowired进行注入 */ public class ApplicationContext { private Set, Object> beanMap = new HashMap(); // key:类;value:实例;(单例模式) //这个类似工厂模式 @Getter private Set> restControllerAdviceClazzes=new HashSet();//存放所有的全局异常处理器 public ApplicationContext(String basePackage,String baseAnnotationPackage) { // 1. 获取所有自定义注解,并筛选出需要注册的注解(这里逻辑上可能还有问题) scanForRegisterAnnotations(baseAnnotationPackage); System.out.println("@Component相关注解:"+registerComponentAnnotations); // 2. 扫描并注册组件类 scanAndRegisterComponents(basePackage); System.out.println("相关bean对象:"+beanMap); // 3. 执行依赖注入 injectDependencies(); System.out.println("执行依赖注入成功。。。"); } /** * 扫描所有自定义注解,筛出所有@Component(包括本身)相关注解 * @param baseAnnotationPackage:注解基础包(只扫描这个包) */ private void scanForRegisterAnnotations(String baseAnnotationPackage) { //把@Component本身加入 registerComponentAnnotations.add(Component.class); //递归扫描所有被@Component修饰的注解 Reflections reflections = new Reflections(baseAnnotationPackage); Set> componentWithComponentAnnotated = reflections.getTypesAnnotatedWith(registerComponentAnnotation);//注意不会识别嵌套注解 // System.out.println(registerComponentAnnotation.getName()+":"+componentWithComponentAnnotated); for (Class clazz : componentWithComponentAnnotated) { //如果为一个普通类,则进行注册实例化 if(!clazz.isInterface() && !clazz.isEnum() && !clazz.isPrimitive() && !clazz.isArray()){ //进行实例化注册 try { Object bean = clazz.getDeclaredConstructor().newInstance(); beanMap.put(clazz, bean); // 将类及其实例包装对象加入beanMap }catch (Exception e) { e.printStackTrace(); } //获取被@RestController修饰的类,后需要注册路由 //注意下:这里就不递归检查了 if(clazz.isAnnotationPresent(RestController.class)){ restControllerClazzes.add(clazz); }else if (clazz.isAnnotationPresent(RestControllerAdvice.class)){//放全局异常处理器类 restControllerAdviceClazzes.add(clazz); } } } } } /** * 执行依赖注入(简化后) * 规则:需要为bean对象(被@Component修饰),会将被@Autowired修饰的属性注入 * - 普通类型:直接根据类型注入 * - 接口类型:当实现类只有一个时,会将这个注入 * */ private void injectDependencies() { //遍历所有bean对象(就是被@Component修饰) for(Map.Entry interfaceType) { // Set
- IOC容器(依赖注入容器):
- ApiFox:用于接口文档的编写与管理,支持团队成员共同编写、更新 API 文档,确保接口的一致性与可用性。ApiFox 还具备接口调试功能,帮助开发人员快速测试接口,保证后端与前端的接口对接准确无误。
- 版本控制与代码托管
-
-
-
-
-
-