电商项目介绍
基于SpringBoot开发的电商管理平台
- 前端用户界面: HTML Vue ElementUI Echarts
- 后端应用层: SpringBoot SpringMvc Spring lombok
- 数据库: Mysql Mybatis MybatisPlus Redis
- 版本管理: Git
- 项目依赖: Maven
项目演示
地址: http://edemo.solitude0325.top/index.html
源码: https://github.com/solitude325/E-commerce-service-platform
实现功能
- 完成电商管理后端接口开发, 实现了管理者登录 权限控制 商品管理等功能
- 利用 Echarts 完成销量报表展示
- 可以完成图片上传和下载
- 完成电商用户端接口开发, 实现用户登录 下单等功能
- 开发完成短信验证码服务, 可以实现验证码登录
- 利用 Knife4j 生成 Swagger 接口文档
- 利用 Redis 及 SpringCache 缓存方案优化数据库访问速度
- Nginx 反向代理, Git 版本控制
一些技术要点
项目依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.solitude</groupId>
<artifactId>E-commerce-service-platform</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>E-commerce-service-platform</name>
<description>E-commerce-service-platform</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.solitude.ECommerceServicePlatformApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
用户登录相关
// 利用filter完成登录检查,未登录返回error(NOTLOGIN),前端接收到跳转至登录页面
@WebFilter(filterName = "LoginCheckFilter", urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request1 = (HttpServletRequest) request;
HttpServletResponse response1 = (HttpServletResponse) response;
String requestURI = request1.getRequestURI();
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/user/sendMsg",
"/user/login",
"/doc.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs",
"/index.html"
};
boolean check = check(urls, requestURI);
if (check) {
chain.doFilter(request1, response1);
return;
}
if (request1.getSession().getAttribute("employee") != null) {
log.info("User ready, id = {}",(request1.getSession().getAttribute("employee")));
Long empId = (Long) request1.getSession().getAttribute("employee");
BaseContext.setThreadLocal(empId);
chain.doFilter(request1, response1);
return;
}
if (request1.getSession().getAttribute("user") != null) {
log.info("User ready, id = {}",(request1.getSession().getAttribute("user")));
Long userId = (Long) request1.getSession().getAttribute("user");
BaseContext.setThreadLocal(userId);
chain.doFilter(request1, response1);
return;
}
log.info("Do Filter: {}", request1.getRequestURI());
response1.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
}
public boolean check(String[] urls, String requestURI) {
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if (match) {
return true;
}
}
return false;
}
}
MybatisPlus
// 定义实体类,实现Serializable
public class Sales implements Serializable {
}
// 定义Mapper接口
@Mapper
public interface ListMapper {
@Select("select name, SUM(amount) AS number FROM order_detail GROUP BY name ORDER BY number DESC LIMIT 5")
List<Sales> listAll();
}
// 或者
@Mapper
public interface AddressBookMapper extends BaseMapper<AddressBook> {
}
// 定义Service接口及实现类
public interface AddressBookService extends IService<AddressBook> {
}
@Service
public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper, AddressBook> implements AddressBookService {
}
// Controller
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
@Autowired
private AddressBookService addressBookService;
/**
* 新增
*/
@PostMapping
public R<AddressBook> save(@RequestBody AddressBook addressBook) {
}
/**
* 设置默认地址
*/
@PutMapping("default")
public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
}
/**
* 根据id查询地址
*/
@GetMapping("/{id}")
public R get(@PathVariable Long id) {
}
/**
* 查询默认地址
*/
@GetMapping("default")
public R<AddressBook> getDefault() {
}
/**
* 查询指定用户的全部地址
*/
@GetMapping("/list")
public R<List<AddressBook>> list(AddressBook addressBook) {
}
}
元数据添加增改信息
@Component
@Slf4j
public class MyMetaObjectMapper implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("[insert]");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("[update]");
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
}
// 对应实体类增加注解
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
JavaScript处理Long数据时精度丢失
由于 JavaScript 中 Number 类型的自身原因,并不能完全表示 Long 型的数字,在 Long 长度大于17位时会出现精度丢失的问题
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance);
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
处理Redis编码问题
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
SpringCache和Redis
cache:
redis:
time-to-live: 1800000
@EnableCaching
@CacheEvict(value = "setmealCache",allEntries = true)
Echarts实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 引入样式 -->
<link rel="stylesheet" href="../../plugins/element-ui/index.css"/>
<link rel="stylesheet" href="../../styles/common.css"/>
<link rel="stylesheet" href="../../styles/page.css"/>
<script src="https://cdn.staticfile.org/echarts/4.3.0/echarts.min.js"></script>
</head>
<body>
<div class="dashboard-container" id="food-app">
<div class="container">
<div id="main" style="width: 600px;height:400px;"></div>
</div>
</div>
<script src="../../plugins/vue/vue.js"></script>
<!-- 引入组件库 -->
<script src="../../plugins/element-ui/index.js"></script>
<!-- 引入axios -->
<script src="../../plugins/axios/axios.min.js"></script>
<script src="../../js/request.js"></script>
<script src="../../api/list.js"></script>
<script>
new Vue({
data() {
return {
option: {
title: {
text: '销量排行'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: []
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: []
}
]
}
}
},
created() {
this.init()
},
methods: {
async init() {
await list().then(res => {
if (String(res.code) === '1') {
console.log(res.data)
// this.option.xAxis.data = res.data.name
// this.option.series.data = res.data.value
for (const item of res.data) {
this.option.xAxis.data.push(item.name)
this.option.series[0].data.push(item.number)
}
console.log(this.option.xAxis.data)
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
this.drawEcharts()
},
drawEcharts () {
let myChart = echarts.init(document.getElementById('main'))
myChart.setOption(this.option)
}
}
})
</script>
</body>
</html>
利用Knife4J生成接口文档
可拓展
- MySQL主从分离实现读写分离
原理为从库运行两个线程,一个负责监听主库日志文件的变化,一个负责保持从库与主库的一致性
可优化
- 前后端分离
- 安全框架 权限框架
- 乐观锁解决并发场景下的超卖问题
BUG
- 用户端与管理端登录信息互通 切换需要手动登出
- Nginx 反向代理后 Session 失效,无法登录