计算机毕业设计|基于SpringBoot+MyBatis框架的电脑商城的设计与实现(用户上传头像+用户收货管理)

news2024/11/24 18:36:08

计算机毕业设计|基于SpringBoot+MyBatis框架的电脑商城的设计与实现(用户上传头像)

该项目分析着重于设计和实现基于SpringBoot+MyBatis框架的电脑商城。首先,通过深入分析项目所需数据,包括用户、商品、商品类别、收藏、订单、购物车、收货地址,建立了数据模型。关于SpringBoot+MyBatis框架的电脑商城的设计与实现,我会按照系统概述与环境搭建、用户注册登录、用户资料修改、用户上传头像、-用户收货管理、商品、购物车、订单、AOP的顺序依次更新。本文内容承接前面博客内容,主要是项目用户上传头像+用户收货管理。

1 MultipartFile接口

MultipartFile接口常用的的API见下表:

方法功能描述
String getOriginalFilename()获取上传文件的原始文件名,即该文件在客户端中的文件名
boolean isEmpty()判断上传的文件是否为空,当没有选择文件就直接上传,或者选中的文件是0字节的空文件时,返回true,否则返回false
long getSize()获取上传的文件大小,以字节为单位
String getContentType()根据所上传的文件的扩展名决定该文件的MIME类型,例如上传.jpg格式的图片,将返回image/jpeg
InputStream getInputStream()获取上传文件的输入字节流,通常用于自定义读取所上传的文件的过程,该方法与transferTo()方法不可以同时使用
void transferTo(File dest)保存上传的文件,该方法与getInputStream()方法不可以同时使用

2 MultipartResolver接口

1.MultipartResolver可以将上传过程中产生的数据封装为MultipartFile类型的对象中。

2.在配置MultipartResovler时,可以为其中的几个属性注入值:

  • maxUploadSize:上传文件的最大大小,假设设置值为10M,一次性上传5个文件,则5个文件的大小总和不允许超过10M。
  • maxUploadSizePerFile:每个上传文件的最大大小,假设设置值为10M,一次性上传5个文件,则每个文件的大小都不可以超过10M,但是5个文件的大小总和可以接近50M。
  • defaultEncoding:默认编码。

3 基于SpringMVC的文件上传案例

3.1 创建项目

1.创建Java Enterprise项目,设置Name为springmvc-upload,Group为com.cy,Artifact为controller的Java企业级项目。

2.将项目com.cy.controller包下自动生成的HelloServlet类删除,并删除webapp下自动生成的index.jsp文件。

3.添加文件上传jar包依赖(关于文件上传需要添加spring-webmvc和commons-fileupload依赖)。

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>4.3.6.RELEASE</version>
    </dependency>
    <!-- 文件上传 -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.4</version>
    </dependency>
</dependencies>

4.在src\main\resources文件夹下创建spring配置文件,并将文件命名为spring-upload.xml。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans
	   http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
	   http://www.springframework.org/schema/context
	   http://www.springframework.org/schema/context/spring-context-4.3.xsd">
	
</beans>
3.2 前端页面设计

在webapp目录下创建upload.html页面,并在页面中添加如下代码。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>文件上传</title>
</head>
<body>
    <h3>文件上传</h3>
    <!-- enctype属性:规定表单中数据在提交给服务器之前如何进行编码。默认表单数据的编码是"application/x-www-form-urlencoded"。
         application/x-www-form-urlencoded:提交前表单中所有数据都会进行编码;编码的规则是:空格转换为"+"加号,特殊符号转换为ASCII HEX值
         text/plain:提交前表单中数据空格转换为"+"加号,但不对特殊字符进行编码。
         multipart/form-data:提交前表单中不对字符进行编码;在使用包含文件上传控件的表单中,必须使用该值
    -->
    <form action="upload.do" method="post" enctype="multipart/form-data">
        <table border="1" cellspacing="0" cellpadding="0">
            <tr>
                <td>文件名(N):</td>
                <td><p><input type="file" name="file"/></p></td>
            </tr>
            <tr>
                <td colspan="2" align="center"><input type="submit" name="上传"/></td>
            </tr>
        </table>
    </form>
</body>
</html>

注意:form表单的请求方式必须设置为POST,并配置属性enctype=“multipart/form-data”,文件上传input控件的name属性值需设置为file值。

3.3 后台功能实现

1.在web.xml文件中配置前端控制器和过滤器,并指定DispatcherServlet加载的配置文件springmvc-upload.xml的位置。

<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:springmvc-upload.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>utf-8</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

2.创建com.cy.controller.UploadController控制器类,在类的声明之前添加@Controller注解,并在控制器中添加处理请求的upload()方法,为此方法添加类型为MultipartFile接口的参数,并为该参数添加@RequestParam注解,表示客户端上传的文件。

package com.cy.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.UUID;

@Controller
public class UploadController {
    /*
    @RequestMapping("upload.do")
    @ResponseBody
    public String upload(@RequestParam("file") MultipartFile file) {
        System.out.println("UploadController.upload()...");
        File dest = new File("D:/1.png");
        try {
            // 调用MultipartFile参数对象的transferTo()方法即可保存上传的文件
            file.transferTo(dest);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "OK";
    }
    */
    
    @RequestMapping("upload.do")
    @ResponseBody
    public String upload(HttpServletRequest request, @RequestParam("file") MultipartFile file) throws IOException {
        // 获取上传文件的原始文件名
        String originalFilename = file.getOriginalFilename();
        // 获取上下文的绝对路径
        String realPath = request.getServletContext().getRealPath("upload");
        System.out.println(realPath);
        // 创建File文件对象
        File dir = new File(realPath);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        // 自定义上传文件名
        String fileName = UUID.randomUUID().toString();
        // 获取上传文件扩展名
        String suffix = "";
        int beginIndex = originalFilename.lastIndexOf(".");
        if (beginIndex > 0) {
            suffix = originalFilename.substring(beginIndex);
        }
        String fullFilename = fileName + suffix;
        // 调用MultipartFile参数对象的transferTo()方法即可保存上传的文件
        file.transferTo(new File(dir, fullFilename));

        return "OK";
    }
}

3.在springmvc-upload.xml配置文件中添加组件扫描和CommonsMultipartResolver类的bean标签配置。

<!-- 组件扫描 -->
<context:component-scan base-package="com.cy" />

<!-- CommonsMultipartResolver -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>

注意:CommonsMultipartResolver类在配置时,id值必须设置成multipartResolver。

4.启动项目,访问http://localhost:8080/springmvc_upload_war_exploded/upload.html网址测试文件提交。

上传头像

1 用户-上传头像-持久层

1.1 规划需要执行的SQL语句

上传文件的操作其实是:先将用户上传的文件保存到服务器端的某个位置,然后将保存文件的路径记录在数据库中。当后续需要使用该文件时,从数据库中读出文件的路径,即可实现在线访问该文件。

在持久层处理数据库中的数据时,只需要关心如何记录头像文件的路径,并不需要考虑上传时保存文件的过程。所以,需要执行的SQL语句大致是:

update t_user set avatar=?, modified_user=?, modified_time=? where uid=?
1.2 接口与抽象方法

在UserMapper接口中添加updateAvatarByUid()抽象方法。

/**
 * 根据uid更新用户的头像
 * @param uid 用户的id
 * @param avatar 新头像的路径
 * @param modifiedUser 修改执行人
 * @param modifiedTime 修改时间
 * @return 受影响的行数
 */
Integer updateAvatarByUid(
		@Param("uid") Integer uid,
		@Param("avatar") String avatar,
		@Param("modifiedUser") String modifiedUser,
		@Param("modifiedTime") Date modifiedTime);
1.3 配置SQL映射

1.在UserMapper.xml中配置updateAvatarByUid()抽象方法的映射。

<!-- 根据uid更新用户的头像
	 Integer updateAvatarByUid(
		@Param("uid") Integer uid,
		@Param("avatar") String avatar,
		@Param("modifiedUser") String modifiedUser,
		@Param("modifiedTime") Date modifiedTime) -->
<update id="updateAvatarByUid">
	UPDATE
		t_user
	SET
		avatar = #{avatar},
		modified_user = #{modifiedUser},
		modified_time = #{modifiedTime}
	WHERE
		uid = #{uid}
</update>

2.在UserMapperTests中编写并执行单元测试。

@Test
public void updateAvatarByUid() {
    Integer uid = 20;
    String avatar = "/upload/avatar.png";
    String modifiedUser = "超级管理员";
    Date modifiedTime = new Date();
    Integer rows = userMapper.updateAvatarByUid(uid, avatar, modifiedUser, modifiedTime);
    System.out.println("rows=" + rows);
}

2 用户-上传头像-业务层

2.1 规划异常

在修改头像值前先检查用户数据状态,可能抛UserNotFoundException异常;由于最终执行的是修改操作还可能抛UpdateException异常。

2.2 接口与抽象方法

在IUserService中添加changeAvatar(Integer uid, String username, String avatar)抽象方法。

/**
 * 修改用户头像
 * @param uid 当前登录的用户的id
 * @param username 当前登录的用户名
 * @param avatar 用户的新头像的路径
 */
void changeAvatar(Integer uid, String username, String avatar);
2.3 实现抽象方法

1.在UserServiceImpl类中实现changeAvatar(Integer uid, String username, String avatar)方法。

@Override
public void changeAvatar(Integer uid, String username, String avatar) {
    // 调用userMapper的findByUid()方法,根据参数uid查询用户数据
    // 检查查询结果是否为null
    // 是:抛出UserNotFoundException

    // 检查查询结果中的isDelete是否为1
    // 是:抛出UserNotFoundException

    // 创建当前时间对象
    // 调用userMapper的updateAvatarByUid()方法执行更新,并获取返回值
    // 判断以上返回的受影响行数是否不为1
    // 是:抛了UpdateException
}

2.changeAvatar(Integer uid, String username, String avatar)方法中代码的具体实现为。

@Override
public void changeAvatar(Integer uid, String username, String avatar) {
	// 调用userMapper的findByUid()方法,根据参数uid查询用户数据
	User result = userMapper.findByUid(uid);
	// 检查查询结果是否为null
	if (result == null) {
		// 是:抛出UserNotFoundException
		throw new UserNotFoundException("用户数据不存在");
	}
	
	// 检查查询结果中的isDelete是否为1
	if (result.getIsDelete().equals(1)) {
		// 是:抛出UserNotFoundException
		throw new UserNotFoundException("用户数据不存在");
	}
	
	// 创建当前时间对象
	Date now = new Date();
	// 调用userMapper的updateAvatarByUid()方法执行更新,并获取返回值
	Integer rows = userMapper.updateAvatarByUid(uid, avatar, username, now);
	// 判断以上返回的受影响行数是否不为1
	if (rows != 1) {
		// 是:抛出UpdateException
		throw new UpdateException("更新用户数据时出现未知错误,请联系系统管理员");
	}
}

3.在UserServiceTests类中进行单元测试。

@Test
public void changeAvatar() {
    try {
        Integer uid = 20;
        String username = "头像管理员";
        String avatar = "/upload/avatar.png";
        userService.changeAvatar(uid, username, avatar);
        System.out.println("OK.");
    } catch (ServiceException e) {
        System.out.println(e.getClass().getSimpleName());
        System.out.println(e.getMessage());
    }
}

3 用户-上传头像-控制器

3.1 处理异常

1.在处理上传文件的过程中,用户可能会选择错误的文件上传,此时就应该抛出对应的异常并进行处理。所以需要创建文件上传相关异常的基类,即在com.cy.store.controller.ex包下创建FileUploadException类,并继承自RuntimeException类。

package com.cy.store.service.ex;

/** 文件上传相关异常的基类 */
public class FileUploadException extends RuntimeException {
    public FileUploadException() {
        super();
    }

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

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

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

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

2.在处理上传的文件过程中,经分析可能会产生以下异常。这些异常类都需要继承自FileUploadException类。

// 上传的文件为空
cn.tedu.store.controller.ex.FileEmptyException
// 上传的文件大小超出了限制值
cn.tedu.store.controller.ex.FileSizeException
// 上传的文件类型超出了限制
cn.tedu.store.controller.ex.FileTypeException
// 上传的文件状态异常
cn.tedu.store.controller.ex.FileStateException
// 上传文件时读写异常
cn.tedu.store.controller.ex.FileUploadIOException

3.创建FileEmptyException异常类,并继承FileUploadException类。

package com.cy.store.service.ex;

/** 上传的文件为空的异常,例如没有选择上传的文件就提交了表单,或选择的文件是0字节的空文件 */
public class FileEmptyException extends FileUploadException {
    // Override Methods...
}

4.创建FileSizeException异常类,并继承FileUploadException类。

package com.cy.store.service.ex;

/** 上传的文件的大小超出了限制值 */
public class FileSizeException extends FileUploadException {
    // Override Methods...
}

5.创建FileTypeException异常类,并继承FileUploadException类。

package com.cy.store.service.ex;

/** 上传的文件类型超出了限制 */
public class FileTypeException extends FileUploadException {
    // Override Methods...
}

6.创建FileStateException异常类,并继承FileUploadException类。

package com.cy.store.service.ex;

/** 上传的文件状态异常 */
public class FileStateException extends FileUploadException {
    // Override Methods...
}

7.创建FileUploadIOException异常类,并继承FileUploadException类。

package com.cy.store.service.ex;

/** 上传文件时读写异常 */
public class FileUploadIOException extends FileUploadException {
    // Override Methods...
}

8.然后在BaseController的handleException()的@ExceptionHandler注解中添加FileUploadException.class异常的处理;最后在方法中处理这些异常。

@ExceptionHandler({ServiceException.class, FileUploadException.class})
public JsonResult<Void> handleException(Throwable e) {
	JsonResult<Void> result = new JsonResult<Void>(e);
	if (e instanceof UsernameDuplicateException) {
		result.setState(4000);
	} else if (e instanceof UserNotFoundException) {
		result.setState(4001);
	} else if (e instanceof PasswordNotMatchException) {
		result.setState(4002);
	} else if (e instanceof InsertException) {
		result.setState(5000);
	} else if (e instanceof UpdateException) {
		result.setState(5001);
	} else if (e instanceof FileEmptyException) {
		result.setState(6000);
	} else if (e instanceof FileSizeException) {
		result.setState(6001);
	} else if (e instanceof FileTypeException) {
		result.setState(6002);
	} else if (e instanceof FileStateException) {
		result.setState(6003);
	} else if (e instanceof FileUploadIOException) {
		result.setState(6004);
	}
	
	return result;
}
3.2 设计请求

设计用户提交的请求,并设计响应的方式:

请求路径:/users/change_avatar
请求参数:MultipartFile file, HttpSession session
请求类型:POST
响应结果:JsonResult<String>
3.3 处理请求

1.在UserController类中添加处理请求的changeAvatar(@RequestParam(“file”) MultipartFile file, HttpSession session)方法。

@PostMapping("change_avatar")
public JsonResult<String> changeAvatar(@RequestParam("file") MultipartFile file, HttpSession session) {
	// 判断上传的文件是否为空
	// 是:抛出异常
	
	// 判断上传的文件大小是否超出限制值
	// 是:抛出异常
	
	// 判断上传的文件类型是否超出限制
	// 是:抛出异常
	
	// 获取当前项目的绝对磁盘路径
	// 保存头像文件的文件夹
	
	// 保存的头像文件的文件名
			
	// 创建文件对象,表示保存的头像文件
	// 执行保存头像文件
	// 如果产生异常则抛出

	// 头像路径
	// 从Session中获取uid和username
	// 将头像写入到数据库中
	
	// 返回成功和头像路径
	return null;
}

2.changeAvatar(@RequestParam(“file”) MultipartFile file, HttpSession session)方法中具体代码实现为。

/** 头像文件大小的上限值(10MB) */
public static final int AVATAR_MAX_SIZE = 10 * 1024 * 1024;
/** 允许上传的头像的文件类型 */
public static final List<String> AVATAR_TYPES = new ArrayList<String>();

/** 初始化允许上传的头像的文件类型 */
static {
	AVATAR_TYPES.add("image/jpeg");
	AVATAR_TYPES.add("image/png");
	AVATAR_TYPES.add("image/bmp");
	AVATAR_TYPES.add("image/gif");
}

@PostMapping("change_avatar")
public JsonResult<String> changeAvatar(@RequestParam("file") MultipartFile file, HttpSession session) {
	// 判断上传的文件是否为空
	if (file.isEmpty()) {
		// 是:抛出异常
		throw new FileEmptyException("上传的头像文件不允许为空");
	}
	
	// 判断上传的文件大小是否超出限制值
	if (file.getSize() > AVATAR_MAX_SIZE) { // getSize():返回文件的大小,以字节为单位
		// 是:抛出异常
		throw new FileSizeException("不允许上传超过" + (AVATAR_MAX_SIZE / 1024) + "KB的头像文件");
	}
	
	// 判断上传的文件类型是否超出限制
	String contentType = file.getContentType();
	// public boolean list.contains(Object o):当前列表若包含某元素,返回结果为true;若不包含该元素,返回结果为false。
	if (!AVATAR_TYPES.contains(contentType)) {
		// 是:抛出异常
		throw new FileTypeException("不支持使用该类型的文件作为头像,允许的文件类型:\n" + AVATAR_TYPES);
	}
	
	// 获取当前项目的绝对磁盘路径
	String parent = session.getServletContext().getRealPath("upload");
	// 保存头像文件的文件夹
	File dir = new File(parent);
	if (!dir.exists()) {
		dir.mkdirs();
	}
	
	// 保存的头像文件的文件名
	String suffix = "";
	String originalFilename = file.getOriginalFilename();
	int beginIndex = originalFilename.lastIndexOf(".");
	if (beginIndex > 0) {
		suffix = originalFilename.substring(beginIndex);
	}
	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/" + filename;
	// 从Session中获取uid和username
	Integer uid = getUidFromSession(session);
	String username = getUsernameFromSession(session);
	// 将头像写入到数据库中
	userService.changeAvatar(uid, username, avatar);
	
	// 返回成功头像路径
	return new JsonResult<String>(OK, avatar);
}

4 用户-上传头像-前端页面

1.然后在upload.html页面中配置用户上传头像的form表单。

在这里插入图片描述

2.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/upload.html进行测试。
在这里插入图片描述

5 用户-上传头像-设置上传文件大小

1.SpringBoot中默认MultipartResolver的最大文件大小值为1M。如果上传的文件的大小超过1M,会抛FileSizeLimitExceededException异常。

在这里插入图片描述

2.如果需要调整上传的限制值,直接在启动类中添加getMultipartConfigElement()方法,并且在启动类之前添加@Configuration注解。

package com.cy.store;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;
import javax.servlet.MultipartConfigElement;

@Configuration
@SpringBootApplication
@MapperScan("com.cy.store.mapper")
public class StoreApplication {

    public static void main(String[] args) {
        SpringApplication.run(StoreApplication.class, args);
    }

    @Bean
    public MultipartConfigElement getMultipartConfigElement() {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        // DataSize dataSize = DataSize.ofMegabytes(10);
        // 设置文件最大10M,DataUnit提供5中类型B,KB,MB,GB,TB
        factory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES));
        factory.setMaxRequestSize(DataSize.of(10, DataUnit.MEGABYTES));
        // 设置总上传数据总大小10M
        return factory.createMultipartConfig();
    }
}

3.除了以上编写方法配置上传的上限值以外,还可以通过在application.properties或application.yml中添加配置来实现。

(1) 低版本:1.X

spring.http.multipart.max-file-size=10MB
spring.http.multipart.max-request-size=10MB

(2) 高版本:2.X

#方式1
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
#方式2
spring.servlet.multipart.maxFileSize=10MB
spring.servlet.multipart.maxRequestSize=10MB

6 用户-上传头像-前端页面BUG解决

6.1 上传后显示头像

1.头像上传成功后,显示上传的头像。在upload.html页面中,是使用img标签来显示头像图片的。首先确定img标签是否添加有id="img-avatar"属性,便于后续访问该标签;而img标签是通过src属性来决定显示哪张图片的,所以修改src该属性的值即可设置需要显示的图片。修改表单添加id="form-change-avatar"属性。修改input标签,添加id="btn-change-avatar"和type="button"属性。

在这里插入图片描述

2.在upload.html页面中body标签内部的最后,添加script标签用于编写JavaScript程序。

  • processData:处理数据。默认情况下,processData的值是true,其代表以对象的形式上传的数据都会被转换为字符串的形式上传。而当上传文件的时候,则不需要把其转换为字符串,因此要改成false。
  • contentType:发送数据的格式。其代表的是前端发送数据的格式,默认值application/x-www-form-urlencoded。代表的是ajax的 data是以字符串的形式传递,使用这种传数据的格式,无法传输复杂的数据,比如多维数组、文件等。把contentType设置为false就会改掉之前默认的数据格式,在上传文件时就不会报错。
<script type="text/javascript">
    $("#btn-change-avatar").click(function() {
        $.ajax({
            url: "/users/change_avatar",
            type: "POST",
            data: new FormData($("#form-change-avatar")[0]),
            dataType: "JSON",
            processData: false, // processData处理数据
            contentType: false, // contentType发送数据的格式
            success: function(json) {
                if (json.state == 200) {
                    $("#img-avatar").attr("src", json.data);
                } else {
                    alert("修改失败!" + json.message);
                }
            },
            error: function(xhr) {
                alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + xhr.status);
                location.href = "login.html";
            }
        });
	});
</script>

3.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/upload.html进行测试。

6.2 登录后显示头像

1.首先检查登录成功后是否返回了头像的数据。访问http://localhost:8080/users/login?username=admin&password=321测试。

在这里插入图片描述

2.用户名、用户Id、用户头像等数据,属于常用数据,在客户端的许多页面都可能需要使用,如果每次都向服务器提交请求获取这些数据,是非常不合适的。可以在用户登录成功后,将这些数据存储在客户端本地,后续在客户端中需要显示这些数据时,直接从本地获取即可,无需再向服务器请求这些数据。在客户端本地存取数据时,可以使用Cookie技术。

3.设计思路:当用户登录成功后,将服务器返回的头像路径存储到本地的Cookie中,在打开“上传头像”页面时,从本地的Cookie中读取头像路径并显示即可。在登录login.html页面中,当登录成功后,将用户头像路径保存到Cookie中。

$("#btn-login").click(function() {
    $.ajax({
        url: "/users/login",
        type: "POST",
        data: $("#form-login").serialize(),
        dataType: "json",
        success: function(json) {
            if (json.state == 200) {
                alert("登录成功!");
                $.cookie("avatar", json.data.avatar, {expires: 7});
                console.log("cookie中的avatar=" + $.cookie("avatar"));
                location.href = "index.html";
            } else {
                alert("登录失败!" + json.message);
            }
        }
    });
});

语法:$.cookie(名称,值,[option])。[option]参数说明:

expires:有限日期,可以是一个整数或一个日期(单位天)。如果不设置这个值,默认情况下浏览器关闭之后此Cookie就会失效。

path:表示Cookie值保存的路径,默认与创建页路径一致。

domin:表示Cookie域名属性,默认与创建页域名一样。要注意跨域的概念,如果要主域名二级域名有效则要设置“.xxx.com”。

secrue:布尔类型的值,表示传输Cookie值时,是否需要一个安全协议。

4.在upload.html页面中,默认并没有引用jqueyr.cookie.js文件,因此无法识别$.cookie()函数;所以需要在upload.html页面head标签内添加jqueyr.cookie.js文件。

<script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>

5.在打开页面时自动读取显示用户图像。获取Cookie中头像的路径,然后将获取到的头像路径设置给img标签的src属性以显示头像。在upload.html页面中的script标签的内部添加自动读取用户图像的jquery代码。

$(document).ready(function () {
    console.log("cookie中的avatar=" + $.cookie("avatar"));
    $("#img-avatar").attr("src", $.cookie("avatar"));
});
6.3 显示最新头像

以上代码表示“每次打开页面时,读取Cookie中的头像并显示”,如果此时重新上传用户头像,而Cookie中所保存的头像还是之前上传的头像路径值,无法显示最新的用户头像。所以当用户重新上传头像后,还应把新头像的路径更新到Cookie中。

1.在upload.html页面中,用户头像修改成功后,并将新的用户头像路径保存到Cookie中。

$.cookie("avatar", json.data, {expires: 7});

在这里插入图片描述

2.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/upload.html进行测试。
要在upload.html页面head标签内添加jqueyr.cookie.js文件。

<script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>

5.在打开页面时自动读取显示用户图像。获取Cookie中头像的路径,然后将获取到的头像路径设置给img标签的src属性以显示头像。在upload.html页面中的script标签的内部添加自动读取用户图像的jquery代码。

$(document).ready(function () {
    console.log("cookie中的avatar=" + $.cookie("avatar"));
    $("#img-avatar").attr("src", $.cookie("avatar"));
});
6.3 显示最新头像

以上代码表示“每次打开页面时,读取Cookie中的头像并显示”,如果此时重新上传用户头像,而Cookie中所保存的头像还是之前上传的头像路径值,无法显示最新的用户头像。所以当用户重新上传头像后,还应把新头像的路径更新到Cookie中。

1.在upload.html页面中,用户头像修改成功后,并将新的用户头像路径保存到Cookie中。

$.cookie("avatar", json.data, {expires: 7});

新增收货地址

1 新增收货地址-创建数据表

1.使用use命令先选中store数据库。

USE store;

2.在store数据库中创建t_address用户数据表。

CREATE TABLE t_address (
	aid INT AUTO_INCREMENT COMMENT '收货地址id',
	uid INT COMMENT '归属的用户id',
	name VARCHAR(20) COMMENT '收货人姓名',
	province_name VARCHAR(15) COMMENT '省-名称',
	province_code CHAR(6) COMMENT '省-行政代号',
	city_name VARCHAR(15) COMMENT '市-名称',
	city_code CHAR(6) COMMENT '市-行政代号',
	area_name VARCHAR(15) COMMENT '区-名称',
	area_code CHAR(6) COMMENT '区-行政代号',
	zip CHAR(6) COMMENT '邮政编码',
	address VARCHAR(50) COMMENT '详细地址',
	phone VARCHAR(20) COMMENT '手机',
	tel VARCHAR(20) COMMENT '固话',
	tag VARCHAR(6) COMMENT '标签',
	is_default INT COMMENT '是否默认:0-不默认,1-默认',
	created_user VARCHAR(20) COMMENT '创建人',
	created_time DATETIME COMMENT '创建时间',
	modified_user VARCHAR(20) COMMENT '修改人',
	modified_time DATETIME COMMENT '修改时间',
	PRIMARY KEY (aid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2 新增收货地址-创建实体类

创建com.cy.store.entity.Address新增收获地址的实体类,继承自BaseEntity类,在类中声明与数据表中对应的属性,添加Getters and Setters方法,基于唯一标识aid生成hashCode()和equals()方法。

package com.cy.store.entity;

/** 收货地址数据的实体类 */
public class Address extends BaseEntity implements Serializable {
    private Integer aid;
    private Integer uid;
    private String name;
    private String provinceName;
    private String provinceCode;
    private String cityName;
    private String cityCode;
    private String areaName;
    private String areaCode;
    private String zip;
    private String address;
    private String phone;
    private String tel;
    private String tag;
    private Integer isDefault;

    // Generate: Getter and Setter、Generate hashCode() and equals()、toString()
}

3 新增收货地址-持久层

3.1 各功能的开发顺序

关于收货地址数据的管理,涉及的功能有:增加,删除,修改,设为默认,显示列表。这些功能的开发顺序为:增加-显示列表-设为默认-删除-修改。

3.2 规划需要执行的SQL语句

增加收货地址的本质是插入新的收货地址数据,需要执行的SQL语句大致是:

INSERT INTO t_address (除了aid以外的字段列表) VALUES (匹配的值列表)

后续在处理业务时,还需要确定“即将增加的收货地址是不是默认收货地址”;可以设定规则“用户的第1条收货地址是默认的,以后添加的每一条都不是默认的”;要应用该规则,就必须知道“即将增加的收货地址是不是第1条”,可以“根据用户id统计收货地址的数量”,如果统计结果为0,则即将增加的就是该用户的第1条收货地址,如果统计结果不是0,则该用户已经有若干条收货地址了,即将增加的就一定不是第1条。关于统计的SQL语句大致是:

SELECT count(*) FROM t_address WHERE uid=?

一般电商平台都会限制每个用户可以创建的收货地址的数量,如“每个用户最多只允许创建20个收货地址”,也可以通过以上查询来实现。

3.3 接口与抽象方法

创建com.cy.store.mapper.AddressMapper接口,并在接口中添加抽象方法。

package com.cy.store.mapper;
import com.cy.store.entity.Address;

/** 处理收货地址数据的持久层接口 */
public interface AddressMapper {
    /**
     * 插入收货地址数据
     * @param address 收货地址数据
     * @return 受影响的行数
     */
    Integer insert(Address address);

    /**
     * 统计某用户的收货地址数据的数量
     * @param uid 用户的id
     * @return 该用户的收货地址数据的数量
     */
    Integer countByUid(Integer uid);
}
3.4 配置SQL映射

1.在src/main/resources/mapper文件夹下复制粘贴得到AddressMapper.xml映射文件,修改根节点mapper的namespace属性的值为com.cy.store.mapper.AddressMapper,并在根节点中配置pojo类属性与数据库中表的字段映射。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cy.store.mapper.AddressMapper">
    <resultMap id="AddressEntityMap" type="com.cy.store.entity.Address">
        <id column="aid" property="aid"/>
        <result column="province_code" property="provinceCode"/>
        <result column="province_name" property="provinceName"/>
        <result column="city_code" property="cityCode"/>
        <result column="city_name" property="cityName"/>
        <result column="area_code" property="areaCode"/>
        <result column="area_name" property="areaName"/>
        <result column="is_default" property="isDefault"/>
        <result column="created_user" property="createdUser"/>
        <result column="created_time" property="createdTime"/>
        <result column="modified_user" property="modifiedUser"/>
        <result column="modified_time" property="modifiedTime"/>
    </resultMap>
</mapper>

2.在AddressMapper.xml映射文件的根节点中配置以上两个抽象方法的映射。

<!-- 插入收货地址数据:Integer insert(Address address) -->
<insert id="insert" useGeneratedKeys="true" keyProperty="aid">
    INSERT INTO t_address (
        uid, name, province_name, province_code, city_name, city_code, area_name, area_code, zip,
        address, phone, tel,tag, is_default, created_user, created_time, modified_user, modified_time
    ) VALUES (
        #{uid}, #{name}, #{provinceName}, #{provinceCode}, #{cityName}, #{cityCode}, #{areaName},
        #{areaCode}, #{zip}, #{address}, #{phone}, #{tel}, #{tag}, #{isDefault}, #{createdUser},
        #{createdTime}, #{modifiedUser}, #{modifiedTime}
    )
</insert>

<!-- 统计某用户的收货地址数据的数量:Integer countByUid(Integer uid) -->
<select id="countByUid" resultType="java.lang.Integer">
    SELECT
    	COUNT(*)
    FROM
   		t_address
    WHERE
    	uid=#{uid}
</select>

3.在src/test/java下创建com.cy.store.mapper.AddressMapperTests测试类,在类定义之前添加测试的两个注解,在类中编写并执行以上两个抽象方法的测试。

package com.cy.store.mapper;
import com.cy.store.entity.Address;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class AddressMapperTests {
    @Autowired
    private AddressMapper addressMapper;

    @Test
    public void insert() {
        Address address = new Address();
        address.setUid(18);
        address.setName("admin");
        address.setPhone("17858802974");
        address.setAddress("雁塔区小寨赛格");
        Integer rows = addressMapper.insert(address);
        System.out.println("rows=" + rows);
    }

    @Test
    public void countByUid() {
        Integer uid = 18;
        Integer count = addressMapper.countByUid(uid);
        System.out.println("count=" + count);
    }
}

4 增收货地址-业务层

4.1 规划异常

1.无论用户将要增加的收货地址是不是默认收货地址,都需正常增加。即通过countByUid()方法统计的结果不管是不是0,都不能代表是错误的操作。

2.在执行插入收货地址数据之前,需判断countByUid()方法返回值是否超出上限值,如果超出上限值则抛AddressCountLimitException异常。

3.在执行插入数据时,还可能抛出InsertException异常,此异常无需再次创建。

4.创建com.cy.store.service.ex.AddressCountLimitException类后,需继承自ServiceException类。

package com.cy.store.service.ex;

/** 收货地址数量达到上限的异常 */
public class AddressCountLimitException extends ServiceException {
    // Override Methods...
}
4.2 接口与抽象方法

创建com.cy.store.service.IAddressService业务层接口,并添加抽象方法。

package com.cy.store.service;
import com.cy.store.entity.Address;

/** 处理收货地址数据的业务层接口 */
public interface IAddressService {
    /**
     * 创建新的收货地址
     * @param uid 当前登录的用户的id
     * @param username 当前登录的用户名
     * @param address 用户提交的收货地址数据
     */
    void addNewAddress(Integer uid, String username, Address address);
}
4.3 实现抽象方法

1.创建com.cy.store.service.impl.AddressServiceImpl业务层实现类,在类定义之前添加@Service注解,并实现IAddressService接口,最后在类中添加持久层对象并使用@Autowired注解修饰。

package com.cy.store.service.impl;
import com.cy.store.entity.Address;
import com.cy.store.mapper.AddressMapper;
import com.cy.store.service.IAddressService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AddressServiceImpl implements IAddressService {
    @Autowired
    private AddressMapper addressMapper;

    @Override
    public void addNewAddress(Integer uid, String username, Address address) {
		// TODO
    }
}

2.分析重写的addNewAddress(Integer uid, String username, Address address)抽象方法中的业务逻辑。

@Override
public void addNewAddress(Integer uid, String username, Address address) {
    // 根据参数uid调用addressMapper的countByUid()方法,统计当前用户的收货地址数据的数量
    // 判断数量是否达到上限值
    // 是:抛出AddressCountLimitException

    // 补全数据:将参数uid封装到参数address中
    // 补全数据:根据以上统计的数量,得到正确的isDefault值(是否默认:0-不默认,1-默认),并封装
    // 补全数据:4项日志

    // 调用addressMapper的insert()方法插入收货地址数据,并获取返回的受影响行数
    // 判断受影响行数是否不为1
    // 是:抛出InsertException
}

3.addNewAddress(Integer uid, String username, Address address)方法的具体代码实现。

package com.cy.store.service.impl;
import com.cy.store.entity.Address;
import com.cy.store.mapper.AddressMapper;
import com.cy.store.service.IAddressService;
import com.cy.store.service.ex.AddressCountLimitException;
import com.cy.store.service.ex.InsertException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Date;

@Service
public class AddressServiceImpl implements IAddressService {
    @Autowired
    private AddressMapper addressMapper;

    @Value("${user.address.max-count}")
    private int maxCount;

    @Override
    public void addNewAddress(Integer uid, String username, Address address) {
        // 根据参数uid调用addressMapper的countByUid(Integer uid)方法,统计当前用户的收货地址数据的数量
        Integer count = addressMapper.countByUid(uid);
        // 判断数量是否达到上限值
        if (count > maxCount) {
            // 是:抛出AddressCountLimitException
            throw new AddressCountLimitException("收货地址数量已经达到上限(" + maxCount + ")!");
        }

        // 补全数据:将参数uid封装到参数address中
        address.setUid(uid);
        // 补全数据:根据以上统计的数量,得到正确的isDefault值(是否默认:0-不默认,1-默认),并封装
        Integer isDefault = count == 0 ? 1 : 0;
        address.setIsDefault(IsDefault);
        // 补全数据:4项日志
        Date now = new Date();
        address.setCreatedUser(username);
        address.setCreatedTime(now);
        address.setModifiedUser(username);
        address.setModifiedTime(now);

        // 调用addressMapper的insert(Address address)方法插入收货地址数据,并获取返回的受影响行数
        Integer rows = addressMapper.insert(address);
        // 判断受影响行数是否不为1
        if (rows != 1) {
            // 是:抛出InsertException
            throw new InsertException("插入收货地址数据时出现未知错误,请联系系统管理员!");
        }
    }
}

4.在application.properties文件中添加收货地址数据上限值的配置。

user.address.max-count=20

5.在src/test/java下创建com.cy.store.service.AddressServiceTests测试类,在测试类中测试以上方法。

package com.cy.store.service;
import com.cy.store.entity.Address;
import com.cy.store.service.ex.ServiceException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class AddressServiceTests {
    @Autowired
    private IAddressService addressService;

    @Test
    public void addNewAddress() {
        try {
            Integer uid = 20;
            String username = "管理员";
            Address address = new Address();
            address.setName("张三");
            address.setPhone("17858805555");
            address.setAddress("雁塔区小寨华旗");
            addressService.addNewAddress(uid, username, address);
            System.out.println("OK.");
        } catch (ServiceException e) {
            System.out.println(e.getClass().getSimpleName());
            System.out.println(e.getMessage());
        }
    }
}

5 新增收货地址-控制器

5.1 处理异常

在控制器层新增收货地址时,如果收货地址已经达到上限值,则抛出AddressCountLimitException异常,并在BaseController类中添加处理AddressCountLimitException的异常。

// ...
else if (e instanceof AddressCountLimitException) {
	result.setState(4003);
}
// ...
5.2 设计请求

设计用户提交的请求,并设计响应的方式。

请求路径:/addresses/add_new_address
请求参数:Address address, HttpSession session
请求类型:POST
响应结果:JsonResult<Void>
5.3 处理请求

1.创建com.cy.store.controller.AddressController控制器类继承自BaseController类,在类的声明添加@RequestMapping(“addresses”)和@RestController注解,在类中声明业务层对象并添加Autowired注解修饰。

package com.cy.store.controller;
import com.cy.store.service.IAddressService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("addresses")
public class AddressController extends BaseController {
    @Autowired
    private IAddressService addressService;    
}

2.然后在AddressController类中添加处理请求的addNewAddress(Address address, HttpSession session)方法。

@RequestMapping("add_new_address")
public JsonResult<Void> addNewAddress(Address address, HttpSession session) {
    // 从Session中获取uid和username
    Integer uid = getUidFromSession(session);
    String username = getUsernameFromSession(session);

    // 调用业务对象的方法执行业务
    addressService.addNewAddress(uid, username, address);
    // 响应成功
    return new JsonResult<Void>(OK);
}

3.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/addresses/add_new_address进行测试。

在这里插入图片描述

6 新增收货地址-前端页面

1.在addAddress.html页面中配置新增收货地址表单的属性。给form表单添加id="form-add-new-address"属性、"请输入收货人姓名"添加name="name"属性、"请输入邮政编码"添加name="zip"属性、"输入详细的收货地址,小区名称、门牌号等"添加name="address"属性、"请输入手机号码"添加name="phone"属性、"请输入固定电话号码"添加name="tel"属性、"请输入地址类型,如:家、公司或者学校"添加name="tag"属性、"保存"按钮添加id="btn-add-new-address"属性。以上属性如果已经添加无需重复添加。

2.在addAddress.html页面中body标签内部的最后,添加script标签用于编写JavaScript程序。

<script type="text/javascript">
    $("#btn-add-new-address").click(function() {
        $.ajax({
            url: "/addresses/add_new_address",
            type: "POST",
            data: $("#form-add-new-address").serialize(),
            dataType: "JSON",
            success: function(json) {
                if (json.state == 200) {
                    alert("新增收货地址成功!");
                } else {
                    alert("新增收货地址失败!" + json.message);
                }
            },
            error: function(xhr) {
                alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + xhr.status);
                location.href = "login.html";
            }
        });
	});
</script>

3.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/addAddress.html页面。

在这里插入图片描述

获取省/市/区的列表

1 获取省/市/区的列表-数据库

1.使用该数据库:

USE store;

2.向数据库中导入省/市/区数据t_dict_district.sql文件,执行以下指令:

mysql> source C:/Users/yuanxin/t_dict_district.sql

3.创建省/市/区数据的com.cy.store.entity实体类,在类中声明与数据表中对应的属性,添加Getters and Setters方法,基于唯一标识id生成equals()方法及hashCode()和toString()方法。

package com.cy.store.entity;
import java.io.Serializable;

/** 省/市/区数据的实体类 */
public class District implements Serializable {
    private Integer id;
    private String parent;
    private String code;
    private String name;

    // Generate: Getter and Setter、Generate hashCode() and equals()、toString()
}

2 获取省/市/区的列表-持久层

2.1 规划需要执行的SQL语句

获取全国所有省/某省所有市/某市所有区的查询SQL语句大致是:

select * from t_dict_district where parent=? order by code ASC;
2.2 接口与抽象方法

创建com.cy.store.mapper.DistrictMapper接口,添加抽象方法。

package com.cy.store.mapper;
import com.cy.store.entity.District;
import java.util.List;

/** 处理省/市/区数据的持久层接口 */
public interface DistrictMapper {
    /**
     * 获取全国所有省/某省所有市/某市所有区
     * @param parent 父级代号,当获取某市所有区时,使用市的代号;当获取省所有市时,使用省的代号;当获取全国所有省时,使用"86"作为父级代号
     * @return 全国所有省/某省所有市/某市所有区的列表
     */
    List<District> findByParent(String parent);
}
2.3 配置SQL映射

1.在src/main/resources/mapper中复制得到DistrictMapper.xml,修改根节点的namespace属性的值为以上接口文件,并配置以上抽象方法的映射。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cy.store.mapper.DistrictMapper">
    <!-- 获取全国所有省/某省所有市/某市所有区:List<District> findByParent(String parent) -->
    <select id="findByParent" resultType="com.cy.store.entity.District">
        SELECT
            *
        FROM
            t_dict_district
        WHERE
            parent=#{parent}
        ORDER BY
            code ASC
    </select>
</mapper>

2.在src/test/java下创建com.cy.store.mapper.DistrictMapperTests测试类,编写并执行以上抽象方法的测试。

package com.cy.store.mapper;
import com.cy.store.entity.District;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class DistrictMapperTests {
    @Autowired
    private DistrictMapper districtMapper;

    @Test
    public void findByParent() {
        String parent = "110100";
        List<District> list = districtMapper.findByParent(parent);
        System.out.println("count=" + list.size());
        for (District district : list) {
            System.out.println(district);
        }
    }
}

3 获取省/市/区的列表-业务层

3.1 规划异常

说明:无异常。

3.2 接口与抽象方法

创建com.cy.store.service.IDistrictService接口,并添加抽象方法。

package com.cy.store.service;
import com.cy.store.entity.District;
import java.util.List;

/** 处理省/市/区数据的业务层接口 */
public interface IDistrictService {
    /**
     * 获取全国所有省/某省所有市/某市所有区
     * @param parent 父级代号,当获取某市所有区时,使用市的代号;当获取某省所有市时,使用省的代号;当获取全国所有省时,使用"86"作为父级代号
     * @return 全国所有省/某省所有市/某市所有区的列表
     */
    List<District> getByParent(String parent);
}
3.3 实现抽象方法

1.创建com.cy.store.service.impl.DistrictServiceImpl类,实现IDistrictService接口,在类之前添加@Service注解,以及在类中添加持久层对象并使用@Autowired修饰。

package com.cy.store.service.impl;
import com.cy.store.entity.District;
import com.cy.store.mapper.DistrictMapper;
import com.cy.store.service.IDistrictService;
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service;
import java.util.List;

/** 处理省/市/区数据的业务层实现类 */
@Service
public class DistrictServiceImpl implements IDistrictService {
    @Autowired
    private DistrictMapper districtMapper;

    @Override
    public List<District> getByParent(String parent) {
        return null;
    }
}

2.在DistrictServiceImpl实现类中实现getByParent(String parent)方法的具体代码。

@Override
public List<District> getByParent(String parent) {
	List<District> list = districtMapper.findByParent(parent);
	for (District district : list) {
		district.setId(null);
		district.setParent(null);
	}
	return list;
}

3.在src/test/java下创建com.cy.store.service.DistrictServiceTests测试类,编写并执行单元测试。

package com.cy.store.service;
import com.cy.store.entity.District;
import com.cy.store.service.ex.ServiceException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class DistrictServiceTests {
    @Autowired
    private IDistrictService districtService;

    @Test
    public void getByParent() {
        try {
            String parent = "86";
            List<District> list = districtService.getByParent(parent);
            System.out.println("count=" + list.size());
            for (District item : list) {
                System.out.println(item);
            }
        } catch (ServiceException e) {
            System.out.println(e.getClass().getSimpleName());
            System.out.println(e.getMessage());
        }
    }
}

4 获取省/市/区的列表-控制器

4.1 处理异常

说明:无异常。

4.2 设计请求

设计用户提交的请求,并设计响应的方式。

请求路径:/districts/
请求参数:String parent
请求类型:GET
响应结果:JsonResult<List<District>>
是否拦截:否,需要在拦截器的配置中添加白名单
4.3 处理请求

1.创建com.cy.store.controller.DistrictController控制器类,继承自BaseController类,在类之前添加@RequestMapping(“districts”)和@RestController注解,并在类中添加业务层对象,对其使用@Autowired注解修饰。

package com.cy.store.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.tedu.store.entity.District;
import cn.tedu.store.service.IDistrictService;
import cn.tedu.store.util.JsonResult;

@RequestMapping("districts")
@RestController
public class DistrictController extends BaseController {
	@Autowired
	private IDistrictService districtService;
	
}

2.在类中添加处理请求的方法getByParent(String parent)及方法的实现。

@GetMapping:是一个组合注解,等价于@RequestMapping(method={RequestMethod.GET}),它将HTTP的GET请求映射到特定的处理方法上。“/”表示方法将处理所有传入的URI请求。简化代码。

@GetMapping({"", "/"})
public JsonResult<List<District>> getByParent(String parent) {
    List<District> data = districtService.getByParent(parent);
    return new JsonResult<>(OK, data);
}

3.在拦截器LoginInterceptorConfigurer类的addInterceptors(InterceptorRegistry registry)方法中将“districts”请求添加为白名单。如果已经添加无需重复添加。

patterns.add("/districts/**");

4.完成后启动项目,打开浏览器(不需要登录),直接访问http://localhost:8080/districts?parent=86进行测试。
在这里插入图片描述

5 获取省/市/区的列表-前端页面

1.在addAddress.html页面中的head标签内导入的distpicker.data.js和distpicker.js文件注释掉。

JQuery实现中国省市区地址三级联动插件Distpicker。

<!--
<script type="text/javascript" src="../js/distpicker.data.js"></script>
<script type="text/javascript" src="../js/distpicker.js"></script>
-->

2.在新增收货地址表单中,给"选择省"控件添加name="provinceCode"和id="province-list"属性,给"选择市"添加name="cityCode"和id="city-list"属性,给"选择区"控件添加name="areaCode"和id="area-list"属性。以上属性如果已经添加无需重复添加。

3.在addAddress.html页面中body标签内的script标签中添加获取省/市/区列表的代码。

<script type="text/javascript">
    let defaultOption = '<option value="0">----- 请选择 -----</option>';

    $(document).ready(function() {
        showProvinceList();
        $("#city-list").append(defaultOption);
        $("#area-list").append(defaultOption);
    });

    $("#province-list").change(function() {
        showCityList();
    });

    $("#city-list").change(function() {
        showAreaList();
    });

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

    function showCityList() {
        let parent = $("#province-list").val();
        $("#city-list").empty();
        $("#area-list").empty();

        $("#city-list").append(defaultOption);
        $("#area-list").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;
                    console.log("count=" + list.length);
                    for (let i = 0; i < list.length; i++) {
                        console.log(list[i].name);
                        let option = '<option value="' + list[i].code + '">' + list[i].name + '</option>';
                        $("#city-list").append(option);
                    }
                }
            }
        });
    }

    function showAreaList() {
        let parent = $("#city-list").val();
        $("#area-list").empty();
        $("#area-list").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;
                    console.log("count=" + list.length);
                    for (let i = 0; i < list.length; i++) {
                        console.log(list[i].name);
                        let option = '<option value="' + list[i].code + '">' + list[i].name + '</option>';
                        $("#area-list").append(option);
                    }
                }
            }
        });
    }
</script>

JQuery事件-change()方法

1.定义和用法

(1)当元素的值发生改变时,会发生change事件。

(2)该事件仅适用于文本域(textfield),以及textarea和select元素。

(3)change()函数触发change事件,或规定当发生change事件时运行的函数。

当用于select元素时,change事件会在选择某个选项时发生。当用于textfield或textarea时,该事件会在元素失去焦点时发生。

2.触发change事件

触发被选元素的change事件。语法:$(selector).change()

3.将函数绑定到change事件

规定当被选元素的 change 事件发生时运行的函数。语法:$(selector).change(function)

4.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/addAddress.html页面。

在这里插入图片描述

5.说明:如果输入的邮政编码位数大于6位数字,则会抛MysqlDataTruncation异常。

在这里插入图片描述

获取省/市/区的名称

此功能模块主要实现根据省/市/区的行政代号获取省/市/区的名称。

1 获取省/市/区的名称-持久层

1.1 规划需要执行的SQL语句

根据省/市/区的行政代号获取省/市/区的名称,需要执行的SQL语句大致是:

select name from t_dict_district where code=?
1.2 接口与抽象方法

在DistrictMapper接口中添加根据省/市/区的行政代号获取省/市/区的名称findNameByCode(String code)抽象方法。

/**
 * 根据省/市/区的行政代号获取省/市/区的名称
 * @param code 省/市/区的行政代号
 * @return 匹配的省/市/区的名称,如果没有匹配的数据则返回null
 */
String findNameByCode(String code);
1.3 配置SQL映射

1.在DistrictMapper.xml文件中配置映射。

<!-- 根据省/市/区的行政代号获取省/市/区的名称:String findNameByCode(String code) -->
<select id="findNameByCode" resultType="java.lang.String">
    SELECT
    	name
    FROM
    	t_dict_district
    WHERE
    	code=#{code}
</select>

2.然后在DistrictMapperTests测试类中编写并执行测试方法。

@Test
public void findNameByCode() {
    String code = "540000";
    String name = districtMapper.findNameByCode(code);
    System.out.println(name);
}

2 获取省/市/区的名称-业务层

2.1 规划异常

说明:无异常。

2.2 接口与抽象方法

在业务层IDistrictService接口中添加getNameByCode(String code)抽象方法。

/**
 * 根据省/市/区的行政代号获取省/市/区的名称
 * @param code 省/市/区的行政代号
 * @return 匹配的省/市/区的名称,如果没有匹配的数据则返回null
 */
String getNameByCode(String code);
2.3 实现抽象方法

1.在业务层DistrictServiceImpl类中重写getNameByCode(String code)方法。

@Override
public String getNameByCode(String code) {
    return districtMapper.findNameByCode(code);
}

2.然后在DistrictServiceTests测试类中编写并执行测试方法。

@Test
public void getNameByCode() {
    try {
        String code = "430000";
        String result = districtService.getNameByCode(code);
        System.out.println(result);
    } catch (ServiceException e) {
        System.out.println(e.getClass().getSimpleName());
        System.out.println(e.getMessage());
    }
}

3 新增收货地址-业务层优化

1.在AddressServiceImpl类中声明处理省/市/区数据的业务层对象。

@Autowired
private IDistrictService districtService;

2.在addNewAddress(Integer uid, String username, Address address)方法中补全省/市/区数据。

// 补全数据:省、市、区的名称
String provinceName = districtService.getNameByCode(address.getProvinceCode());
String cityName = districtService.getNameByCode(address.getCityCode());
String areaName = districtService.getNameByCode(address.getAreaCode());
address.setProvinceName(provinceName);
address.setCityName(cityName);
address.setAreaName(areaName);

4 新增收货地址-前端页面测试

1.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/addAddress.html页面。输入收货人相关的信息并保存。

在这里插入图片描述

2.在后台数据库中检查数据是否被正常的插入到t_address表中。

在这里插入图片描述

收货地址列表

1 收货地址列表显示-持久层

1.1 规划需要执行的SQL语句

显示当前登录用户的收货地址列表的SQL语句大致是:

select * from t_address where uid=? order by is_default desc, created_time desc;
1.2 接口与抽象方法

在AddressMapper接口中添加findByUid(Integer uid)抽象方法。

/**
 * 查询某用户的收货地址列表数据
 * @param uid 收货地址归属的用户id
 * @return 该用户的收货地址列表数据
 */
List<Address> findByUid(Integer uid);
1.3 配置SQL映射

1.在AddressMapper.xml文件中配置findByUid(Integer uid)方法的映射。

<!--
<resultMap id="AddressEntityMap" type="cn.tedu.store.entity.Address">
	<id column="aid" property="aid"/>
	<result column="province_code" property="provinceCode"/>
	<result column="province_name" property="provinceName"/>
	<result column="city_code" property="cityCode"/>
	<result column="city_name" property="cityName"/>
	<result column="area_code" property="areaCode"/>
	<result column="area_name" property="areaName"/>
	<result column="is_default" property="isDefault"/>
	<result column="created_user" property="createdUser"/>
	<result column="created_time" property="createdTime"/>
	<result column="modified_user" property="modifiedUser"/>
	<result column="modified_time" property="modifiedTime"/>
</resultMap>
-->

<!-- 查询某用户的收货地址列表数据:List<Address> findByUid(Integer uid) -->
<select id="findByUid" resultMap="AddressEntityMap">
	SELECT
		*
	FROM
		t_address
	WHERE
		uid=#{uid}
	ORDER BY
		is_default DESC, created_time DESC
</select>

2.在AddressMapperTests测试类中添加findByUid()测试方法。

@Test
public void findByUid() {
    Integer uid = 26;
    List<Address> list = addressMapper.findByUid(uid);
    System.out.println("count=" + list.size());
    for (Address item : list) {
        System.out.println(item);
    }
}

2 收货地址列表显示-业务层

2.1 规划异常

说明:无异常。

2.2 接口与抽象方法

在IAddressService接口中添加getByUid(Integer uid)抽象方法。

/**
 * 查询某用户的收货地址列表数据
 * @param uid 收货地址归属的用户id
 * @return 该用户的收货地址列表数据
 */
List<Address> getByUid(Integer uid);
2.3 实现抽象方法

1.在AddressServiceImpl类中实现getByUid(Integer uid)抽象方法。

@Override
public List<Address> getByUid(Integer uid) {
	List<Address> list = addressMapper.findByUid(uid);
	for (Address address : list) {
		address.setUid(null);
		address.setProvinceCode(null);
		address.setCityCode(null);
		address.setAreaCode(null);
		address.setCreatedUser(null);
		address.setCreatedTime(null);
		address.setModifiedUser(null);
		address.setModifiedTime(null);
	}
	return list;
}

2.在AddressServiceTests测试类中添加getByUid()测试方法。

@Test
public void getByUid() {
    Integer uid = 26;
    List<Address> list = addressService.getByUid(uid);
    System.out.println("count=" + list.size());
    for (Address item : list) {
        System.out.println(item);
    }
}

3 收货地址列表显示-控制器

3.1 处理异常

说明:无异常。

3.2 设计请求

设计用户提交的请求,并设计响应的方式。

请求路径:/addresses
请求参数:HttpSession session
请求类型:GET
响应结果:JsonResult<List<Address>>
3.3 处理请求

1.在AddressController类中添加处理请求的getByUid(HttpSession session)方法。

@GetMapping({"", "/"})
public JsonResult<List<Address>> getByUid(HttpSession session) {
	Integer uid = getUidFromSession(session);
	List<Address> data = addressService.getByUid(uid);
	return new JsonResult<>(OK, data);
}

4 收货地址列表显示-前端页面

1.在address.html页面中body标签内部的最后,添加展示用户收货地址列表数据的JavaScript代码。

<script type="text/javascript">
$(document).ready(function () {
    showAddressList();
});

function showAddressList() {
    $("#address-list").empty();
    $.ajax({
        url: "/addresses",
        type: "GET",
        dataType: "JSON",
        success: function (json) {
            let list = json.data;
            for (let i = 0; i < list.length; i++) {
                console.log(list[i].name);
                let address = '<tr>'
                    + '<td>#{tag}</td>'
                    + '<td>#{name}</td>'
                    + '<td>#{province}#{city}#{area}#{address}</td>'
                    + '<td>#{phone}</td>'
                    + '<td><a class="btn btn-xs btn-info"><span class="fa fa-edit"></span> 修改</a></td>'
                    + '<td><a class="btn btn-xs add-del btn-info"><span class="fa fa-trash-o"></span> 删除</a></td>'
                    + '<td><a class="btn btn-xs add-def btn-default">设为默认</a></td>'
                + '</tr>';

                address = address.replace(/#{aid}/g, list[i].aid);
                address = address.replace(/#{tag}/g, list[i].tag);
                address = address.replace("#{name}", list[i].name);
                address = address.replace("#{province}", list[i].provinceName);
                address = address.replace("#{city}", list[i].cityName);
                address = address.replace("#{area}", list[i].areaName);
                address = address.replace("#{address}", list[i].address);
                address = address.replace("#{phone}", list[i].phone);

                $("#address-list").append(address);
            }
            $(".add-def:eq(0)").hide();
        }
    });
}
</script>

2.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/address.html页面。
在这里插入图片描述

默认收货地址

1 默认收货地址-持久层

1.1 规划需要执行的SQL语句

1.将某用户的所有收货地址设置为非默认地址(是否默认:0-不默认,1-默认)。

update t_address set is_default=0 where uid=?

2.将某用户指定的收货地址设置为默认地址。

update t_address set is_default=1, modified_user=?, modified_time=? where aid=?

3.检查该收货地址是否存在,并检查数据归属是否正确。可根据收货地址aid值,查询收货地址详情数据。

select * from t_address where aid=?
1.2 接口与抽象方法

在AddressMapper接口中声明三个抽象方法。

/**
 * 将某用户的所有收货地址设置为非默认地址
 * @param uid 收货地址归属的用户id
 * @return 受影响的行数
 */
Integer updateNonDefaultByUid(Integer uid);

/**
 * 将指定的收货地址设置为默认地址
 * @param aid 收货地址id
 * @param modifiedUser 修改执行人
 * @param modifiedTime 修改时间
 * @return 受影响的行数
 */
Integer updateDefaultByAid(
        @Param("aid") Integer aid,
        @Param("modifiedUser") String modifiedUser,
        @Param("modifiedTime") Date modifiedTime);

/**
 * 根据收货地址aid值,查询收货地址详情
 * @param aid 收货地址id
 * @return 匹配的收货地址详情,如果没有匹配的数据,则返回null
 */
Address findByAid(Integer aid);
1.3 配置SQL映射

1.在AddressMapper.xml映射文件,配置以上三个抽象方法的映射。

<!-- 将某用户的所有收货地址设置为非默认地址:Integer updateNonDefaultByUid(Integer uid) -->
<update id="updateNonDefaultByUid">
    UPDATE
    	t_address
    SET
    	is_default=0
    WHERE
    	uid=#{uid}
</update>

<!-- 将指定的收货地址设置为默认地址:
         Integer updateDefaultByAid(
            @Param("aid") Integer aid,
            @Param("modifiedUser") String modifiedUser,
            @Param("modifiedTime") Date modifiedTime) -->
<update id="updateDefaultByAid">
    UPDATE
    	t_address
    SET
        is_default=1,
        modified_user=#{modifiedUser},
        modified_time=#{modifiedTime}
    WHERE
    	aid=#{aid}
</update>

<!-- 根据收货地址aid值,查询收货地址详情:Address findByAid(Integer aid) -->
<select id="findByAid" resultMap="AddressEntityMap">
    SELECT
    	*
    FROM
    	t_address
    WHERE
    	aid=#{aid}
</select>

2.在AddressMapperTests类中编写并执行以上三个抽象方法的测试。

@Test
public void updateNonDefaultByUid() {
    Integer uid = 26;
    Integer rows = addressMapper.updateNonDefaultByUid(uid);
    System.out.println("rows=" + rows);
}

@Test
public void updateDefaultByAid() {
    Integer aid = 11;
    String modifiedUser = "管理员";
    Date modifiedTime = new Date();
    Integer rows = addressMapper.updateDefaultByAid(aid, modifiedUser, modifiedTime);
    System.out.println("rows=" + rows);
}

@Test
public void findByAid() {
    Integer aid = 11;
    Address result = addressMapper.findByAid(aid);
    System.out.println(result);
}

2 默认收货地址-业务层

2.1 规划异常

1.在执行设置默认收货地址之前,需要先检查该收货地址数据是否存在,如果不存在则抛出AddressNotFoundException异常。

2.然后还需要检查数据归属是否正确,也就是不可以操作他人的数据,如果该数据中记录的uid与当前登录的用户的uid不一致,则抛出AccessDeniedException异常。

3.检查通过后先全部设置为非默认,然后将指定的收货地址设置为默认;这两种操作都是更新数据的操作,则可能抛出UpdateException异常。

4.在com.cy.store.service.ex包下创建AddressNotFoundException和AccessDeniedException异常类。

package com.cy.store.service.ex;

/** 收货地址数据不存在的异常 */
public class AddressNotFoundException extends ServiceException {
    // Override Methods...
}
package com.cy.store.service.ex;

/** 非法访问的异常 */
public class AccessDeniedException extends ServiceException {
    // Override Methods...
}
2.2 接口与抽象方法

在IAddressService接口中添加setDefault(Integer aid, Integer uid, String username)抽象方法。

/**
 * 设置默认收货地址
 * @param aid 收货地址id
 * @param uid 归属的用户id
 * @param username 当前登录的用户名
 */
void setDefault(Integer aid, Integer uid, String username);
2.3 实现抽象方法

1.在AddressServiceImpl类中重写setDefault(Integer aid, Integer uid, String username)方法。该方法需要添加@Transactional注解。

事务:基于Spring JDBC的事务(Transaction)处理,使用事务可以保证一系列的增删改操作,要么全部执行成功,要么全部执行失败。@Transactional注解可以用来修饰类也可以用来修饰方法。如果添加在业务类之前,则该业务类中的方法均以事务的机制运行,但是一般并不推荐这样处理。

@Transactional
@Override
public void setDefault(Integer aid, Integer uid, String username) {
	// 根据参数aid,调用addressMapper中的findByAid()查询收货地址数据
	// 判断查询结果是否为null
	// 是:抛出AddressNotFoundException

	// 判断查询结果中的uid与参数uid是否不一致(使用equals()判断)
	// 是:抛出AccessDeniedException:非法访问

	// 调用addressMapepr的updateNonDefaultByUid()将该用户的所有收货地址全部设置为非默认,并获取返回的受影响的行数
	// 判断受影响的行数是否小于1(不大于0)
	// 是:抛出UpdateException

	// 调用addressMapepr的updateDefaultByAid()将指定aid的收货地址设置为默认,并获取返回的受影响的行数
	// 判断受影响的行数是否不为1
	// 是:抛出UpdateException
}

2.setDefault(Integer aid, Integer uid, String username)方法的具体代码实现。

@Transactional
@Override
public void setDefault(Integer aid, Integer uid, String username) {
    // 根据参数aid,调用addressMapper中的findByAid()查询收货地址数据
    Address result = addressMapper.findByAid(aid);
    // 判断查询结果是否为null
    if (result == null) {
        // 是:抛出AddressNotFoundException
        throw new AddressNotFoundException("尝试访问的收货地址数据不存在");
    }

    // 判断查询结果中的uid与参数uid是否不一致(使用equals()判断)
    if (!result.getUid().equals(uid)) {
        // 是:抛出AccessDeniedException
        throw new AccessDeniedException("非法访问的异常");
    }

    // 调用addressMapper的updateNonDefaultByUid()将该用户的所有收货地址全部设置为非默认,并获取返回受影响的行数
    Integer rows = addressMapper.updateNonDefaultByUid(uid);
    // 判断受影响的行数是否小于1(不大于0)
    if (rows < 1) {
        // 是:抛出UpdateException
        throw new UpdateException("设置默认收货地址时出现未知错误[1]");
    }

    // 调用addressMapper的updateDefaultByAid()将指定aid的收货地址设置为默认,并获取返回的受影响的行数
    rows = addressMapper.updateDefaultByAid(aid, username, new Date());
    // 判断受影响的行数是否不为1
    if (rows != 1) {
        // 是:抛出UpdateException
        throw new UpdateException("设置默认收货地址时出现未知错误[2]");
    }
}

3.在AddressServiceTests测试类,编写并执行单元测试。

@Test
public void setDefault() {
    try {
        Integer aid = 13;
        Integer uid = 27;
        String username = "系统管理员";
        addressService.setDefault(aid, uid, username);
        System.out.println("OK.");
    } catch (ServiceException e) {
        System.out.println(e.getClass().getSimpleName());
        System.out.println(e.getMessage());
    }
}

3 默认收货地址-控制器

3.1 处理异常

在BaseController类中添加处理AddressNotFoundException和AccessDeniedException的异常。

// ...
else if (e instanceof AddressNotFoundException) {
    result.setState(4004);
} else if (e instanceof AccessDeniedException) {
    result.setState(4005);
}
// ...
3.2 设计请求

设计用户提交的请求,并设计响应的方式。

请求路径:/addresses/{aid}/set_default
请求参数:@PathVaraible("aid") Integer aid, HttpSession sesion
请求类型:POST
响应结果:JsonResult<Void>

REST即表述性状态传递(Representational State Transfer,简称REST)是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。它是一种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。

3.3 处理请求

1.在AddressController类中添加处理请求的setDefault(@PathVariable(“aid”) Integer aid, HttpSession session)方法。

@RequestMapping("{aid}/set_default")
public JsonResult<Void> setDefault(@PathVariable("aid") Integer aid, HttpSession session) {
    Integer uid = getUidFromSession(session);
    String username = getUsernameFromSession(session);
    addressService.setDefault(aid, uid, username);
    return new JsonResult<Void>(OK);
}

2.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/addresses/13/set_default进行测试。

在这里插入图片描述

4 默认收货地址-前端页面

1.在address.html页面中body标签内部的script标签内,添加设置用户默认收货地址的代码。

function setDefault(aid) {
    $.ajax({
        url: "/addresses/" + aid + "/set_default",
        type: "POST",
        dataType: "JSON",
        success: function(json) {
            if (json.state == 200) {
                showAddressList();
            } else {
                alert("设置默认收货地址失败!" + json.message);
            }
        },
        error: function(xhr) {
            alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + xhr.status);
            location.href = "login.html";
        }
    });
}

2.给showAddressList()方法中的“设为默认”超链接按钮添加设置默认收货地址的点击事件。

<td><a onclick="setDefault(#{aid})" class="btn btn-xs add-def btn-default">设为默认</a></td>

3.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/address.html页面,点击“设为默认”超链接按钮进行功能测试。

删除收货地址

1 删除收货地址-持久层

1.1 规划需要执行的SQL语句

1.在删除之前,需检查数据是否存在,数据归属是否正确。此功能已完成,无需再次开发。

2.删除指定的收货地址的SQL语句大致是。

delete from t_address where aid=?

3.如果删除的这条数据是默认收货地址,则应该将剩余的收货地址中的某一条设置为默认收货地址,可以设定规则“将最近修改的设置为默认收货地址”,要实现此功能就必须要知道“最近修改的收货地址的id是多少”。则通过以下查询语句完成。

select * from t_address where uid=? order by modified_time desc limit 0,1

4.在执行以上操作之前,还需检查该用户的收货地址数据的数量,如果删除的收货地址是最后一条收货地址,则删除成功后无需再执行其他操作。统计收货地址数量的功能此前已经完成,无需再次开发。

1.2 接口与抽象方法

在AddressMapper接口中添加抽象方法。

/**
 * 根据收货地址id删除数据
 * @param aid 收货地址id
 * @return 受影响的行数
 */
Integer deleteByAid(Integer aid);

/**
 * 查询某用户最后修改的收货地址
 * @param uid 归属的用户id
 * @return 该用户最后修改的收货地址,如果该用户没有收货地址数据则返回null
 */
Address findLastModified(Integer uid);
1.3 配置SQL映射

1.在AddressMapper.xml文件中添加以上两个抽象方法的映射。

<!-- 根据收货地址id删除数据:Integer deleteByAid(Integer aid) -->
<delete id="deleteByAid">
    DELETE FROM
        t_address
    WHERE
        aid=#{aid}
</delete>

<!-- 查询某用户最后修改的收货地址:Address findLastModified(Integer uid) -->
<select id="findLastModified" resultMap="AddressEntityMap">
    SELECT
        *
    FROM
        t_address
    WHERE
        uid=#{uid}
    ORDER BY
        modified_time DESC
        LIMIT 0,1
</select>

2.在AddressMapperTests测试类中添加单元测试方法。

@Test
public void deleteByAid() {
    Integer aid = 4;
    Integer rows = addressMapper.deleteByAid(aid);
    System.out.println("rows=" + rows);
}

@Test
public void findLastModified() {
    Integer uid = 30;
    Address result = addressMapper.findLastModified(uid);
    System.out.println(result);
}

2 删除收货地址-业务层

2.1 规划异常

在执行删除操作时,可能会删除数据失败,此时抛出DeleteException异常。在创建com.cy.store.service.ex.DeleteException异常类,并继承自ServiceException类。

package com.cy.store.service.ex;

/** 删除数据失败的异常 */
public class DeleteException extends ServiceException {
    // Override Methods...
}
2.2 接口与抽象方法

在IAddressService接口中添加删除收货地址的抽象方法。

/**
 * 删除收货地址
 * @param aid 收货地址id
 * @param uid 归属的用户id
 * @param username 当前登录的用户名
 */
void delete(Integer aid, Integer uid, String username);
2.3 实现抽象方法

1.在AddressServiceImpl实现类中实现以上两个抽象方法。

@Transactional
@Override
public void delete(Integer aid, Integer uid, String username) {
    // 根据参数aid,调用findByAid()查询收货地址数据
    Address result = addressMapper.findByAid(aid);
    // 判断查询结果是否为null
    if (result == null) {
        // 是:抛出AddressNotFoundException
        throw new AddressNotFoundException("尝试访问的收货地址数据不存在");
    }

    // 判断查询结果中的uid与参数uid是否不一致(使用equals()判断)
    if (!result.getUid().equals(uid)) {
        // 是:抛出AccessDeniedException:非法访问
        throw new AccessDeniedException("非常访问");
    }

    // 根据参数aid,调用deleteByAid()执行删除
    Integer rows1 = addressMapper.deleteByAid(aid);
    if (rows1 != 1) {
        throw new DeleteException("删除收货地址数据时出现未知错误,请联系系统管理员");
    }

    // 判断查询结果中的isDefault是否为0
    if (result.getIsDefault() == 0) {
        return;
    }

    // 调用持久层的countByUid()统计目前还有多少收货地址
    Integer count = addressMapper.countByUid(uid);
    // 判断目前的收货地址的数量是否为0
    if (count == 0) {
        return;
    }

    // 调用findLastModified()找出用户最近修改的收货地址数据
    Address lastModified = addressMapper.findLastModified(uid);
    // 从以上查询结果中找出aid属性值
    Integer lastModifiedAid = lastModified.getAid();
    // 调用持久层的updateDefaultByAid()方法执行设置默认收货地址,并获取返回的受影响的行数
    Integer rows2 = addressMapper.updateDefaultByAid(lastModifiedAid, username, new Date());
    // 判断受影响的行数是否不为1
    if (rows2 != 1) {
        // 是:抛出UpdateException
        throw new UpdateException("更新收货地址数据时出现未知错误,请联系系统管理员");
    }
}

2.在AddressServiceTests测试类中添加单元测试方法。

@Test
public void delete() {
    try {
        Integer aid = 18;
        Integer uid = 30;
        String username = "明明";
        addressService.delete(aid, uid, username);
        System.out.println("OK.");
    } catch (ServiceException e) {
        System.out.println(e.getClass().getSimpleName());
        System.out.println(e.getMessage());
    }
}

3 删除收货地址-控制器

3.1 处理异常

在BaseController类中添加DeleteException异常的处理。

// ...
else if (e instanceof DeleteException) {
    result.setState(5002);
}
// ...
3.2 设计请求

设计用户提交的请求,并设计响应的方式。

请求路径:/addresses/{aid}/delete
请求参数:@PathVariable("aid") Integer aid, HttpSession session
请求类型:POST
响应结果:JsonResult<Void>
3.3 处理请求

1.在AddressController类中添加处理请求的delete()方法。

@RequestMapping("{aid}/delete")
public JsonResult<Void> delete(@PathVariable("aid") Integer aid, HttpSession session) {
    Integer uid = getUidFromSession(session);
    String username = getUsernameFromSession(session);
    addressService.delete(aid, uid, username);
    return new JsonResult<Void>(OK);
}

2.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/addresses/26/delete进行测试。

4 删除收货地址-前端页面

1.在address.html页面中body标签内部的script标签内,添加设置用户删除收货地址的代码。

function deleteByAid(aid) {
    $.ajax({
        url: "/addresses/" + aid + "/delete",
        type: "POST",
        dataType: "JSON",
        success: function(json) {
            if (json.state == 200) {
                showAddressList();
            } else {
                alert("删除收货地址失败!" + json.message);
            }
        },
        error: function(json) {
            alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + json.status);
            location.href = "login.html";
        }
    });
}

2.给showAddressList()方法中的“设为默认”超链接按钮添加设置默认收货地址的点击事件。

<td><a onclick="deleteByAid(#{aid})" class="btn btn-xs add-del btn-info"><span class="fa fa-trash-o"></span> 删除</a></td>

3.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/address.html页面,点击“删除”超链接按钮进行功能测试。

{
System.out.println(e.getClass().getSimpleName());
System.out.println(e.getMessage());
}
}


### 3 删除收货地址-控制器

#### 3.1 处理异常

在BaseController类中添加DeleteException异常的处理。

```java
// ...
else if (e instanceof DeleteException) {
    result.setState(5002);
}
// ...
3.2 设计请求

设计用户提交的请求,并设计响应的方式。

请求路径:/addresses/{aid}/delete
请求参数:@PathVariable("aid") Integer aid, HttpSession session
请求类型:POST
响应结果:JsonResult<Void>
3.3 处理请求

1.在AddressController类中添加处理请求的delete()方法。

@RequestMapping("{aid}/delete")
public JsonResult<Void> delete(@PathVariable("aid") Integer aid, HttpSession session) {
    Integer uid = getUidFromSession(session);
    String username = getUsernameFromSession(session);
    addressService.delete(aid, uid, username);
    return new JsonResult<Void>(OK);
}

2.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/addresses/26/delete进行测试。

4 删除收货地址-前端页面

1.在address.html页面中body标签内部的script标签内,添加设置用户删除收货地址的代码。

function deleteByAid(aid) {
    $.ajax({
        url: "/addresses/" + aid + "/delete",
        type: "POST",
        dataType: "JSON",
        success: function(json) {
            if (json.state == 200) {
                showAddressList();
            } else {
                alert("删除收货地址失败!" + json.message);
            }
        },
        error: function(json) {
            alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + json.status);
            location.href = "login.html";
        }
    });
}

2.给showAddressList()方法中的“设为默认”超链接按钮添加设置默认收货地址的点击事件。

<td><a onclick="deleteByAid(#{aid})" class="btn btn-xs add-del btn-info"><span class="fa fa-trash-o"></span> 删除</a></td>

3.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/address.html页面,点击“删除”超链接按钮进行功能测试。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1258257.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日汇评:黄金有望在美欧通货数据周回升至2020美元上方

金价在2000美元以上占据主导地位&#xff0c;巩固了其2018美元的六个月高点&#xff1b; 美元在避险情绪中暂停下跌&#xff0c;美债收益率小幅上升&#xff1b; 金价本周收于2000美元以上&#xff0c;在关键通胀数据公布之前将有更多涨幅&#xff1b; 黄金价格已经从周一亚洲早…

SOLIDWORKS髙级孔命令及相关问题

本文介绍的是SOLIDWORKS髙级孔的命令应用。髙级孔主要用来做一些模型上的组合孔特征。我们先来看一个典型的例子&#xff0c;如图1所示&#xff0c;将轴侧视图能看到的3个面先进行“更改透明度”操作&#xff0c;以便看到模型内部孔的特征。 添加图片注释&#xff0c;不超过 14…

配电房无人值守监控系统

配电房无人值守监控系统是一种特殊的智能化监控系统&#xff0c;专为无人值守的配电房设计。依托电易云-智慧电力物联网&#xff0c;它采用先进的技术手段&#xff0c;实现对配电房环境、设备等的全方位实时监测和自动控制&#xff0c;确保配电房在无人员在场的情况下仍能安全、…

小波降噪的原理,以及软阈值函数和硬阈值函数的详细定义,应用和区别,以及数学公式的解释!!!看完你就懂了软阈值函数和硬阈值函数

文章目录 前言一、软阈值函数和硬阈值函数是什么&#xff1f;二、软阈值函数和硬阈值函数的区别三、软阈值函数和硬阈值函数的应用四、软阈值函数和硬阈值函数的数学公式总结 前言 小波降噪是一种应用小波理论的信号降噪方法&#xff0c;主要通过减少噪声的干扰&#xff0c;同…

Intellij Idea 断点小圆变成灰色怎么处理

场景1&#xff1a;变成了灰色实心圆 原因 断点变成灰色通常表示该断点处于失效状态。这可能是由于无意中点击了debug调试下方的“mute breakpoints”按钮导致的。 解决方案 依次点击设置小图标->View Options->Mute BreakPoints. 点击后 Mute BrakPoints左侧显示✔ 符号…

亥姆霍兹线圈的组成

亥姆霍兹线圈是由两个半径、匝数、电流★全相同的线圈&#xff0c;距离为半径长度&#xff0c;运行电流方向相同组成。 亥姆霍兹线圈是一种产生均匀磁场的线圈&#xff0c;其磁场特点是在内部产生均匀度较高的磁场&#xff0c;一般长螺线管的均匀度要优于赫姆霍兹线圈&#xf…

2020年06月 Scratch(四级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共15题,每题2分,共30分) 第1题 执行下图程序后,“花名”列表的第3项是? A:莲花 B:丁香 C:合欢 D:月季 答案:C 列表基本知识,选C。 第2题 执行如下图所示程序后,其结果为? A: B:

Linux(CentOS7.5):新增硬盘分区纪实

一、服务器概述 1、既有一块系统硬盘&#xff0c;新增一块100G硬盘。 2、要求&#xff0c;将新插入硬盘分为&#xff1a;20G、30G、50G。 二、操作步骤 1、确认新硬盘是否插入成功&#xff1a; fdisk -l# 红色框出来的&#xff0c;为识别出来的新硬盘信息 # 黄色框出来的&#…

C语言——输入 10 个数,分别统计其中正数、负数、零的个数

#include <stdio.h> int main() {int numbers[10]; // 存储输入的10个数int positive_count 0; // 正数计数器int negative_count 0; // 负数计数器int zero_count 0; // 零计数器// 输入10个数printf("请输入10个数&#xff1a;\n");for (int i 0; i …

Flask Session 登录认证模块

Flask 框架提供了强大的 Session 模块组件&#xff0c;为 Web 应用实现用户注册与登录系统提供了方便的机制。结合 Flask-WTF 表单组件&#xff0c;我们能够轻松地设计出用户友好且具备美观界面的注册和登录页面&#xff0c;使这一功能能够直接应用到我们的项目中。本文将深入探…

解读向量数据库

不论是RAG&#xff0c;还是Agent&#xff0c;几乎每个LLM 驱动的应用程序都可能会用到向量数据库。那么&#xff0c;向量数据库是什么&#xff1f;与传统数据库有何不同&#xff1f; 又如何选择向量数据库呢&#xff1f; 本文是老码农关于向量数据库的学习笔记。 1. 什么是向量…

PC删除数据,并提示删除成功

<template<el-button size"mini" type"text">分配权限</el-button><el-button size"mini" type"text" click"btnEditRow(row)">编辑</el-button ><el-popconfirmtitle"这是一段内容确定…

通付盾Web3专题 | SharkTeam:起底朝鲜APT组织Lazarus Group,攻击手法及洗钱模式

国家级APT&#xff08;Advanced Persistent Threat&#xff0c;高级持续性威胁&#xff09;组织是有国家背景支持的顶尖黑客团伙&#xff0c;专门针对特定目标进行长期的持续性网络攻击。朝鲜APT组织Lazarus Group就是非常活跃的一个APT团伙&#xff0c;其攻击目的主要以窃取资…

P2704 [NOI2001] 炮兵阵地 题解

P2704 题目题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1提示 解题思路分析Code更多方法 题目 原题链接 题目描述 司令部的将军们打算在 N M N\times M NM 的网格地图上部署他们的炮兵部队。 一个 N M N\times M NM 的地图由 N N N 行 M M M 列组成&#x…

Android 12 打开网络ADB并禁用USB连接ADB

平台 RK3588 Android 12 Android 调试桥 (adb) Android 调试桥 (adb) 是一种功能多样的命令行工具&#xff0c;可让您与设备进行通信。adb 命令可用于执行各种设备操作&#xff0c;例如安装和调试应用。adb 提供对 Unix shell&#xff08;可用来在设备上运行各种命令&am…

代码块02使用细节-Java

代码块02使用细节 四、使用细节1、static代码块/静态代码块&#xff0c;随着类的加载而执行&#xff0c;且只执行一次2、 类什么时候被加载 [重要 ! ]案例演示&#xff1a;static代码块 3、普通代码块&#xff0c;在创建对象实例时&#xff0c;会被隐式的调用。案例演示&#x…

2023 中国 Serverless 用户调查,邀您填写!

当前云计算已成为数字时代的基础设施&#xff0c;支撑众多企业进行数字化转型升级。随着企业上云的范围更加广泛&#xff0c;国内云计算正在迈向云原生时代。Serverless技术因其以应用为中心、屏蔽底层复杂逻辑&#xff0c;灵活扩展&#xff0c;按需取用的特点&#xff0c;已经…

【解决方案】安科瑞智能照明系统在福建二建大厦项目上的研究与应用

【摘要】&#xff1a;智能化已经成为当今建筑发展的主流技术、涵盖从空调系统、消防系统到安全防范系统以及完善的计算机网络和通信系统。但是长期以来、智能照明在国内一直被忽视、大多数建筑物仍然沿用传统的照明控制方式、部分智能大厦采用楼宇自控&#xff08;BA&#xff0…

如何在vs2017及以前版本(vs2010、vs2015)上添加 添加类型库中的MFC类

有时候当我们新建MFC工程需要使用到微软的一些自带控件&#xff0c;如播放视频要用到Windows media player控件&#xff0c;这时&#xff0c;我们可以通过添加“ActiveX控件中的mfc类(A)”这一选项. 还有有时候我们需要用到“类型库中的MFC类(T)及“MFC ODBC使用者(O)”。那我们…

如何获取抖音订单API数据接口?

在开放平台中&#xff0c;每个API接口都有相应的文档说明和授权机制&#xff0c;以确保数据的安全性和可靠性。开发者可以根据自己的需求选择相应的API接口&#xff0c;并根据文档说明进行调用和使用。 开放平台API接口是一套REST方式的开放应用程序编程接口&#xff0c;它…