1. 用户注册
1.1 用户名登录注册流程
1.2 邮箱注册流程
1.3 手机号注册
2.检验用户名是否存在
<!-- apache 工具类 -->
<!-- 文件检测 -->
<!-- https://mvnrepository.com/artifact/org.apache.tika/tika-core -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.2.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
3. 状态码修改
4. 使用自定义响应数据类型
https://www.yuque.com/meishimokeqide/myi2rx/gahnvo/edit
5. 接受前端注册消息
我们统一将前端传过来的信息定义为xxxBO类
加密,设置时间,全局唯一id
https://www.yuque.com/meishimokeqide/myi2rx/iugpp7
6. 实现登录功能
7. 页面显示信息,使用Cookie
7.1 CookieUtils
package com.lv.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
*
* @Title: CookieUtils.java
* @Package com.lv.utils
* @Description: Cookie 工具类
* Copyright: Copyright (c)
* Company: www.lv.com
*
* @author imooc
* @version V1.0
*/
public final class
{
final static Logger logger = LoggerFactory.getLogger(CookieUtils.class);
/**
*
* @Description: 得到Cookie的值, 不编码
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
return getCookieValue(request, cookieName, false);
}
/**
*
* @Description: 得到Cookie的值
* @param request
* @param cookieName
* @param isDecoder
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (isDecoder) {
retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
*
* @Description: 得到Cookie的值
* @param request
* @param cookieName
* @param encodeString
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
*
* @Description: 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
* @param request
* @param response
* @param cookieName
* @param cookieValue
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}
/**
*
* @Description: 设置Cookie的值 在指定时间内生效,但不编码
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage) {
setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
}
/**
*
* @Description: 设置Cookie的值 不设置生效时间,但编码
* 在服务器被创建,返回给客户端,并且保存客户端
* 如果设置了SETMAXAGE(int seconds),会把cookie保存在客户端的硬盘中
* 如果没有设置,会默认把cookie保存在浏览器的内存中
* 一旦设置setPath():只能通过设置的路径才能获取到当前的cookie信息
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param isEncode
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, boolean isEncode) {
setCookie(request, response, cookieName, cookieValue, -1, isEncode);
}
/**
*
* @Description: 设置Cookie的值 在指定时间内生效, 编码参数
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage
* @param isEncode
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
}
/**
*
* @Description: 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage
* @param encodeString
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, String encodeString) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
}
/**
*
* @Description: 删除Cookie带cookie域名
* @param request
* @param response
* @param cookieName
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName) {
doSetCookie(request, response, cookieName, null, -1, false);
// doSetCookie(request, response, cookieName, "", -1, false);
}
/**
*
* @Description: 设置Cookie的值,并使其在指定时间内生效
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage cookie生效的最大秒数
* @param isEncode
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
logger.info("========== domainName: {} ==========", domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
* @Description: 设置Cookie的值,并使其在指定时间内生效
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage cookie生效的最大秒数
* @param encodeString
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
try {
if (cookieValue == null) {
cookieValue = "";
} else {
cookieValue = URLEncoder.encode(cookieValue, encodeString);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
logger.info("========== domainName: {} ==========", domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
* @Description: 得到cookie的域名
* @return
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
serverName = serverName.toLowerCase();
serverName = serverName.substring(7);
final int end = serverName.indexOf("/");
serverName = serverName.substring(0, end);
if (serverName.indexOf(":") > 0) {
String[] ary = serverName.split("\\:");
serverName = ary[0];
}
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3 && !isIp(serverName)) {
// www.xxx.com.cn
domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = "." + domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
return domainName;
}
public static String trimSpaces(String IP){//去掉IP字符串前后所有的空格
while(IP.startsWith(" ")){
IP= IP.substring(1,IP.length()).trim();
}
while(IP.endsWith(" ")){
IP= IP.substring(0,IP.length()-1).trim();
}
return IP;
}
public static boolean isIp(String IP){//判断是否是一个IP
boolean b = false;
IP = trimSpaces(IP);
if(IP.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")){
String s[] = IP.split("\\.");
if(Integer.parseInt(s[0])<255)
if(Integer.parseInt(s[1])<255)
if(Integer.parseInt(s[2])<255)
if(Integer.parseInt(s[3])<255)
b = true;
}
return b;
}
}
7.2 JsonUtils
package com.lv.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
/**
*
* @Title: JsonUtils.java
* @Package com.lv.utils
* @Description: json转换类
* Copyright: Copyright (c)
* Company: www.lv.com
*
* @author imooc
*/
public class JsonUtils {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 将对象转换成json字符串。
* @param data
* @return
*/
public static String objectToJson(Object data) {
try {
return MAPPER.writeValueAsString(data);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 将json结果集转化为对象
*
* @param jsonData json数据
* @param beanType 对象中的object类型
* @return
*/
public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
try {
T t = MAPPER.readValue(jsonData, beanType);
return t;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将json数据转换成pojo对象list
* @param jsonData
* @param beanType
* @return
*/
public static <T>List<T> jsonToList(String jsonData, Class<T> beanType) {
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = MAPPER.readValue(jsonData, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
前后端对应
在注册后也将信息保存到cookie
8 用户退出,清除信息
9. 前端轮播图
创建servicel和serviceImpl
对固定的数据创建枚举类
创建controller
10. 首页分类需求分析
10.1 查询一级分类
10.2通过自连接查询二级分类
![image.png](https://cdn.nlark.com/yuque/0/2022/png/23122776/1641562629652-b4284cd6-99ba-452b-968f-428fe7296acf.png#crop=0&crop=0&crop=1&crop=1&height=250&id=u09f58102&margin=%5Bobject%20Object%5D&name=image.png&originHeight=500&originWidth=512&originalType=binary&ratio=1&rotation=0&showTitle=false&size=123094&status=done&style=none&title=&width=256)
10.3 通过自定义Mapper实现查询子分类
1.自定义mapper类
我们这里需要多表联查所以单体查询已经不能满足需求,要创建自定义mapper
package com.lv.mapper;
import com.lv.pojo.vo.CategoryVO;
import com.lv.pojo.vo.NewItemsVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* @author ziming.xing
* Create Date:2021/5/18
*/
@Mapper
public interface CategoryCustomMapper {
List<CategoryVO> getSubCatList(@Param("rootCatId") Integer rootCatId);
List<NewItemsVO> getSixNewItemLazy(@Param("paramsMap") Map<String, Object> paramsMap);
}
2.创建vo包,之前的bo是前端传给后端的数据,这个vo是后端传给前端,前端view上显示的数据
3.创建mybatis的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.lv.mapper.CategoryCustomMapper">
<!--
collection 标签:用于定义关联的list集合类型的封装规则
property:对应三级分类的list属性名
ofType:集合的类型,三级分类的VO
-->
<resultMap id="MyCategoryVO" type="com.lv.pojo.vo.CategoryVO">
<id column="id" jdbcType="INTEGER" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="type" jdbcType="INTEGER" property="type"/>
<result column="father_id" jdbcType="INTEGER" property="fatherId"/>
<collection property="subCatList" ofType="com.lv.pojo.vo.SubCategoryVO">
<id column="sub_id" jdbcType="INTEGER" property="subId"/>
<result column="sub_name" jdbcType="VARCHAR" property="subName"/>
<result column="sub_type" jdbcType="INTEGER" property="subType"/>
<result column="sub_father_id" jdbcType="INTEGER" property="subFatherId"/>
</collection>
</resultMap>
<select id="getSubCatList" resultMap="MyCategoryVO">
SELECT f.id,
f.`name`,
f.type,
f.father_id,
c.id AS sub_id,
c.`name` AS sub_name,
c.type AS sub_type,
c.father_id AS sub_father_id
FROM `category` AS f
LEFT JOIN `category` AS c ON f.id = c.father_id
WHERE f.father_id = #{rootCatId,jdbcType=INTEGER}
</select>
</mapper>
10.4.首页商品推荐
<resultMap id="MyNewItemsVO" type="com.lv.pojo.vo.NewItemsVO">
<id column="root_cat_id" jdbcType="INTEGER" property="rootCatId"/>
<result column="root_cat_name" jdbcType="VARCHAR" property="rootCatName"/>
<result column="slogan" jdbcType="VARCHAR" property="slogan"/>
<result column="cat_image" jdbcType="VARCHAR" property="catImage"/>
<result column="bg_color" jdbcType="VARCHAR" property="bgColor"/>
<collection property="simpleItemList" ofType="cn.myst.web.pojo.vo.SimpleItemVO">
<result column="item_id" jdbcType="VARCHAR" property="itemId"/>
<result column="item_name" jdbcType="VARCHAR" property="itemName"/>
<result column="item_url" jdbcType="VARCHAR" property="itemUrl"/>
</collection>
</resultMap>
<select id="getSixNewItemLazy" resultMap="MyNewItemsVO">
SELECT f.id AS root_cat_id,
f.`name` AS root_cat_name,
f.slogan,
f.cat_image,
f.bg_color,
i.id AS item_id,
i.item_name AS item_name,
ii.url AS item_url,
i.created_time
FROM category f
LEFT JOIN items i ON f.id = i.root_cat_id
LEFT JOIN items_img ii ON i.id = ii.item_id
WHERE f.type = 1
AND i.root_cat_id = #{paramsMap.rootCatId}
AND ii.is_main = 1
ORDER BY i.created_time DESC
LIMIT 0,6
</select>
11.商品详情功能分析
11.1 搜索信息
11.2 编写相关查询servcie
我们查询都是和商品相关的,并且都是单表查询,所以我们使用通用mapper就可以
创建一个vo将数据传给前端
12.商品的评价
12.1 查询商品评价等级数量
创建评价vo
、
创建评价枚举
service层,查询评价等级和各个等级数量,在做和
controller层
12.2 自定义mapper查询
因为涉及到多表关联,通用mapper不能完成
- 数据库设计
- mapper层
- service层
12.3 mybatis分页插件
引入相关依赖,设置yml
- controller层
创建一个BaseController
12.4 信息脱敏
/**
* 通用脱敏工具类
* 可用于:
* 用户名
* 手机号
* 邮箱
* 地址等
*/
public class DesensitizationUtil {
private static final int SIZE = 6;
private static final String SYMBOL = "*";
/**
* 通用脱敏方法
* @param value
* @return
*/
public static String commonDisplay(String value) {
if (null == value || "".equals(value)) {
return value;
}
int len = value.length();
int pamaone = len / 2;
int pamatwo = pamaone - 1;
int pamathree = len % 2;
StringBuilder stringBuilder = new StringBuilder();
if (len <= 2) {
if (pamathree == 1) {
return SYMBOL;
}
stringBuilder.append(SYMBOL);
stringBuilder.append(value.charAt(len - 1));
} else {
if (pamatwo <= 0) {
stringBuilder.append(value.substring(0, 1));
stringBuilder.append(SYMBOL);
stringBuilder.append(value.substring(len - 1, len));
} else if (pamatwo >= SIZE / 2 && SIZE + 1 != len) {
int pamafive = (len - SIZE) / 2;
stringBuilder.append(value.substring(0, pamafive));
for (int i = 0; i < SIZE; i++) {
stringBuilder.append(SYMBOL);
}
if ((pamathree == 0 && SIZE / 2 == 0) || (pamathree != 0 && SIZE % 2 != 0)) {
stringBuilder.append(value.substring(len - pamafive, len));
} else {
stringBuilder.append(value.substring(len - (pamafive + 1), len));
}
} else {
int pamafour = len - 2;
stringBuilder.append(value.substring(0, 1));
for (int i = 0; i < pamafour; i++) {
stringBuilder.append(SYMBOL);
}
stringBuilder.append(value.substring(len - 1, len));
}
}
return stringBuilder.toString();
}
}
13. 商品搜索功能
13.1主页搜索框关键字商品搜索
- 会用到多表关联,也会用到分页操作
sql语句 ```sql SELECT i.id as itemId, i.item_name as itemName, i.sell_counts as sellCounts, ii.url as imgUrl, tempSpec.price_discount as price FROM items i LEFT JOIN items_img ii ON i.id=ii.item_id LEFT JOIN (
SELECT
item_id,MIN(price_discount) as price_discount
FROM
items_spec
GROUP BY
item_id
) tempSpec ON i.id =tempSpec.item_id WHERE ii.is_main =1;
xml
```xml
<!-- k:默认,代表默认排序,根据name-->
<!-- c:根据销量排序-->
<!-- p:根据价格排序-->
<!-- -->
<select id="searchItems" parameterType="Map" resultType="com.lv.pojo.vo.SearchItemsVO">
SELECT
i.id as itemId,
i.item_name as itemName,
i.sell_counts as sellCounts,
ii.url as imgUrl,
tempSpec.price_discount as price
FROM
items i
LEFT JOIN
items_img ii
ON
i.id=ii.item_id
LEFT JOIN
( SELECT item_id,MIN(price_discount) as price_discount FROM items_spec GROUP BY item_id ) tempSpec
ON
i.id =tempSpec.item_id
WHERE
ii.is_main =1;
<if test="paramsMap.keywords !=null and paramsMap.keywords !='' ">
AND i.item_name LIKE '%${paramsMap.keywords}%'
</if>
ORDER BY
<choose>
<when test="paramsMap.sort=="c"">
i.sell_counts desc
</when>
<when test="paramsMap.sort=="p"">
tempSpec.price_discount asc
</when>
<otherwise>
i.item_name asc
</otherwise>
</choose>
</select>
多表联查,要用到自定义mapper和VO
mapper
service
controller
13.2 前端业务与商品分类搜索
通过主页的三级分类id,进行搜索
<select id="searchItemsByThirdCat" parameterType="Map" resultType="com.lv.pojo.vo.SearchItemsVO">
SELECT
i.id as itemId,
i.item_name as itemName,
i.sell_counts as sellCounts,
ii.url as imgUrl,
tempSpec.price_discount as price
FROM
items i
LEFT JOIN
items_img ii
ON
i.id=ii.item_id
LEFT JOIN
( SELECT item_id,MIN(price_discount) as price_discount FROM items_spec GROUP BY item_id ) tempSpec
ON
i.id =tempSpec.item_id
WHERE
ii.is_main =1
AND
i.cat_id = #{paramsMap.catId}
ORDER BY
<choose>
<when test="paramsMap.sort=="c"">
i.sell_counts desc
</when>
<when test="paramsMap.sort=="p"">
tempSpec.price_discount asc
</when>
<otherwise>
i.item_name asc
</otherwise>
</choose>
</select>
14.购物车
14.1 购物车存储形式
使用Cookie和redis一起做,同时我们可以仿造京东购物车
14.2 使用Cookie和Redis,编写未登录和已登录加入购物车的代码
14.3 渲染(刷新)购物车
不能直接使用Cookie中的信息,因为有可能商品的信息,价格发生了更改,我们需要通过商品规格id重新刷新一下购物车信息。
- 要使用到多表关联,我们要用自定义mapper和数据层的VO,封装购物车信息
mapper.xml ```xml
- **自定义mapper**
![image.png](https://cdn.nlark.com/yuque/0/2022/png/23122776/1641818351909-4a2b5115-f56a-481a-a35a-30d135f2e809.png#crop=0&crop=0&crop=1&crop=1&height=339&id=uc306c198&margin=%5Bobject%20Object%5D&name=image.png&originHeight=677&originWidth=1622&originalType=binary&ratio=1&rotation=0&showTitle=false&size=142066&status=done&style=none&title=&width=811)
- **service和serviceImpl**
![image.png](https://cdn.nlark.com/yuque/0/2022/png/23122776/1641818513489-42d26bc4-0906-445d-afcf-b7ca65464c4d.png#crop=0&crop=0&crop=1&crop=1&height=78&id=u32f05707&margin=%5Bobject%20Object%5D&name=image.png&originHeight=156&originWidth=711&originalType=binary&ratio=1&rotation=0&showTitle=false&size=20808&status=done&style=none&title=&width=355.5)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/23122776/1641818729805-ce733816-0c06-44a5-806a-97a42e122164.png#crop=0&crop=0&crop=1&crop=1&height=170&id=u679d5e47&margin=%5Bobject%20Object%5D&name=image.png&originHeight=339&originWidth=810&originalType=binary&ratio=1&rotation=0&showTitle=false&size=50269&status=done&style=none&title=&width=405)
- **controller**
![image.png](https://cdn.nlark.com/yuque/0/2022/png/23122776/1641819250247-36d03f1d-b7c1-45cd-9baa-0b1ecda63760.png#crop=0&crop=0&crop=1&crop=1&height=313&id=ub6d15422&margin=%5Bobject%20Object%5D&name=image.png&originHeight=626&originWidth=1756&originalType=binary&ratio=1&rotation=0&showTitle=false&size=156856&status=done&style=none&title=&width=878)
<a name="fpFQe"></a>
## 14.4 其他业务操作
删除操作
![image.png](https://cdn.nlark.com/yuque/0/2022/png/23122776/1641868535792-9c9096e9-4cf0-4df3-b3eb-72676fdb9019.png#crop=0&crop=0&crop=1&crop=1&height=261&id=u637dee5b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=522&originWidth=1047&originalType=binary&ratio=1&rotation=0&showTitle=false&size=88953&status=done&style=none&title=&width=523.5)
<a name="wKXlm"></a>
# 15.收货地址
<a name="pDcFe"></a>
## 15.1 需求分析
- 进入页面后,需要将所有的收货地址列出来
- 可以新增地址
- 可以修改之前的地址
- 可以设置为默认:默认会被选中
<a name="G6QNI"></a>
## 15.2 表设计
![image.png](https://cdn.nlark.com/yuque/0/2022/png/23122776/1641877492404-44980249-5741-4e70-be8e-620a639c3e6e.png#crop=0&crop=0&crop=1&crop=1&height=269&id=u17865d1f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=538&originWidth=577&originalType=binary&ratio=1&rotation=0&showTitle=false&size=206971&status=done&style=none&title=&width=288.5)
- 关联用户 ID:一个用户会有多个收货地址
- 扩展字段:可能会有详细地址 2
用户在确认订单页面,可以针对收货地址做如下操作:
1. 查询用户的所有收货地址列表
2. 新增收货地址
3. 删除收货地址
4. 修改收货地址
5. 设置默认地址
另外在编辑地址时,所需要的省市区信息的维护:本课程是放在前端 js 中维护的,这种信息一般半年才维护一次,变动不太大。
<a name="qv42K"></a>
## 15.3 查询用户的所有收货地址列表
controller
```java
@ApiOperation(value = "根据用户id查询收货地址列表", notes = "根据用户id查询收货地址列表", httpMethod = "POST")
@PostMapping("/list")
public JSONResult list(
@RequestParam String userId) {
if (StringUtils.isBlank(userId)) {
return JSONResult.errorMsg("");
}
List<UserAddress> list = addressService.queryAll(userId);
return JSONResult.ok(list);
}
service
@Transactional(propagation = Propagation.SUPPORTS)
@Override
public List<UserAddress> queryAll(String userId) {
UserAddress ua = new UserAddress();
ua.setUserId(userId);
return userAddressMapper.select(ua);
}
15.4 新增收获地址
@ApiOperation(value = "用户新增地址", notes = "用户新增地址", httpMethod = "POST")
@PostMapping("/add")
public JSONResult add(@RequestBody AddressBO addressBO) {
JSONResult checkRes = checkAddress(addressBO);
if (checkRes.getStatus() != 200) {
return checkRes;
}
addressService.addNewUserAddress(addressBO);
return JSONResult.ok();
}
private JSONResult checkAddress(AddressBO addressBO) {
String receiver = addressBO.getReceiver();
if (StringUtils.isBlank(receiver)) {
return JSONResult.errorMsg("收货人不能为空");
}
if (receiver.length() > 12) {
return JSONResult.errorMsg("收货人姓名不能太长");
}
String mobile = addressBO.getMobile();
if (StringUtils.isBlank(mobile)) {
return JSONResult.errorMsg("收货人手机号不能为空");
}
if (mobile.length() != 11) {
return JSONResult.errorMsg("收货人手机号长度不正确");
}
boolean isMobileOk = MobileEmailUtils.checkMobileIsOk(mobile);
if (!isMobileOk) {
return JSONResult.errorMsg("收货人手机号格式不正确");
}
String province = addressBO.getProvince();
String city = addressBO.getCity();
String district = addressBO.getDistrict();
String detail = addressBO.getDetail();
if (StringUtils.isBlank(province) ||
StringUtils.isBlank(city) ||
StringUtils.isBlank(district) ||
StringUtils.isBlank(detail)) {
return JSONResult.errorMsg("收货地址信息不能为空");
}
return JSONResult.ok();
}
Bo
/**
* 用户新增或修改地址的 BO
*/
public class AddressBO {
private String addressId;
private String userId;
private String receiver;
private String mobile;
private String province;
private String city;
private String district;
private String detail;
service
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void addNewUserAddress(AddressBO addressBO) {
// 1. 判断当前用户是否存在地址,如果没有,则新增为‘默认地址’
Integer isDefault = 0;
List<UserAddress> addressList = this.queryAll(addressBO.getUserId());
if (addressList == null || addressList.isEmpty() || addressList.size() == 0) {
isDefault = 1;
}
String addressId = sid.nextShort();
// 2. 保存地址到数据库
UserAddress newAddress = new UserAddress();
BeanUtils.copyProperties(addressBO, newAddress);
newAddress.setId(addressId);
newAddress.setIsDefault(isDefault);
newAddress.setCreatedTime(new Date());
newAddress.setUpdatedTime(new Date());
userAddressMapper.insert(newAddress);
}
15.5 删除收货地址
controller
@ApiOperation(value = "用户删除地址", notes = "用户删除地址", httpMethod = "POST")
@PostMapping("/delete")
public JSONResult delete(
@RequestParam String userId,
@RequestParam String addressId) {
if (StringUtils.isBlank(userId) || StringUtils.isBlank(addressId)) {
return JSONResult.errorMsg("");
}
addressService.deleteUserAddress(userId, addressId);
return JSONResult.ok();
}
service
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void deleteUserAddress(String userId, String addressId) {
UserAddress address = new UserAddress();
address.setId(addressId);
address.setUserId(userId);
userAddressMapper.delete(address);
}
15.6 修改收货地址
controller
@ApiOperation(value = "用户修改地址", notes = "用户修改地址", httpMethod = "POST")
@PostMapping("/update")
public JSONResult update(@RequestBody AddressBO addressBO) {
if (StringUtils.isBlank(addressBO.getAddressId())) {
return JSONResult.errorMsg("修改地址错误:addressId不能为空");
}
JSONResult checkRes = checkAddress(addressBO);
if (checkRes.getStatus() != 200) {
return checkRes;
}
addressService.updateUserAddress(addressBO);
return JSONResult.ok();
}
service
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void updateUserAddress(AddressBO addressBO) {
String addressId = addressBO.getAddressId();
UserAddress pendingAddress = new UserAddress();
BeanUtils.copyProperties(addressBO, pendingAddress);
pendingAddress.setId(addressId);
pendingAddress.setUpdatedTime(new Date());
userAddressMapper.updateByPrimaryKeySelective(pendingAddress);
}
15.7 设置默认地址
controller
@ApiOperation(value = "用户设置默认地址", notes = "用户设置默认地址", httpMethod = "POST")
@PostMapping("/setDefalut")
public JSONResult setDefalut(
@RequestParam String userId,
@RequestParam String addressId) {
if (StringUtils.isBlank(userId) || StringUtils.isBlank(addressId)) {
return JSONResult.errorMsg("");
}
addressService.updateUserAddressToBeDefault(userId, addressId);
return JSONResult.ok();
}
servcie
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void updateUserAddressToBeDefault(String userId, String addressId) {
// 1. 查找默认地址,设置为不默认
UserAddress queryAddress = new UserAddress();
queryAddress.setUserId(userId);
queryAddress.setIsDefault(YesOrNo.YES.type);
List<UserAddress> list = userAddressMapper.select(queryAddress);
for (UserAddress ua : list) {
ua.setIsDefault(YesOrNo.NO.type);
userAddressMapper.updateByPrimaryKeySelective(ua);
}
// 2. 根据地址id修改为默认的地址
UserAddress defaultAddress = new UserAddress();
defaultAddress.setId(addressId);
defaultAddress.setUserId(userId);
defaultAddress.setIsDefault(YesOrNo.YES.type);
userAddressMapper.updateByPrimaryKeySelective(defaultAddress);
}
16. 订单流程和状态
16.1 需求分析
- 用户浏览商品
- 添加到购物车
- 订单结算:相当于是订单确认
- 发起支付
- 前往支付:这里生成订单?
- 待发货 变成 待收货,这里需要有 客服系统或则商家系统 来做这件事,本阶段不涉及该系统,可以直接修改状态来演示
- 付款中:有可能是一些第三方平台,或则政府平台,同步等待结果的话,可能会超过 10 秒,长的可能几分钟,这里异步等待回调
-
16.2 表设计
订单表: 收货 xx 快照:基本上下达填写收货地址后,再去更改收货地址信息,订单中的收货地址是不会被影响的
- 实际支付总价格:可能会有优惠卷之类的;但是在本阶段没有这些优惠功能
- 邮费:本阶段邮费都是包邮的
- 买家是否评价:判断是否对该订单是否进行了评价
- 逻辑删除状态:订单这种数据对我们公司来说是宝贵的数据,对于用户来说是看不到了,但是对于我们公司来说它是有价值的
订单商品关联表:子订单表
- 这里关于商品的一些信息,也是快照信息,而不是直接是一个 ID
- 商品 ID、规格 ID:这些数据也是比较珍贵的,可以用来做一些大数据分析之类的
订单状态表:可以和订单表进行合并的,但是为了性能,就拆开来了(列多,查询性能低)
- 订单 ID:与订单表的 ID 是一样的
- 订单状态:订单状态表;订单的每个状态更改都需要进行记录
10:待付款 20:已付款,待发货 30:已发货,待收货(7天自动确认) 40:交易成功(此时可以评价)50:交易关闭(待付款时,用户取消 或 长时间未付款,系统识别后自动关闭)
退货/退货(退款),此分支流程不做,所以不加入
16.4 聚合支付中心
16.5 提交并接受订单信息
前端代码
// 提交订单,创建订单
submitOrder() {
// 判断提交的商品不能为空
var orderItemList = this.orderItemList;
if (orderItemList == null || orderItemList == undefined || orderItemList == '' || orderItemList.length <= 0) {
alert("没有商品信息,订单无法提交~!");
return;
}
// 拼接规格ids
var itemSpecIds = "";
for (var i = 0 ; i < orderItemList.length ; i ++) {
var tmpSpecId = orderItemList[i].specId;
itemSpecIds += tmpSpecId;
if (i < orderItemList.length-1) {
itemSpecIds += ",";
}
}
// 判断选中的地址id不能为空
var choosedAddressId = this.choosedAddressId;
if (choosedAddressId == null || choosedAddressId == undefined || choosedAddressId == '') {
alert("请选择收货地址!");
return;
}
// 判断支付方式不能为空
var choosedPayMethod = parseInt(this.choosedPayMethod);
if (choosedPayMethod != 1 && choosedPayMethod != 2) {
alert("请选择支付方式!");
return;
}
// var newWindow = window.open();
// 买家备注可以为空
var orderRemarker = this.orderRemarker;
// console.log(orderRemarker);
var userInfo = this.userInfo;
var serverUrl = app.serverUrl;
axios.defaults.withCredentials = true;
axios.post(
serverUrl + '/orders/create',
{
"userId": userInfo.id,
"itemSpecIds": itemSpecIds,
"addressId": choosedAddressId,
"payMethod": choosedPayMethod,
"leftMsg": orderRemarker,
},
{
headers: {
'headerUserId': userInfo.id,
'headerUserToken': userInfo.userUniqueToken
}
})
.then(res => {
if (res.data.status == 200) {
// alert("OK");
var orderId = res.data.data;
// 判断是否微信还是支付宝支付
if (choosedPayMethod == 1) {
// 微信支付则跳转到微信支付页面,并且获得支付二维码
window.location.href = "wxpay.html?orderId=" + orderId;
} else if (choosedPayMethod == 2) {
this.orderId = orderId;
// 支付宝支付直接跳转
window.location.href = "alipay.html?orderId=" + orderId + "&amount="+this.totalAmount;
window.open("alipayTempTransit.html?orderId=" + orderId);
// const newWindow = window.open();
// 弹出的新窗口进行支付
// newWindow.location = "alipayTempTransit.html?orderId=" + orderId;
// this.$nextTick(()=> {
// // 当前页面跳转后会轮训支付结果
// newWindow.location.href = "alipay.html?orderId=" + orderId;
// })
} else {
alert("目前只支持微信或支付宝支付!");
}
} else {
alert(res.data.msg);
}
});
},
大致流程如下:
- 获取到商品的规格 ID
- 拿到选择的地址 ID
- 拿到支付的方式
- 创建订单
/orders/create
- 订单创建成功之后,再处理支付相关的业务,后面再说
这里主要是需要先实现创建订单的功能
用户下单(创建订单)接口
BO
/**
* 用于创建订单的 BO 对象
*/
public class SubmitOrderBO {
private String userId;
private String itemSpecIds;
private String addressId;
private Integer payMethod;
private String leftMsg;
service
//@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Service
public class OrderServiceImpl implements OrderService {
//@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Autowired
private OrdersMapper ordersMapper;
@Autowired
private Sid sid;
@Autowired
private AddressService addressService;
@Autowired
private ItemService itemService;
@Autowired
private OrderItemsMapper orderItemsMapper;
@Autowired
private OrderStatusMapper orderStatusMapper;
@Transactional(propagation = Propagation.REQUIRED)
@Override
public OrderVO createOrder(SubmitOrderBO submitOrderBO) {
String userId = submitOrderBO.getUserId();
String addressId = submitOrderBO.getAddressId();
String itemSpecIds = submitOrderBO.getItemSpecIds();
Integer payMethod = submitOrderBO.getPayMethod();
String leftMsg = submitOrderBO.getLeftMsg();
// 包邮费用设置为0
Integer postAmount = 0;
// 使用 sid 生成
String orderId = sid.nextShort();
UserAddress address = addressService.queryUserAddres(userId, addressId);
// 1. 新订单数据保存
Orders newOrder = new Orders();
newOrder.setId(orderId);
newOrder.setUserId(userId);
// 地址快照
newOrder.setReceiverName(address.getReceiver());
newOrder.setReceiverMobile(address.getMobile());
newOrder.setReceiverAddress(address.getProvince() + " "
+ address.getCity() + " "
+ address.getDistrict() + " "
+ address.getDetail());
// 在保存子订单后,计算出总价和真实价格后再填充
// newOrder.setTotalAmount();
// newOrder.setRealPayAmount();
// 设置运费,真实的业务中,会动态设置的,要获取到
newOrder.setPostAmount(postAmount);
newOrder.setPayMethod(payMethod);
newOrder.setLeftMsg(leftMsg); // 买家留言
newOrder.setIsComment(YesOrNo.NO.type);
newOrder.setIsDelete(YesOrNo.NO.type);
newOrder.setCreatedTime(new Date());
newOrder.setUpdatedTime(new Date());
// 2. 循环根据 itemSpecIds 保存订单商品信息表
String itemSpecIdArr[] = itemSpecIds.split(",");
Integer totalAmount = 0; // 商品原价累计
Integer realPayAmount = 0; // 优惠后的实际支付价格累计
for (String itemSpecId : itemSpecIdArr) {
// TODO 整合 redis 后,商品购买的数量重新从 redis 的购物车中获取
int buyCounts = 1;
// 2.1 根据规格 id,查询规格的具体信息,主要获取价格
ItemsSpec itemSpec = itemService.queryItemSpecById(itemSpecId);
totalAmount += itemSpec.getPriceNormal() * buyCounts;
realPayAmount += itemSpec.getPriceDiscount() * buyCounts;
// 2.2 根据商品 id,获得商品信息以及商品主图
String itemId = itemSpec.getItemId();
Items item = itemService.queryItemById(itemId);
String imgUrl = itemService.queryItemMainImgById(itemId);
// 2.3 循环保存子订单数据到数据库
String subOrderId = sid.nextShort();
OrderItems subOrderItem = new OrderItems();
subOrderItem.setId(subOrderId);
subOrderItem.setOrderId(orderId);
subOrderItem.setItemId(itemId);
subOrderItem.setItemName(item.getItemName());
subOrderItem.setItemImg(imgUrl);
subOrderItem.setBuyCounts(buyCounts);
subOrderItem.setItemSpecId(itemSpecId);
subOrderItem.setItemSpecName(itemSpec.getName());
subOrderItem.setPrice(itemSpec.getPriceDiscount());
orderItemsMapper.insert(subOrderItem);
// 2.4 在用户提交订单以后,规格表中需要扣除库存
itemService.decreaseItemSpecStock(itemSpecId, buyCounts);
}
newOrder.setTotalAmount(totalAmount);
newOrder.setRealPayAmount(realPayAmount);
ordersMapper.insert(newOrder);
// 3. 保存订单状态表
OrderStatus waitPayOrderStatus = new OrderStatus();
waitPayOrderStatus.setOrderId(orderId);
waitPayOrderStatus.setOrderStatus(OrderStatusEnum.WAIT_PAY.type);
waitPayOrderStatus.setCreatedTime(new Date());
orderStatusMapper.insert(waitPayOrderStatus);
// 4. 构建商户订单,用于传给支付中心
MerchantOrdersVO merchantOrdersVO = new MerchantOrdersVO();
merchantOrdersVO.setMerchantOrderId(orderId);
merchantOrdersVO.setMerchantUserId(userId);
merchantOrdersVO.setAmount(realPayAmount + postAmount);
merchantOrdersVO.setPayMethod(payMethod);
// 5. 构建自定义订单 vo
OrderVO orderVO = new OrderVO();
orderVO.setOrderId(orderId);
orderVO.setMerchantOrdersVO(merchantOrdersVO);
return orderVO;
}
}