eshop商城项目实训系列教程导航

  1. eshop商城项目实训源码
  2. eshop商城项目实训代码重构 <= 当前位置

基本架构

要先把基本框架搭建起来,才能够愉快的写代码

1. entity

先看下数据库表结构

image-20230330174111302

要在eshop-business模块下新建src\main\java的文件夹,在该文件夹下创建com.eshop.entity的包,在该包下创建StoreProductRelation的实体类与之数据表一一对应,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.eshop.domain;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class StoreProductRelation extends BaseDomain{

private static final long serialVersionUID = 1L;

@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Long uid;
private Long productId;
private String type;
private String category;

}

因为该表继承了BaseDomain,拥有父的属性

BaseDomain的部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static final long serialVersionUID = 1L;
@TableField(
fill = FieldFill.INSERT
)
@JsonFormat(
pattern = "yyyy-MM-dd HH:mm:ss",
timezone = "GMT+8"
)
private Date createTime;
@TableField(
fill = FieldFill.UPDATE
)
@JsonFormat(
pattern = "yyyy-MM-dd HH:mm:ss",
timezone = "GMT+8"
)
private Date updateTime;
@TableLogic
@JsonIgnore
@TableField(
fill = FieldFill.INSERT
)
private Integer isDel;

2. mapper

mapper的包下新建ProductRelationMapper的类,代码如下:

1
2
3
4
5
6
7
8
9
package com.eshop.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.eshop.domain.StoreProductRelation;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ProductRelationMapper extends BaseMapper<StoreProductRelation> {
}

3. service

service的包下新建ProductRelationService的类,代码如下:

1
2
3
4
5
6
7
package com.eshop.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.eshop.domain.StoreProductRelation;

public interface ProductRelationService extends IService<StoreProductRelation> {
}

新建ProductRelationServiceImpl的实现类,实现ProductRelationService的接口,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.eshop.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.eshop.api.EshopException;
import com.eshop.domain.StoreProductRelation;
import com.eshop.mapper.ProductRelationMapper;
import com.eshop.service.ProductRelationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ProductRelationServiceImpl extends ServiceImpl<ProductRelationMapper, StoreProductRelation> implements ProductRelationService {

@Autowired
private ProductRelationMapper productRelationMapper;

}

4. controller

com.eshop下新建一个包,包名叫controller,新建一个类,类名叫ProductCollectController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.eshop.controller;

import cn.hutool.core.util.NumberUtil;
import com.eshop.api.ApiResult;
import com.eshop.api.EshopException;
import com.eshop.common.bean.LocalUser;
import com.eshop.common.interceptor.AuthCheck;
import com.eshop.modules.product.param.StoreProductRelationQueryParam;
import com.eshop.service.ProductRelationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/collect")
public class ProductCollectController {

@Autowired
private ProductRelationService productRelationService;

}

功能编码

1. 商品添加收藏

前端发过来的请求(使用的是post方式):http://localhost:8008/api/collect/add

image-20230329114943308

看报错Request method 'POST' not supported,不支持请求方法“POST”

开始编写controller层的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 添加收藏
* @param param body部分,需要查询的参数
* @return 是否成功:成功 true,失败 false
*/
@PostMapping("/add")
@AuthCheck
public ApiResult<Boolean> addProductCollect(@Validated @RequestBody StoreProductRelationQueryParam param){
Long uid = LocalUser.getUser().getUid();
if (!NumberUtil.isNumber(param.getId())){
throw new EshopException("参数非法");
}
productRelationService.addProductCollect(Long.valueOf(param.getId()), uid, param.getCategory());
return ApiResult.ok();
}

部分注解说明:

  • @NoRepeatSubmit:防止重复提交自定义注解
  • @AuthCheck:自定义注解实现用户行为认证
  • @Validated:参数验证注解

1.1 是否收藏

service接口:

1
2
3
4
5
6
7
/**
* 是否收藏
* @param productId 商品ID
* @param uid 用户ID
* @return Boolean
*/
Boolean isProductRelation(long productId, long uid);

实现该业务功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 是否收藏
* @param productId 商品Id
* @param uid 用户Id
* @return
*/
@Override
public boolean isProductCollect(long productId, long uid) {
LambdaQueryWrapper<StoreProductRelation> lqw = new LambdaQueryWrapper<>();
lqw.eq(StoreProductRelation::getProductId, productId)
.eq(StoreProductRelation::getUid, uid)
.eq(StoreProductRelation::getType, "collect");
int count = productRelationMapper.selectCount(lqw);
if (count > 0) {
return true;
}
return false;
}

1.2 添加收藏

service接口:

1
2
3
4
5
6
7
/**
* 添加收藏
* @param productId 商品Id
* @param uid 用户Id
* @param category
*/
void addProductCollect(long productId, long uid, String category);

实现该业务功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 添加收藏
* @param productId 商品Id
* @param uid 用户Id
* @param category
*/
@Override
public void addProductCollect(long productId, long uid, String category) {
if (isProductCollect(productId, uid)){
throw new EshopException("已收藏");
}
StoreProductRelation storeProductRelation = new StoreProductRelation();
storeProductRelation.setProductId(productId);
storeProductRelation.setUid(uid);
storeProductRelation.setType(category);
productRelationMapper.insert(storeProductRelation);
}

2. 商品取消收藏

前端发过来的请求(使用的是post方式):http://localhost:8008/api/collect/del

controller层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 取消收藏
* @param param body部分,需要查询的参数
* @return 是否成功:成功 true,失败 false
*/
@NoRepeatSubmit
@PostMapping("/del")
@AuthCheck
public ApiResult<Boolean> delProductCollect(@Validated @RequestBody StoreProductRelationQueryParam param){
Long uid = LocalUser.getUser().getUid();
if (!NumberUtil.isNumber(param.getId())){
throw new EshopException("参数非法");
}
productRelationService.delProductCollect(Long.valueOf(param.getId()), uid, param.getCategory());
return ApiResult.ok();
}

service接口:

1
2
3
4
5
6
7
/**
* 取消收藏
* @param productId 商品Id
* @param uid 用户Id
* @param category
*/
void delProductCollect(long productId, long uid, String category);

实现该业务功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 取消收藏
*
* @param productId 商品Id
* @param uid 用户Id
* @param category
*/
@Override
public void delProductCollect(long productId, long uid, String category) {
LambdaQueryWrapper<StoreProductRelation> lqw = new LambdaQueryWrapper<>();
lqw.eq(StoreProductRelation::getProductId, productId)
.eq(StoreProductRelation::getUid, uid)
.eq(StoreProductRelation::getType, category);
StoreProductRelation productRelation = productRelationMapper.selectOne(lqw);
if (productRelation == null) {
throw new EshopException("已取消");
}
this.removeById(productRelation.getId());
}

3. 批量删除收藏/足迹

前端发过来的请求(使用的是post方式):http://localhost:8008/api/collect/dels/{productIds}

controller层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 批量删除收藏/足迹 collect收藏 foot 足迹
* @param productIds 商品Id
* @param param body部分,需要查询的参数
* @return 是否成功:成功 true,失败 false
*/
@NoRepeatSubmit
@PostMapping("/dels/{productIds}")
@AuthCheck
@Transactional
public ApiResult<Boolean> delCollects(@PathVariable String productIds,@RequestBody StoreProductRelationQueryParam param) {
Long uid = LocalUser.getUser().getUid();
String[] ids = productIds.split(",");
if (ids.length > 0){
for (String id : ids) {
productRelationService.delProductCollect(Long.valueOf(id), uid, param.getCategory());
}
}else {
throw new EshopException("参数非法");
}
return ApiResult.ok();
}

4. 获取收藏或足迹

前端发过来的请求(使用的是get方式):http://localhost:8008/api/collect/user?limit=10&page=1&type=foot

后端接受的请求:

1
2
3
4
5
6
7
8
@GetMapping("/user")
@AuthCheck
public ApiResult<List<StoreProductRelationQueryVo>> UserCollect(@RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "limit", defaultValue = "10") int limit, @RequestParam(value = "type") String type) {
log.info("limit: {}", limit);
log.info("page: {}", page);
log.info("type: {}", type);
return null;
}

发现可以接收到前端参数后,开始补全controller层代码,添加下面的接口代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 获取用户收藏列表
* @param page 页码,默认为 1
* @param limit 页大小,默认为 10
* @param type foot为足迹,collect为收藏
* @return
*/
@GetMapping("/user")
@AuthCheck
public ApiResult<List<StoreProductRelationQueryVo>> UserCollect(@RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "limit", defaultValue = "10") int limit, @RequestParam(value = "type") String type) {
Long uid = LocalUser.getUser().getUid();
List<StoreProductRelationQueryVo> storeProductRelationQueryVos = productRelationService.userProductCollect(page, limit, uid, type);
return ApiResult.ok(storeProductRelationQueryVos);
}

此时我们发现API文档要返回的数据并不能够满足我们的需求

image-20230330225306494

通过分析返回的数据是来自store_product_relationstore_product两张表的字段,因此需要在vo包下造个StoreProductRelationQueryVo的类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.eshop.vo;

import com.eshop.serializer.DoubleSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

@Data
public class StoreProductRelationQueryVo implements Serializable {

private static final Long serialVersion = 1L;
private Long id;
private Long uid;
private Long productId;
private String type;
private String category;
private Date createTime;
private Long pid;
private String image;
private String storeName;
private Double price;
@JsonSerialize(using = DoubleSerializer.class)
private Double otPrice;
private Integer sales;
private Integer isShow;
private Integer isIntegral;
private Integer integer;

}

image-20230330225930287

image-20230330230551222

service层:

1
2
3
4
5
6
7
8
9
/**
* 获取用户收藏列表
* @param limit 页大小
* @param page 页码
* @param type foot为足迹 collect为收藏
* @param uid 用户Id
* @return
*/
List<StoreProductRelationQueryVo> userProductCollect(int page, int limit, Long uid, String type);

实现该业务功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 获取用户收藏列表
* @param limit 页大小
* @param page 页码
* @param type foot为足迹 collect为收藏
* @param uid 用户Id
* @return
*/
@Override
public List<StoreProductRelationQueryVo> userProductCollect(int page, int limit, Long uid, String type) {
// IPage<StoreProductRelation> pageModel = new Page<>(page, limit);
Page<StoreProductRelation> pageModel = new Page<>(page, limit);
List<StoreProductRelationQueryVo> list = productRelationMapper.selectRelationList(pageModel, uid, type);
return list;
}

注意:这里不能够用mybatis-plus的分页插件,控制台会报Handler dispatch failed; nested exception is java.lang.NoSuchMethodError的错误,我推测的sql语句的问题

因为该业务涉及到多表查询,mybatisplus并未给我们提供相关可以调用的接口,所以我们需要自己编写sql语句,去实现我们的需求。sql语句如下:

1
select B.id pid,A.type as category,B.store_name as storeName,B.price,B.is_integral as isIntegral, B.ot_price as otPrice,B.sales,B.image,B.is_show as isShow,B.integral as integral from store_product_relation A left join store_product B on A.product_id = B.id where A.type='foot' and A.uid=1 and A.is_del = 0 and B.is_del = 0 order by A.create_time desc

mapper层:

1
2
3
4
5
@Select("select B.id pid,A.type as category,B.store_name as storeName,B.price,B.is_integral as isIntegral," +
"B.ot_price as otPrice,B.sales,B.image,B.is_show as isShow,B.integral as integral" +
" from store_product_relation A left join store_product B " +
"on A.product_id = B.id where A.type=#{type} and A.uid=#{uid} and A.is_del = 0 and B.is_del = 0 order by A.create_time desc")
List<StoreProductRelationQueryVo> selectRelationList(Page page, @Param("uid") Long uid, @Param("type") String type);

至此功能编码完毕!!

程序排错

任务描述

接口地址/api/register输入相同的手机号注册新用户时,页面显示了SQL错误。正常情况应该显示该手机号已存在。

1
\r\n### Error updating database.  Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '13512345678' for key 'username'\r\n### The error may exist in com/eshop/modules/user/service/mapper/UserMapper.java (best guess)\r\n### The error may involve com.eshop.modules.user.service.mapper.UserMapper.insert-Inline\r\n### The error occurred while setting parameters\r\n### SQL: INSERT INTO eshop_user  ( username, password,       nickname, avatar, phone, add_ip, last_ip,         user_type,        create_time,  is_del )  VALUES  ( ?, ?,       ?, ?, ?, ?, ?,         ?,        ?,  ? )\r\n### Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '13512345678' for key 'username'\n; Duplicate entry '13512345678' for key 'username'; nested exception is java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '13512345678' for key 'username'

要求

修改完代码之后,相同手机号注册时应提示手机号已存在。

代码编写

对该接口代码进行分析,发现并未对获得shopUser的对象做判断是否为空对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PostMapping("/register")
@ApiOperation(value = "H5/APP注册新用户", notes = "H5/APP注册新用户")
public ApiResult<String> register(@Validated @RequestBody RegParam param) {
Object codeObj = redisUtil.get("code_" + param.getAccount());
if(codeObj == null){
return ApiResult.fail("请先获取验证码");
}
String code = codeObj.toString();
if (!StrUtil.equals(code, param.getCaptcha())) {
return ApiResult.fail("验证码错误");
}
ShopUser shopUser = userService.getOne(Wrappers.<ShopUser>lambdaQuery()
.eq(ShopUser::getPhone,param.getAccount()),false);

authService.register(param);
return ApiResult.ok("","注册成功");
}

因此编写以下代码对shopUser进行是否为空对象判断:

1
2
3
if (ObjectUtil.isNotNull(shopUser)){
return ApiResult.fail("用户已存在");
}

经测试后成功!!