store

·本篇:27.1k字 大约需要: 142分钟

项目实践

store商城

项目介绍

基于SpringBoot+MybatisPlus+JQuery的前后端分离项目

项目功能

  • 为用户提供登录、注册功能,继承kaptcha验证码防机器人以及用户管理,如:用户信息修改、修改密码、上传头像等
  • 分页展示商品信息、支持通过模糊搜索查找相关商品
  • 购物车相关功能管理,如:购物车商品的展示和结算、删除购物车中的商品等
  • 订单管理页面可以查看不同状态的订单,如:未支付、已付款和已完成等,集成了支付宝沙箱模拟商品支付全过程
  • 收藏界面分页展示用户收藏商品信息,以及取消收藏和加入购物车

环境搭建

  • JDK 1.8
  • Maven 3.6.3
  • SpringBoot 2.7.3
  • MySQL 5.5.37
  • 集成开发环境 IDEA 2020.3.4

用户管理

创建数据表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE `user`  (
`uid` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
`phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '电话号码',
`email` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`gender` int(11) NULL DEFAULT NULL COMMENT '性别',
`avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '/upload/defalut/user.jpg' COMMENT '头像',
`created_user` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
`created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`modified_user` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改人',
`modified_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`uid`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

每个用户都有一张默认头像

创建实体类

考虑到每张表中都有created_user、created_time、modified_user、modified_time这四个固定的字段,所以创建一个基类BaseEntity来对应这四个字段,并且之后的实体类都继承BaseEntity

BaseEntity

1
2
3
4
5
6
7
8
9
10
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BaseEntity implements Serializable
{
private String createdUser;
private Date createdTime;
private String modifiedUser;
private Date modifiedTime;
}

User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "用户实体类:User", description = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;对应数据库中的user表")
public class User extends BaseEntity implements Serializable
{
@TableId(value = "uid", type = IdType.AUTO)
@ApiModelProperty(value = "用户ID")
private Integer uid;
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "电话")
private String phone;
@ApiModelProperty(value = "电子邮箱")
private String email;
@ApiModelProperty(value = "性别")
private Integer gender;
@ApiModelProperty(value = "头像")
private String avatar;
}

注册

后端-持久层

通过MyBatisPlus框架与数据库进行交互,所以这种简单的curd语句可以定义Mapper使其继承BaseMapper

UserMapper
1
2
/** 处理用户数据操作的持久层接口 */
public interface UserMapper extends BaseMapper<User>{}

单元测试:略

后端-业务层

在Service包下创建ex包,主要放置在业务层中会出现的各种异常类,所以在编写代码前先要规划异常

  • 规划异常处理机制

    考虑到在后端处理业务的过程中,会出现各种异常情况,如执行过程中数据库宕机、用户未登录等情况,虽然在代码中可以抛出RuntimeException异常,但是这样做对异常的定位不够明确

    因此在业务层的制定中,需要考虑对异常的定义处理

    所以在业务层制定一个继承RuntimeException异常的异常基类,再让其他具体的异常类继承该异常基类

ServiceException
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
/** 业务层异常基类 */
public class ServiceException extends RuntimeException
{
public ServiceException()
{
super();
}

public ServiceException(String message)
{
super(message);
}

public ServiceException(String message, Throwable cause)
{
super(message, cause);
}

public ServiceException(Throwable cause)
{
super(cause);
}

protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
{
super(message, cause, enableSuppression, writableStackTrace);
}
}

根据业务不同详细定义具体异常类型,然后统一继承ServiceException

其他异常类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 用户名重复的异常 */
public class UsernameDuplicateException extends ServiceException
/** 用户数据不存在的异常 */
public class UserNotFoundException extends ServiceException
/** 密码验证失败的异常 */
public class PasswordNotMatchException extends ServiceException
/** 插入数据的异常 */
public class InsertException extends ServiceException
/** 更新数据的异常 */
public class UpdateException extends ServiceException
/** 删除数据失败的异常 */
public class DeleteException extends ServiceException
/** 收货地址数量达到上限的异常 */
public class AddressCountLimitException extends ServiceException
/** 收货地址数据不存在的异常 */
public class AddressNotFoundException extends ServiceException
/** 购物车数据不存在的异常 */
public class CartNotFoundException extends ServiceException
/** 商品数据不存在的异常 */
public class ProductNotFoundException extends ServiceException
/** 非法访问的异常 */
public class AccessDeniedException extends ServiceException
/** 订单数据不存在的异常*/
public class OrderNotFoundException extends ServiceException
IUserService
1
2
3
4
5
6
7
8
9
10
/** 处理用户数据的业务层接口 */
public interface IUserService
{
/**
* 用户注册
* @param user 用户数据对象
*/
void reg(User user);
}

UserServiceImpl
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
/** 处理用户数据的业务层实现类 */
@Service
public class UserServiceImpl implements IUserService
{
@Resource
UserMapper userMapper;
@Resource
PasswordEncoder encoder;

@Override
public void reg(User user)
{
// 判断该用户名是否已被注册
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, user.getUsername())
.select(User::getUid);
if(!Objects.isNull(userMapper.selectOne(wrapper))) throw new UsernameDuplicateException("该用户名已被注册");
// 日志
user.setCreatedUser(user.getUsername());
user.setModifiedUser(user.getUsername());
Date date = new Date();
user.setCreatedTime(date);
user.setModifiedTime(date);
// 加密密码
user.setPassword(encoder.encode(user.getPassword()));
int rows = userMapper.insert(user);
if(rows != 1) throw new InsertException("插入时产生未知异常");
}
}

用户注册功能,前端将值返回后,需要先判断该用户名是否已被注册,如果被注册,则抛出对应异常,没有被注册,则补全日志四个字段,然后将该条数据插入数据库

单元测试:略

后端-控制层

前后端分离中,后端返回给前端的是包装好的响应类,里面包含状态码,响应信息和响应数据

JsonResult
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
32
33
34
35
36
37
38
39
40
/**
* 响应结果类
* @param <E> 响应数据的类型
*/
@Data
public class JsonResult<E> implements Serializable
{
/** 状态码 */
private Integer state;
/** 状态描述信息 */
private String message;
/** 数据 */
private E data;

public JsonResult() {}

public JsonResult(Integer state)
{
this.state = state;
}

/** 出现异常时调用 */
public JsonResult(Throwable e)
{
// 获取异常对象中的异常信息
this.message = e.getMessage();
}

public JsonResult(Integer state, E data)
{
this.state = state;
this.data = data;
}

public JsonResult(Integer state, String message)
{
this.state = state;
this.message = message;
}
}

在控制层也会出现各种异常情况,所以需要进行异常规划,如业务层一样,在控制层也定义一些异常类

异常类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** 表示session错误的异常 */
public class SessionEmptyException extends RuntimeException
/** 表示验证码错误的异常 */
public class CodeNotMatchException extends RuntimeException
/** 上传的文件为空的异常,例如没有选择上传的文件就提交了表单,或选择的文件是0字节的空文件 */
public class FileEmptyException extends FileUploadException
/** 上传的文件的大小超出了限制值 */
public class FileSizeException extends FileUploadException
/** 上传的文件状态异常 */
public class FileStateException extends FileUploadException
/** 上传的文件类型超出了限制 */
public class FileTypeException extends FileUploadException
/** 文件上传相关异常的基类 */
public class FileUploadException extends RuntimeException
/** 上传文件时读写异常 */
public class FileUploadIOException extends FileUploadException

定义一个控制层基类,在这个基类中进行各种异常处理和定义一些公用的方法

BaseController
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class BaseController
{
public final static int OK = 200;

/**
* 从HttpSession对象中获取uid
* @param session HttpSession对象
* @return 当前登录的用户的id
*/
protected final Integer getUidFromSession(HttpSession session)
{
if(Objects.isNull(session.getAttribute("uid"))) throw new SessionEmptyException("用户未登录");
return Integer.valueOf(session.getAttribute("uid").toString());
}

/**
* 从HttpSession对象中获取用户名
* @param session HttpSession对象
* @return 当前登录的用户名
*/
protected final String getUsernameFromSession(HttpSession session)
{
if(Objects.isNull(session.getAttribute("username"))) throw new SessionEmptyException("用户未登录");
return session.getAttribute("username").toString();
}

/**
* 从HttpSession对象中获取用户头像
* @param session HttpSession对象
* @return 当前登录的用户名
*/
protected final String getAvatarFromSession(HttpSession session)
{
if(Objects.isNull(session.getAttribute("avatar"))) throw new SessionEmptyException("用户未登录");
return session.getAttribute("avatar").toString();
}

@ExceptionHandler({ServiceException.class, FileUploadException.class, SessionEmptyException.class, CodeNotMatchException.class})
public JsonResult<Void> handleException(Throwable e)
{
JsonResult<Void> result = new JsonResult<>();
if(e instanceof InsertException){
result.setState(4000);
result.setMessage("数据插入失败");
} else if(e instanceof UpdateException){
result.setState(4001);
result.setMessage("数据更新失败");
} else if(e instanceof DeleteException){
result.setState(4002);
result.setMessage("数据删除失败");
} else if(e instanceof UsernameDuplicateException){
result.setState(5000);
result.setMessage("用户名重复");
} else if(e instanceof UserNotFoundException){
result.setState(5001);
result.setMessage("用户不存在");
} else if(e instanceof PasswordNotMatchException){
result.setState(5002);
result.setMessage("密码不匹配");
} else if(e instanceof CodeNotMatchException){
result.setState(5003);
result.setMessage("验证码错误");
} else if(e instanceof AddressCountLimitException){
result.setState(6001);
result.setMessage("地址数量已超限制");
} else if(e instanceof AddressNotFoundException){
result.setState(6002);
result.setMessage("地址不存在");
} else if(e instanceof AccessDeniedException){
result.setState(6003);
result.setMessage("访问被拒绝");
} else if(e instanceof ProductNotFoundException){
result.setState(6004);
result.setMessage("商品不存在");
} else if(e instanceof CartNotFoundException){
result.setState(6005);
result.setMessage("购物车数据不存在");
} else if(e instanceof OrderNotFoundException){
result.setState(6006);
result.setMessage("订单数据不存在");
} else if(e instanceof FileEmptyException){
result.setState(7001);
result.setMessage("文件为空");
} else if(e instanceof FileSizeException){
result.setState(7002);
result.setMessage("文件大小超出限制");
} else if(e instanceof FileTypeException){
result.setState(7003);
result.setMessage("文件类型不正确");
} else if(e instanceof FileStateException){
result.setState(7004);
result.setMessage("文件状态异常");
} else if(e instanceof FileUploadIOException){
result.setState(7005);
result.setMessage("文件上传失败");
} else if(e instanceof SessionEmptyException){
result.setState(8000);
result.setMessage("用户未登录");
} else if(e instanceof ServiceException){
result.setState(9000);
result.setMessage("未知错误");
}
return result;
}
}
设计请求

请求路径:/api/auth/reg

请求参数:User user

请求类型:POST

响应结果:JsonResult< Void >

AuthController
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
32
33
34
35
@RestController
@RequestMapping("/api/auth")
@Api(tags = "用户验证相关接口描述")
public class AuthController extends BaseController
{
@Resource
IUserService userService;

@ApiOperation(value = "用户注册", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户注册")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4000, message = "数据插入失败"),
@ApiResponse(code = 5000, message = "用户名重复"),
})
@PostMapping("reg")
public JsonResult<Void> reg(@RequestBody User user)
{
userService.reg(user);
return new JsonResult<>(OK);
}

@ApiOperation(value = "验证验证码", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于验证用户验证码是否填写正确")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5003, message = "验证码错误")
})
@ApiImplicitParam(name = "code", value = "验证码", required = true)
@PostMapping("code")
public JsonResult<Void> code(@ApiParam(hidden = true) HttpSession session, String code)
{
String iCode = session.getAttribute(Constants.KAPTCHA_SESSION_KEY).toString();
if(!iCode.equals(code)) throw new CodeNotMatchException("验证码错误");
return new JsonResult<>(OK);
}
}

前端返回参数后,调用业务层的注册方法,并且返回响应实体类,如果在过程中抛出异常,则会在BaseController中进行处理,应该判断前端返回的参数是否为空

接口测试:使用postman进行接口测试

前端界面

编写js函数判断用户是否按规定输入,如:用户名长度是否为3以上,验证码是否正确等

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<script type="text/javascript">
let kaptcha = $("#kaptcha") // 绑定验证码控件
kaptcha.click(function (){ // 验证码点击事件
reFlashImg()
})
function reFlashImg() {
// 向后端请求验证码时需要加上时间参数
document.getElementById("kaptcha").src = "http://localhost:8080/kaptcha/kaptcha-image?time="+ new Date();
}

let ctr = $("#info") // 绑定信息控件
// 判断各种数据格式是否正确
function judgeData(){
let flag = true
// 正则表达式
let reg = /^[a-zA-Z]\w{2,5}$/i
let reg1 = /^\w{2,5}$/i
let username = $("#username").val()
let p1 = $("#password1").val()
let p2 = $("#password2").val()
let code = $("#code").val()
if(username.trim().length < 3){
setInfo("用户名长度必须为3及以上", ctr)
flag = false
} else if(!reg.test(username)){
setInfo("用户名必须为数字和字母且第一位不能为数字", ctr)
flag = false
} else if(p1.trim().length < 3){
setInfo("密码长度必须为3及以上", ctr)
flag = false
} else if(!reg1.test(p1)){
setInfo("密码必须为数字和字母", ctr)
flag = false
} else if(p1 !== p2){
setInfo("两次密码不相同", ctr)
flag = false
} else if(code.trim().length === 0){
setInfo("请填写验证码", ctr)
flag = false
}
return flag
}

// 注册按钮点击事件
$("#btn-reg").click(function (){
if(judgeData()){
// 先判断验证码是否正确
$.ajax({
url: "/api/auth/code",
type: "post",
data: {
"code": $("#code").val()
},
dataType: "json",
success: function (json){
if(json.state !== 200){
setInfo("验证码错误", ctr)
} else{
// $("#form-reg").serialize(),将表单中带有name字段的控件的值一起提交到后端
$.ajax({
url: "/api/auth/reg",
type: "POST",
data: {
"username": $("#username").val(),
"password": $("#password1").val()
},
dataType: "JSON",
success: function (json) {
if(json.state === 200){
confirm("注册成功")
}
else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}
},
error: function (xhr){
error(xhr)
}
})
}
})
</script>

登录

后端-持久层

UserMapper
1
2
/** 处理用户数据操作的持久层接口 */
public interface UserMapper extends BaseMapper<User> {}

后端-业务层

因为使用了Security安全框架,所以定义一个类实现UserDetailsService,并重写loadUserByUsername(String username)方法

AuthService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class AuthService implements UserDetailsService
{
@Resource
UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
User user = userMapper.selectOne(wrapper);
if(ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("该用户不存在");
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles("user") // 用户权限
.build();
}
}
SecurityConfiguration
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
@Resource
AuthService service;
@Resource
DataSource dataSource;
@Resource
PersistentTokenRepository repository;

// 数据源无法被自动注入
// remember-me底层依赖spring-JDBC
// 是因为mybatis的依赖被我注释

@Bean
public PersistentTokenRepository jdbcRepository()
{
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
// repository.setCreateTableOnStartup(true); 第一次启动会创建一张表存储token
return repository;
}

@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.antMatchers( "/api/auth/**", "/districts/**").permitAll() // 这些接口对所有人放行
.antMatchers("/api/**").authenticated() // 这些接口需要登录验证
.anyRequest().permitAll() // 其余所有接口对所有人放行
.and()
.formLogin()
.loginPage("/api/auth/login-deny")
.loginProcessingUrl("/api/auth/login")
.successForwardUrl("/api/auth/login-success")
.failureForwardUrl("/api/auth/login-failure")
.and()
.logout()
.logoutUrl("/api/auth/logout")
.logoutSuccessUrl("/api/auth/logout-success")
.and()
.csrf().disable()
.rememberMe()
.rememberMeParameter("remember")
.tokenValiditySeconds(60 * 60 * 24 * 7)
.tokenRepository(repository)
.userDetailsService(service);
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth
.userDetailsService(service)
.passwordEncoder(new BCryptPasswordEncoder());
}

@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
}

在使用remember-me的过程中,因为想将token持久化存储到数据库中,然后自动注入数据源时报错,是因为禁用了mybatis的依赖,并且还要导入spring-boot-starter-jdbc依赖

后端-控制层

设计请求

请求路径:/api/auth/login

请求参数:无

请求类型:POST

响应类型:无

实际上,登录请求没有走我定义的方法,是从Security中进行了验证

AuthController

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@RestController
@RequestMapping("/api/auth")
@Api(tags = "用户验证相关接口描述")
public class AuthController extends BaseController
{
@Resource
IUserService userService;

@ApiOperation(value = "用户登录", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户登录")
@PostMapping("login")
public void login(){}

@ApiOperation(value = "登录成功", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;表示登录成功,并将用户信息插入session中")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
})
@PostMapping("login-success")
public JsonResult<Void> loginSuccess(@ApiParam(hidden = true) HttpSession session)
{
SecurityContext context = SecurityContextHolder.getContext();
String username = context.getAuthentication().getName();
User user = userService.getByUsername(username);
session.setAttribute("username", user.getUsername());
session.setAttribute("uid", user.getUid());
session.setAttribute("avatar", user.getAvatar());
return new JsonResult<>(OK);
}

@ApiOperation(value = "登录失败", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;表示登录失败")
@ApiResponses({
@ApiResponse(code = 300, message = "登录失败"),
})
@PostMapping("login-failure")
public JsonResult<Void> loginFailure()
{
return new JsonResult<>(300, "登陆失败");
}

@ApiOperation(value = "未登录", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;验证用户是否登录")
@ApiResponses({
@ApiResponse(code = 301, message = "未验证,请登录"),
})
@PostMapping("login-deny")
public JsonResult<Void> loginDeny()
{
return new JsonResult<>(301, "未验证,请登录");
}

@ApiOperation(value = "验证记住我", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于验证用户是否使用记住我")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 9000, message = "没有使用记住我")
})
@GetMapping("remember")
public JsonResult<Void> remember(@ApiParam(hidden = true) HttpSession session)
{
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(Objects.isNull(authentication)) throw new SessionEmptyException("没有remember");
String username = authentication.getName();
User user = userService.getByUsername(username);
session.setAttribute("username", user.getUsername());
session.setAttribute("uid", user.getUid());
session.setAttribute("avatar", user.getAvatar());
return new JsonResult<>(OK);
}

@ApiOperation(value = "验证验证码", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于验证用户验证码是否填写正确")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5003, message = "验证码错误")
})
@ApiImplicitParam(name = "code", value = "验证码", required = true)
@PostMapping("code")
public JsonResult<Void> code(@ApiParam(hidden = true) HttpSession session, String code)
{
String iCode = session.getAttribute(Constants.KAPTCHA_SESSION_KEY).toString();
if(!iCode.equals(code)) throw new CodeNotMatchException("验证码错误");
return new JsonResult<>(OK);
}
}

接口测试:略

前端界面

与注册界面中的逻辑几乎相同,所有数据格式正确后,向登录接口发起POST请求即可,若请求成功则用location.herf=”index.html”,跳转到index页面;未成功则输出相关信息

退出登录

后端-持久层

与前面一致

后端-业务层

后端-控制层

设计请求

请求路径:/api/auth/logout

请求参数:HttpSession session

请求类型:GET

响应类型:JsonResult< Void >

AuthController
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
@RestController
@RequestMapping("/api/auth")
@Api(tags = "用户验证相关接口描述")
public class AuthController extends BaseController
{
@Resource
IUserService userService;

@ApiOperation(value = "退出登录", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户退出登录,并清除session中存储的用户信息")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
})
@GetMapping("logout")
public JsonResult<Void> logout(@ApiParam(hidden = true) HttpSession session)
{
session.setAttribute("username", null);
session.setAttribute("uid", null);
session.setAttribute("avatar", null);
return new JsonResult<>(OK);
}

@ApiOperation(value = "退出登录成功", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;表示用户退出登录成功")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
})
@GetMapping("logout-success")
public JsonResult<Void> logoutSuccess()
{
return new JsonResult<>(OK);
}
}

退出登录时清空session中存储的用户信息即可

修改密码

后端-持久层

与前面一致

后端-业务层

IUserService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 处理用户数据的业务层接口 */
public interface IUserService
{
/**
* 修改密码
* @param uid 当前登录的用户id
* @param username 用户名
* @param oldPassword 原密码
* @param newPassword 新密码
*/
public void changePassword(Integer uid, String username, String oldPassword, String newPassword);

/**
* 判断密码是否相同
* @param uid 当前登录的用户id
* @param password1 原密码
* @param password2 新密码
*/
void JudgePassword(Integer uid, String password1, String password2);
}

UserServiceImpl

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
32
33
34
35
36
37
38
39
40
41
42
/** 处理用户数据的业务层实现类 */
@Service
public class UserServiceImpl implements IUserService
{
@Resource
UserMapper userMapper;
@Resource
PasswordEncoder encoder;

@Override
public void changePassword(Integer uid, String username, String oldPassword, String newPassword)
{
User user = userMapper.selectById(uid);
if(ObjectUtils.isEmpty(user)) throw new UserNotFoundException("该用户不存在");
else
{
// 校验原密码是否一致
JudgePassword(uid, oldPassword, user.getPassword());
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getUid, uid)
.set(User::getPassword, encoder.encode(newPassword));
int row = userMapper.update(null, wrapper);
if(row != 1) throw new UpdateException("更新数据时发生异常");
}
}

@Override
public void JudgePassword(Integer uid, String password1, String password2)
{
if(ObjectUtils.isEmpty(password2)){
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getPassword)
.eq(User::getUid, uid);
User user = userMapper.selectOne(wrapper);
if(!encoder.matches(password1, user.getPassword())) throw new PasswordNotMatchException("密码不匹配");
}
else
{
if(!encoder.matches(password1, password2)) throw new PasswordNotMatchException("密码不匹配");
}
}
}

单元测试:略

后端-控制层

设计请求

请求路径:/user/change_password

请求参数:HttpSession session,String oldPassword,String newPassword

请求类型:POST

响应类型:JsonResult< Void >

UserController
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
@RestController
@RequestMapping("/user")
@Api(tags = "用户相关接口描述")
public class UserController extends BaseController
{
@Resource
IUserService service;

@ApiOperation(value = "修改密码", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户修改密码")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4001, message = "数据更新失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 5002, message = "原密码不匹配"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParams({
@ApiImplicitParam(name = "oldPassword", value = "原密码", required = true),
@ApiImplicitParam(name = "newPassword", value = "新密码", required = true),
})
@PostMapping("change_password")
public JsonResult<Void> changePassword(HttpSession session, String oldPassword, String newPassword)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
service.changePassword(uid, username, oldPassword, newPassword);
return new JsonResult<>(OK);
}
}

接口测试:略

前端界面

首先判断数据格式是否正确,如新密码不可与原密码相同等,当验证正确后发起请求修改密码,请求成功后,调用/api/auth/logout接口退出登录,并跳转页面到index.html;未成功则输出相关信息

修改个人资料

后端-持久层

与前面一致

后端-业务层

IUserService
1
2
3
4
5
6
7
8
9
10
11
/** 处理用户数据的业务层接口 */
public interface IUserService
{
/**
* 修改用户资料
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param user 用户新的数据
*/
void changeInfo(Integer uid, String username, User user);
}

UserServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** 处理用户数据的业务层实现类 */
@Service
public class UserServiceImpl implements IUserService
{
@Override
public void changeInfo(Integer uid, String username, User user)
{
JudgeUser(uid, username);
user.setUid(uid);
user.setCreatedUser(user.getUsername());
user.setModifiedUser(user.getUsername());
Date date = new Date();
user.setCreatedTime(date);
user.setModifiedTime(date);
int row = userMapper.updateById(user);
if(row != 1) throw new UpdateException("更新数据时发生异常");
}
}

单元测试:略

后端-控制层

设计请求

请求路径:/user/change_info

请求参数:HttpSession session,User user

请求类型:POST

响应类型:JsonResult< Void >

UserController
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
32
33
34
35
36
37
38
39
40
41
42
43
@RestController
@RequestMapping("/user")
@Api(tags = "用户相关接口描述")
public class UserController extends BaseController
{
@Resource
IUserService service;

@ApiOperation(value = "获取用户信息", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于获取用户信息,返回User对象")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@GetMapping("get_perInfo")
public JsonResult<User> getPerInfo(HttpSession session)
{
Integer uid = getUidFromSession(session);
User temp = service.getByUid(uid);
User user = new User();
user.setUsername(temp.getUsername());
user.setPhone(temp.getPhone());
user.setEmail(temp.getEmail());
user.setGender(temp.getGender());
return new JsonResult<>(OK, user);
}

@ApiOperation(value = "修改用户资料", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户修改个人资料")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4001, message = "数据更新失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@PostMapping("change_info")
public JsonResult<Void> changeInfo(HttpSession session, @RequestBody User user)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
service.changeInfo(uid, username, user);
return new JsonResult<>(OK);
}
}

接口测试:略

前端界面

页面开始先加载个人信息,然后校验用户输入的数据格式是否正确,最后发起请求,请求成功则跳转到修改个人资料页面;未成功则输出相关信息

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<script type="text/javascript">
$(function (){
getUserData()
})
function getUserData(){
$.ajax({
url : "/user/get_perInfo" ,
type: "get",
dataType: "json",
success: function (json) {
if (json.state === 200){
$("#name").val(json.data.username) //修改用户名
$("#phone").val(json.data.phone) //修改电话
$("#email").val(json.data.email) //修改邮箱
//修改性别
let gender = json.data.gender === 0 ? $("#gender-female") : $("#gender-male");
gender.prop("checked","checked")
} else{
confirm(json.state + ": " +json.message)
}
},
error: function (xhr){
error(xhr)
}
});
}

let erms = $("#error-msg")
function judgePhone(){
let ans = true
let phone = $("#phone").val()
let regex = /^1(?:3\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8\d|9\d)\d{8}$/
if(!regex.test(phone)){
setInfo("请输入正确的号码格式", erms)
ans = false
}
return ans
}

function judgeEmail(){
let regex = /[a-zA-Z0-9]+([-_.][A-Za-zd]+)*@([a-zA-Z0-9]+[-.])+[A-Za-zd]{2,5}$/
let ans = true
let email = $("#email").val()
if(!regex.test(email)){
setInfo("请输入正确的邮箱格式", erms)
ans = false
}
return ans
}

function changeInfo(){
if(judgePhone() === true && judgeEmail() === true){
$.ajax({
url: "/user/change_info",
type: "post",
data: $("#form-change-info").serialize(), // 将表单中带有name属性的标签的值序列化
dataType: "json",
success: function (json) {
if(json.state === 200){
confirm("个人资料修改成功")
location.href= "userdata.html"
} else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}
}

$("#btn-change-info").click(function (){
changeInfo()
})
</scripy>

上传头像

后端-持久层

与前面一致

后端-业务层

IUserService
1
2
3
4
5
6
7
8
9
10
11
/** 处理用户数据的业务层接口 */
public interface IUserService
{
/**
* 修改用户头像
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param avatar 用户新头像的路径
*/
void changeAvatar(Integer uid, String username, String avatar);
}
UserService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** 处理用户数据的业务层实现类 */
@Service
public class UserServiceImpl implements IUserService
{
@Resource
UserMapper userMapper;

@Override
public void changeAvatar(Integer uid, String username, String avatar)
{
JudgeUser(uid, username);
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getUid, uid)
.set(User::getAvatar, avatar);
int row = userMapper.update(null, wrapper);
if(row != 1) throw new UpdateException("更新数据时发生异常");
}
}

单元测试:略

后端-控制层

请求设计

请求路径:/user/change_avatar

请求参数:HttpSession session,MultipartFile file

请求类型:POST

响应类型:JsonResult< String >

UserController
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@RestController
@RequestMapping("/user")
@Api(tags = "用户相关接口描述")
public class UserController extends BaseController
{
/** 头像文件大小的上限值(10MB) */
private static final int AVATAR_MAX_SIZE = 10 * 1024 * 1024;
/** 允许上传的头像的文件类型 */
private static final List<String> AVATAR_TYPES = new ArrayList<String>(){{
add("image/jpeg");
add("image/jpg");
add("image/png");
add("image/bmp");
add("image/gif");
}};

@Value("${web.upload-path}")
private String uploadPath;

@ApiOperation(value = "修改用户头像", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户上传个人头像,返回新头像的路径")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4001, message = "数据更新失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 7001, message = "文件为空"),
@ApiResponse(code = 7002, message = "文件大小超出限制"),
@ApiResponse(code = 7003, message = "文件类型不正确"),
@ApiResponse(code = 7004, message = "文件状态异常"),
@ApiResponse(code = 7005, message = "文件上传失败"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParam(name = "file", value = "头像文件", required = true)
@PostMapping("change_avatar")
public JsonResult<String> changeAvatar(HttpSession session, MultipartFile file) // 后端接收文件类型用MultipartFile
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
if(Objects.isNull(file)){
throw new FileEmptyException("文件为空");
}
if(file.isEmpty()){
throw new FileEmptyException("文件不能为空");
}
if(file.getSize() > AVATAR_MAX_SIZE){
throw new FileSizeException("上传文件不能超过" + (AVATAR_MAX_SIZE / 1024) + "KB");
}
if(!AVATAR_TYPES.contains(file.getContentType())){
throw new FileTypeException("不支持该文件类型");
}
// String parent = session.getServletContext().getRealPath("upload") + '\\' + username;
String parent = uploadPath + username;

File dir = new File(parent);
if(!dir.exists()){
dir.mkdirs();
}
// 保存的头像文件的文件名
String originalFilename = file.getOriginalFilename();
if(StringUtils.isBlank(originalFilename)) throw new FileTypeException("文件类型异常");
int index = originalFilename.lastIndexOf(".");
String suffix = originalFilename.substring(index); // 获取文件后缀
String filename = UUID.randomUUID().toString() + suffix;

// 创建文件对象,表示保存的头像文件
File dest = new File(dir, filename);
// 执行保存头像文件
try {
file.transferTo(dest);
} catch (IllegalStateException e) {
throw new FileStateException("文件状态异常,可能文件已被移动或删除");
} catch (IOException e) {
throw new FileUploadIOException("上传文件时读写错误,请稍后重新尝试");
}

// 头像路径
String avatar = "/upload/" + username + File.separator + filename;
// 从Session中获取uid和username
// 将头像写入到数据库中
service.changeAvatar(uid, username, avatar);
session.setAttribute("avatar", avatar); // 更新session中的头像文件
return new JsonResult<>(OK, avatar);
}
}
MvcConfig

映射静态资源路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer
{
@Value("${web.upload-path}")
private String uploadPath;

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry)
{
// 映射静态资源路径
registry.addResourceHandler("/upload/**") // 映射用户头像路径
.addResourceLocations("file:" + uploadPath);
registry.addResourceHandler("/web/**")
.addResourceLocations("classpath:/static/web/");
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/static/");
registry.addResourceHandler("/swagger-ui.html") // swagger配置静态资源映射
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}

接口测试:略

前端界面

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
32
33
<script type="text/javascript">
$(function (){
$.get('/api/user/info', function (json){
if(json.state === 200){
$("#img-avatar").prop("src", json.data.avatar)
} else{
confirm(json.state + ": " + json.message)
}
})
});

$("#btn-upload").click(function (){
$.ajax({
url: "/user/change_avatar",
type: "post",
data: new FormData($("#form-change-avatar")[0]), // 选择表单的第一个元素,通常用来传文件
processData: false, // 关闭默认以字符串处理数据
contentType: false, // 关闭默认以字符串发送数据
dataType: "json",
success: function (json) {
if(json.state === 200){
confirm("上传成功")
$("#img-avatar").attr("src", json.data)
} else{
confirm(json.state + ":" + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
})
</script>

如果将文件保存在tomcat中,那么项目每次重启都会创建一个新的临时目录,通过静态资源映射已解决

地址管理

创建数据表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE `address`  (
`aid` int(11) NOT NULL AUTO_INCREMENT COMMENT '收货地址id',
`uid` int(11) NULL DEFAULT NULL COMMENT '归属的用户id',
`name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收货人姓名',
`province_name` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '省-名称',
`province_code` char(6) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '省-行政代号',
`city_name` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '市-名称',
`city_code` char(6) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '市-行政代号',
`area_name` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '区-名称',
`area_code` char(6) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '区-行政代号',
`zip` char(6) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮政编码',
`address` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '详细地址',
`phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机',
`tel` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '固话',
`tag` varchar(6) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标签',
`is_default` int(11) NULL DEFAULT NULL COMMENT '是否默认:0-不默认,1-默认',
`created_user` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
`created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`modified_user` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改人',
`modified_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`aid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

创建实体类

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
32
33
34
35
36
37
38
39
40
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
/* 收货地址数据的实体类 */
@ApiModel(value = "地址实体类:Address", description = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;对应数据库中的address表")
public class Address extends BaseEntity implements Serializable
{
@TableId(value = "aid", type = IdType.AUTO)
@ApiModelProperty(value = "地址ID")
private Integer aid;
@ApiModelProperty(value = "用户ID")
private Integer uid;
@ApiModelProperty(value = "收货人姓名")
private String name;
@ApiModelProperty(value = "省-名称")
private String provinceName;
@ApiModelProperty(value = "省-行政代号")
private String provinceCode;
@ApiModelProperty(value = "市-名称")
private String cityName;
@ApiModelProperty(value = "市-行政代号")
private String cityCode;
@ApiModelProperty(value = "区-名称")
private String areaName;
@ApiModelProperty(value = "区-行政代号")
private String areaCode;
@ApiModelProperty(value = "邮政编码")
private String zip;
@ApiModelProperty(value = "详细地址")
private String address;
@ApiModelProperty(value = "电话")
private String phone;
@ApiModelProperty(value = "固话")
private String tel;
@ApiModelProperty(value = "标签")
private String tag;
@ApiModelProperty(value = "是否为默认地址,1-默认,0-非默认")
private Integer isDefault;
}

添加收货地址

后端-持久层

AddressMapper
1
2
/** 处理收货地址操作的持久层接口 */
public interface AddressMapper extends BaseMapper<Address> {}
DistrictMapper
1
2
/** 处理省市区操作的持久层接口 */
public interface DistrictMapper extends BaseMapper<District> {}

在收货地址中需要用到省市区的信息

后端-业务层

IDistrictService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 处理省市区数据的业务层接口 */
public interface IDistrictService
{
/**
* 根据父代号获取省市区信息
* @param parent 父代号
* @return 省市区信息
*/
List<District> getByParent(String parent);

/**
* 根据自身代号获取省市区名称
* @param code 自身代号
* @return 省市区名称
*/
String getNameByCode(String code);
}
DistrictService
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
@Service
public class DistrictServiceImpl implements IDistrictService
{
@Resource
DistrictMapper mapper;

@Override
public List<District> getByParent(String parent)
{
LambdaQueryWrapper<District> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(District::getParent, parent);
List<District> list = mapper.selectList(wrapper);
list.forEach(district -> {
district.setParent(null);
district.setId(null);
});
return list;
}

@Override
public String getNameByCode(String code)
{
final LambdaQueryWrapper<District> wrapper = new LambdaQueryWrapper<>();
wrapper.select(District::getName)
.eq(District::getCode, code);
District district = mapper.selectOne(wrapper);
if(Objects.isNull(district.getName())) throw new ServiceException("无法获取省市区名称");
return district.getName();
}
}
IAddressService
1
2
3
4
5
6
7
8
9
10
11
/** 处理收货地址数据的业务层接口 */
public interface IAddressService
{
/**
* 添加收货地址
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param address 收货地址数据
*/
void addAddress(Integer uid, String username, Address address);
}
AddressService
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
32
33
34
35
36
37
38
39
@Service
public class AddressServiceImpl implements IAddressService
{
@Resource
IUserService userService;
@Resource
AddressMapper mapper;
@Resource
IDistrictService districtService;

@Value("${user.address.max-size}")
private int ADDRESS_MAX_SIZE; // 最大收货地址条数

@Override
public void addAddress(Integer uid, String username, Address address)
{
userService.JudgeUser(uid, username);
LambdaQueryWrapper<Address> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Address::getUid, uid)
.eq(Address::getUid, uid);
int size = Math.toIntExact(mapper.selectCount(wrapper));
if(size >= ADDRESS_MAX_SIZE) throw new AddressCountLimitException("达到收货地址最大条数");
address.setUid(uid);
address.setIsDefault((size == 0 ? 1 : 0)); // 1表示默认,0表示不默认
String provinceCode = address.getProvinceCode();
String areaCode = address.getAreaCode();
String cityCode = address.getCityCode();
address.setProvinceName(districtService.getNameByCode(provinceCode));
address.setAreaName(districtService.getNameByCode(areaCode));
address.setCityName(districtService.getNameByCode(cityCode));
address.setCreatedUser(username);
address.setModifiedUser(username);
Date date = new Date();
address.setCreatedTime(date);
address.setModifiedTime(date);
int rows = mapper.insert(address);
if(rows != 1) throw new InsertException("插入时产生未知异常");
}
}

如果插入的收货地址是该用户的第一条收货地址那么就将其设为默认地址

后端-控制层

请求设计

请求路径:/address/add_address

请求参数:HttpSession session,Address address

请求类型:POST

响应类型:JsonResult< Void >

AddressController

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
@RestController
@RequestMapping("/address")
@Api(tags = "收货地址接口相关描述")
public class AddressController extends BaseController
{
@Resource
IAddressService addressService;

@ApiOperation(value = "添加收货地址", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户添加收货地址")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4000, message = "数据插入失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 6001, message = "地址数量已超限制"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParam(name = "address", value = "Address对象", required = true)
@PostMapping("add_address")
public JsonResult<Void> addAddress(HttpSession session, Address address)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
addressService.addAddress(uid, username, address);
return new JsonResult<>(OK);
}
}
请求设计

请求路径:/districts

请求参数:String parent

请求类型:POST

响应类型:List< District >

DistrictController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@RequestMapping("/districts")
@Api(tags = "城市名称相关接口描述")
public class DistrictController extends BaseController
{
@Resource
IDistrictService service;

@ApiOperation(value = "获取城市名称", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于获取城市名称,返回List<District>")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功")
})
@ApiImplicitParam(name = "parent", value = "城市父代码", required = true)
@PostMapping({"/", ""})
public JsonResult<List<District>> getDistricts(String parent)
{
List<District> list = service.getByParent(parent);
return new JsonResult<>(OK, list);
}
}

接口测试:略

前端界面

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<script type="text/javascript">
let defaultOption = '<option value="0">----- 请选择 -----</option>'
$(function () {
showProvinceList()
cityList.append(defaultOption)
areaList.append(defaultOption)
})

let proList = $("#province-list")
let cityList = $("#city-list")
let areaList = $("#area-list")

proList.change(function (){
showCityList()
})

cityList.change(function (){
showAreaList()
})

function showProvinceList(){
proList.append(defaultOption)
let parent = 86
$.ajax({
url: "/districts",
type: "get",
data: {
"parent": parent
},
dataType: "JSON",
success: function (json){
if(json.state === 200){
let list = json.data
for (let i = 0; i < list.length; i++){
let opt = '<option value="'+list[i].code+'">'+list[i].name+'</option>'
proList.append(opt)
}
}
}
})
}

function showCityList(){
let parent = proList.val()
cityList.empty()
areaList.empty()
cityList.append(defaultOption)
areaList.append(defaultOption)
if(parent === 0){
return ;
}
$.ajax({
url: "/districts",
type: "get",
data: {
"parent": parent
},
dataType: "JSON",
success: function (json){
if(json.state === 200){
let list = json.data
for (let i = 0; i < list.length; i++){
let opt = '<option value="'+list[i].code+'">'+list[i].name+'</option>'
cityList.append(opt)
}
}
}
})
}

function showAreaList(){
let parent = cityList.val()
areaList.empty()
areaList.append(defaultOption)
if(parent === 0){
return ;
}
$.ajax({
url: "/districts",
type: "get",
data: {
"parent": parent
},
dataType: "JSON",
success: function (json){
if(json.state === 200){
let list = json.data
for (let i = 0; i < list.length; i++){
let opt = '<option value="'+list[i].code+'">'+list[i].name+'</option>'
areaList.append(opt)
}
}
}
})
}

$("#btn-add-new-address").click(function (){
$.ajax({
url: "/address/add_address",
type: "POST",
data: $("#form-add-new-address").serialize(),
dataType: "JSON",
success: function (json) {
if(json.state === 200){
alert("新增收货地址成功")
location.href="addAddress.html"
}
else{
alert("新增收货地址失败: " + json.message)
}
}
})
})
</script>

在增加收货地址页,主要是省市区信息显示

设置默认收货地址

后端-持久层

AddressMapper
1
2
/** 处理收货地址操作的持久层接口 */
public interface AddressMapper extends BaseMapper<Address> {}

后端-业务层

IAddressService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** 处理收货地址数据的业务层接口 */
public interface IAddressService
{
/**
* 获取收货地址列表
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @return 返回收货地址列表
*/
List<Address> getAddressList(Integer uid, String username);

/**
* 设置默认收货地址
* @param aid 收货地址id
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
*/
void setDefault(Integer aid, Integer uid, String username);
}
AddressServiceImpl
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
32
33
34
35
36
37
38
39
40
41
42
43
@Service
public class AddressServiceImpl implements IAddressService
{
@Resource
IUserService userService;
@Resource
AddressMapper mapper;

@Override
public List<Address> getAddressList(Integer uid, String username)
{
userService.JudgeUser(uid, username);
LambdaQueryWrapper<Address> wrapper = new LambdaQueryWrapper<>();
//noinspection unchecked
wrapper.select(Address::getName, Address::getAddress,
Address::getTag, Address::getPhone,
Address::getCityName, Address::getAreaName, Address::getAid, Address::getProvinceName)
.eq(Address::getUid, uid)
.orderByDesc(Address::getIsDefault, Address::getCreatedTime); // is_default为1表示默认,降序排序,没有默认就按创建时间排序
return mapper.selectList(wrapper);
}

@Transactional // 开启事务
@Override
public void setDefault(Integer aid, Integer uid, String username)
{
userService.JudgeUser(uid, username);
Address address = mapper.selectById(aid);
if(ObjectUtils.isEmpty(address)) throw new AddressNotFoundException("收获地址不存在");
if(!address.getUid().equals(uid)) throw new AccessDeniedException("非法数据访问");
LambdaUpdateWrapper<Address> wrapper = new LambdaUpdateWrapper<>();
// 将该用户所有收货地址设为非默认
wrapper.set(Address::getIsDefault, 0)
.eq(Address::getUid, uid);
int rows = mapper.update(null, wrapper);
if(rows < 1) throw new UpdateException("更新数据时产生未知异常");
address.setIsDefault(1); // 将选中的收获地址设为默认
address.setModifiedUser(username);
address.setModifiedTime(new Date());
int row = mapper.updateById(address);
if(row < 1) throw new UpdateException("更新数据时产生未知异常");
}
}

后端-控制层

请求设计

请求路径:/address/set_default

请求参数:HttpSession session,Integer aid

请求类型:POST

响应类型:JsonResult< Void >

AddressController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
@RequestMapping("/address")
@Api(tags = "收货地址接口相关描述")
public class AddressController extends BaseController
{
@ApiOperation(value = "设置默认收货地址", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户设置默认收货地址")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4001, message = "数据更新失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 6002, message = "地址不存在"),
@ApiResponse(code = 6003, message = "非法数据访问"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParam(name = "aid", value = "地址ID", required = true)
@PostMapping("set_default")
public JsonResult<Void> setDefault(HttpSession session, Integer aid)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
addressService.setDefault(aid, uid, username);
return new JsonResult<>(OK);
}
}

接口测试:略

前端界面

设置默认收货地址后,重新显示收货地址列表

删除收货地址

后端-持久层

与前面一致

后端-业务层

IAddressService
1
2
3
4
5
6
7
8
9
10
11
/** 处理收货地址数据的业务层接口 */
public interface IAddressService
{
/**
* 删除收货地址
* @param aid 收货地址id
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
*/
void delAddress(Integer aid, Integer uid, String username);
}
AddressServiceImpl
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
32
33
34
@Service
public class AddressServiceImpl implements IAddressService
{
@Resource
IUserService userService;
@Resource
AddressMapper mapper;

@Transactional
@Override
public void delAddress(Integer aid, Integer uid, String username)
{
userService.JudgeUser(uid, username);
Address address = mapper.selectById(aid);
if(ObjectUtils.isEmpty(address)) throw new AddressNotFoundException("收获地址不存在");
if(!address.getUid().equals(uid)) throw new AccessDeniedException("非法数据访问");
Integer status = address.getIsDefault();
int row = mapper.deleteById(aid);
if(row < 1) throw new DeleteException("删除数据时产生未知异常");
LambdaQueryWrapper<Address> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Address::getUid, uid);
Integer count = mapper.selectCount(wrapper);
if(count == 0) return ; // 如果已经没有收货地址,返回空
if(status == 1){ // 如果删除的这条收货地址为默认收货地址
wrapper.clear();
//noinspection unchecked
wrapper.eq(Address::getUid, uid)
.orderByDesc(Address::getModifiedTime)
.last("limit 1"); // 将最新修改的第一条收货地址设为默认地址
Integer aid1 = mapper.selectOne(wrapper).getAid();
setDefault(aid1, uid, username);
}
}
}

后端-控制层

请求设计

请求路径:/address/del_address

请求参数:HttpSession session,Integer aid

请求类型:POST

响应类型:JsonResult< Void >

AddressController
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
@RestController
@RequestMapping("/address")
@Api(tags = "收货地址接口相关描述")
public class AddressController extends BaseController
{
@Resource
IAddressService addressService;

@ApiOperation(value = "删除收货地址", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户删除收货地址")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4002, message = "数据删除失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 6002, message = "地址不存在"),
@ApiResponse(code = 6003, message = "非法数据访问"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParam(name = "aid", value = "地址ID", required = true)
@PostMapping("del_address")
public JsonResult<Void> delAddress(HttpSession session, Integer aid)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
addressService.delAddress(aid, uid, username);
return new JsonResult<>(OK);
}
}

接口测试:略

前端界面

删除收货地址后,重新显示收货地址列表

修改收货地址

后端-持久层

与前面一致

后端-业务层

IAddressService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 处理收货地址数据的业务层接口 */
public interface IAddressService
{
/**
* 获取收货地址信息
* @param aid 收货地址id
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @return 返回收货地址信息
*/
Address getAddress(Integer aid, Integer uid, String username);

/**
* 修改收货地址
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param address 收货地址信息
*/
void modAddress(Integer uid, String username, Address address);
}
AddressServiceImpl
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
@Service
public class AddressServiceImpl implements IAddressService
{
@Resource
IUserService userService;
@Resource
AddressMapper mapper;

@Override
public Address getAddress(Integer aid, Integer uid, String username)
{
userService.JudgeUser(uid, username);
Address address = mapper.selectById(aid);
if(ObjectUtils.isEmpty(address)) throw new AddressNotFoundException("收获地址不存在");
if(!address.getUid().equals(uid)) throw new AccessDeniedException("非法数据访问");
return address;
}

@Override
public void modAddress(Integer uid, String username, Address address)
{
userService.JudgeUser(uid, username);
address.setModifiedUser(username);
address.setModifiedTime(new Date());
int row = mapper.updateById(address);
if(row < 1) throw new UpdateException("更新数据时产生未知异常");
}
}

修改收货地址信息前,先要将该收货地址信息显示出来

后端-控制层

请求设计

请求路径:/address/get_address

请求参数:HttpSession session,Integer aid

请求类型:POST

响应类型:JsonResult< Address >

AddressController
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
32
33
34
35
36
37
38
39
40
@RestController
@RequestMapping("/address")
@Api(tags = "收货地址接口相关描述")
public class AddressController extends BaseController
{
@ApiOperation(value = "获取收货地址", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户获取收货地址")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 6002, message = "地址不存在"),
@ApiResponse(code = 6003, message = "非法数据访问"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParam(name = "aid", value = "地址ID", required = true)
@PostMapping("get_address")
public JsonResult<Address> getAddress(HttpSession session, Integer aid)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
Address address = addressService.getAddress(aid, uid, username);
return new JsonResult<>(OK, address);
}

@ApiOperation(value = "修改收货地址", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户修改收货地址")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4001, message = "数据更新失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParam(name = "address", value = "Address对象", required = true)
@PostMapping("mod_address")
public JsonResult<Void> modAddress(HttpSession session, Address address)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
addressService.modAddress(uid, username, address);
return new JsonResult<>(OK);
}
}

前端界面

修改收货地址请求成功后跳转到收货地址显示页面

商品管理

创建数据表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE TABLE `product`  (
`id` int(20) NOT NULL COMMENT '商品id',
`category_id` int(20) NULL DEFAULT NULL COMMENT '分类id',
`item_type` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品系列',
`title` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品标题',
`sell_point` varchar(150) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品卖点',
`price` bigint(20) NULL DEFAULT NULL COMMENT '商品单价',
`num` int(10) NULL DEFAULT NULL COMMENT '库存数量',
`image` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '图片路径',
`status` int(1) NULL DEFAULT 1 COMMENT '商品状态 1:上架 2:下架 3:删除',
`priority` int(10) NULL DEFAULT NULL COMMENT '显示优先级,数字越大优先级越高',
`created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`modified_time` datetime NULL DEFAULT NULL COMMENT '最后修改时间',
`created_user` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
`modified_user` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后修改人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

创建实体类

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
/** 商品数据的实体类 */
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "商品实体类:Product", description = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;对应数据库中的product表")
public class Product extends BaseEntity implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty(value = "商品ID")
private Integer id;
@ApiModelProperty(value = "分类ID")
private Integer categoryId;
@ApiModelProperty(value = "商品系列")
private String itemType;
@ApiModelProperty(value = "商品标题")
private String title;
@ApiModelProperty(value = "商品卖点")
private String sellPoint;
@ApiModelProperty(value = "商品单价")
private Long price;
@ApiModelProperty(value = "商品数量")
private Integer num;
@ApiModelProperty(value = "商品图片路径")
private String image;
@ApiModelProperty(value = "商品状态:1-上架,2-下架,3-删除")
private Integer status;
@ApiModelProperty(value = "显示优先级,数字越大优先级越高")
private Integer priority;
}

热销商品列表

后端-持久层

ProductMapper
1
2
/** 处理商品数据操作的持久层接口 */
public interface ProductMapper extends BaseMapper<Product> {}

后端-业务层

IProductService
1
2
3
4
5
6
7
8
9
10
/** 处理商品数据的业务层接口 */
public interface IProductService
{
/**
* 获取热销商品列表
* @param page 当前分页的信息,包含页码和每页数据条数
* @return 热销商品列表
*/
List<Product> getHotProducts(IPage<Product> page);
}
ProductServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class ProductServiceImpl implements IProductService
{
@Resource
ProductMapper mapper;
@Resource
ICollectService collectService;

@Override
public List<Product> getHotProducts(IPage<Product> page)
{
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
// 必须是上架的商品
//noinspection unchecked
wrapper.select(Product::getId, Product::getPrice, Product::getImage, Product::getTitle)
.eq(Product::getStatus, 1)
.orderByDesc(Product::getPriority); // 根据优先级进行降序排序
List<Product> list = mapper.selectPage(page, wrapper).getRecords();
if(ObjectUtils.isEmpty(list)) throw new ProductNotFoundException("没有商品列表");
return list;
}
}

后端-控制层

请求设计

请求路径:/product/get_hotProducts

请求参数:Long page,Long limit

请求类型:POST

**响应类型:JsonResult<List< Product >> **

ProductController

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
@RestController
@RequestMapping("/product")
@Api(tags = "商品相关接口描述")
public class ProductController extends BaseController
{
@Resource
IProductService service;

@ApiOperation(value = "获取热销商品", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于获取热销商品,返回List<Product>")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 6004, message = "商品不存在"),
})
@ApiImplicitParams({
@ApiImplicitParam(name = "page", value = "页码", required = true),
@ApiImplicitParam(name = "limit", value = "每页数据条数", required = true),
})
@PostMapping("get_hotProducts")
public JsonResult<List<Product>> getHotProducts(@RequestParam("page") Long page, @RequestParam("limit") Long limit)
{
Page<Product> iPage = new Page<>(page, limit);
List<Product> list = service.getHotProducts(iPage);
return new JsonResult<>(OK, list);
}
}

接口测试:略

前端界面

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<script type="text/javascript">
let page = 1 // 设置首页页码
let limit = 6 // 每页的数据条数
let total = 48 // 总条数

$(document).ready(function (){
showHotProducts()
showNewProducts()
hotPaging()
newPaging()
});

function showList(json, ct){
let list = json.data
if(total > list.length){
total = list.length
}
for (let i = 0; i < list.length; i++) {
let html = '<div class="col-md-12">' +
'<div class="col-md-7 text-row-2"><a href="product.html?id=#{id}">#{title}</a></div>' +
'<div class="col-md-2">¥#{price}</div>' +
'<div class="col-md-3"><img src="..#{image}collect.png" class="img-responsive" /></div>' +
'</div>'
html = html.replace(/#{title}/g, list[i].title)
html = html.replace(/#{price}/g, list[i].price)
html = html.replace(/#{image}/g, list[i].image)
html = html.replace(/#{id}/g, list[i].id)
ct.append(html)
}
}

function showHotProducts(){
$.ajax({
url: "/product/get_hotProducts",
type: "post",
data: {
"page": page,
"limit": limit
},
dataType: "json",
success: function(json){
if(json.state === 200){
let ct = $("#hot-list")
showList(json, ct)
} else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
});
}

function showNewProducts(){
$.ajax({
url: "/product/get_newProducts",
type: "post",
data: {
"page": page,
"limit": limit
},
dataType: "json",
success: function (json) {
if(json.state === 200){
let ct = $("#new-list")
showList(json, ct)
} else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}

function hotPaging(){
layui.use('laypage', function (){
let laypage = layui.laypage
laypage.render({
elem: 'bar-pages-hot',
count: total,
limit: limit,
jump: function (obj, first) {
// obj包含了当前分页的所有参数, 比如
page = obj.curr // 改变当前页码
limit = obj.limit
if(!first){
$("#hot-list").empty()
showHotProducts() // 首次不执行
}
}
})
})
}

function newPaging(){
layui.use('laypage', function (){
let laypage = layui.laypage
laypage.render({
elem: 'bar-pages-new',
count: total,
limit: limit,
jump: function (obj, first) {
// obj包含了当前分页的所有参数, 比如
page = obj.curr // 改变当前页码
limit = obj.limit
if(!first){
$("#new-list").empty()
showNewProducts() // 首次不执行
}
}
})
})
}
</script>

使用laypage.js进行分页

新到商品列表

与热销商品列表基本一致

显示商品信息

后端-持久层

ProductMapper
1
2
/** 处理商品数据操作的持久层接口 */
public interface ProductMapper extends BaseMapper<Product> {}

后端-业务层

IProductService
1
2
3
4
5
6
7
8
9
10
/** 处理商品数据的业务层接口 */
public interface IProductService
{
/**
* 根据商品id获取商品信息
* @param id 商品id
* @return 商品信息
*/
Product getProductById(Integer id);
}
ProductServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class ProductServiceImpl implements IProductService
{
@Resource
ProductMapper mapper;

@Override
public Product getProductById(Integer id)
{
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
wrapper.select(Product::getTitle, Product::getSellPoint, Product::getPrice, Product::getImage)
.eq(Product::getId, id);
Product product = mapper.selectOne(wrapper);
if(ObjectUtils.isEmpty(product)) throw new ProductNotFoundException("没有该商品");
return product;
}
}

后端-控制层

请求设计

请求路径:/product/get_product

请求参数:Integer id

请求类型:POST

响应类型:JsonResult< Product >

ProductController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@RequestMapping("/product")
@Api(tags = "商品相关接口描述")
public class ProductController extends BaseController
{
@Resource
IProductService service;

@ApiOperation(value = "获取商品信息", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于获取商品信息,返回Product对象")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 6004, message = "商品不存在"),
})
@ApiImplicitParam(name = "id", value = "商品ID", required = true)
@PostMapping("get_product")
public JsonResult<Product> getProduct(Integer id)
{
return new JsonResult<>(OK, service.getProductById(id));
}
}

前端界面

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<script type="text/javascript">
$(function (){
getProduct()
getCollectS()
})

let pid = $.getUrlParam("id")

function getProduct(){
$.ajax({
url: "/product/get_product",
type: "post",
data: {
"id": pid,
},
dataType: "json",
success: function (json){
if(json.state === 200){
let product = json.data
$("#product-title").html(product.title)
$("#product-sell-point").html(product.sellPoint)
$("#product-price").html(product.price)
for (let i = 1; i <= 5; i++){
$("#product-image-" + i +"-big").attr("src", ".." + product.image + i +"_big.png")
$("#product-image-" + i).attr("src", ".." + product.image + i +".jpg")
}
}else if(json.state === 6004){
location.href="index.html"
}else{
confirm(json.state + ": " +json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}

let heart = $("#heart")
let collect = $("#collect")
let btn = $("#btn-add-to-collect")

function setCollect(str, cls){
collect.empty().html(str)
heart.prop("class", cls)
}

function getCollectS(){
$.ajax({
url: "/product/get_status",
type: "post",
data: {
"pid": pid
},
dataType: "json",
success: function (json)
{
let status = json.data
if (status === 1){
setCollect(" 已收藏", "fa fa-heart")
}
else if (status === null || status === 0){
setCollect(" 加入收藏", "fa fa-heart-o")
}
else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}

$("#btn-add-to-cart").click(function (){
let num = $("#num").val()
$.ajax({
url: "/cart/add_to_cart",
type: "post",
data: {
"id": pid,
"num": num
},
dataType: "json",
success: function (json){
if(json.state === 200){
location.href="cart.html"
} else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
})

function judgeCls(){
let cls = heart.prop("class")
if(cls === "fa fa-heart"){
cancelCollect()
} else if(cls === "fa fa-heart-o"){
addCollect()
}
}

function cancelCollect(){
$.ajax({
url: "/product/cancel_collect",
type: "post",
data: {
"pid": pid,
"status": 0
},
dataType: "json",
success: function (json){
if(json.state === 200){
setCollect(" 加入收藏", "fa fa-heart-o")
} else{
setCollect(" 已收藏", "fa fa-heart")
}
},
error: function (xhr){
error(xhr)
}
})
}

function addCollect(){
$.ajax({
url: "/product/add_collect",
type: "post",
data: {
"pid": pid
},
dataType: "json",
success: function (json){
if(json.state === 200){
setCollect(" 已收藏", "fa fa-heart")
} else{
setCollect(" 加入收藏", "fa fa-heart-o")
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}

btn.click(function (){
judgeCls()
})
</script>

因为商品页面可以加入收藏,所以这里还需要调用加入收藏和取消收藏商品的接口,商品信息显示的时候需要判断其是不是处于收藏状态

立即购买

后端-持久层

ProductMapper
1
2
/** 处理商品数据操作的持久层接口 */
public interface ProductMapper extends BaseMapper<Product>{}
OrderMapper
1
2
3
4
5
6
7
8
9
10
11
12
13
/** 处理订单用户数据操作的持久层接口 */
public interface OrderMapper extends MPJBaseMapper<Order>
{
@Select("select a.oid, a.aid, a.recv_name as recvName, a.total_price as totalPrice, a.status, a.order_time as orderTime, a.pay_time as payTime, " +
"b.image, b.title, b.price, b.num, " +
"c.zip, c.address, c.province_name as provinceName, c.phone, c.city_name as cityName, c.area_name as areaName " +
"from (order_user a " +
"left join order_item b on a.oid = b.oid) " +
"left join address c on a.aid = c.aid " +
"where a.uid=#{uid} " +
"order by a.status ASC, a.created_time DESC")
List<OrderVO> getOrderVOs(Integer uid);
}

这里最后没有使用自己定义的getOrderVOs方法,而是使用了MPJ进行左连接查询

后端-业务层

IOrderService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 处理订单数据的业务层接口 */
public interface IOrderService
{
/**
* 创建订单
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param aid 收货地址id
* @param pid 商品id
* @param num 商品数量
* @return 订单id
*/
Integer createOrderByP(Integer uid, String username, Integer aid, Integer pid, Integer num);
}
OrderServiceImpl
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Service
public class OrderServiceImpl implements IOrderService
{
@Resource
IUserService userService;
@Resource
AddressServiceImpl addressService;
@Resource
OrderMapper orderMapper;
@Resource
OrderItemMapper orderItemMapper;
@Resource
ProductMapper productMapper;

@Override
public Integer createOrderByP(Integer uid, String username, Integer aid, Integer pid, Integer num)
{
Date date = new Date();
Address address = addressService.getAddress(aid, uid, username);
Product product = productMapper.selectById(pid);
Long totalPrice = product.getPrice() * num;
Order order = new Order();
order.setUid(uid);
order.setAid(aid);
order.setRecvName(address.getName());
order.setRecvPhone(address.getPhone());
order.setRecvProvince(address.getProvinceName());
order.setRecvCity(address.getCityName());
order.setRecvArea(address.getAreaName());
order.setRecvAddress(address.getAddress());
order.setTotalPrice(totalPrice);
order.setStatus(0);
order.setOrderTime(date);
// 补全数据:日志
order.setCreatedUser(username);
order.setCreatedTime(date);
order.setModifiedUser(username);
order.setModifiedTime(date);

int row = orderMapper.insert(order);
if(row != 1) throw new InsertException("插入数据时发生异常");

// 创建订单商品数据
OrderItem item = new OrderItem();
// 补全数据:
item.setOid(order.getOid());
// 补全数据:pid, title, image, price, num
item.setPid(pid);
item.setTitle(product.getTitle());
item.setImage(product.getImage());
item.setPrice(product.getPrice());
item.setNum(num);
// 补全数据:4项日志
item.setCreatedUser(username);
item.setCreatedTime(date);
item.setModifiedUser(username);
item.setModifiedTime(date);
// 插入订单商品数据
int rows2 = orderItemMapper.insert(item);
if (rows2 != 1) throw new InsertException("插入数据时发生异常");

return order.getOid();
}
}
IProductService
1
2
3
4
5
6
7
8
9
10
/** 处理商品数据的业务层接口 */
public interface IProductService
{
/**
* 根据商品id获取商品信息
* @param id 商品id
* @return 商品信息
*/
Product getProductById(Integer id);
}
ProductServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class ProductServiceImpl implements IProductService
{
@Resource
ProductMapper mapper;

@Override
public Product getProductById(Integer id)
{
System.err.println("id = " + id);
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
wrapper.select(Product::getTitle, Product::getSellPoint, Product::getPrice, Product::getImage)
.eq(Product::getId, id);
Product product = mapper.selectOne(wrapper);
if(ObjectUtils.isEmpty(product)) throw new ProductNotFoundException("没有该商品");
return product;
}
}

根据商品id查出商品信息,然后创建订单

后端-控制层

设计请求

请求路径:/order/create_orderByP

请求参数:HttpSession session, Integer aid, Integer pid, Integer num

请求类型:POST

响应类型:JsonResult< Integer >

OrderController
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
@RequestMapping("/order")
@RestController
@Api(tags = "订单相关接口描述")
public class OrderController extends BaseController
{
@ApiOperation(value = "根据商品数据创建订单", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户创建订单,返回Oid")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4000, message = "数据插入失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 6004, message = "商品不存在"),
@ApiResponse(code = 8000, message = "用户未登录"),
@ApiResponse(code = 9000, message = "参数错误")
})
@ApiImplicitParams({
@ApiImplicitParam(name = "aid", value = "收货地址ID", required = true),
@ApiImplicitParam(name = "pid", value = "商品ID", required = true),
@ApiImplicitParam(name = "num", value = "商品数量", required = true)
})
@PostMapping("create_orderByP")
public JsonResult<Integer> createOrderByP(HttpSession session, Integer aid, Integer pid, Integer num){
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
Integer oid = service.createOrderByP(uid, username, aid, pid, num);
return new JsonResult<>(OK, oid);
}
}

接口测试:略

前端界面

点击立即购买后,用sessionStorage将pid和num存储到session中,然后跳转到订单页面

购物车管理

创建数据表

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `cart`  (
`cid` int(11) NOT NULL AUTO_INCREMENT COMMENT '购物车数据id',
`uid` int(11) NOT NULL COMMENT '用户id',
`pid` int(11) NOT NULL COMMENT '商品id',
`price` bigint(20) NULL DEFAULT NULL COMMENT '加入时商品单价',
`num` int(11) NULL DEFAULT NULL COMMENT '商品数量',
`created_user` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
`created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`modified_user` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改人',
`modified_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`cid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

创建实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** 购物车数据的实体类 */
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "购物车数据实体类:Cart", description = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;对应数据库中的cart表")
public class Cart extends BaseEntity implements Serializable {
@TableId(value = "cid", type = IdType.AUTO)
@ApiModelProperty(value = "购物车数据ID")
private Integer cid;
@ApiModelProperty(value = "用户ID")
private Integer uid;
@ApiModelProperty(value = "商品ID")
private Integer pid;
@ApiModelProperty(value = "商品单价")
private Long price;
@ApiModelProperty(value = "商品数量")
private Integer num;
}

创建VO类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 购物车数据的Value Object类 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "购物车数据VO类:CartVO")
public class CartVO implements Serializable
{
@ApiModelProperty(value = "购物车数据ID")
private Integer cid;
@ApiModelProperty(value = "用户ID")
private Integer uid;
@ApiModelProperty(value = "商品ID")
private Integer pid;
@ApiModelProperty(value = "商品单价")
private Long price;
@ApiModelProperty(value = "商品数量")
private Integer num;
@ApiModelProperty(value = "商品标题")
private String title;
@ApiModelProperty(value = "商品真实价格")
private Long realPrice;
@ApiModelProperty(value = "商品图片")
private String image;
}

加入购物车

后端-持久层

CartMapper
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
32
/** 处理购物车数据操作的持久层接口 */
public interface CartMapper extends BaseMapper<Cart>
{
@Select("select cid, uid, pid, cart.price, cart.num, product.title, product.image, product.price AS realprice " +
"from cart left join product on cart.pid = product.id " +
"where uid=#{uid} " +
"order by cart.created_time desc")
List<CartVO> getCartVOs(Integer uid);

// 注解在使用诸如foreach标签之类时,需要使用<script></script>包裹,
// 在foreach标签中,
// 如果参数是数组类型那么keyName为array
// 如果参数是集合类型那么keyName为list

@Select("<script> " +
"select cid, uid, pid, cart.price, cart.num, product.title, product.image, product.price AS realprice " +
"from cart left join product on cart.pid = product.id " +
"where cid IN " +
"<foreach collection='list' open='(' item='cid' separator=',' close=')'> #{cid} </foreach> " +
"order by cart.created_time desc " +
"</script>")
List<CartVO> getCartVOsByCid(@Param("list") List<Integer> cids);

// 这里类型为List,却需要指定
@Delete("<script>" +
"delete from cart " +
"where uid=#{uid} " +
"and cid in " +
"<foreach collection='list' open='(' item='cid' separator=',' close=')'> #{cid} </foreach> " +
"</script>")
int selDelCart(Integer uid, @Param("list") List<Integer> cids);
}

后端-业务层

ICartService
1
2
3
4
5
6
7
8
9
10
11
12
/** 处理购物车数据的业务层接口 */
public interface ICartService
{
/**
* 将商品添加到购物车中
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param pid 商品id
* @param amount 商品数量
*/
Integer addToCart(Integer uid, String username, Integer pid, Integer amount);
}
CartServiceImpl
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Service
public class CartServiceImpl implements ICartService
{
@Resource
CartMapper mapper;
@Resource
ProductMapper productMapper;
@Resource
UserServiceImpl userService;

@Override
public Integer addToCart(Integer uid, String username, Integer pid, Integer amount)
{
Integer cid;
userService.JudgeUser(uid, username);
LambdaQueryWrapper<Cart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Cart::getUid, uid)
.eq(Cart::getPid, pid);
Cart cart = mapper.selectOne(wrapper);
Date date = new Date();
if(ObjectUtils.isEmpty(cart)) // 购物车中没有该商品,将该商品添加到购物车
{
cart = new Cart();
cart.setUid(uid);
cart.setPid(pid);
cart.setNum(amount);
LambdaQueryWrapper<Product> wrapper1 = new LambdaQueryWrapper<>();
wrapper1.eq(Product::getId, pid)
.select(Product::getPrice);
cart.setPrice(productMapper.selectOne(wrapper1).getPrice());
cart.setCreatedTime(date);
cart.setModifiedTime(date);
cart.setCreatedUser(username);
cart.setModifiedUser(username);
int row = mapper.insert(cart);
cid = cart.getCid();
if(row != 1) throw new InsertException("插入数据时发生未知异常");
}
else // 购物车中有该商品,更新该商品的数量
{
LambdaUpdateWrapper<Cart> wrapper1 = new LambdaUpdateWrapper<>();
wrapper1.set(Cart::getNum, amount + cart.getNum())
.set(Cart::getModifiedTime, date)
.set(Cart::getModifiedUser, username)
.eq(Cart::getUid, uid) // 忘记加限定条件,后端数据库cart被污染
.eq(Cart::getPid, pid);
int row = mapper.update(cart, wrapper1);
cid = cart.getCid();
if(row != 1) throw new UpdateException("更新数据时发生未知异常");
}
return cid;
}
}

**单元测试:略 **

后端-控制层

设计请求

请求路径:/cart/add_to_cart

请求参数:HttpSession session,Integer pid, Integer amount

请求类型:POST

响应类型:JsonResult< Integer >

CartController
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
32
33
@RestController
@RequestMapping("/cart")
@Api(tags = "购物车接口相关描述")
public class CartController extends BaseController
{
@Resource
ICartService service;

@ApiOperation(value = "加入购物车", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户将商品加入购物车")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4000, message = "数据插入失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 6004, message = "商品不存在"),
@ApiResponse(code = 6005, message = "购物车数据不存在"),
@ApiResponse(code = 8000, message = "用户未登录"),
@ApiResponse(code = 9000, message = "参数错误")
})
@ApiImplicitParams({
@ApiImplicitParam(name = "pid", value = "商品ID", required = true),
@ApiImplicitParam(name = "amount", value = "商品数量", required = true)
})
@PostMapping("add_to_cart")
public JsonResult<Integer> addToCart(HttpSession session,
@RequestParam("id") Integer pid,
@RequestParam("num") Integer amount)
{
String username = getUsernameFromSession(session);
Integer uid = getUidFromSession(session);
service.addToCart(uid, username, pid, amount);
return new JsonResult<>(OK, service.addToCart(uid, username, pid, amount));
}
}

**接口测试:略 **

前端界面

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
<script type="text/javascript">
$(function (){
showCarts()
})

let totalNum = 0
let totalPrice = 0

function showCarts(){
$.ajax({
url: "/cart/get_cartVOs",
type: "get",
dataType: "json",
success: function (json){
if(json.state === 200){
let list = json.data
if(list.length > 0){
for (let i = 0; i < list.length; i++){
let ckId = "ckid" + i.toString()
let priceId = "goodsPrice" + i.toString()
let countId = "goodsCount" + i.toString()
let castId = "goodsCast" + i.toString()
let tr = '<tr>' +
'<td>' +
'<input id="#{ckId}" onclick="checkOne(#{i})" name="cids" value="#{cid}" type="checkbox" class="ckitem" />' +
'</td>' +
'<td><img src="..#{image}collect.png" class="img-responsive" /></td>' +
'<td>#{title}</td>' +
'<td>¥<span id="#{priceId}">#{price}</span></td>' +
'<td>' +
'<input type="button" value="-" class="num-btn" onclick="reduceNum(#{cid}, #{i})" />' +
'<input id= "#{countId}" type="text" size="2" readonly="readonly" class="num-text" value=#{num}>' +
'<input class="num-btn" type="button" value="+" onclick="addNum(#{cid}, #{i})" />' +
'</td>' +
'<td>¥<span id="#{castId}">#{money}</span></td>' +
'<td>' +
'<input type="button" onclick="delCart(#{cid})" class="cart-del btn btn-default btn-xs" value="删除" />' +
'</td>' +
'</tr>'
tr = tr.replace(/#{ckId}/g, ckId)
tr = tr.replace(/#{priceId}/g, priceId)
tr = tr.replace(/#{countId}/g, countId)
tr = tr.replace(/#{castId}/g, castId)
tr = tr.replace(/#{i}/g, i.toString())
tr = tr.replace(/#{image}/g, list[i].image)
tr = tr.replace(/#{title}/g, list[i].title)
tr = tr.replace(/#{price}/g, list[i].realPrice)
let money = list[i].realPrice * list[i].num
tr = tr.replace(/#{money}/g, money.toString())
tr = tr.replace(/#{num}/g, list[i].num)
tr = tr.replace(/#{cid}/g, list[i].cid)
$("#cart-list").append(tr)
totalNum += list[i].num
totalPrice += money
}
}
} else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}

let price // 商品价格控件
let count // 商品数量控件
let cast // 该商品总金额控件

function getCtrl(rid){
price = $("#goodsPrice"+rid)
count = $("#goodsCount"+rid)
cast = $("#goodsCast"+rid)
}

//计算单行小计价格的方法
function calcRow(){
//取单价
let vprice = parseFloat(price.html());
//取数量
let vnum = parseFloat(count.val());
//小计金额
let vtotal = vprice * vnum;
//赋值
cast.html(vtotal);
}

function reduceNum(cid, rid){
getCtrl(rid)
let num = parseInt(count.val()) - 1;
if (num === 0)
return;
count.val(num);
calcRow();
changeNum(cid, num)
}

function addNum(cid, rid){
getCtrl(rid)
let num = parseInt(count.val()) + 1;
if (num === 20){
confirm("修改失败,超出库存或购买限制")
return;
}
count.val(num);
calcRow();
changeNum(cid, num)
}

function changeNum(cid, num){
$.ajax({
url: "/cart/change_cartNum",
type: "post",
data: {
"cid": cid,
"num": num
},
dataType: "json",
success: function (json){
if(json.state !== 200){
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}

let countCtrl = $("#selectCount")
let totalCtrl = $("#selectTotal")
let cids = Array.of()

function setCids(){
// 获取当前购物车中有多少商品
let cartLength = $("#cart-list").children("tr").length
for (let i = 0; i < cartLength; i++){
let ck = $("#ckid" + i.toString())
cids.push(parseInt(ck.val()))
}
}

function clearCids(){
cids.length = 0
}

// 全选全不选
function checkAll(ckbtn) {
//根据全选按钮的状态设置所有class为ckitem的选中状态
$(".ckitem").prop("checked", $(ckbtn).prop("checked"));
// 表示全选状态为取消
if($(ckbtn).prop("checked") === false){
countCtrl.empty().html(0)
totalCtrl.empty().html(0)
clearCids()
} else{
// 设置选择的商品数量和总价
countCtrl.empty().html(totalNum)
totalCtrl.empty().html(totalPrice)
setCids()
}
}

let theNum = 0
let thePrice = 0

function checkOne(rid){
let ck = $("#ckid" + rid)
// 获取当前购物车中有多少商品
let cartLength = $("#cart-list").children("tr").length
// 获取已被选中的单选框的数量
let ckNum = $(".ckitem:checked").length
// 判断是否全被选中
if(cartLength === ckNum){
let ckall = $(".ckall")
ckall.prop("checked", true)
checkAll(ckall)
} else{
getCtrl(rid)
if(ck.prop("checked") === true){
cids.push(parseInt(ck.val()))
theNum += parseInt(count.val())
thePrice += parseFloat(cast.html())

} else{
cids.splice(parseInt(ck.val()), 1)
theNum -= parseInt(count.val())
thePrice -= parseFloat(cast.html())
}
countCtrl.empty().html(theNum)
totalCtrl.empty().html(thePrice)
}
}

function delCart(cid){
if(confirm("确定要删除该商品吗?")){
$.ajax({
url: "/cart/del_cart",
type: "post",
data: {
"cid": cid,
},
dataType: "json",
success: function (json){
if(json.state === 200) {
location.href="cart.html"
} else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}
}

function selDelCart(){
if (cids.length > 0){
cids = JSON.stringify(cids) // 将数组转换为字符串返回
if (confirm("确定要删除所选商品吗?")){
$.ajax({
url: "/cart/sel_del_cart",
type: "post",
data: {
"cids": cids
}, // 不能采用"cids="+cids这样加在url的方式返回,因为tomcat中特殊字符(例如" < > [ \ ] ^ ` { | } .)报错
dataType: "json",
success: function (json){
if (json.state === 200){
location.href = "cart.html"
} else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}
clearCids()
} else{
confirm("您没有选择任何商品")
}
}

function judgeCheck(){
let flag = true
if(cids.length === 0){
confirm("请先选择任意商品再进行结算")
flag = false
}
return flag
}

function settlement(){
if(judgeCheck()){
sessionStorage.setItem("cids", $("#form-cart").serialize())
location.href="orderConfirm.html"
}
}
</script>

显示商品

后端-持久层

与前面一致

后端-业务层

ICartService
1
2
3
4
5
6
7
8
9
10
11
/** 处理购物车数据的业务层接口 */
public interface ICartService
{
/**
* 获取购物车数据信息
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @return 购物车数据信息列表
*/
List<CartVO> getCartVOs(Integer uid, String username);
}
CartServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class CartServiceImpl implements ICartService
{
@Resource
CartMapper mapper;
@Resource
ProductMapper productMapper;
@Resource
UserServiceImpl userService;

@Override
public List<CartVO> getCartVOs(Integer uid, String username)
{
userService.JudgeUser(uid, username);
List<CartVO> cartVOs = mapper.getCartVOs(uid);
if(ObjectUtils.isEmpty(cartVOs)) throw new CartNotFoundException("没有购物车数据");
return cartVOs;
}
}

单元测试:略

后端-控制层

设计请求

请求路径:/cart/get_cartVOs

请求参数:HttpSession session

请求类型:GET

响应类型:JsonResult<List< CartVO >>

CartController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@RequestMapping("/cart")
@Api(tags = "购物车接口相关描述")
public class CartController extends BaseController
{
@ApiOperation(value = "获取购物车中商品信息", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户获取购物车中商品信息")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 6005, message = "购物车数据不存在"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@GetMapping("get_cartVOs")
public JsonResult<List<CartVO>> getCartVOs(HttpSession session)
{
String username = getUsernameFromSession(session);
Integer uid = getUidFromSession(session);
return new JsonResult<>(OK, service.getCartVOs(uid, username));
}
}

接口测试:略

前端界面

移除商品

后端-持久层

与前面一致

后端-业务层

ICartService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** 处理购物车数据的业务层接口 */
public interface ICartService
{
/**
* 删除购物车数据
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param cid 购物车数据id
*/
void delCart(Integer uid, String username, Integer cid);

/**
* 删除选择的购物车数据
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param cids 购物车数据id
*/
void selDelCart(Integer uid, String username, List<Integer> cids);
}
CartServiceImpl
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
@Service
public class CartServiceImpl implements ICartService
{
@Resource
CartMapper mapper;
@Resource
ProductMapper productMapper;
@Resource
UserServiceImpl userService;

@Override
public void delCart(Integer uid, String username, Integer cid)
{
userService.JudgeUser(uid, username);
LambdaQueryWrapper<Cart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Cart::getCid, cid);
int row = mapper.delete(wrapper);
if(row != 1) throw new DeleteException("删除数据时发生未知异常");
}

@Override
public void selDelCart(Integer uid, String username, List<Integer> cids)
{
if(ObjectUtils.isEmpty(cids)) throw new ServiceException("参数错误");
userService.JudgeUser(uid, username);
final int row = mapper.selDelCart(uid, cids);
if(row < 1) throw new CartNotFoundException("没有购物车数据");
}
}

单元测试:略

后端-控制层

设计请求

请求路径:/cart/del_cart

请求参数:HttpSession session,Integer cid

请求类型:POST

响应类型:JsonResult< Void >

请求路径:/cart/sel_del_cart

请求参数:HttpSession session,String cids

请求类型:POST

响应类型:JsonResult< Void >

CartController
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@RestController
@RequestMapping("/cart")
@Api(tags = "购物车接口相关描述")
public class CartController extends BaseController
{
@Resource
ICartService service;

@ApiOperation(value = "删除购物车商品", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户删除购物车商品")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4002, message = "数据删除失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParam(name = "cid", value = "购物车ID", required = true)
@PostMapping("del_cart")
public JsonResult<Void> delCart(HttpSession session, Integer cid)
{
String username = getUsernameFromSession(session);
Integer uid = getUidFromSession(session);
service.delCart(uid, username, cid);
return new JsonResult<>(OK);
}

@ApiOperation(value = "选择购物车商品删除", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户选择购物车商品删除")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 6005, message = "购物车数据不存在"),
@ApiResponse(code = 8000, message = "用户未登录"),
@ApiResponse(code = 9000, message = "参数错误")
})
@ApiImplicitParam(name = "cids", value = "购物车ID", required = true)
@PostMapping("sel_del_cart")
public JsonResult<Void> selDelCart(HttpSession session, String cids)
{
// 前端用json传送数据,后端需要加上@RequestBody
// 使用fastjson将前端传回的JsonString转换为List
List<Integer> params = JSON.parseArray(cids, Integer.class);
String username = getUsernameFromSession(session);
Integer uid = getUidFromSession(session);
service.selDelCart(uid, username, params);
return new JsonResult<>(OK);
}
}

接口测试:略

前端界面

订单管理

创建数据表

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
32
33
34
35
CREATE TABLE `order_user`  (
`oid` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单id',
`uid` int(11) NOT NULL COMMENT '用户id',
`aid` int(11) NOT NULL COMMENT '地址id',
`recv_name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '收货人姓名',
`recv_phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收货人电话',
`recv_province` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收货人所在省',
`recv_city` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收货人所在市',
`recv_area` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收货人所在区',
`recv_address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收货详细地址',
`total_price` bigint(20) NULL DEFAULT NULL COMMENT '总价',
`status` int(11) NULL DEFAULT NULL COMMENT '状态:0-未支付,1-已支付,2-已取消,3-已关闭,4-已完成',
`order_time` datetime NULL DEFAULT NULL COMMENT '下单时间',
`pay_time` datetime NULL DEFAULT NULL COMMENT '支付时间',
`created_user` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
`created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`modified_user` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改人',
`modified_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`oid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 67 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

CREATE TABLE `order_item` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`oid` int(11) NOT NULL COMMENT '所归属的订单的id',
`pid` int(11) NOT NULL COMMENT '商品的id',
`title` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '商品标题',
`image` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品图片',
`price` bigint(20) NULL DEFAULT NULL COMMENT '商品价格',
`num` int(11) NULL DEFAULT NULL COMMENT '购买数量',
`created_user` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
`created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`modified_user` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改人',
`modified_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 166 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

订单包括两部分,一部分是用户信息,一部分是商品信息

创建实体类

Order
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
32
33
34
35
36
37
/** 订单数据的实体类 */
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("order_user") // 不能使用order为表名
@ApiModel(value = "订单实体类:Order", description = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;对应数据库中的order_user表")
public class Order extends BaseEntity implements Serializable
{
@TableId(value = "oid", type = IdType.AUTO)
@ApiModelProperty(value = "订单ID")
private Integer oid;
@ApiModelProperty(value = "用户ID")
private Integer uid;
@ApiModelProperty(value = "收货地址ID")
private Integer aid;
@ApiModelProperty(value = "收货人姓名")
private String recvName;
@ApiModelProperty(value = "收货人电话")
private String recvPhone;
@ApiModelProperty(value = "收货人所在省")
private String recvProvince;
@ApiModelProperty(value = "收货人所在市")
private String recvCity;
@ApiModelProperty(value = "收货人所在区")
private String recvArea;
@ApiModelProperty(value = "收货人详细地址")
private String recvAddress;
@ApiModelProperty(value = "订单总价")
private Long totalPrice;
@ApiModelProperty(value = "订单状态:0-未支付,1-已支付,2-已取消,3-已关闭,4-已完成")
private Integer status;
@ApiModelProperty(value = "下单时间")
private Date orderTime;
@ApiModelProperty(value = "支付时间")
private Date payTime;
}
OrderItem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 订单中的商品数据 */
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "订单实体类:OrderItem", description = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;对应数据库中的order_item表")
public class OrderItem extends BaseEntity implements Serializable
{
@TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty(value = "主键ID")
private Integer id;
@ApiModelProperty(value = "订单ID")
private Integer oid;
@ApiModelProperty(value = "商品ID")
private Integer pid;
@ApiModelProperty(value = "商品标题")
private String title;
@ApiModelProperty(value = "商品图片")
private String image;
@ApiModelProperty(value = "商品单价")
private Long price;
@ApiModelProperty(value = "商品数量")
private Integer num;
}

创建VO类

OrderVO
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
32
33
34
35
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "订单数据VO类:OrderVO")
public class OrderVO implements Serializable
{
@ApiModelProperty(value = "订单ID")
private Integer oid;
@ApiModelProperty(value = "收货地址ID")
private Integer aid;
@ApiModelProperty(value = "收货人姓名")
private String recvName;
@ApiModelProperty(value = "电话")
private String phone;
@ApiModelProperty(value = "收货人所在省")
private String provinceName;
@ApiModelProperty(value = "收货人所在市")
private String cityName;
@ApiModelProperty(value = "收货人所在区")
private String areaName;
@ApiModelProperty(value = "详细地址")
private String address;
@ApiModelProperty(value = "订单状态:0-未支付,1-已支付,2-已取消,3-已关闭,4-已完成")
private Integer status;
@ApiModelProperty(value = "下单时间")
private Date orderTime;
@ApiModelProperty(value = "支付时间")
private Date payTime;
@ApiModelProperty(value = "订单总价")
private Long totalPrice;
@ApiModelProperty(value = "邮政编码")
private String zip;
@ApiModelProperty(value = "商品详细信息")
List<OrderItem> orderItems;
}

显示订单

后端-持久层

OrderMapper
1
2
3
4
5
6
7
8
9
10
11
12
13
/** 处理订单用户数据操作的持久层接口 */
public interface OrderMapper extends MPJBaseMapper<Order>
{
@Select("select a.oid, a.aid, a.recv_name as recvName, a.total_price as totalPrice, a.status, a.order_time as orderTime, a.pay_time as payTime, " +
"b.image, b.title, b.price, b.num, " +
"c.zip, c.address, c.province_name as provinceName, c.phone, c.city_name as cityName, c.area_name as areaName " +
"from (order_user a " +
"left join order_item b on a.oid = b.oid) " +
"left join address c on a.aid = c.aid " +
"where a.uid=#{uid} " +
"order by a.status ASC, a.created_time DESC")
List<OrderVO> getOrderVOs(Integer uid);
}
OrderItemMapper
1
2
/** 处理订单商品数据操作的持久层接口 */
public interface OrderItemMapper extends BaseMapper<OrderItem> {}
OrderVOMapper
1
2
3
4
5
6
7
8
9
/** 处理订单VO数据操作的持久层接口 */
public interface OrderVOMapper extends MPJBaseMapper<OrderVO>
{
List<OrderVO> getOrderVOs(Integer uid);

// List<OrderVO> getOrderVOsByS(Integer uid, Integer status);

OrderVO getOrderVO(Integer uid, Integer oid);
}
OrderVOMapper.xml
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<mapper namespace = "com.t1k.store.mapper.OrderVOMapper">
<resultMap id="getOrderVOs" type="com.t1k.store.vo.OrderVO">
<id column="oid" property="oid"/>
<result column="aid" property="aid"/>
<result column="recvName" property="recvName"/>
<result column="totalPrice" property="totalPrice"/>
<result column="status" property="status"/>
<result column="orderTime" property="orderTime"/>
<result column="payTime" property="payTime"/>
<result column="zip" property="zip"/>
<result column="address" property="address"/>
<result column="provinceName" property="provinceName"/>
<result column="phone" property="phone"/>
<result column="cityName" property="cityName"/>
<result column="areaName" property="areaName"/>
<collection property="orderItems" ofType="com.t1k.store.entity.OrderItem" javaType="java.util.List">
<id column="id" property="id"/>
<result column="image" property="image"/>
<result column="title" property="title"/>
<result column="price" property="price"/>
<result column="num" property="num"/>
</collection>
</resultMap>
<select id="getOrderVOs" resultMap="getOrderVOs" resultType="com.t1k.store.vo.OrderVO">
SELECT a.oid, a.aid, a.recv_name AS recvName, a.total_price AS totalPrice,
a.status, a.order_time AS orderTime, a.pay_time AS payTime,
b.image, b.title, b.price, b.num,
c.zip, c.address, c.province_name AS provinceName, c.phone, c.city_name AS cityName, c.area_name AS areaName
FROM
order_user a
LEFT JOIN
address c ON a.aid = c.aid
LEFT JOIN
order_item b ON a.oid = b.oid
WHERE
a.uid=#{uid}
ORDER BY a.status ASC, a.created_time DESC
</select>

<resultMap id="getOrderVOsByS" type="com.t1k.store.vo.OrderVO">
<id column="oid" property="oid"/>
<result column="aid" property="aid"/>
<result column="recvName" property="recvName"/>
<result column="totalPrice" property="totalPrice"/>
<result column="status" property="status"/>
<result column="orderTime" property="orderTime"/>
<result column="payTime" property="payTime"/>
<result column="zip" property="zip"/>
<result column="address" property="address"/>
<result column="provinceName" property="provinceName"/>
<result column="phone" property="phone"/>
<result column="cityName" property="cityName"/>
<result column="areaName" property="areaName"/>
<collection property="orderItems" ofType="com.t1k.store.entity.OrderItem" javaType="java.util.List">
<id column="id" property="id"/>
<result column="image" property="image"/>
<result column="title" property="title"/>
<result column="price" property="price"/>
<result column="num" property="num"/>
</collection>
</resultMap>
<select id="getOrderVOsByS" resultMap="getOrderVOsByS" resultType="com.t1k.store.vo.OrderVO">
SELECT a.oid, a.aid, a.recv_name AS recvName, a.total_price AS totalPrice,
a.status, a.order_time AS orderTime, a.pay_time AS payTime,
b.image, b.title, b.price, b.num,
c.zip, c.address, c.province_name AS provinceName, c.phone, c.city_name AS cityName, c.area_name AS areaName
FROM
order_user a
LEFT JOIN
address c ON a.aid = c.aid
LEFT JOIN
order_item b ON a.oid = b.oid
WHERE
a.uid=#{uid} AND a.status=#{status}
ORDER BY a.status ASC, a.created_time DESC
</select>

<resultMap id="getOrderVO" type="com.t1k.store.vo.OrderVO">
<id column="oid" property="oid"/>
<result column="aid" property="aid"/>
<result column="recvName" property="recvName"/>
<result column="totalPrice" property="totalPrice"/>
<result column="status" property="status"/>
<result column="orderTime" property="orderTime"/>
<result column="payTime" property="payTime"/>
<result column="zip" property="zip"/>
<result column="address" property="address"/>
<result column="provinceName" property="provinceName"/>
<result column="phone" property="phone"/>
<result column="cityName" property="cityName"/>
<result column="areaName" property="areaName"/>
<collection property="orderItems" ofType="com.t1k.store.entity.OrderItem" javaType="java.util.List">
<id column="id" property="id"/>
<result column="image" property="image"/>
<result column="title" property="title"/>
<result column="price" property="price"/>
<result column="num" property="num"/>
</collection>
</resultMap>
<select id="getOrderVO" resultMap="getOrderVO" resultType="com.t1k.store.vo.OrderVO">
SELECT a.oid, a.aid, a.recv_name AS recvName, a.total_price AS totalPrice,
a.status, a.order_time AS orderTime, a.pay_time AS payTime,
b.image, b.title, b.price, b.num,
c.zip, c.address, c.province_name AS provinceName, c.phone, c.city_name AS cityName, c.area_name AS areaName
FROM
order_user a
LEFT JOIN
address c ON a.aid = c.aid
LEFT JOIN
order_item b ON a.oid = b.oid
WHERE
a.uid=#{uid} and a.oid=#{oid}
ORDER BY a.status ASC, a.created_time DESC
</select>
</mapper>

后端-业务层

IOrderService
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
32
33
34
35
36
37
38
/** 处理订单数据的业务层接口 */
public interface IOrderService
{
/**
* 获取订单数据列表
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @return 订单数据列表
*/
List<OrderVO> getOrderVOs(Integer uid, String username);

/**
* 根据订单状态获取订单信息
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param status 订单的状态:0-未支付,1-已支付,2-已取消,3-已关闭,4-已完成
* @return 订单信息列表
*/
List<OrderVO> getOrderVOsByS(Integer uid, String username, List<Integer> status);

/**
* 根据订单id获取订单信息
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param oid 订单id
* @return 订单信息
*/
OrderVO getOrderVO(Integer uid, String username, Integer oid);

/**
* 在支付界面获取订单信息
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param oid 订单id
* @return 订单信息
*/
OrderVO getOrderInfo(Integer uid, String username, Integer oid);
}
OrderServiceImpl
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@Service
public class OrderServiceImpl implements IOrderService
{
@Resource
IUserService userService;
@Resource
AddressServiceImpl addressService;
@Resource
CartServiceImpl cartService;
@Resource
OrderMapper orderMapper;
@Resource
OrderItemMapper orderItemMapper;
@Resource
OrderVOMapper orderVOMapper;
@Resource
ProductMapper productMapper;

@Override
public List<OrderVO> getOrderVOs(Integer uid, String username)
{
userService.JudgeUser(uid, username);
List<OrderVO> orderVOS = orderVOMapper.getOrderVOs(uid);
if(ObjectUtils.isEmpty(orderVOS)) throw new ServiceException("订单数据不存在");
return orderVOS;
}

@Override
public List<OrderVO> getOrderVOsByS(Integer uid, String username, List<Integer> status)
{
userService.JudgeUser(uid, username);
MPJLambdaWrapper<Order> wrapper = new MPJLambdaWrapper<>();
wrapper.select(Order::getOid, Order::getAid, Order::getStatus, Order::getRecvName,
Order::getTotalPrice, Order::getOrderTime, Order::getPayTime)
.select(Address::getZip, Address::getAddress, Address::getPhone,
Address::getProvinceName, Address::getCityName, Address::getAreaName)
.leftJoin(Address.class, Address::getAid, Order::getAid)
.eq(Order::getUid, uid)
.in(Order::getStatus, status)
.selectCollection(OrderItem.class, OrderVO::getOrderItems)
.leftJoin(OrderItem.class, OrderItem::getOid, Order::getOid);
List<OrderVO> orderVOS = orderMapper.selectJoinList(OrderVO.class, wrapper);
if(ObjectUtils.isEmpty(orderVOS)) throw new ServiceException("订单数据不存在");
return orderVOS;
}

@Override
public OrderVO getOrderVO(Integer uid, String username, Integer oid)
{
userService.JudgeUser(uid, username);
OrderVO orderVO = orderVOMapper.getOrderVO(uid, oid);
if(ObjectUtils.isEmpty(orderVO)) throw new ServiceException("订单数据不存在");
return orderVO;
}

@Override
public OrderVO getOrderInfo(Integer uid, String username, Integer oid)
{
userService.JudgeUser(uid, username);
Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getUid, uid).eq(Order::getOid, oid));
if(ObjectUtils.isEmpty(order)) throw new OrderNotFoundException("没有该订单");
OrderVO orderVO = getOrderVO(uid, username, oid);
orderVO.setAid(null);
orderVO.setRecvName(null);
orderVO.setPhone(null);
orderVO.setProvinceName(null);
orderVO.setCityName(null);
orderVO.setAreaName(null);
orderVO.setAddress(null);
orderVO.setStatus(null);
orderVO.setOrderTime(null);
orderVO.setPayTime(null);
orderVO.setZip(null);
orderVO.setOrderItems(null);
return orderVO;
}
}

后端-控制层

设计请求

请求路径:/order/get_orderVOs

请求参数:HttpSession session

请求类型:GET

响应类型:JsonResult<List< OrderVO >>

请求路径:/order/get_orderVOsByS

请求参数:HttpSession session

请求类型:POST

响应类型:JsonResult<List< OrderVO >>

请求路径:/order/get_orderInfo

请求参数:HttpSession session,Integer oid

请求类型:POST

响应类型:JsonResult<List< OrderVO >>

OrderController
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@RequestMapping("/order")
@RestController
@Api(tags = "订单相关接口描述")
public class OrderController extends BaseController
{
@Resource
IOrderService service;

@ApiOperation(value = "获取订单列表", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户获取订单列表,返回List<OrderVO>")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 8000, message = "用户未登录"),
@ApiResponse(code = 9000, message = "订单数据不存在")
})
@RequestMapping(value = "get_orderVOs", method = {RequestMethod.GET, RequestMethod.POST})
public JsonResult<List<OrderVO>> getOrderVOs(HttpSession session)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
return new JsonResult<>(OK, service.getOrderVOs(uid, username));
}

@ApiOperation(value = "获取订单", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户获取订单,返回OrderVO对象")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 8000, message = "用户未登录"),
@ApiResponse(code = 9000, message = "参数错误")
})
@ApiImplicitParam(name = "oid", value = "订单ID", required = true)
@PostMapping("get_orderVO")
public JsonResult<OrderVO> getOrderVO(HttpSession session, Integer oid)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
return new JsonResult<>(OK, service.getOrderVO(uid, username, oid));
}

@ApiOperation(value = "通过订单状态获取订单列表", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户通过订单状态获取订单列表,返回List<OrderVO>")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 8000, message = "用户未登录"),
@ApiResponse(code = 9000, message = "订单数据不存在")
})
@ApiImplicitParam(name = "oid", value = "订单ID", required = true)
@PostMapping("get_orderVOsByS")
public JsonResult<List<OrderVO>> geOrderVOsByS(HttpSession session, String status)
{
List<Integer> params = JSON.parseArray(status, Integer.class);
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
return new JsonResult<>(OK, service.getOrderVOsByS(uid, username, params));
}

@ApiOperation(value = "获得订单信息", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于获得订单信息,返回OrderVO对象")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 6006, message = "订单数据不存在"),
@ApiResponse(code = 8000, message = "用户未登录"),
})
@ApiImplicitParam(name = "oid", value = "订单ID", required = true)
@PostMapping(value = "get_orderInfo")
public JsonResult<OrderVO> getOrderInfo(HttpSession session, Integer oid)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
return new JsonResult<>(OK, service.getOrderInfo(uid, username, oid));
}
}

接口测试:略

前端界面

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
<script type="text/javascript">
$(function (){
let url = "/order/get_orderVOs"
let status = $.getUrlParam("status")
if(status !== null && status !== ""){
if(status === "0" || status === "1"){
url = "/order/get_orderVOsByS"
} else{
let res = [2, 3, 4]
status = JSON.stringify(res)
url = "/order/get_orderVOsByS"
}
}
showList(url, status)
})

function showList(url, status){
$("#orderDetail").empty()
showOrderVOs(url, status)
}

function setStatus(status){
let str = ""
switch (status){
case 0: str = "未付款"; break
case 1: str = "已付款"; break
case 2: str = "已取消"; break
case 3: str = "已关闭"; break
case 4: str = "已完成"; break
default: confirm("订单状态错误")
}
return str
}

function setOption(status){
let str = ""
if(status === "未付款"){
str = "去付款"
} else if(status === "已付款"){
str = "确定收货"
}
return str
}
function delOrder(oid){
if(confirm("确定要删除该订单吗?")){
$.ajax({
url: "/order/del_order",
type: "post",
data: {
"oid": oid
},
dataType: "json",
success: function (json){
if(json.state === 200){
let url = "/order/get_orderVOs"
showList(url)
} else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}
}

function operation(oid){
let opId = "op" + oid.toString()
let str = $("#"+opId).html()
if(str === "去付款"){
sessionStorage.setItem("oid", oid)
location.href="payment.html"
} else if(str === "确定收货"){
if(confirm("确认收到货了吗?")){
receipt(oid)
}
}
}

// 确认收货
function receipt(oid){
$.ajax({
url: "/order/receipt",
type: "post",
data: {
"oid": oid,
"status": 4
},
dataType: "json",
success: function (json){
if(json.state === 200){
let url = "/order/get_orderVOs"
showList(url)
} else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}

function elementHandle(orderVO, tbId, apId, opId){
let oid = orderVO.oid
let element =
'<div class="panel panel-default">' +
'<div class="panel-heading">' +
'<p class="panel-title">' +
'订单号:<label id="#{oid}" style="color: red">11010003#{oid}</label>' +
',下单时间<label style="color: #4288c3">#{orderTime}</label>' +
',收货人<label style="color: #4288c3">#{name}</label>' +
'<a class="pull-right" style="cursor: pointer; padding: 0 15px; color: #72ACE3" onclick="delOrder(#{oid})">删除订单</a>'+
'</p>' +
'</div>' +
'<div class="panel-body">' +
'<table class="orders-table" width="100%">' +
'<thead>' +
'<tr>' +
'<th width="15%"></th>' +
'<th width="30%">商品</th>' +
'<th width="8%">单价</th>' +
'<th width="8%">数量</th>' +
'<th width="9%">小计</th>' +
'<th width="10%">状态</th>' +
'</tr>' +
'</thead>' +
'<tbody class="orders-body" id=#{tbId}>' +
'</tbody>' +
'</table>' +
'<div>' +
'<a class="pull-right" style="cursor: pointer; color: #72ACE3" id=#{opId} onclick="operation(#{oid})"></a>' +
'<a class="pull-right" style="cursor: pointer; color: #72ACE3; padding: 0 15px;" href="orderInfo.html?oid=#{oid}">订单详情</a>' +
'<span class="pull-right" style="padding: 0 15px;">订单总金额:¥<label style="color: red">#{totalPrice}</label>元</span>' +
'</div>' +
'</div>' +
'</div>'

element = element.replace(/#{oid}/g, oid)
let orderTime = getDate(orderVO.orderTime)
element = element.replace(/#{orderTime}/g, orderTime)
element = element.replace(/#{name}/g, orderVO.recvName)
element = element.replace(/#{tbId}/g, tbId)
element = element.replace(/#{allPriceId}/g, apId)
element = element.replace(/#{opId}/g, opId)
element = element.replace(/#{totalPrice}/g, orderVO.totalPrice)
$("#orderDetail").append(element)
}

function showOrderVOs(url, status){
$.ajax({
url: url,
type: "post",
data: {
"status": status
},
dataType: "json",
success: function (json){
if(json.state === 200){
let list = json.data
if(list.length > 0){
for (let i = 0; i < list.length; i++){
// 先处理该用户第一条订单的地址等信息
let orderVO = list[i]
let oid = orderVO.oid
let tbId = "tb" + oid.toString()
let apId = "ap" + oid.toString()
let opId = "op" + oid.toString()
elementHandle(orderVO, tbId, apId, opId)
let odItList = list[i].orderItems
if(odItList.length > 0){
for (let j = 0; j < odItList.length; j++){
let tr = '<tr>' +
'<td><img src="..#{image}collect.png" class="img-responsive" /></td>' +
'<td>#{title}</td>' +
'<td>¥<span>#{price}</span></td>' +
'<td>#{num}件</td>' +
'<td>¥<span>#{subPrice}</span></td>' +
'<td>' +
'<div>#{status}</div>' +
'</td>' +
'</tr>'
tr = tr.replace(/#{image}/g, odItList[j].image)
tr = tr.replace(/#{title}/g, odItList[j].title)
tr = tr.replace(/#{price}/g, odItList[j].price)
tr = tr.replace(/#{num}/g, odItList[j].num)
let subPrice = parseFloat((odItList[j].price * odItList[j].num).toString())
tr = tr.replace(/#{subPrice}/g, subPrice)
let status = setStatus(orderVO.status)
let option = setOption(status)
tr = tr.replace(/#{status}/g, status)
$("#"+tbId).append(tr)
$("#"+opId).html(option)
}
}
}
}
} else{
confirm(json.state + ": " + json.message)
}
},
error: function (xhr){
error(xhr)
}
})
}
</script>

创建订单

后端-持久层

与前面一致

后端-业务层

IOrderService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 处理订单数据的业务层接口 */
public interface IOrderService
{
/**
* 创建订单
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param aid 收货地址id
* @param cids 购物车数据id
* @return 订单id
*/
Integer createOrderByC(Integer uid, String username, Integer aid, List<Integer> cids);

/**
* 创建订单
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param aid 收货地址id
* @param pid 商品id
* @param num 商品数量
* @return 订单id
*/
Integer createOrderByP(Integer uid, String username, Integer aid, Integer pid, Integer num);
}
OrderServiceImpl
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
@Service
public class OrderServiceImpl implements IOrderService
{
@Resource
IUserService userService;
@Resource
AddressServiceImpl addressService;
@Resource
CartServiceImpl cartService;
@Resource
OrderMapper orderMapper;
@Resource
OrderItemMapper orderItemMapper;
@Resource
OrderVOMapper orderVOMapper;
@Resource
ProductMapper productMapper;

@Transactional
@Override
public Integer createOrderByC(Integer uid, String username, Integer aid, List<Integer> cids)
{
// 根据uid和aid查地址创建order,根据uid和cids查购物车数据
// 先插入order,再插入orderItem
// 先查出VO
List<CartVO> carts = cartService.getCartVOsByCids(uid, username, cids);
Date date = new Date();
// 计算商品总价
Long totalPrice = carts.stream().mapToLong(cart -> (cart.getPrice() * cart.getNum())).sum();

Address address = addressService.getAddress(aid, uid, username);
Order order = new Order();
order.setUid(uid);
order.setAid(aid);
order.setRecvName(address.getName());
order.setRecvPhone(address.getPhone());
order.setRecvProvince(address.getProvinceName());
order.setRecvCity(address.getCityName());
order.setRecvArea(address.getAreaName());
order.setRecvAddress(address.getAddress());
order.setTotalPrice(totalPrice);
order.setStatus(0);
order.setOrderTime(date);
// 补全数据:日志
order.setCreatedUser(username);
order.setCreatedTime(date);
order.setModifiedUser(username);
order.setModifiedTime(date);
int row = orderMapper.insert(order);
if(row != 1) throw new InsertException("插入数据时发生异常");

carts.forEach(cart -> {
// 创建订单商品数据
OrderItem item = new OrderItem();
// 补全数据:setOid(order.getOid())
item.setOid(order.getOid());
// 补全数据:pid, title, image, price, num
item.setPid(cart.getPid());
item.setTitle(cart.getTitle());
item.setImage(cart.getImage());
item.setPrice(cart.getRealPrice());
item.setNum(cart.getNum());
// 补全数据:4项日志
item.setCreatedUser(username);
item.setCreatedTime(date);
item.setModifiedUser(username);
item.setModifiedTime(date);
// 插入订单商品数据
int rows2 = orderItemMapper.insert(item);
if (rows2 != 1) throw new InsertException("插入数据时发生异常");
});
return order.getOid();
}

@Override
public Integer createOrderByP(Integer uid, String username, Integer aid, Integer pid, Integer num)
{
Date date = new Date();
Address address = addressService.getAddress(aid, uid, username);
Product product = productMapper.selectById(pid);
Long totalPrice = product.getPrice() * num;
Order order = new Order();
order.setUid(uid);
order.setAid(aid);
order.setRecvName(address.getName());
order.setRecvPhone(address.getPhone());
order.setRecvProvince(address.getProvinceName());
order.setRecvCity(address.getCityName());
order.setRecvArea(address.getAreaName());
order.setRecvAddress(address.getAddress());
order.setTotalPrice(totalPrice);
order.setStatus(0);
order.setOrderTime(date);
// 补全数据:日志
order.setCreatedUser(username);
order.setCreatedTime(date);
order.setModifiedUser(username);
order.setModifiedTime(date);

int row = orderMapper.insert(order);
if(row != 1) throw new InsertException("插入数据时发生异常");

// 创建订单商品数据
OrderItem item = new OrderItem();
// 补全数据:
item.setOid(order.getOid());
// 补全数据:pid, title, image, price, num
item.setPid(pid);
item.setTitle(product.getTitle());
item.setImage(product.getImage());
item.setPrice(product.getPrice());
item.setNum(num);
// 补全数据:4项日志
item.setCreatedUser(username);
item.setCreatedTime(date);
item.setModifiedUser(username);
item.setModifiedTime(date);
// 插入订单商品数据
int rows2 = orderItemMapper.insert(item);
if (rows2 != 1) throw new InsertException("插入数据时发生异常");

return order.getOid();
}
}

单元测试:略

后端-控制层

设计请求

请求路径:/order/create_orderByC

请求参数:HttpSession session,Integer aid,String cids HttpSession session,Integer aid,Integer pid,Integer num

请求类型:POST

响应类型:JsonResult< Integer >

请求路径:/order/create_orderByP

请求参数:HttpSession session,Integer aid,Integer pid,Integer num

请求类型:POST

响应类型:JsonResult< Integer >

OrderController
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@RequestMapping("/order")
@RestController
@Api(tags = "订单相关接口描述")
public class OrderController extends BaseController
{
@Resource
IOrderService service;

@ApiOperation(value = "根据购物车数据创建订单", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户创建订单,返回Oid")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4000, message = "数据插入失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 6004, message = "商品不存在"),
@ApiResponse(code = 6005, message = "购物车数据不存在"),
@ApiResponse(code = 8000, message = "用户未登录"),
@ApiResponse(code = 9000, message = "参数错误")
})
@ApiImplicitParams({
@ApiImplicitParam(name = "aid", value = "收货地址ID", required = true),
@ApiImplicitParam(name = "cids", value = "购物车ID", required = true)
})
@PostMapping("create_orderByC")
public JsonResult<Integer> createOrderByC(HttpSession session, Integer aid, String cids)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
Integer oid = service.createOrderByC(uid, username, aid, JSON.parseArray(cids, Integer.class));
return new JsonResult<>(OK, oid);
}

@ApiOperation(value = "根据商品数据创建订单", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户创建订单,返回Oid")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4000, message = "数据插入失败"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 6004, message = "商品不存在"),
@ApiResponse(code = 8000, message = "用户未登录"),
@ApiResponse(code = 9000, message = "参数错误")
})
@ApiImplicitParams({
@ApiImplicitParam(name = "aid", value = "收货地址ID", required = true),
@ApiImplicitParam(name = "pid", value = "商品ID", required = true),
@ApiImplicitParam(name = "num", value = "商品数量", required = true)
})
@PostMapping("create_orderByP")
public JsonResult<Integer> createOrderByP(HttpSession session, Integer aid, Integer pid, Integer num){
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
Integer oid = service.createOrderByP(uid, username, aid, pid, num);
return new JsonResult<>(OK, oid);
}
}

接口测试:略

前端界面

去付款

与商品管理中立即购买基本一致

删除订单

后端-持久层

与前面一致

后端-业务层

IOrderService
1
2
3
4
5
6
7
8
9
10
11
/** 处理订单数据的业务层接口 */
public interface IOrderService
{
/**
* 删除订单
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param oid 订单id
*/
void deleteOrder(Integer uid, String username, Integer oid);
}
OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class OrderServiceImpl implements IOrderService
{
@Resource
IUserService userService;
@Resource
OrderMapper orderMapper;
@Resource
OrderItemMapper orderItemMapper;

@Override
public void deleteOrder(Integer uid, String username, Integer oid)
{
userService.JudgeUser(uid, username);
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getOid, oid);
orderMapper.delete(wrapper);
LambdaQueryWrapper<OrderItem> wrapper1 = new LambdaQueryWrapper<>();
wrapper1.eq(OrderItem::getOid, oid);
orderItemMapper.delete(wrapper1);
}
}

单元测试:略

后端-控制层

设计请求

请求路径:/order/del_order

请求参数:HttpSession session,Integer oid

请求类型:POST

响应类型:JsonResult< Void >

OrderController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping("/order")
@RestController
@Api(tags = "订单相关接口描述")
public class OrderController extends BaseController
{
@ApiOperation(value = "删除订单", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户删除订单")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 5001, message = "用户不存在"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParam(name = "oid", value = "订单ID", required = true)
@PostMapping("del_order")
public JsonResult<Void> delOrder(HttpSession session, Integer oid)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
service.deleteOrder(uid, username, oid);
return new JsonResult<>(OK);
}
}

接口测试:略

前端界面

收藏管理

创建数据表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE `collect`  (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT '主键',
`uid` int(10) NOT NULL COMMENT '用户id',
`pid` int(10) NOT NULL COMMENT '商品id',
`image` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品图片',
`title` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品标题',
`price` bigint(10) NULL DEFAULT NULL COMMENT '商品单价',
`status` int(1) NULL DEFAULT NULL COMMENT '商品状态:1收藏中,0取消收藏',
`created_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`modified_time` datetime NULL DEFAULT NULL COMMENT '最后修改时间',
`created_user` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
`modified_user` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后修改人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 28 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

创建实体类

Collect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "收藏商品实体类:Collect", description = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;对应数据库中的collect表")
public class Collect extends BaseEntity
{
@TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty(value = "主键ID")
private Integer id;
@ApiModelProperty(value = "用户ID")
private Integer uid;
@ApiModelProperty(value = "商品ID")
private Integer pid;
@ApiModelProperty(value = "商品图片")
private String image;
@ApiModelProperty(value = "商品标题")
private String title;
@ApiModelProperty(value = "商品单价")
private Long price;
@ApiModelProperty(value = "商品状态:1-收藏中,0-未收藏")
private Integer status;
}

显示收藏

后端-持久层

CollectMapper
1
2
/** 处理收藏商品数据操作的持久层接口 */
public interface CollectMapper extends BaseMapper<Collect> {}

后端-业务层

ICollectService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 处理收藏商品数据的业务层接口 */
public interface ICollectService
{
/**
* 获取收藏商品列表
* @param page 当前分页的信息,包含页码和每页数据条数
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @return 收藏商品列表
*/
List<Collect> getCollects(IPage<Collect> page, Integer uid, String username);

/**
* 获取在收藏中的收藏商品数量
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @return 在收藏中的收藏商品数量
*/
Integer getCount(Integer uid, String username);
}
CollectServiceImpl
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
32
33
@Service
public class CollectServiceImpl implements ICollectService
{
@Resource
IUserService userService;
@Resource
CollectMapper mapper;
@Resource
IProductService productService;

@Override
public List<Collect>getCollects(IPage<Collect> iPage, Integer uid, String username)
{
userService.JudgeUser(uid, username);
LambdaQueryWrapper<Collect> wrapper = new LambdaQueryWrapper<>();
wrapper.select(Collect::getImage, Collect::getTitle, Collect::getPid, Collect::getPrice)
.eq(Collect::getUid, uid)
.ne(Collect::getStatus, 0);
List<Collect> records = mapper.selectPage(iPage, wrapper).getRecords();
if(Objects.isNull(records)) throw new ProductNotFoundException("没有商品信息");
return records;
}

@Override
public Integer getCount(Integer uid, String username)
{
userService.JudgeUser(uid, username);
LambdaQueryWrapper<Collect> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Collect::getUid, uid)
.eq(Collect::getStatus, 1);
return mapper.selectCount(wrapper);
}
}

单元测试:略

后端-控制层

设计请求

请求路径:/product/get_collects

请求参数:HttpSession session,Long page,Long limit

请求类型:POST

响应类型:JsonResult<List< Collect >>

ProductController
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
@RestController
@RequestMapping("/product")
@Api(tags = "商品相关接口描述")
public class ProductController extends BaseController
{
@ApiOperation(value = "获取收藏商品", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户获取收藏商品,返回List<Collect>")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 6004, message = "商品不存在"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParams({
@ApiImplicitParam(name = "page", value = "页码", required = true),
@ApiImplicitParam(name = "limit", value = "每页数据条数", required = true),
})
@PostMapping("get_collects")
public JsonResult<List<Collect>> getCollects(HttpSession session, Long page, Long limit)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
Page<Collect> iPage = new Page<>(page, limit);
List<Collect> collects = collectService.getCollects(iPage, uid, username);
return new JsonResult<>(OK, collects);
}
}

接口测试:略

前端界面

加入/取消收藏

后端-持久层

与前面一致

后端-业务层

ICollectService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** 处理收藏商品数据的业务层接口 */
public interface ICollectService
{
/**
* 添加收藏商品
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param pid 商品id
*/
void addCollect(Integer uid, String username, Integer pid);

/**
* 设置收藏商品状态
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param pid 商品id
* @param status 收藏商品状态:1-收藏中,0-未收藏
*/
void setStatus(Integer uid, String username, Integer pid, Integer status);
}
CollectServiceImpl
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Service
public class CollectServiceImpl implements ICollectService
{
@Resource
IUserService userService;
@Resource
CollectMapper mapper;
@Resource
IProductService productService;

@Override
public void addCollect(Integer uid, String username, Integer pid)
{
LambdaQueryWrapper<Collect> wrapper = new LambdaQueryWrapper<>();
wrapper.select(Collect::getId)
.eq(Collect::getPid, pid);
Collect data = mapper.selectOne(wrapper);
if(!Objects.isNull(data)) {
data.setStatus(1);
int row = mapper.updateById(data);
if(row != 1) throw new UpdateException("更新数据时发生异常");
}
else
{
Product produce = productService.getProductById(pid);
Collect collect = new Collect();
collect.setUid(uid);
collect.setPid(pid);
collect.setImage(produce.getImage());
collect.setTitle(produce.getTitle());
collect.setPrice(produce.getPrice());
collect.setStatus(1);
Date date = new Date();
collect.setCreatedUser(username);
collect.setModifiedUser(username);
collect.setModifiedTime(date);
collect.setCreatedTime(date);
int row = mapper.insert(collect);
if(row != 1) throw new InsertException("插入数据时发生异常");
}
}

@Override
public void setStatus(Integer uid, String username, Integer pid, Integer status)
{
userService.JudgeUser(uid, username);
Product product = productService.getProductById(pid);
if(Objects.isNull(product)) throw new ProductNotFoundException("没有商品信息");
LambdaUpdateWrapper<Collect> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(Collect::getStatus, status)
.eq(Collect::getPid, pid)
.eq(Collect::getUid, uid);
int row = mapper.update(null, wrapper);
if(row != 1) throw new UpdateException("更新数据时发生异常");
}
}

单元测试:略

后端-控制层

设计请求

请求路径:/product/add_collect

请求参数:HttpSession session,Integer pid

请求类型:POST

响应类型:JsonResult< Void >

请求路径:/product/cancel_collect

请求参数:HttpSession session,Integer pid,Integer status

请求类型:POST

**响应类型:JsonResult< Void > **

ProductController
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@RestController
@RequestMapping("/product")
@Api(tags = "商品相关接口描述")
public class ProductController extends BaseController
{
@Resource
IProductService service;
@Resource
ICollectService collectService;

@ApiOperation(value = "添加收藏", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户收藏商品")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4000, message = "数据插入失败"),
@ApiResponse(code = 4001, message = "数据更新失败"),
@ApiResponse(code = 6004, message = "商品不存在"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParam(name = "pid", value = "商品ID", required = true)
@PostMapping("add_collect")
public JsonResult<Void> addCollect(HttpSession session, Integer pid)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
collectService.addCollect(uid, username, pid);
return new JsonResult<>(OK);
}

@ApiOperation(value = "取消收藏", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户取消收藏")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 4001, message = "数据更新失败"),
@ApiResponse(code = 6004, message = "商品不存在"),
@ApiResponse(code = 8000, message = "用户未登录")
})
@ApiImplicitParams({
@ApiImplicitParam(name = "pid", value = "商品ID", required = true),
@ApiImplicitParam(name = "status", value = "商品状态", required = true),
})
@PostMapping("cancel_collect")
public JsonResult<Void> cancelCollect(HttpSession session, Integer pid, Integer status)
{
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
collectService.setStatus(uid, username, pid, status);
return new JsonResult<>(OK);
}
}

接口测试:略

前端界面

商品搜索

后端-持久层

与商品管理和收藏管理一致

后端-业务层

IProductService
1
2
3
4
5
6
7
8
9
10
11
12
/** 处理商品数据的业务层接口 */
public interface IProductService
{
/**
* 搜索商品
* @param uid 当前登录的用户id
* @param username 当前登录的用户名
* @param keyWord 关键字
* @return 符合关键字的商品列表
*/
List<Collect> searchProducts(Integer uid, String username, String keyWord);
}
ProductServiceImpl
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
32
33
34
35
36
37
38
39
40
41
42
43
44
@Service
public class ProductServiceImpl implements IProductService
{
@Resource
ProductMapper mapper;
@Resource
ICollectService collectService;

@Override
public List<Collect> searchProducts(Integer uid, String username, String keyWord)
{
List<Collect> collects = new ArrayList<>();
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
wrapper.like(Product::getTitle, keyWord);
List<Product> products = mapper.selectList(wrapper);
if(uid == -1 && username.equals("-1"))
{
products.forEach(product -> {
Collect collect = new Collect(){{
setPid(product.getId());
setPrice(product.getPrice());
setImage(product.getImage());
setTitle(product.getTitle());
}};
collects.add(collect);
});
}
else
{
products.forEach(product -> {
Integer status = collectService.getStatus(uid, username, product.getId());
Collect collect = new Collect(){{
setPid(product.getId());
setPrice(product.getPrice());
setImage(product.getImage());
setTitle(product.getTitle());
setStatus(status);
}};
collects.add(collect);
});
}
return collects;
}
}

单元测试:略

后端-控制层

设计请求

请求路径:/product/search_product

请求参数:HttpSession session,String keyWord

请求类型:POST

响应类型:JsonResult<List< Collect >>

ProductController
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
@RestController
@RequestMapping("/product")
@Api(tags = "商品相关接口描述")
public class ProductController extends BaseController
{
@Resource
IProductService service;
@Resource
ICollectService collectService;

@ApiOperation(value = "获取搜索商品", notes = "<span style='color:red;'>描述:</span>&nbsp;&nbsp;用于用户获取搜索商品,返回List<Collect>")
@ApiResponses({
@ApiResponse(code = 200, message = "请求成功"),
})
@ApiImplicitParam(name = "keyWord", value = "关键词", required = true)
@PostMapping("search_product")
public JsonResult<List<Collect>> searchProduce(HttpSession session, String keyWord)
{
Integer uid = -1;
String username = "-1";
if(!ObjectUtils.isEmpty(session)){
uid = getUidFromSession(session);
username = getUsernameFromSession(session);
}
List<Collect> collects = service.searchProducts(uid, username, keyWord);
return new JsonResult<>(OK, collects);
}
}

接口测试:略

前端界面

抽离公共页面

前端界面

将每个页面共有的部分抽取出来用js加载,如:

1
2
3
4
5
6
7
8
9
10
11
12
$(function (){
$(".header").load("template/head.html")
$(".footer").load("template/footer.html")
$(".middleBar").load("template/middleBar.html")
})

<!--头部-->
<header class="header"></header>
<!--导航 -->
<!--分割导航和顶部-->
<div class="middleBar"></div>
<footer class="footer"></footer>