电商项目介绍

基于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 失效,无法登录