搭建开始
创建数据库表字段
- 在主包下创建数据库表相关包pojo,在包下创建相关类,比如:Category表,
即按照表字段写入代码:
import lombok.Data; import java.util.Date; @Data public class Category { private Integer id; private Integer parentId; private String name; private Integer status; private Integer sortOrder; private Date createTime; private Date updateTime; }
其中,lombok配置作用为:省去了写get、set、toString等繁琐操作,配置lombok需要在pom.xml中加入依赖:
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
加入依赖后,在类名上方添加注解@Data即可 成功配置lombok
-
连接数据库,需要在application.yml配置文件中写入连接mysql数据库信息。
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root url:jdbc:mysql://127.0.0.1:3306/tmallcharacterEncoding=utf8 &useSSL=false&serverTimezone=UTC
同时,为了方便起见,这里使用mybatis来连接读取数据库,(目前国外用的比较多的是jpA,国内比较流行mybatis)
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency>
注意,需要同时加入mysql驱动!
-
接着就是读取数据库操作:
第一种方式是使用mybatis自动配置的读取注解,在主包下创建dao包,dao包下创建类CategoryMapper.
import com.imooc.mall.pojo.Category; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; public interface CategoryMapper { @Select("select * from mall_category where id = #{id}") Category findById(@Param("id") Integer id); Category queryById(Integer id); }
注意,这里没有加@mapper注解,(这个注解是告诉mybatis这里需要读取数据库),是因为在主函数中加入了mapper扫描注解@MapperScan(basePackages=""),注意:包名需要写全!
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = "com.imooc.mall.dao") public class MallApplication { public static void main(String[] args) { SpringApplication.run(MallApplication.class, args); } }
这里有时候可能读取不到数据库某些字段的值,可能是因为字段命名不一致:需要在配置文件中设置驼峰命名识别:
mybatis: configuration: map-underscore-to-camel-case: true
第二种方式是配置xml文件,在resource文件夹下创建mappers包,并添加CategoryMapper.xml:
<?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.imooc.mall.dao.CategoryMapper"> <sql id="Base_Column_List"> id, parent_id, name, status, sort_order, create_time, update_time </sql> <select id="queryById" resultType="com.imooc.mall.pojo.Category"> select <include refid="Base_Column_List" /> from mall_category where id = #{id} </select> </mapper>
配置xml文件需要编译器找到,需要在配置文件application.yml中加入:
mybatis: configuration: map-underscore-to-camel-case: true mapper-locations: classpath:mappers/*.xml
- 测试连接数据库:
为了让测试类看起来更整洁,这边设计成一个表一个测试类:
import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class MallApplicationTests { }
import com.imooc.mall.MallApplicationTests; import com.imooc.mall.pojo.Category; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; public class CategoryMapperTest extends MallApplicationTests{ @Autowired private CategoryMapper categoryMapper; @Test public void findById() { Category category = categoryMapper.findById(100001); System.out.println(category.toString()); } @Test public void queryById() { Category category = categoryMapper.queryById(100001); System.out.println(category.toString()); } }
注意:这里有时候可能8080默认端口被占用,可以通过设置配置文件修改默认端口:
server: port: 8088
如果有时端口不知道为何被占用,可以打开cmd进行进程kill,假设进程为8088,则输入:netstat -ano|findstr 8088查看进程号,再taskkill -f -pid 进程号 杀死进程,重新运行项目即可。
- mybatis三剑客
5-1. mybatis-generator
在resource文件夹下创建mybatisConfig.xml文件,并加入如下配置(自己修改数据库连接属性,和表名字等)
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- 自己修改对应字段--> <classPathEntry location="D:\mysql-connector-java-5.1.6.jar" /> <context id="DB2Tables" targetRuntime="MyBatis3"> <!-- 不再追加xml内容--> <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" /> <commentGenerator> <property name="suppressAllComments" value="true"/> </commentGenerator> <!-- 自己修改对应字段--> <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://127.0.0.1:3306/tmall?characterEncoding=utf-8" userId="root" password="root"> </jdbcConnection> <javaTypeResolver > <property name="forceBigDecimals" value="false" /> </javaTypeResolver> <javaModelGenerator targetPackage="com.imooc.mall.pojo" targetProject="src/main/java"> <property name="enableSubPackages" value="true" /> </javaModelGenerator> <!-- 自己修改对应字段--> <sqlMapGenerator targetPackage="mappers" targetProject="src/main/resources"> <property name="enableSubPackages" value="true" /> </sqlMapGenerator> <!-- 自己修改对应字段--> <javaClientGenerator type="XMLMAPPER" targetPackage="com.imooc.mall.dao" targetProject="src/main/java"> <property name="enableSubPackages" value="true" /> </javaClientGenerator> <!-- 自己修改对应字段--> <table tableName="mall_order" domainObjectName="Order" enableCountByExample="false" enableDeleteByExample="false" enableSelectByExample="false" enableUpdateByExample="false"/> <table tableName="mall_order_item" domainObjectName="OrderItem" enableCountByExample="false" enableDeleteByExample="false" enableSelectByExample="false" enableUpdateByExample="false"/> </context> </generatorConfiguration>
然后,打开idea终端,定位到对应文件,运行命令:mvn mybatis-generator:generate
那么在dao文件夹和mappers文件夹下会新增几个数据库操作文件啦!
5-2. mybatis plugin
这个插件比较坑,官网已经deprecated(淘汰)了,如果你硬要本地安装,重启的时候可能会直接闪退
那么你需要进入C:\Users\xxx\.IntelliJIdea2019.1\config文件夹下找到plugins文件夹,进入删除mybatis文件夹,重启
才能恢复!所以不可以安装这个插件,可以安装免费版free-mybatis-plugin,效果差不多~
5-3. mybatis-pagehelper(后续加入)
- 用户模块开发
6-1. 用户注册模块
首先,实现dao层,在主包下创建service包,创建一个接口(IUserService)
import com.imooc.mall.pojo.User; public interface IUserService { void register(User user); }
在service包下创建impl包,创建类UserServiceImpl
import com.imooc.mall.dao.UserMapper; import com.imooc.mall.enums.RoleEnum; import com.imooc.mall.pojo.User; import com.imooc.mall.service.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import java.nio.charset.StandardCharsets; @Service public class UserServiceImpl implements IUserService { @Autowired private UserMapper userMapper; /** * 注册 * @param user */ @Override public void register(User user) { //username不能重复 int countByUsername = userMapper.countByUsername(user.getUsername()); if (countByUsername > 0) { return ResponseVo.error(USERNAME_EXIST); } //email不能重复 int countByEmail = userMapper.countByEmail(user.getEmail()); if (countByEmail > 0) { return ResponseVo.error(EMAIL_EXIST); } user.setRole(RoleEnum.CUSTOMER.getCode()); //MD5摘要算法(Spring自带) user.setPassword(DigestUtils.md5DigestAsHex( user.getPassword().getBytes(StandardCharsets.UTF_8) )); //写入数据库 int resultCount = userMapper.insertSelective(user); if (resultCount == 0) { return ResponseVo.error(ERROR); } } }
其中,这里调用了两个自定义方法,需要在userMapper.xml文件中添加如下配置:
<select id="countByUsername" parameterType="java.lang.String" resultType="java.lang.Integer"> select count(1) from mall_user where username = #{username,jdbcType=VARCHAR} </select> <select id="countByEmail" parameterType="java.lang.String" resultType="java.lang.Integer"> select count(1) from mall_user where email = #{email,jdbcType=VARCHAR} </select>
同时,记得在dao包下的userMapper接口加入这两个接口方法。
在主包下创建controller包,包下创建UserController类,
@RequestMapping("/user") @Slf4j public class UserController{ @PostMapping("/register") public void register(@RequestBody User user){ log.info("username={}", user.getUsername()); } }
备注,如要在控制台打印输出sql语句,需要在yml文件下添加配置:
mybatis:
configuration:
map-underscore-to-camel-case: true
# 控制台日志配置
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mappers/*.xml这里注意,这里的@RequestBody是接收前端传过来的json格式的报文,如果前端传过来的是form-urlencoded,这边接收需要使用@RequestParam。
-
错误状态码使用枚举
在enum包下创建类ResponseEnum
import lombok.Getter; @Getter public enum ResponseEnum { ERROR(-1, "服务端错误"), SUCCESS(0, "成功"), PASSWORD_ERROR(1,"密码错误"), USERNAME_EXIST(2, "用户名已存在"), PARAM_ERROR(3, "参数错误"), EMAIL_EXIST(4, "邮箱已存在"), NEED_LOGIN(10, "用户未登录, 请先登录"), USERNAME_OR_PASSWORD_ERROR(11, "用户名或密码错误"), PRODUCT_OFF_SALE_OR_DELETE(12, "商品下架或删除"), PRODUCT_NOT_EXIST(13, "商品不存在"), PROODUCT_STOCK_ERROR(14, "库存不正确"), CART_PRODUCT_NOT_EXIST(15, "购物车里无此商品"), DELETE_SHIPPING_FAIL(16, "删除收货地址失败"), SHIPPING_NOT_EXIST(17, "收货地址不存在"), CART_SELECTED_IS_EMPTY(18, "请选择商品后下单"), ORDER_NOT_EXIST(19, "订单不存在"), ORDER_STATUS_ERROR(20, "订单状态有误"), ; Integer code; String desc; ResponseEnum(Integer code, String desc) { this.code = code; this.desc = desc; } }
在主包下创建vo包,创建类ResponseVo,
import com.fasterxml.jackson.annotation.JsonInclude; import com.imooc.mall.enums.ResponseEnum; import lombok.Data; import org.springframework.validation.BindingResult; import java.util.Objects; @Data @JsonInclude(value = JsonInclude.Include.NON_NULL) public class ResponseVo<T> { private Integer status; private String msg; private T data; private ResponseVo(Integer status, String msg) { this.status = status; this.msg = msg; } public static <T> ResponseVo<T> success(T data) { return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(), data); } public static <T> ResponseVo<T> success() { return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getDesc()); } public static <T> ResponseVo<T> error(ResponseEnum responseEnum) { return new ResponseVo<>(responseEnum.getCode(), responseEnum.getDesc()); } }
- 表单验证
import javax.validation.constraints.NotBlank; @Data public class UserRegisterForm { //@NotBlank 用于 String 判断空格 //@NotEmpty 用于集合 //@NotNull @NotBlank private String username; @NotBlank private String password; @NotBlank private String email; }
注意,务必引入
import javax.validation.constraints.NotBlank;
此包
-
登录功能模块
首先进入userMapper.xml修改添加代码
<select id="selectByUsername" parameterType="java.lang.String" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from mall_user where username = #{username,jdbcType=VARCHAR} </select>
随后实现service层中的userService
@Override public ResponseVo<User> login(String username, String password) { User user = userMapper.selectByUsername(username); if (user == null) { //用户不存在(返回:用户名或密码错误 ) return ResponseVo.error(ResponseEnum.USERNAME_OR_PASSWORD_ERROR); } if (!user.getPassword().equalsIgnoreCase( DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8)))) { //密码错误(返回:用户名或密码错误 ) return ResponseVo.error(ResponseEnum.USERNAME_OR_PASSWORD_ERROR); } user.setPassword(""); return ResponseVo.success(user); }
随后进入controller层实现/user/login接口和获取用户信息的/user接口
@PostMapping("/user/login") public ResponseVo<User> login(@Valid @RequestBody UserLoginForm userLoginForm, HttpSession session) { ResponseVo<User> userResponseVo = userService.login(userLoginForm.getUsername(), userLoginForm.getPassword()); //设置Session session.setAttribute(MallConst.CURRENT_USER, userResponseVo.getData()); log.info("/login sessionId={}", session.getId()); return userResponseVo; } //session保存在内存里,改进版:token+redis @GetMapping("/user") public ResponseVo<User> userInfo(HttpSession session) { log.info("/user sessionId={}", session.getId()); User user = (User) session.getAttribute(MallConst.CURRENT_USER); return ResponseVo.success(user); }
这里需要注意cookie跨域的问题,localhost和127.0.0.1两个domain之间也属于跨域,生成的cookie是不一样的!
-
登出功能
@PostMapping("/user/logout") public ResponseVo logout(HttpSession session) { log.info("/user/logout sessionId={}", session.getId()); session.removeAttribute(MallConst.CURRENT_USER); return ResponseVo.success(); }
如果要设置session过期时间,可以进入yml文件修改:
server: servlet: session: timeout: 120
- 统一拦截器
首先,Interceptor拦截url,AOP拦截是以包为单位拦截,这里使用了Interceptor拦截器。首先创建类:
@Slf4j public class UserLoginInterceptor implements HandlerInterceptor { /** * true 表示继续流程,false表示中断 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("preHandle..."); User user = (User) request.getSession().getAttribute(MallConst.CURRENT_USER); if (user == null) { log.info("user=null"); throw new UserLoginException(); } return true; } }
为了实现拦截,需要此类:
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserLoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/error", "/user/login", "/user/register", "/categories", "/products", "/products/*"); } }
这边实现统一异常处理是为了能返回前端json字符串:
@ControllerAdvice public class RuntimeExceptionHandler { @ExceptionHandler(RuntimeException.class) @ResponseBody // @ResponseStatus(HttpStatus.FORBIDDEN) public ResponseVo handle(RuntimeException e) { return ResponseVo.error(ERROR, e.getMessage()); } @ExceptionHandler(UserLoginException.class) @ResponseBody public ResponseVo userLoginHandle() { return ResponseVo.error(ResponseEnum.NEED_LOGIN); } public class UserLoginException extends RuntimeException { }
- 商品分类模块
首先,创建数据库查询语句,进入CategoryMapper.xml
<select id="selectAll" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from mall_category where status = 1 </select>
定义接口
public interface ICategoryService { ResponseVo<List<CategoryVo>> selectAll(); void findSubCategoryId(Integer id, Set<Integer> resultSet); }
定义该接口实现类:(使用了lambda表达式)
@Service public class CategoryServiceImpl implements ICategoryService { @Autowired private CategoryMapper categoryMapper; /** * 耗时:http(请求微信api) > 磁盘 > 内存 * mysql(内网+磁盘) * @return */ @Override public ResponseVo<List<CategoryVo>> selectAll() { List<Category> categories = categoryMapper.selectAll(); //查出parent_id=0 // for (Category category : categories) { // if (category.getParentId().equals(ROOT_PARENT_ID)) { // CategoryVo categoryVo = new CategoryVo(); // BeanUtils.copyProperties(category, categoryVo); // categoryVoList.add(categoryVo); // } // } //lambda + stream List<CategoryVo> categoryVoList = categories.stream() .filter(e -> e.getParentId().equals(ROOT_PARENT_ID)) .map(this::category2CategoryVo) .sorted(Comparator.comparing(CategoryVo::getSortOrder).reversed()) .collect(Collectors.toList()); //查询子目录 findSubCategory(categoryVoList, categories); return ResponseVo.success(categoryVoList); } @Override public void findSubCategoryId(Integer id, Set<Integer> resultSet) { List<Category> categories = categoryMapper.selectAll(); findSubCategoryId(id, resultSet, categories); } private void findSubCategoryId(Integer id, Set<Integer> resultSet, List<Category> categories) { for (Category category : categories) { if (category.getParentId().equals(id)) { resultSet.add(category.getId()); findSubCategoryId(category.getId(), resultSet, categories); } } } private void findSubCategory(List<CategoryVo> categoryVoList, List<Category> categories) { for (CategoryVo categoryVo : categoryVoList) { List<CategoryVo> subCategoryVoList = new ArrayList<>(); for (Category category : categories) { //如果查到内容,设置subCategory, 继续往下查 if (categoryVo.getId().equals(category.getParentId())) { CategoryVo subCategoryVo = category2CategoryVo(category); subCategoryVoList.add(subCategoryVo); } subCategoryVoList.sort(Comparator.comparing(CategoryVo::getSortOrder).reversed()); categoryVo.setSubCategories(subCategoryVoList); findSubCategory(subCategoryVoList, categories); } } } private CategoryVo category2CategoryVo(Category category) { CategoryVo categoryVo = new CategoryVo(); BeanUtils.copyProperties(category, categoryVo); return categoryVo; } }
实现controller:
@RestController public class CategoryController { @Autowired private ICategoryService categoryService; @GetMapping("/categories") public ResponseVo<List<CategoryVo>> selectAll() { return categoryService.selectAll(); } }
- 商品列表接口
在CategoryServiceImpl中实现如下两个功能,根据商品id查询此商品以及此商品的子商品:
@Override public void findSubCategoryId(Integer id, Set<Integer> resultSet) { List<Category> categories = categoryMapper.selectAll(); findSubCategoryId(id, resultSet, categories); } private void findSubCategoryId(Integer id, Set<Integer> resultSet, List<Category> categories) { for (Category category : categories) { if (category.getParentId().equals(id)) { resultSet.add(category.getId()); findSubCategoryId(category.getId(), resultSet, categories); } } }
在productMapper中定义此方法:
public interface ProductMapper { int deleteByPrimaryKey(Integer id); int insert(Product record); int insertSelective(Product record); Product selectByPrimaryKey(Integer id); int updateByPrimaryKeySelective(Product record); int updateByPrimaryKey(Product record); List<Product> selectByCategoryIdSet(@Param("categoryIdSet") Set<Integer> categoryIdSet); //注意,这里需要使用@Param指定你所自定义的参数名,否则,查询数据库会提示,没有该字段类型。
在productMapper.xml中添加:
<select id="selectByCategoryIdSet" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from mall_product where status = 1 <if test="categoryIdSet.size() > 0"> and category_id in <foreach collection="categoryIdSet" item="item" index="index" open="(" separator="," close=")"> #{item} </foreach> </if> </select>
实现productServiceImpl类中的list方法(需要在接口中定义,这里省略):
@Service @Slf4j public class ProductServiceImpl implements IProductService { @Autowired private ICategoryService categoryService; @Autowired private ProductMapper productMapper; @Override public ResponseVo<PageInfo> list(Integer categoryId, Integer pageNum, Integer pageSize) { Set<Integer> categoryIdSet = new HashSet<>(); if (categoryId != null) { categoryService.findSubCategoryId(categoryId, categoryIdSet); categoryIdSet.add(categoryId); } PageHelper.startPage(pageNum, pageSize); List<Product> productList = productMapper.selectByCategoryIdSet(categoryIdSet); List<ProductVo> productVoList = productList.stream() .map(e -> { ProductVo productVo = new ProductVo(); BeanUtils.copyProperties(e, productVo); return productVo; }) .collect(Collectors.toList()); PageInfo pageInfo = new PageInfo<>(productList); pageInfo.setList(productVoList); return ResponseVo.success(pageInfo); } }
这里使用到了mybatis的分页插件,在pom.xml中导入依赖,
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.13</version> </dependency>
然后调用pageHelper.startPage(pageNum, pageSize)即可实现分页功能,使用pageInfo打印分页信息。将会返回前端如下信息:
{
"status": 0,
"data": {
"pageNum": 1,
"pageSize": 10,
"size": 2,
"orderBy": null,
"startRow": 1,
"endRow": 2,
"total": 2,
"pages": 1,
"list": [
{
"id": 1,
"categoryId": 3,
"name": "iphone7",
"subtitle": "双十一促销",
"mainImage": "mainimage.jpg",
"status":1,
"price": 7199.22
},
{
"id": 2,
"categoryId": 2,
"name": "oppo R8",
"subtitle": "oppo促销进行中",
"mainImage": "mainimage.jpg",
"status":1,
"price": 2999.11
}
],
"firstPage": 1,
"prePage": 0,
"nextPage": 0,
"lastPage": 1,
"isFirstPage": true,
"isLastPage": true,
"hasPreviousPage": false,
"hasNextPage": false,
"navigatePages": 8,
"navigatepageNums": [
1
]
}
} - 商品详情功能
先创建productDetailVo(商品详情信息)类,
@Data public class ProductDetailVo { private Integer id; private Integer categoryId; private String name; private String subtitle; private String mainImage; private String subImages; private String detail; private BigDecimal price; private Integer stock; private Integer status; private Date createTime; private Date updateTime; }
在productServiceImpl类中加入:
@Override public ResponseVo<ProductDetailVo> detail(Integer productId) { Product product = productMapper.selectByPrimaryKey(productId); //只对确定性条件判断 if (product.getStatus().equals(OFF_SALE.getCode()) || product.getStatus().equals(DELETE.getCode())) { return ResponseVo.error(PRODUCT_OFF_SALE_OR_DELETE); } ProductDetailVo productDetailVo = new ProductDetailVo(); BeanUtils.copyProperties(product, productDetailVo); //敏感数据处理 productDetailVo.setStock(product.getStock() > 100 ? 100 : product.getStock()); return ResponseVo.success(productDetailVo); }
实现controller类:
@GetMapping("/products/{productId}") public ResponseVo<ProductDetailVo> detail(@PathVariable Integer productId) { return productService.detail(productId); }
- redis安装
redis安装包(win下)
[redis-5.0.5-bin.zip(Win)](https://imcfile.oss-cn-beijing.aliyuncs.com/shizhan/file/liaoshixiong/redis-5.0.5-bin.zip)
redis可视化软件
[Another.Redis.Desktop.Manager.1.2.5.exe(Win)](https://imcfile.oss-cn-beijing.aliyuncs.com/shizhan/file/liaoshixiong/Another.Redis.Desktop.Manager.1.2.5.exe)
[Another.Redis.Desktop.Manager.1.2.5.dmg(Mac)](https://imcfile.oss-cn-beijing.aliyuncs.com/shizhan/file/liaoshixiong/Another.Redis.Desktop.Manager.1.2.5.dmg)
在pom.xml中加入redis依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
在yml配置文件中加入:
spring: redis: host: 127.0.0.1 port: 6379
- 购物车模块
16-1. 增加CartAddForm表单
@Data public class CartAddForm { @NotNull private Integer productId; private Boolean selected = true; }
16-2. 购物车添加商品存入redis数据库:(首先,封装一个list方法,便于返回前端数据)
@Override public ResponseVo<CartVo> list(Integer uid) { HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash(); String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid); Map<String, String> entries = opsForHash.entries(redisKey); boolean selectAll = true; Integer cartTotalQuantity = 0; BigDecimal cartTotalPrice = BigDecimal.ZERO; CartVo cartVo = new CartVo(); List<CartProductVo> cartProductVoList = new ArrayList<>(); for (Map.Entry<String, String> entry : entries.entrySet()) { Integer productId = Integer.valueOf(entry.getKey()); Cart cart = gson.fromJson(entry.getValue(), Cart.class); //TODO 需要优化,使用mysql里的in Product product = productMapper.selectByPrimaryKey(productId); if (product != null) { CartProductVo cartProductVo = new CartProductVo(productId, cart.getQuantity(), product.getName(), product.getSubtitle(), product.getMainImage(), product.getPrice(), product.getStatus(), product.getPrice().multiply(BigDecimal.valueOf(cart.getQuantity())), product.getStock(), cart.getProductSelected() ); cartProductVoList.add(cartProductVo); if (!cart.getProductSelected()) { selectAll = false; } //计算总价(只计算选中的) if (cart.getProductSelected()) { cartTotalPrice = cartTotalPrice.add(cartProductVo.getProductTotalPrice()); } } cartTotalQuantity += cart.getQuantity(); } //有一个没有选中,就不叫全选 cartVo.setSelectedAll(selectAll); cartVo.setCartTotalQuantity(cartTotalQuantity); cartVo.setCartTotalPrice(cartTotalPrice); cartVo.setCartProductVoList(cartProductVoList); return ResponseVo.success(cartVo); }
@Service public class CartServiceImpl implements ICartService { private final static String CART_REDIS_KEY_TEMPLATE = "cart_%d"; @Autowired private ProductMapper productMapper; @Autowired private StringRedisTemplate redisTemplate; private Gson gson = new Gson(); @Override public ResponseVo<CartVo> add(Integer uid, CartAddForm form) { Integer quantity = 1; Product product = productMapper.selectByPrimaryKey(form.getProductId()); //商品是否存在 if (product == null) { return ResponseVo.error(ResponseEnum.PRODUCT_NOT_EXIST); } //商品是否正常在售 if (!product.getStatus().equals(ProductStatusEnum.ON_SALE.getCode())) { return ResponseVo.error(ResponseEnum.PRODUCT_OFF_SALE_OR_DELETE); } //商品库存是否充足 if (product.getStock() <= 0) { return ResponseVo.error(ResponseEnum.PROODUCT_STOCK_ERROR); } //写入到redis //key: cart_1 HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash(); String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid); Cart cart; String value = opsForHash.get(redisKey, String.valueOf(product.getId())); if (StringUtils.isEmpty(value)) { //没有该商品, 新增 cart = new Cart(product.getId(), quantity, form.getSelected()); }else { //已经有了,数量+1 cart = gson.fromJson(value, Cart.class); cart.setQuantity(cart.getQuantity() + quantity); } opsForHash.put(redisKey, String.valueOf(product.getId()), gson.toJson(cart)); return list(uid); } }
16-3. 更新购物车
首先创建类CartUpdateForm
@Data public class CartUpdateForm { private Integer quantity; private Boolean selected; }
在CartServiceImpl中添加类方法update
@Override public ResponseVo<CartVo> update(Integer uid, Integer productId, CartUpdateForm form) { HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash(); String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid); String value = opsForHash.get(redisKey, String.valueOf(productId)); if (StringUtils.isEmpty(value)) { //没有该商品, 报错 return ResponseVo.error(ResponseEnum.CART_PRODUCT_NOT_EXIST); } //已经有了,修改内容 Cart cart = gson.fromJson(value, Cart.class); if (form.getQuantity() != null && form.getQuantity() >= 0) { cart.setQuantity(form.getQuantity()); } if (form.getSelected() != null) { cart.setProductSelected(form.getSelected()); } opsForHash.put(redisKey, String.valueOf(productId), gson.toJson(cart)); return list(uid); }
16-4. 购物车删除商品
@Override public ResponseVo<CartVo> delete(Integer uid, Integer productId) { HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash(); String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid); String value = opsForHash.get(redisKey, String.valueOf(productId)); if (StringUtils.isEmpty(value)) { //没有该商品, 报错 return ResponseVo.error(ResponseEnum.CART_PRODUCT_NOT_EXIST); } opsForHash.delete(redisKey, String.valueOf(productId)); return list(uid); }
16-5. 购物车中商品的全选/全不选/数量总和:
@Override public ResponseVo<CartVo> selectAll(Integer uid) { HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash(); String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid); for (Cart cart : listForCart(uid)) { cart.setProductSelected(true); opsForHash.put(redisKey, String.valueOf(cart.getProductId()), gson.toJson(cart)); } return list(uid); } @Override public ResponseVo<CartVo> unSelectAll(Integer uid) { HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash(); String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid); for (Cart cart : listForCart(uid)) { cart.setProductSelected(false); opsForHash.put(redisKey, String.valueOf(cart.getProductId()), gson.toJson(cart)); } return list(uid); } @Override public ResponseVo<Integer> sum(Integer uid) { Integer sum = listForCart(uid).stream() .map(Cart::getQuantity) .reduce(0, Integer::sum); return ResponseVo.success(sum); } public List<Cart> listForCart(Integer uid) { HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash(); String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid); Map<String, String> entries = opsForHash.entries(redisKey); List<Cart> cartList = new ArrayList<>(); for (Map.Entry<String, String> entry : entries.entrySet()) { cartList.add(gson.fromJson(entry.getValue(), Cart.class)); } return cartList; }
其中,uid是指某个购物车的id。
16-6. controller层实现:
@RestController public class CartController { @Autowired private ICartService cartService; @GetMapping("/carts") public ResponseVo<CartVo> list(HttpSession session) { User user = (User) session.getAttribute(MallConst.CURRENT_USER); return cartService.list(user.getId()); } @PostMapping("/carts") public ResponseVo<CartVo> add(@Valid @RequestBody CartAddForm cartAddForm, HttpSession session) { User user = (User) session.getAttribute(MallConst.CURRENT_USER); return cartService.add(user.getId(), cartAddForm); } @PutMapping("/carts/{productId}") public ResponseVo<CartVo> update(@PathVariable Integer productId, @Valid @RequestBody CartUpdateForm form, HttpSession session) { User user = (User) session.getAttribute(MallConst.CURRENT_USER); return cartService.update(user.getId(), productId, form); } @DeleteMapping("/carts/{productId}") public ResponseVo<CartVo> delete(@PathVariable Integer productId, HttpSession session) { User user = (User) session.getAttribute(MallConst.CURRENT_USER); return cartService.delete(user.getId(), productId); } @PutMapping("/carts/selectAll") public ResponseVo<CartVo> selectAll(HttpSession session) { User user = (User) session.getAttribute(MallConst.CURRENT_USER); return cartService.selectAll(user.getId()); } @PutMapping("/carts/unSelectAll") public ResponseVo<CartVo> unSelectAll(HttpSession session) { User user = (User) session.getAttribute(MallConst.CURRENT_USER); return cartService.unSelectAll(user.getId()); } @GetMapping("/carts/products/sum") public ResponseVo<Integer> sum(HttpSession session) { User user = (User) session.getAttribute(MallConst.CURRENT_USER); return cartService.sum(user.getId()); } }
- 收货地址模块
@Service public class ShippingServiceImpl implements IShippingService { @Autowired private ShippingMapper shippingMapper; @Override public ResponseVo<Map<String, Integer>> add(Integer uid, ShippingForm form) { Shipping shipping = new Shipping(); BeanUtils.copyProperties(form, shipping); shipping.setUserId(uid); int row = shippingMapper.insertSelective(shipping); if (row == 0) { return ResponseVo.error(ResponseEnum.ERROR); } Map<String, Integer> map = new HashMap<>(); map.put("shippingId", shipping.getId()); return ResponseVo.success(map); } @Override public ResponseVo delete(Integer uid, Integer shippingId) { int row = shippingMapper.deleteByIdAndUid(uid, shippingId); if (row == 0) { return ResponseVo.error(ResponseEnum.DELETE_SHIPPING_FAIL); } return ResponseVo.success(); } @Override public ResponseVo update(Integer uid, Integer shippingId, ShippingForm form) { Shipping shipping = new Shipping(); BeanUtils.copyProperties(form, shipping); shipping.setUserId(uid); shipping.setId(shippingId); int row = shippingMapper.updateByPrimaryKeySelective(shipping); if (row == 0) { return ResponseVo.error(ResponseEnum.ERROR); } return ResponseVo.success(); } @Override public ResponseVo<PageInfo> list(Integer uid, Integer pageNum, Integer pageSize) { PageHelper.startPage(pageNum, pageSize); List<Shipping> shippings = shippingMapper.selectByUid(uid); PageInfo pageInfo = new PageInfo(shippings); return ResponseVo.success(pageInfo); } }
-
订单模块开发
定义一个付款类型的枚举:import lombok.Getter; @Getter public enum PaymentTypeEnum { PAY_ONLINE(1), ; Integer code; PaymentTypeEnum(Integer code) { this.code = code; } }
订单状态的枚举:
@Getter public enum OrderStatusEnum { CANCELED(0, "已取消"), NO_PAY(10, "未付款"), PAID(20, "已付款"), SHIPPED(40, "已发货"), TRADE_SUCCESS(50, "交易成功"), TRADE_CLOSE(60, "交易关闭"), ; Integer code; String desc; OrderStatusEnum(Integer code, String desc) { this.code = code; this.desc = desc; } }
注意,@Transactional事务是数据库支持的,而不是springboot支持的,如果使用的是myIsam引擎,并不能实现事务功能!回滚并不是数据库写入或者修改失败才会回滚,当标注了@transactional代码段出现runtimeException异常时,程序也会回滚!
先看下需要传给前端的数据 :
{
"status": 0,
"data": {
"orderNo": 1291136461000,
"payment": 2999.11,
"paymentType": 1,
"postage": 0,
"status": 10,
"paymentTime": null,
"sendTime": null,
"endTime": null,
"closeTime": null,
"createTime": 1291136461000,
"orderItemVoList": [
{
"orderNo": 1291136461000,
"productId": 2,
"productName": "oppo R8",
"productImage": "mainimage.jpg",
"currentUnitPrice": 2999.11,
"quantity": 1,
"totalPrice": 2999.11,
"createTime": null
}
],
"shippingId": 5,
"shippingVo": {
"id": 4,
"userId": 13,
"receiverName": "廖师兄",
"receiverPhone": "010",
"receiverMobile": "18688888888",
"receiverProvince": "北京",
"receiverCity": "北京市",
"receiverDistrict": "海淀区",
"receiverAddress": "中关村",
"receiverZip": "100000",
"createTime": 1485066385000,
"updateTime": 1485066385000
}
}
}
创建订单方法:
@Autowired private ShippingMapper shippingMapper; @Autowired private ICartService cartService; @Autowired private ProductMapper productMapper; @Autowired private OrderMapper orderMapper; @Autowired private OrderItemMapper orderItemMapper; @Override @Transactional public ResponseVo<OrderVo> create(Integer uid, Integer shippingId) { //收货地址校验(总之要查出来的) Shipping shipping = shippingMapper.selectByUidAndShippingId(uid, shippingId); if (shipping == null) { return ResponseVo.error(ResponseEnum.SHIPPING_NOT_EXIST); } //获取购物车,校验(是否有商品、库存) List<Cart> cartList = cartService.listForCart(uid).stream() .filter(Cart::getProductSelected) .collect(Collectors.toList()); if (CollectionUtils.isEmpty(cartList)) { return ResponseVo.error(ResponseEnum.CART_SELECTED_IS_EMPTY); } //获取cartList里的productIds Set<Integer> productIdSet = cartList.stream() .map(Cart::getProductId) .collect(Collectors.toSet()); List<Product> productList = productMapper.selectByProductIdSet(productIdSet); Map<Integer, Product> map = productList.stream() .collect(Collectors.toMap(Product::getId, product -> product)); List<OrderItem> orderItemList = new ArrayList<>(); Long orderNo = generateOrderNo(); for (Cart cart : cartList) { //根据productId查数据库 Product product = map.get(cart.getProductId()); //是否有商品 if (product == null) { return ResponseVo.error(ResponseEnum.PRODUCT_NOT_EXIST, "商品不存在. productId = " + cart.getProductId()); } //商品上下架状态 if (!ProductStatusEnum.ON_SALE.getCode().equals(product.getStatus())) { return ResponseVo.error(ResponseEnum.PRODUCT_OFF_SALE_OR_DELETE, "商品不是在售状态. " + product.getName()); } //库存是否充足 if (product.getStock() < cart.getQuantity()) { return ResponseVo.error(ResponseEnum.PROODUCT_STOCK_ERROR, "库存不正确. " + product.getName()); } OrderItem orderItem = buildOrderItem(uid, orderNo, cart.getQuantity(), product); orderItemList.add(orderItem); //减库存 product.setStock(product.getStock() - cart.getQuantity()); int row = productMapper.updateByPrimaryKeySelective(product); if (row <= 0) { return ResponseVo.error(ResponseEnum.ERROR); } } //计算总价,只计算选中的商品 //生成订单,入库:order和order_item,事务 Order order = buildOrder(uid, orderNo, shippingId, orderItemList); int rowForOrder = orderMapper.insertSelective(order); if (rowForOrder <= 0) { return ResponseVo.error(ResponseEnum.ERROR); } int rowForOrderItem = orderItemMapper.batchInsert(orderItemList); if (rowForOrderItem <= 0) { return ResponseVo.error(ResponseEnum.ERROR); } //更新购物车(选中的商品) //Redis有事务(打包命令),不能回滚 for (Cart cart : cartList) { cartService.delete(uid, cart.getProductId()); } //构造orderVo OrderVo orderVo = buildOrderVo(order, orderItemList, shipping); return ResponseVo.success(orderVo); }
展示用户订单列表:(在OrderMapper.xml中增加selectByUid方法)
<select id="selectByUid" parameterType="java.lang.Integer" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from mall_order where user_id = #{uid,jdbcType=INTEGER} </select>
在orderItemMapper接口中增加:
List<OrderItem> selectByOrderNoSet(@Param("orderNoSet") Set orderNoSet);
在xml文件中实现sql语句:
<select id="selectByOrderNoSet" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from mall_order_item <where> <if test="orderNoSet.size() > 0"> order_no in <foreach collection="orderNoSet" item="item" index="index" open="(" separator="," close=")"> #{item} </foreach> </if> </where> </select>
在shippingMapper接口中添加:
List<Shipping> selectByIdSet(@Param("idSet") Set idSet);
在xml文件中实现:
<select id="selectByIdSet" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from mall_shipping <where> <if test="idSet.size() > 0"> id in <foreach collection="idSet" item="item" index="index" open="(" separator="," close=")"> #{item} </foreach> </if> </where> </select>
其中,比较常用的一个list转map的语法:
Map<Integer, Product> map = productList.stream()
.collect(Collectors.toMap(Product::getId, product -> product));Map<Long, List<OrderItem>> orderItemMap = orderItemList.stream()
.collect(Collectors.groupingBy(OrderItem::getOrderNo));展示某用户所有订单方法:
@Override public ResponseVo<PageInfo> list(Integer uid, Integer pageNum, Integer pageSize) { PageHelper.startPage(pageNum, pageSize); List<Order> orderList = orderMapper.selectByUid(uid); Set<Long> orderNoSet = orderList.stream() .map(Order::getOrderNo) .collect(Collectors.toSet()); List<OrderItem> orderItemList = orderItemMapper.selectByOrderNoSet(orderNoSet); Map<Long, List<OrderItem>> orderItemMap = orderItemList.stream() .collect(Collectors.groupingBy(OrderItem::getOrderNo)); Set<Integer> shippingIdSet = orderList.stream() .map(Order::getShippingId) .collect(Collectors.toSet()); List<Shipping> shippingList = shippingMapper.selectByIdSet(shippingIdSet); Map<Integer, Shipping> shippingMap = shippingList.stream() .collect(Collectors.toMap(Shipping::getId, shipping -> shipping)); List<OrderVo> orderVoList = new ArrayList<>(); for (Order order : orderList) { OrderVo orderVo = buildOrderVo(order, orderItemMap.get(order.getOrderNo()), shippingMap.get(order.getShippingId())); orderVoList.add(orderVo); } PageInfo pageInfo = new PageInfo<>(orderList); pageInfo.setList(orderVoList); return ResponseVo.success(pageInfo); }
订单详情模块:
在orderMapper.xml中加入如下方法实现语句:
<select id="selectByOrderNo" parameterType="java.lang.Long" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from mall_order where order_no = #{orderNo,jdbcType=INTEGER} </select>
在 OrderServiceImpl中实现该详情方法:
@Override public ResponseVo<OrderVo> detail(Integer uid, Long orderNo) { Order order = orderMapper.selectByOrderNo(orderNo); if (order == null || !order.getUserId().equals(uid)) { return ResponseVo.error(ResponseEnum.ORDER_NOT_EXIST); } Set<Long> orderNoSet = new HashSet<>(); orderNoSet.add(order.getOrderNo()); List<OrderItem> orderItemList = orderItemMapper.selectByOrderNoSet(orderNoSet); Shipping shipping = shippingMapper.selectByPrimaryKey(order.getShippingId()); OrderVo orderVo = buildOrderVo(order, orderItemList, shipping); return ResponseVo.success(orderVo); }
订单取消模块:
@Override public ResponseVo cancel(Integer uid, Long orderNo) { Order order = orderMapper.selectByOrderNo(orderNo); if (order == null || !order.getUserId().equals(uid)) { return ResponseVo.error(ResponseEnum.ORDER_NOT_EXIST); } //只有[未付款]订单可以取消,看自己公司业务 if (!order.getStatus().equals(OrderStatusEnum.NO_PAY.getCode())) { return ResponseVo.error(ResponseEnum.ORDER_STATUS_ERROR); } order.setStatus(OrderStatusEnum.CANCELED.getCode()); order.setCloseTime(new Date()); int row = orderMapper.updateByPrimaryKeySelective(order); if (row <= 0) { return ResponseVo.error(ResponseEnum.ERROR); } return ResponseVo.success(); }
orderController的实现:
@RestController public class OrderController { @Autowired private IOrderService orderService; @PostMapping("/orders") public ResponseVo<OrderVo> create(@Valid @RequestBody OrderCreateForm form, HttpSession session) { User user = (User) session.getAttribute(MallConst.CURRENT_USER); return orderService.create(user.getId(), form.getShippingId()); } @GetMapping("/orders") public ResponseVo<PageInfo> list(@RequestParam Integer pageNum, @RequestParam Integer pageSize, HttpSession session) { User user = (User) session.getAttribute(MallConst.CURRENT_USER); return orderService.list(user.getId(), pageNum, pageSize); } @GetMapping("/orders/{orderNo}") public ResponseVo<OrderVo> detail(@PathVariable Long orderNo, HttpSession session) { User user = (User) session.getAttribute(MallConst.CURRENT_USER); return orderService.detail(user.getId(), orderNo); } @PutMapping("/orders/{orderNo}") public ResponseVo cancel(@PathVariable Long orderNo, HttpSession session) { User user = (User) session.getAttribute(MallConst.CURRENT_USER); return orderService.cancel(user.getId(), orderNo); } }
- RabbitMq使用入项目
首先进入虚拟机安装docker,遇到yum update错误,错误提示为:"Cannot find a valid baseurl for repo"
进入vi /etc/resolv.conf加入nameserver 8.8.8.8 nameserver 8.8.4.4 保存退出,重新yum update即可
安装docker出现: No package docker available 执行
sudo yum install epel-release
随后执行,yum install -y docker-io, 报错: no docker-io package found. 那么我们用镜像源下载:
直接用下载源安装,执行命令:yum install https://get.docker.com/rpm/1.7.1/centos-6/RPMS/x86_64/docker-engine-1.7.1-1.el6.x86_64.rpm