[TOC]

项目介绍

尚筹网项目介绍

本项目视频为B站尚硅谷的尚筹网课程,为单一架构过渡到分布式架构的项目,使用Maven管理项目,后台使用SSM框架单一架构,前台使用Springboot和SpringCloud分布式架构(其中会使用SpringSecurity做权限控制,SpringSeesion等Spring家族的产品),数据存储使用mysql,redis数据库,页面显示部分后台页面使用jsp,前台页面使用html+Thymeleaf视图解析器。

尚筹网项目构架

尚筹网 - 图1

环境搭建

总体目标

尚筹网 - 图2

创建工程

  • 项目关系如下

尚筹网 - 图3

  • 工程创建计划
  • atcrowdfunding01-admin-parent

groupId:com.atguigu.crowd

artifactId:atcrowdfunding01-admin-parent

packaging:pom

atcrowdfunding02-admin-webui

groupId:com.atguigu.crowd

artifactId:atcrowdfunding02-admin-webui

packaging:war

atcrowdfunding03-admin-component

groupId:com.atguigu.crowd

artifactId:atcrowdfunding03-admin-component

packaging:jar

atcrowdfunding04-admin-entity

groupId:com.atguigu.crowd

artifactId:atcrowdfunding04-admin-entity

packaging:jar

atcrowdfunding05-common-util

groupId:com.atguigu.crowd

artifactId:atcrowdfunding05-common-util

packaging:jar

atcrowdfunding06-common-reverse

groupId:com.atguigu.crowd

artifactId:atcrowdfunding06-common-reverse

packaging:jar

  • 建立工程之间的依赖关系
  • webui 依赖 component
  • component 依赖 entity
  • component 依赖 util
  • 在IDEA模板pom中导入dependence依赖即可
  • 尚筹网 - 图4

创建数据库和数据库表

  • 物理建模
    • 第一范式:数据库表中的每一列都不可再分,也就是原子性
  • 尚筹网 - 图5

  • 这个表中“部门”和“岗位”应该拆分成两个字段:“部门名称”、“岗位”。
  • 尚筹网 - 图6

  • 这样才能够专门针对“部门”或“岗位”进行查询。
    • 第二范式:在满足第一范式基础上要求每个字段都和主键完整相关,而不是仅和主键部分相关(主要针对联合主键而言)
    • 尚筹网 - 图7

    • “订单详情表”使用“订单编号”和“产品编号”作为联合主键。此时“产品价格”、“产品数量”都和联合主键整体相关,但“订单金额”和“下单时间” 只和联合主键中的“订单编号”相关,和“产品编号”无关。所以只关联了主键中的部分字段,不满足第二范式。
    • 把“订单金额”和“下单时间”移到订单表就符合第二范式了
    • 尚筹网 - 图8

    • 第三范式:表中的非主键字段和主键字段直接相关,不允许间接相关
    • 尚筹网 - 图9

    • 上面表中的“部门名称”和“员工编号”的关系是“员工编号”→“部门编号”
    • →“部门名称”,不是直接相关。此时会带来下列问题:
      • 数据冗余:“部门名称”多次重复出现。
      • 插入异常:组建一个新部门时没有员工信息,也就无法单独插入部门 信息。就算强行插入部门信息,员工表中没有员工信息的记录同样是 非法记录。
      • 删除异常:删除员工信息会连带删除部门信息导致部门信息意外丢失。
      • 更新异常:哪怕只修改一个部门的名称也要更新多条员工记录。 正确的做法是:把上表拆分成两张表,以外键形式关联
      • 尚筹网 - 图10

      • “部门编号”和“员工编号”是直接相关的。
      • 第二范式的另一种表述方式是:两张表要通过外键关联,不保存冗余字段。例 如:不能在“员工表”中存储“部门名称”

建表:

CREATE DATABASE project_rowd CHARACTER SET utf8;

USE project_rowd;
drop table if exists t_admin; # 如果存在t_admin则删除存在的表
CREATE TABLE t_admin (
id INT NOT NULL auto_increment, # 主键
login_acct VARCHAR ( 255 ) NOT NULL, # 登录账号
user_pswd CHAR ( 32 ) NOT NULL, # 登录密码
user_name VARCHAR ( 255 ) NOT NULL, # 昵称
email VARCHAR ( 255 ) NOT NULL, # 邮件地址
create_time CHAR ( 19 ), # 创建时间
PRIMARY KEY ( id ) # 设置主键
);
进行基于Maven的逆向工程(根据已存在的表,在项目中逆向生成对应的实体类、Mapper文件、Mapper接口)

在reverse模块中进行逆向:

pom.xml中导入依赖





org.mybatis.generator
mybatis-generator-maven-plugin
1.3.0


org.mybatis.generator
mybatis-generator-core
1.3.2


com.mchange
c3p0
0.9.2



mysql
mysql-connector-java
8.0.15




#### 编写generatorConfig.xml文 <?xml version=”1.0” encoding=”UTF-8”?>
<!DOCTYPE generatorConfiguration
PUBLIC “-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN”
http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">





driverClass=”com.mysql.cj.jdbc.Driver”
connectionURL=”jdbc:mysql://localhost:3306/project_crowd?serverTimezone=UTC”
userId=”root”
password=”root”>






















在IDEA中进行逆向工程的方法:
尚筹网 - 图11 运行完后,应当对产生的所有文件各归各位(Mapper接口放入component的mapper包下;实体类放入entity模块的entity包;xxxMapper.xml放入webui的resources文件夹下(xml放在web模块下方便寻找) ## 通过父工程管理依赖版本 在父工程通过dependencyManagement标签管理依赖版本,但是在子工程正式通过dependencies标签导入依赖前,这些依赖并不会生效


4.3.20.RELEASE
4.2.10.RELEASE





org.springframework
spring-orm
${fall.spring.version}



org.springframework
spring-webmvc
${fall.spring.version}


org.springframework
spring-test
${fall.spring.version}




org.aspectj
aspectjweaver
1.9.2



cglib
cglib
2.2



mysql
mysql-connector-java
8.0.15



com.alibaba
druid
1.1.17



org.mybatis
mybatis
3.2.8



org.mybatis
mybatis-spring
1.2.2



com.github.pagehelper
pagehelper
4.0.0


org.slf4j
slf4j-api
1.7.7


ch.qos.logback
logback-classic
1.2.3



org.slf4j
jcl-over-slf4j
1.7.25


org.slf4j
jul-to-slf4j
1.7.25


com.fasterxml.jackson.core
jackson-core
2.11.0


com.fasterxml.jackson.core
jackson-databind
2.11.0


jstl
jstl
1.2


junit
junit
4.12
test


javax.servlet
servlet-api
2.5
provided


javax.servlet.jsp
jsp-api
2.1.3-b06
provided



com.google.code.gson
gson
2.8.5


org.springframework.security
spring-security-web
${fall.spring.security.version}



org.springframework.security
spring-security-config
${fall.spring.security.version}



org.springframework.security
spring-security-taglibs
${fall.spring.security.version}

## Spring整合MyBatis 思路:
尚筹网 - 图12 ### 1、配置Maven依赖


org.example
crowdfunding04-admin-entity
1.0-SNAPSHOT



org.example
crowdfunding05-common-util
1.0-SNAPSHOT



org.springframework
spring-orm


commons-logging
commons-logging





org.springframework
spring-webmvc




org.aspectj
aspectjweaver



cglib
cglib



mysql
mysql-connector-java



com.alibaba
druid



org.mybatis
mybatis



org.mybatis
mybatis-spring



com.github.pagehelper
pagehelper



com.fasterxml.jackson.core
jackson-core


com.fasterxml.jackson.core
jackson-databind


jstl
jstl



com.google.code.gson
gson

junit
junit
test

org.springframework
spring-test

### 2.创建配置文件 - mybatis-config.xml 全局配置文件 - <?xml version=”1.0” encoding=”UTF-8”?>
<!DOCTYPE configuration PUBLIC “-//mybatis.org//DTD Config 3.0/EN”
http://mybatis.org/dtd/mybatis-3-config.dtd">


- jdbc.properties - jdbc.user=root
jdbc.password=…
jdbc.Driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/project_crowd?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8 - spring-persist-mybatis.xml 整合mybatis文件 - <?xml version=”1.0” encoding=”UTF-8”?>
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance
xmlns:context=”http://www.springframework.org/schema/context
xsi:schemaLocation=”http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd”>









### 3.测试 package test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException; @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {“classpath:spring-persist-mybatis.xml”})
public class CrowdTest {
@Autowired
private DataSource dataSource; @Test
public void testConnect() throws SQLException {
Connection connection = dataSource.getConnection();
System.out.println(connection);
}
}
可以打印出connection的数据,而不是报空指针异常,得出结论:整合成功。 #### 遇到的bug 无法创建sqlsessionfactory,原因是在逆向工程生成后,mybatis文件进行了移动,导致mybatis文件中namespace,resultmap中全类名错误。 ## 日志系统整合 ### 日志的意义 大量使用sysout不仅耗时,而且消耗人力,通过日志运行级别可以批量控制打印信息
尚筹网 - 图13 ### 技术选型 尚筹网 - 图14 ### 不同日志系统的整合 尚筹网 - 图15 ### 替换Spring自带的日志 由于spring自带common-logging的日志包,使用 jcl-over-slf4j.jar来代替common-logging,做接口转换功能,在jcl-over-slf4j下可以实现slf4j-api.jar接口,进而使用logback #### 添加依赖 在component中添加


org.slf4j
slf4j-api


ch.qos.logback
logback-classic



org.slf4j
jcl-over-slf4j


org.slf4j
jul-to-slf4j
#### 日志级别 - DEBUG - INFO - WARN - ERROR 等级 DEBUG < INFO < WARN < ERROR 会打印和自己一样和比自己高的运行级别
使用方法:
@Test
public void logTest(){
//获取Logger对象,这里传入的Class就是当前打印日志的类
Logger logger = LoggerFactory.getLogger(CrowdTest.class);
logger.debug(“DEBUG!!!”);
logger.info(“INFO!!!”);
logger.warn(“WARN!!!”);
logger.error(“ERROR!!!”);
} #### logback.xml实现自定义配置日志 <?xml version=”1.0” encoding=”UTF-8”?>






[%d{HH:mm:ss.SSS}] [%-5level] [%-8thread] [%logger] [%msg]%n










## 声明式事务 ### 事务 - 要么成功,要么失败 - 特性: - 一致性 - 原子性 - 持久性 - 隔离性 - 对应AOP中通知类型 - 尚筹网 - 图16 -
### 目标 在框架下通过一系列配置使spring来管理事务操作 ### 思路 xml使用配置事务的流程:
尚筹网 - 图17 ### 配置 - 在component中创建service包 - 尚筹网 - 图18 -
- 创建spring-persist-tx.xml文件单独进行事务配置 - (注:基于xml事务配置中,事务属性method必须配置,如果某个方法没有配置对应的txmethod,事务对方法可能不生效) <?xml version=”1.0” encoding=”UTF-8”?>
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance
xmlns:context=”http://www.springframework.org/schema/context
xmlns:tx=”http://www.springframework.org/schema/tx
xmlns:aop=”http://www.springframework.org/schema/aop
xsi:schemaLocation=”http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd”>






























- 测试: - 测试时,抛出异常,回滚不插入数据,则配置事务。 - @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {“classpath:spring-persist-mybatis.xml”,”classpath:spring-persist-tx.xml”})
public class CrowdTest {
@Autowired
private DataSource dataSource; @Autowired
private AdminMapper adminMapper; @Autowired
private AdminService adminService;
@Test
public void txTest(){
Admin admin = new Admin(null,”lily”,”321”,”丽丽”,”lily@qq.com”,null);
adminService.saveAdmin(admin);
}
} ## Spring整合SpringMVC ### 目标 1. handler中装配Service 1. 页面能够访问handler ### 思路 表述层配置文件关系
尚筹网 - 图19 ### 配置 #### web.xml具体配置 - 在web.xml中配置ContextLoaderListener -

contextConfigLocation
classpath:spring-persist-.xml



org.springframework.web.context.ContextLoaderListener
- 在web.xml中配置filter -


characterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter

encoding
UTF-8



forceRequestEncoding
true



forceResponseEncoding
true


<!—配置过滤器的过滤路径,/
全部路径—>

characterEncodingFilter
/*
- 在web.xml中配置前端控制器DispatcherServlet -

dispatcherServlet
org.springframework.web.servlet.DispatcherServlet


contextConfigLocation
classpath:spring-web-mvc.xml


1



dispatcherServlet
<!--  url-pattern配置方式二:配置请求扩展名  --><br />    <!--  优点:<br />                1.静态资源不通过SpringMVC,不需要特殊处理<br />                2.实现伪静态效果<br />                  (1)给黑客入侵增加难度<br />                   (2)有利于SEO优化<br />           缺点:不符合RESTFul风格--><br />    <url-pattern>*.html</url-pattern><br />    <!--如果一个Ajax请求扩展名时是html,但实际返回json数据,与实际不匹配,会报406错误<br />        为了让Ajax顺利拿到json数据,配置json扩展名--><br />    <url-pattern>*.json</url-pattern><br />  </servlet-mapping>

SpringMVC配置文件具体配置

测试

  • index.jsp
  • <%@ page contentType=”text/html;charset=UTF-8” language=”java” %>




    <%—
    base 标签必须写在 head 标签内部
    base 标签必须在所有“带具体路径”的标签的前面
    serverName 部分 EL 表达式和 serverPort 部分 EL 表达式之间必须写“:”
    serverPort 部分 EL 表达式和 contextPath 部分 EL 表达式之间绝对不能写“/”
    原因:contextPath 部分 EL 表达式本身就是“/”开头
    如果多写一个“/”会干扰 Cookie 的工作机制
    serverPort 部分 EL 表达式后面必须写“/”
    —%>


    Hello World!


    测试页面

  • handler
  • @Controller
    public class TestHandler {

    @Autowired
    AdminService adminService;

    @RequestMapping(“/test/ssm.html”)
    public String testSSM(Model model){
    List admins = adminService.getAll();
    model.addAttribute(“admins”, admins);
    return “target”;
    }
    }

AJAX请求

简述

尚筹网 - 图20

@RequestBody和@RespondBody要生效需要jackson依赖,确定是否导入依赖,同时配置mvc:annotation-driven。


com.fasterxml.jackson.core
jackson-core


com.fasterxml.jackson.core
jackson-databind

导入jquery

尚筹网 - 图21

导入后记得刷新项目

Ajax测试

jsp页面





请求出现了错误QAQ


错误消息:${requestScope.exception.message}









## 管理员登录页 引入静态资源,放入webapp下
尚筹网 - 图22 创建admin-login.jsp页面,修改表单,添加base标签(注意放在引入css,js前面)

配置视图控制器,进行页面跳转

## 使用Layer弹框组件 引入Layer文件,并在页面引入js(注意在jquery后面引入)
# 管理员系统 ## 管理员登录 ### 目标 识别登陆者身份,控制他行为,赋予他权限。 ### 思路 尚筹网 - 图23 ### 创建MD5加密工具方法 在CrowdUtils类加入静态方法
/
对明文字符进行MD5加密

@param source 传入明文字符
@return
*/
public static String md5(String source) {
// 1.判断source是否有效
if (source == null || source.length() == 0) {
// 2.如果不是有效字符抛出异常
throw new RuntimeException(CrowdConstant.MESSAGE_STRING_INVALIDATE);
}
try {
// 3.获取MessageDigest对象
String algorithm = “md5”;
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
// 4.获取字符串解析数组
byte[] input = source.getBytes();
// 5.执行加密
byte[] output = messageDigest.digest(input);
// 6.创建BigInteger对象
int signum = 1;
BigInteger bigInteger = new BigInteger(signum, output);
// 7.按照16进制将值转化为字符串
int radix = 16;
String encoded = bigInteger.toString(radix).toUpperCase();
// 8.返回加密字符串
return encoded;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
} ### 自定义登录失败异常类 - 在exception包下创建LoginFailedException - package org.fall.exception;
/

登录失败的异常
/
public class LoginFailedException extends RuntimeException {
private static final long serialVersionUID = 1L; public LoginFailedException() {
} public LoginFailedException(String message) {
super(message);
} public LoginFailedException(String message, Throwable cause) {
super(message, cause);
} public LoginFailedException(Throwable cause) {
super(cause);
} public LoginFailedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
} - 修改异常处理器 - //处理登录失败异常
@ExceptionHandler(value = {LoginFailedException.class})
public ModelAndView nullLoginFailedExceptionResolver(
//实际捕获到的类型
NullPointerException nullPointerException,
//当前请求对象
HttpServletRequest request,
//当前响应对象
HttpServletResponse response
//指定普通页时去的错误页面
) throws IOException {
String viewName=”system-error”;
return conmonResolver(nullPointerException,request,response,viewName);
} - 将错误信息显示到admin-login.jsp页面 -

${requestScope.exception.message}

### 创建控制管理员登录的Handler方法 @Controller
public class AdminHandler { @Autowired
AdminService adminService; @RequestMapping(“/admin/do/login.html”)
public String doLogin(
@RequestParam(“loginAcct”) String loginAcct,
@RequestParam(“loginPswd”) String loginPswd,
HttpSession session
) {
// 调用Service方法检查登录
// 这个方法如果返回Admin对象则登录成功,如果账号密码不正确则抛出异常
Admin admin = adminService.getAdminByLoginAcct(loginAcct, loginPswd);
// 将登录成功返回的对象存入session域中
session.setAttribute(CrowdConstant.ATTR_NAME_LOGIN_ADMIN, admin);
// 返回主页面
return “admin-main”;
}
} ### Service层实现业务逻辑 获取管理员的登录信息方法体
@Override
public Admin getAdminByLoginAcct(String loginAcct, String loginPswd) {
// 1.根据登录账号查询Admin对象
// 创建Example对象
AdminExample adminExample = new AdminExample();
// 创建Criteria对象
AdminExample.Criteria criteria = adminExample.createCriteria();
// 封装查找的条件
criteria.andLoginAcctEqualTo(loginAcct);
// 调用adminMapper进行查找
List admins = adminMapper.selectByExample(adminExample);
// 2.判断Admin是否为空
if (admins == null && admins.size() == 0) {
// 3.Admin对象为空则抛出异常
throw new LoginFailedException(CrowdConstant.MESSAGE_LOGIN_FAILED);
}
// 是否出现多条数据
if (admins.size() > 1) {
throw new LoginFailedException(CrowdConstant.MESSAGE_SYSTEM_ERROR_LOGIN_NOT_UNIQUE);
}
// 4.Admin对象不为空则取出Admin对象
Admin admin = admins.get(0);
// 5.为空抛出异常,不为空取出密码
if (admin == null) {
throw new LoginFailedException(CrowdConstant.MESSAGE_LOGIN_FAILED);
}
String userPswdDb = admin.getUserPswd();
// 5.将表单提交的数据进行明文加密
String userPswdForm = CrowdUtils.md5(loginPswd);
// 6.对密码进行比较
if (!Objects.equals(userPswdDb, userPswdForm)) {
// 7.不相等抛出异常
throw new LoginFailedException(CrowdConstant.MESSAGE_LOGIN_FAILED);
} else {
// 8.相等则返回Admin对象
return admin;
}
} ### 重定向主页面 - 将admin-main.html页面导入后,修改base标签,和用户名 - doLogin重定向主页面,防止表单重复提交 - // 返回主页面
return “redirect:/admin/to/main/page.html”; - 配置视图控制器,重新响应页面 - ### 退出登录 - 修改超链接地址 -
  • 退出系统
  • - Handler中doLogout方法 - @RequestMapping(“/admin/do/logout.html”)
    public String doLogout(HttpSession session){
    // 强制session失败
    session.invalidate();
    return “redirect:/admin/to/login/page.html”;
    } ### 抽取主页面的公共部分 - 抽取为后,需要引入jsp文件 - <%@include file=”/WEB-INF/include-head.jsp” %>

    <%@include file=”/WEB-INF/include-nav.jsp” %>


    <%@include file=”/WEB-INF/include-sidebar.jsp”%> - setting->Editor->File an Code Templates中创建模板方便下次使用 ## 登录检查 ### 目标 将部分资源保护起来,让没有登录的用户不能访问 ### 思路 尚筹网 - 图24 ### 创建拦截器类 拦截器只需重写preHandle使其继承HandlerInterceptorAdapter即可,在拦截器拦截后,抛出异常来处理
    public class LoginInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
    // 1.通过request对象获取session域
    HttpSession session = httpServletRequest.getSession();
    // 2.获取session中的Admin对象
    Admin admin = (Admin) session.getAttribute(CrowdConstant.ATTR_NAME_LOGIN_ADMIN);
    // 3.admin为空抛出异常
    if (admin == null) {
    throw new AccessForbiddenException(CrowdConstant.MESSAGE_ACCESS_FORBIDEN);
    }
    // 4.不为空则放行
    return true;
    }
    } ### 自定义拒绝访问异常类 处理拦截器抛出的异常,使其返回到登录页面
    public class AccessForbiddenException extends RuntimeException {
    private static final long serialVersionUID = 1L; public AccessForbiddenException() {
    super();
    } public AccessForbiddenException(String message) {
    super(message);
    } public AccessForbiddenException(String message, Throwable cause) {
    super(message, cause);
    } public AccessForbiddenException(Throwable cause) {
    super(cause);
    } protected AccessForbiddenException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
    super(message, cause, enableSuppression, writableStackTrace);
    }
    } ### 配置拦截器 拦截器拦截登录页面,退出,登录请求以外的路径













    ### 配置异常类的映射路径 触发异常则返回登陆页面
    class=”org.springframework.web.servlet.handler.SimpleMappingExceptionResolver”>




    system-error

    admin-login


    ## 管理员维护 ### 查询用户 #### 目标 将数据库的Admin数据在页面上已分页的形式显示,在后端将“带关键字”和“不带关键字”的分页合并为同一套代码。 #### 思路 尚筹网 - 图25 #### 导入pagehelper依赖

    com.github.pagehelper
    pagehelper
    4.0.0
    #### 在mybatis配置文件中配置分页插件








    mysql

    true





    #### AdminMapper添加根据关键字查询sql #### AdminServiceImpl中接口中的实现方法 // 按关键字查询数据,并返回分页对象
    PageInfo getPageInfo(String keyword,Integer pageNum,Integer pageSize);
    使用pagehelper插件封装List对象为pageInfo对象
    @Override
    public PageInfo getPageInfo(String keyword, Integer pageNum, Integer pageSize) {
    // 1.开启pageHelper功能
    // 体现了pageHelper的”非侵入设计“,原本要做的查询不必有任何修改
    PageHelper.startPage(pageNum, pageSize);
    // 2.按照关键字进行查询
    List admins = adminMapper.selectAdminByKeyword(keyword);
    // 3.返回封装的pageInfo对象
    return new PageInfo(admins);
    } #### 分页hanlder处理请求 当不点击页数是时,需要设置默认值
    @RequestMapping(“/admin/get/page.html”)
    public String getPageInfo(
    // 当值为空时,需要指定默认值
    @RequestParam(value = “keyword”, defaultValue = “”) String keyword,
    @RequestParam(value = “pageNum”, defaultValue = “1”) Integer pageNum,
    @RequestParam(value = “pageSize”, defaultValue = “5”) Integer pageSize,
    ModelMap modelMap
    ) {
    PageInfo pageInfo = adminService.getPageInfo(keyword, pageNum, pageSize);
    modelMap.addAttribute(CrowdConstant.ATTR_NAME_PAGE_INFO,pageInfo);
    return “admin-page”;
    } #### 在分页页面显示信息 - 先导入jstl标签库,进行调用信息显示 - <%@taglib prefix=”c” uri=”http://java.sun.com/jsp/jstl/core“ %> - table标签显示数据 -











    <%— jstl —%>


















    # 账号 名称 邮箱地址 操作
    抱歉,查不到相关的数据!
    ${status.count} ${admin.loginAcct} ${admin.userName} ${admin.email}
    class=”btn btn-success btn-xs”>
    class=”btn btn-primary btn-xs”>
    class=”btn btn-danger btn-xs”>


    #### 分页导航条 - 使用Pagination来选择页码 - 导入pagination的css和jquery文件 - <%—引入pagination的css—%>

    <%—引入基于jquery的paginationjs—%>
    - 分页导航条显示 -






    - js代码 - - 修复造成一直刷新的回调bug - 在jquery.pagination.js中将其注释掉 - // 回调函数
    // opts.callback(current_page, this); #### 关键字查询 - 修改form标签 -
    style=”float:left;”>


    查询条件

    value=”${param.keyword}”/>



    - 在页面切换后,关键字不在请求参数中,在转发请求路径中加入关键字 - window.location.href = “admin/get/page.html?pageNum=” + pageNum+”&keyword=”+${param.keyword}; ### 删除用户 #### 目标 点击删除将数据删除后,返回当前页面。 #### 思路 尚筹网 - 图26 #### 设置按钮跳转路径 使用Restful风格,pageNum在重定向时需要在路径中传入需要显示的页面,keyword在查询条件后删除,在重定向时也需要传入显示关键字查询页面
    class=”btn btn-danger btn-xs”> #### Hander方法 返回时的问题: 1. 直接返回页面会因为没有发送分页请求而无法显示数据 1. 使用转发发送分页请求可以实现页面的显示,但用户刷新后会造成后台重复删除 1. 使用重定向可以防止重复删除 @RequestMapping(“/admin/remove/{adminId}/{pageNum}/{keyword}.html”)
    public String remove(
    @PathVariable(“adminId”) Integer adminId,
    @PathVariable(“pageNum”) Integer pageNum,
    @PathVariable(“keyword”) String keyword
    ){
    adminService.removeOne(adminId);
    return “redirect:/admin/get/page.html?pageNum=”+pageNum+”&keyword=”+keyword;
    } #### AdminService接口 void removeOne(Integer adminId); #### AdminServiceImpl @Override
    public void removeOne(Integer adminId) {
    adminMapper.deleteByPrimaryKey(adminId);
    } ### 新增用户 #### 目标 将表单提交的admin对象保存到数据库中 - loginAcct不能重复 - 密码加密 #### 思路 尚筹网 - 图27 #### 添加唯一索引 给数据库中login_acct字段添加唯一索引,防止用户名重复(在添加时注意表中不能有重复的数据,会报错1062)
    ALTER TABLE t_admin ADD UNIQUE INDEX(login_acct); #### 修改admin-add.jsp页面 修改字段中的name属性和Admin实体对应,修改提交请求路径




    表单数据
    class=”glyphicon glyphicon-question-sign”>




    ${requestScope.exception.message}




    placeholder=”请输入登录账号”>



    placeholder=”请输入用户密码”>



    placeholder=”请输入用户昵称”>
                        <div class="form-group"><br />                            <label for="exampleInputEmail1">邮箱地址</label><br />                            <input type="email" name="email" class="form-control" id="exampleInputEmail1"<br />                                   placeholder="请输入邮箱地址"><br />                            <p class="help-block label label-warning">请输入合法的邮箱地址, 格式为: xxxx@xxxx.com</p><br />                        </div><br />                        <button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-plus"></i> 新增<br />                        </button><br />                        <button type="reset" class="btn btn-danger"><i class="glyphicon glyphicon-refresh"></i> 重置<br />                        </button><br />                    </form><br />                </div><br />            </div><br />        </div><br />    </div><br /></div>
    

    Handler方法

    注意重定向到最后一页
    @RequestMapping(“admin/save.html”)
    public String saveAdmin(Admin admin){
    // 1.调用service保存数据
    adminService.saveAdmin(admin);
    // 2.返回页面,由于新增数据在最后一条,将数据定位到最后一页
    return “redirect:/admin/get/page.html?pageNum=”+Integer.MAX_VALUE;
    }

    AdminServiceImpl中实现方法

    @Override
    public void saveAdmin(Admin admin) {
    // 1.取出密码进行md5加密
    String password=admin.getUserPswd();
    String passwordMd5 = CrowdUtils.md5(password);
    admin.setUserPswd(passwordMd5);
    // 2.设置新增时间
    Date date = new Date();
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);
    String createTime = simpleDateFormat.format(date);
    admin.setCreateTime(createTime);
    // 3.添加数据
    adminMapper.insert(admin);
    }

    处理账户一致异常

    • 由于在mysql中设置了唯一约束,在进行添加时spring会报DuplicateKeyException异常,自定义异常类进行处理
    • public class LoginAcctAlreadyInUseException extends RuntimeException{
      public LoginAcctAlreadyInUseException() {
      }

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

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

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

      public LoginAcctAlreadyInUseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
      super(message, cause, enableSuppression, writableStackTrace);
      }
      }

    • 在CrowdExceptionResolver中添加异常处理
    • // 处理用户名重复的异常
      @ExceptionHandler(value = {LoginAcctAlreadyInUseException.class})
      public ModelAndView loginAcctAlreadyInUseExceptionResolver(
      // 实际捕获到的类型
      LoginAcctAlreadyInUseException LoginAcctAlreadyInUseException,
      // 当前请求对象
      HttpServletRequest request,
      // 当前响应对象
      HttpServletResponse response
      // 指定普通页时去的错误页面
      ) throws IOException {
      String viewName = “admin-add”;
      return conmonResolver(LoginAcctAlreadyInUseException, request, response, viewName);
      }
    • 在增加记录时捕获异常,并抛出
    • // 3.添加数据
      try {
      adminMapper.insert(admin);
      } catch (Exception exception) {
      if(exception instanceof DuplicateKeyException){
      throw new LoginAcctAlreadyInUseException(CrowdConstant.MESSAGE_LOGIN_ACCT_ALREADY_IN_USE);
      }
      }

    更新用户

    目标

    修改现有的Admin,不修改密码和创建时间。

    思路

    尚筹网 - 图28

    表单回显

    • Handler方法
    • @RequestMapping(“/admin/to/edit/page.html”)
      public String toEditPage(
      @RequestParam(“adminId”) Integer adminId,
      @RequestParam(“pageNum”) Integer pageNum,
      @RequestParam(“keyword”) String keyword,
      ModelMap modelMap
      ){
      // 1.调用serivce查询数据
      Admin admin=adminService.getAdminById(adminId);
      // 2.将admin数据保存到model中,用于表单回显数据,pageNum和keyword返回
      modelMap.addAttribute(“admin”,admin);
      modelMap.addAttribute(“pageNum”,pageNum);
      modelMap.addAttribute(“keyword”,keyword);
      // 3.返回到修改页面
      return “admin-edit”;
      }
    • 修改页面和新增页面相似
    • 注意将pageNum和keyword设置在隐藏域中,用于更新后返回分页
    • (报错400,注意createTime也需要传进去才能自动封装Admin)




    • 表单数据






      <%— 显示错误信息 —%>

      ${requestScope.exception.message}


      <%— type=hidden,因为这些数据不需要(pageNum、keyword)或不应该被修改(id、createTime)只需要传递给后端即可 —%>






      <%— 通过value给各个文本框赋原始值 —%>
      value=”${requestScope.admin.loginAcct}” placeholder=”请输入登录账号”>



      value=”${requestScope.admin.userName}” placeholder=”请输入用户昵称”>
                    <div class="form-group"><br />                        <label for="exampleInputEmail1">邮箱地址</label><br />                        <input type="email" name="email" class="form-control" id="exampleInputEmail1"<br />                               value="${requestScope.admin.email}" placeholder="请输入邮箱地址"><br />                        <p class="help-block label label-warning">请输入合法的邮箱地址, 格式为: xxxx@xxxx.com</p><br />                    </div><br />                    <button type="submit" class="btn btn-success"><br />                        <i class="glyphicon glyphicon-plus">修改</i><br />                    </button><br />                    <button type="reset" class="btn btn-danger"><br />                        <i class="glyphicon glyphicon-refresh">重置</i><br />                    </button><br />                </form><br />            </div><br />        </div><br />    </div><br /></div>
      

    更新数据

    • Handler方法
    • @RequestMapping(“admin/update.html”)
      public String updateAdmin(
      Admin admin,
      @RequestParam(“pageNum”) Integer pageNum,
      @RequestParam(“keyword”) String keyword
      ){
      // 1.调用service更新信息
      adminService.updateAdmin(admin);
      // 2.重定向到分页页面
      System.out.println(keyword);
      return “redirect:/admin/get/page.html?pageNum=”+pageNum+”&keyword=”+keyword;
      }
    • 接口中实现更新操作
    • 用户名相同时,抛出异常
    • @Override
      public void updateAdmin(Admin admin) {
      try {
      // 有选择的更新
      adminMapper.updateByPrimaryKeySelective(admin);
      } catch (Exception exception) {
      // 抛出更新时,用户名相同异常
      throw new LoginAcctAlreadyUpdateException(CrowdConstant.MESSAGE_LOGIN_ACCT_ALREADY_IN_USE);
      }
      }
    • 自定义异常类和登录一致
    • /*
      更新Admin如果有相同的账户,则抛出这个异常
      */
      public class LoginAcctAlreadyUpdateException extends RuntimeException{

      }
    • 异常处理方法
    • // 处理新增用户名重复的异常
      @ExceptionHandler(value = {LoginAcctAlreadyInUseException.class})
      public ModelAndView loginAcctAlreadyInUseExceptionResolver(
      // 实际捕获到的类型
      LoginAcctAlreadyInUseException LoginAcctAlreadyInUseException,
      // 当前请求对象
      HttpServletRequest request,
      // 当前响应对象
      HttpServletResponse response
      // 指定普通页时去的错误页面
      ) throws IOException {
      String viewName = “admin-add”;
      return conmonResolver(LoginAcctAlreadyInUseException, request, response, viewName);
      }

    角色系统

    RBAC模型

    简介

    为什么要进行权限控制

    如果没有权限控制,系统的功能完全不设防,全部暴露在所有用户面前。用户登录 以后可以使用系统中的所有功能。这是实际运行中不能接受的。
    所以权限控制系统的目标就是管理用户行为,保护系统功能。

    什么是权限控制

    “权限”=“权力”+“限制”

    如何进行权限控制

    定义资源

    资源就是系统中需要保护起来的功能。具体形式很多:URL 地址、handler 方 法、service 方法、页面元素等等都可以定义为资源使用权限控制系统保护起来。

    创建权限

    一个功能复杂的项目会包含很多具体资源,成千上万都有可能。这么多资源逐 个进行操作太麻烦了。为了简化操作,可以将相关的几个资源封装到一起,打包成 一个“权限”同时分配给有需要的人。
    创建角色
    对于一个庞大系统来说,一方面需要保护的资源非常多,另一方面操作系统的 人也非常多。把资源打包为权限是对操作的简化,同样把用户划分为不同角色也是 对操作的简化。否则直接针对一个个用户进行管理就会很繁琐 所以角色就是用户的分组、分类。先给角色分配权限,然后再把角色分配给用户,用户以这个角色的身份操作系统就享有角色对应的权限了。
    管理用户
    系统中的用户其实是人操作系统时用来登录系统的账号、密码。
    建立关联关系
    权限→资源:单向多对多
    Java 类之间单向:从权限实体类可以获取到资源对象的集合,但是通过资 源获取不到权限 。
    数据库表之间多对多: 一个权限可以包含多个资源 ,一个资源可以被分配给多个不同权限 。
    角色→权限:单向多对多
    Java 类之间单向:从角色实体类可以获取到权限对象的集合,但是通过权限获取不到角色 。
    数据库表之间多对多: 一个角色可以包含多个权限 ,一个权限可以被分配给多个不同角色 。
    用户→角色:双向多对多
    Java 类之间双向:可以通过用户获取它具备的角色,也可以看一个角色下,包含哪些用户 。
    数据库表之间: 一个角色可以包含多个用户 ,一个用户可以身兼数职。

    角色维护

    分页显示角色

    目标

    将角色数据分页显示,使用异步AJAX完成

    思路

    尚筹网 - 图29

    创建数据库表

    create table t_role(
    id INT not null PRIMARY key auto_increment,
    name char(100)
    )

    逆向工程

    在generatorConfig中修改


    生成后归类到各自的包中,生成Role的构造方法

    后端

    查询语句

    RoleMapper接口
    List selectRoleByKeyword(String keyword);
    RoleMapper.xml中

    接口实现查询操作

    RoleService接口
    public interface RoleService {
    // 根据关键字查询Role
    PageInfo getRoleByKeyword(String keyword,Integer pageNum,Integer pageSize);
    }
    RoleServiceImpl实现类
    @Service
    public class RoleServiceImpl implements RoleService {

    @Autowired<br />    private RoleMapper roleMapper;
    
    @Override<br />    public PageInfo<Role> getRoleByKeyword(String keyword, Integer pageNum, Integer pageSize) {<br />        // 1.开启分页插件<br />        PageHelper.startPage(pageNum,pageSize);<br />        // 2.进行查询返回List对象<br />        List<Role> roles = roleMapper.selectRoleByKeyword(keyword);<br />        // 3.将roles封装在pageInfo中<br />        return new PageInfo<>(roles);<br />    }<br />}
    

    Handler方法

    处理json请求,斌返回封装到ResultEntity中的json数据(异常已通过异常机制抛出,这里不做处理)
    @ResponseBody
    @RequestMapping(“/role/get/page/info.json”)
    public ResultEntity> getPageInfo(
    @RequestParam(value = “keyword”,defaultValue = “”) String keyword,
    @RequestParam(value = “pageNum”,defaultValue = “1”) Integer pageNum,
    @RequestParam(value = “pageSize”,defaultValue = “5”) Integer pageSize
    ){
    // 1.调用service查询
    PageInfo roleByKeyword = roleService.getRoleByKeyword(keyword, pageNum, pageSize);
    // 2.封装到ResultEntity中,上面抛出异常是交给异常映射机制处理
    return ResultEntity.successWithData(roleByKeyword);
    }

    前端

    设置a标签路径


  • 角色维护
  • 配置视图控制器

    跳转到role-page页面

    role-page页面
    • 引入外部js

    <%—引入pagination的css—%>

    <%—引入基于jquery的paginationjs—%>



    <%—引入自定义的js代码—%>

    • 主题表格部分



    数据列表







    查询条件

    placeholder=”请输入查询条件”>




    style=”float:right;margin-left:10px;”> class=” glyphicon glyphicon-remove”> 删除

    style=”float:right;” id=”showAddModalBtn”>
    新增















    <%— tbody的id=rolePageTBody,用于绑定on()函数 —%>







    # 名称 操作






    填充表格

    使用jquery处理json数据,并填充在表格中
    // 执行分页,生成分页效果
    function generatePage() {
    // 通过getPageInfoRemote()方法得到pageInfo
    var pageInfo = getPageInfoRemote();

    // 将pageInfo传入fillTableTBody()方法,在tbody中生成分页后的数据<br />    fillTableTBody(pageInfo);<br />}
    

    // 从远程服务器端获取PageInfo数据
    function getPageInfoRemote() {

    // 调用$.ajax()函数发送请求,并用ajaxResult接收函数返回值<br />    var ajaxResult = $.ajax({<br />        url: "role/get/page/info.json",<br />        type: "post",<br />        // 页码、页大小、关键字均从全局变量中获取<br />        data: {<br />            "pageNum": window.pageNum,<br />            "pageSize": window.pageSize,<br />            "keyword": window.keyword<br />        },<br />        async: false,        //关闭异步模式,使用同步,这是为了显示页面时保持现有的顺序<br />        dataType: "json"<br />    });
    
    // 取得当前的响应状态码<br />    var statusCode = ajaxResult.status;
    
    // 判断当前状态码是不是200,不是200表示发生错误,通过layer提示错误消息<br />    if (statusCode != 200) {<br />        layer.msg("失败!状态码=" + statusCode + "错误信息=" + ajaxResult.statusText);<br />        return null;<br />    }
    
    // 响应状态码为200,进入下面的代码<br />    // 通过responseJSON取得handler中的返回值<br />    var resultEntity = ajaxResult.responseJSON;
    
    // 从resultEntity取得result属性<br />    var result = resultEntity.result;
    
    // 判断result是否是FAILED<br />    if (result == "FAILED") {<br />        // 显示失败的信息<br />        layer.msg(resultEntity.message);<br />        return null;<br />    }
    
    // result不是失败时,获取pageInfo<br />    var pageInfo = resultEntity.data;
    
    // 返回pageInfo<br />    return pageInfo;<br />}
    

    // 根据PageInfo填充表格
    function fillTableTBody(pageInfo) {

    // 清除tbody中的旧内容<br />    $("#rolePageTBody").empty();
    
    // 使无查询结果时,不显示导航条<br />    $("#Pagination").empty();
    
    // 判断pageInfo对象是否有效,无效则表示未查到数据<br />    if (pageInfo == null || pageInfo == undefined || pageInfo.list == null || pageInfo.list.length == 0) {<br />        $("#rolePageTBody").append("<tr><td colspan='4' align='center'>抱歉!没有查询到想要的数据</td></tr>");<br />        return;<br />    }
    
    // pageInfo有效,使用pageInfo的list填充tbody<br />    for (var i = 0; i < pageInfo.list.length; i++) {
    
        var role = pageInfo.list[i];<br />        var roleId = role.id;<br />        var roleName = role.name;<br />        var numberTd = "<td>" + (i + 1) + "</td>";<br />        var checkboxTd = "<td><input type='checkbox'/></td>";<br />        var roleNameTd = "<td>" + roleName + "</td>";
    
        var checkBtn = "<button type='button' class='btn btn-success btn-xs'><i class=' glyphicon glyphicon-check'></i></button>"
    
        var pencilBtn = "<button type='button' class='btn btn-primary btn-xs'><i class=' glyphicon glyphicon-pencil'></i></button>"
    
        var removeBtn = "<button type='button' class='btn btn-danger btn-xs'><i class=' glyphicon glyphicon-remove'></i></button>"
    
        // 拼接三个小按钮成一个td<br />        var buttonTd = "<td>" + checkBtn + " " + pencilBtn + " " + removeBtn + "</td>";
    
        // 将所有的td拼接成tr<br />        var tr = "<tr>" + numberTd + checkboxTd + roleNameTd + buttonTd + "</tr>";
    
        // 将拼接后的结果,放入id=rolePageTBody<br />        $("#rolePageTBody").append(tr);<br />    }
    
    // 调用generateNavigator()方法传入pageInfo,进行生成分页页码导航条<br />    generateNavigator(pageInfo);
    

    }

    // 生成分页页码导航条
    function generateNavigator(pageInfo) {

    //获取分页数据中的总记录数<br />    var totalRecord = pageInfo.total;
    
    //声明Pagination设置属性的JSON对象<br />    var properties = {<br />        num_edge_entries: 3,                                //边缘页数<br />        num_display_entries: 5,                             //主体页数<br />        callback: paginationCallback,                       //点击各种翻页反扭时触发的回调函数(执行翻页操作)<br />        current_page: (pageInfo.pageNum - 1),                 //当前页码<br />        prev_text: "上一页",                                 //在对应上一页操作的按钮上的文本<br />        next_text: "下一页",                                 //在对应下一页操作的按钮上的文本<br />        items_per_page: pageInfo.pageSize               //每页显示的数量<br />    };
    
    // 调用pagination()函数,生成导航条<br />    $("#Pagination").pagination(totalRecord, properties);
    

    }

    // 翻页时的回调函数
    function paginationCallback(pageIndex, jQuery) {

    // pageIndex是当前页码的索引,因此比pageNum小1<br />    window.pageNum = pageIndex + 1;
    
    // 重新执行分页代码<br />    generatePage();
    
    // 取消当前超链接的默认行为<br />    return false;
    

    }

    乱码问题
    • 方法一:可以修改tomcat启动参数

    尚筹网 - 图30

    • 方法二:将js文件修改为带有BOM的utf-8格式
    • 方法三:在引入js时规定字符编码charset=”UTF-8”

    关键字查询

    目标

    把页面上的“查询”表单和已经封装好的执行分页的函数连起来即可

    思路

    尚筹网 - 图31

    绑定单击事件

    // 给查询按钮绑定单击事件
    $(“#searchBtn”).click(function () {
    // 查询后的页面从第一页显示
    window.pageNum = 1;
    // 获取关键字数据给对应的全局变量
    window.keyword = $(“#inputKeyword”).val();
    // 调用分页函数刷新
    generatePage();
    });

    新增角色

    目标

    通过在打开的模态框中输入角色名称,执行对新角色的保存

    思路

    尚筹网 - 图32

    前端

    新增模态框的引入
    • 创建model-role-add.jsp
    • <%@ page contentType=”text/html;charset=UTF-8” language=”java” %>
    • 将静态资源在页尾引入
    • <%@include file=”/WEB-INF/model-role-add.jsp” %>
    • 绑定新增按钮,弹出静态框
    • // 点击新增按钮打开模态框
      $(“#showAddModalBtn”).click(function () {
      $(“#addRoleModal”).modal(“show”);
      });

    绑定保存按钮

    点击保存按钮,返送Ajax异步请求
    // 给新增模态框的保存按钮绑定单击事件
    $(“#saveRoleBtn”).click(function () {
    // 获取用户在文本框中输入角色的名称
    // #addModal表示找到整个模态框
    // 空格表示后代元素中继续查找
    // [name=roleName] 表示匹配name属性roleName的元素
    var roleName = $.trim($(“#addRoleModal [name=roleName]”).val());

    // 发送Ajax请求<br />    $.ajax({<br />        url: "role/save.json",<br />        type: "post",<br />        data: {<br />            name: roleName<br />        },<br />        success: function (response) {<br />            var result = response.result;<br />            // 成功则弹框输出<br />            if (result == "SUCCESS") {<br />                layer.msg("操作成功!");
    
                // 进入最后一页 方便显示添加的内容<br />                window.pageNum = 999;<br />                // 重新生成分页<br />                generatePage();<br />            }
    
            // 失败弹出原因<br />            if (result == "FAILED") {<br />                layer.msg("操作失败!" + response.message);<br />            }<br />        },<br />        error: function () {<br />            layer.msg(response.status + " " + response.statusText);<br />        }<br />    });
    
    // 关闭模态框<br />    $("#addRoleModal").modal("hide");
    
    // 清理模态框<br />    $("#addRoleModal [name=roleName]").val("");<br />});
    

    后端

    handler方法

    @ResponseBody
    @RequestMapping(“/role/save.json”)
    public ResultEntity saveRole(Role role){
    roleService.saveRole(role);
    return ResultEntity.successWithData();
    }

    RoleService的实现

    @Override
    public void saveRole(Role role) {
    roleMapper.insert(role);
    }

    更新角色

    目标

    修改角色信息

    思路

    尚筹网 - 图33

    前端

    修改模态框的引入
    • modal-role-update.jsp页面

    <%@ page contentType=”text/html;charset=UTF-8” language=”java” pageEncoding=”UTF-8” %>

    • 引入到role-page中
    • <%@include file=”/WEB-INF/modal-role-update.jsp”%>

    回显数据
    • 在动态生成的编辑按钮处添加选择器,以及添加id值,方便修改时获取id的值
    • var pencilBtn = “
    • 给动态生成的编辑按钮绑定单击事件,弹出静态框,并回显信息在输入文本框中
    • 普通单击事件无法绑定翻页后的编辑按钮,使用on函数利用静态元素取绑定动态生成的按钮
    • // 传统的事件绑定方式只有那个在第一个页面有效,在翻页后失效,使用jQuery对象on()函数解决
      //首先找到“动态生成”的元素附着的“静态”元素
      // on:第一个参数:事件类型
      // on:第二个参数:找到真正的绑定元素的选择器
      // on:第一个参数:事件的响应函数
      $(“#rolePageTBody”).on(“click”,”.pencilBtn”,function () {
      // 打开模态框
      $(“#editModal”).modal(“show”);

      // 获取表格中当前行的角色名称
      var roleName=$(this).parent().prev().text();

      // 获取当前角色的id,为了发送Ajax请求,将它设置为全局变量
      window.roleId=this.id;

      // 使用roleName设置模态框中的文本框
      $(“#editModal [name=roleName]”).val(roleName);

      // 绑定更新,发送Ajax请求
      $(“#updateRoleBtn”).click(function () {
      // 获取用户填写的更新用户名
      $(“#editModal [name=roleName]”).val();

    发送Ajax修改数据

    最后不执行清除模态框,(需要回显信息),不返回最后一页
    // 绑定更新,发送Ajax请求
    $(“#updateRoleBtn”).click(function () {
    // 从模态框的文本框中获得修改后的roleName
    var roleName = $(“#editModal [name=roleName]”).val();
    $.ajax({
    url: “role/update.json”,
    type: “post”,
    data: {
    id:window.roleId, // 从全局遍历取得当前角色的id
    name:roleName
    },
    dataType: “json”,
    success:function (response) {
    if (response.result == “SUCCESS”){
    layer.msg(“操作成功!”);
    generatePage();
    }
    if (response.result == “FAILED”)
    layer.msg(“操作失败”+response.message)
    },
    error:function (response) {
    layer.msg(“statusCode=”+response.status + “ message=”+response.statusText);
    }
    });

                // 关闭模态框<br />                $("#editModal").modal("hide");<br />            });<br />        });
    

    后端

    Hander方法
    @ResponseBody
    @RequestMapping(“/role/update.json”)
    public ResultEntity updateRole(Role role){
    roleService.updateRole(role);
    return ResultEntity.successWithData();
    }
    RoleService实现
    @Override
    public void updateRole(Role role) {
    roleMapper.updateByPrimaryKey(role);
    }

    删除角色

    目标

    前端的“单条删除”和“批量删除”在后端合并为同一套操作,合并的依据是:单条删除时id也被放在数组中,后端完全根据id的数组进行删除

    思路

    尚筹网 - 图34

    前端

    删除模态框的引入
    • modal-role-confirm.jsp模态框
    • <%@ page contentType=”text/html;charset=UTF-8” language=”java” %>

    • 引入到role-page页面
    • <%@include file=”/WEB-INF/modal-role-confirm.jsp”%>

    确认删除框的函数

    显示已选择的角色的角色名,封装函数在my-role.js中
    // 打开确认删除的模态框
    function showConfirmModal(roleArray) {
    // 显示模态框
    $(“#confirmRoleModal”).modal(“show”);

    // 清除旧的模态框中的数据<br />    $("#confirmList").empty();
    
    // 创建一个全局变量数组,用于存放要删除的roleId<br />    window.roleIdArray = [];
    
    // 填充数据<br />    for (var i = 0; i < roleArray.length; i++) {
    
        var roleId = roleArray[i].id;
    
        // 将当前遍历到的roleId放入全局变量<br />        window.roleIdArray.push(roleId);
    
        var roleName = roleArray[i].name;
    
        // 显示出要删除的数据<br />        $("#confirmList").append(roleName + "<br/>");<br />    }
    

    点击删除弹出模态框(单个删除)
    • 在每页的删除按钮添加id(方便后面用来删除操作)和class的属性
    • var removeBtn = “
    • 给单击X删除绑定单击事件
    • // 给单击删除把昂顶单击响应函数
      $(“#rolePageTBody”).on(“click”, “.removeBtn”, function () {
      // 通过x按钮删除时,只有一个角色,因此只需要建一个特殊的数组,存放单个对象即可
      var roleArray = [{
      id: this.id,
      name: $(this).parent().prev().text()
      }];
      // 调用删除静态框函数,传入roleArray
      showConfirmModal(roleArray);
      });

    绑定确认删除单击事件

    需要先转换为json字符串,后台才能使用请求体获取请求数据
    // 给确认删除按钮绑定单击事件
    $(“#confirmRoleBtn”).click(function () {
    // 将id信息封装到请求体
    var requestBody = JSON.stringify(window.roleIdArray);
    $.ajax({
    url: “role/remove.json”,
    type: “post”,
    data: requestBody, // 将转换后的数据传给后端
    dataType: “json”,
    contentType: “application/json;charset=UTF-8”, // 表明发送json格式数据
    success: function (response) {
    if (response.result == “SUCCESS”) {
    layer.msg(“操作成功!”);
    generatePage();
    }
    if (response.result == “FAILED”)
    layer.msg(“操作失败” + response.message)
    },
    error: function (response) {
    layer.msg(“statusCode=” + response.status + “ message=” + response.statusText);
    }
    });

    // 关闭模态框<br />    $("#confirmRoleModal").modal("hide");<br />});
    

    多选框删除角色(多个删除)
    • 在外部js生成表单td绑定id,方便多选框勾取时获取id值
    • var checkboxTd = ““;
    • 给全选框设置id(id=summaryBox)


    • #

      名称
      操作

    • 多选框全选,全不选处理
    • // 全选,全不选的反向操作
      $(“#rolePageTBody”).on(“click”, “.itemBox”, function () {
      // 获取当前已选中的多选框的数量
      var checkedBoxCount = $(“.itemBox:checked”).length;
      // 获取全部checkBox的数量
      var checkBoxAll = $(“.itemBox”).length;
      // 两者比较设置总的多选框状态
      $(“#summaryBox”).prop(“checked”, checkedBoxCount == checkBoxAll);
      });

    // 给多选删除按钮绑定单击事件
    $(“#batchRemoveBtn”).click(function (){

    // 创建一个数组对象,用来存放后面获得的角色对象<br />    var roleArray = [];
    
    // 遍历被勾选的内容<br />    $(".itemBox:checked").each(function () {<br />        // 通过this引用当前遍历得到的多选框的id<br />        var roleId = this.id;
    
        // 通过DOM操作获取角色名称<br />        var roleName = $(this).parent().next().text();
    
        roleArray.push({<br />            "id":roleId,<br />            "name":roleName<br />        });<br />    });
    
    • 点击删除多选框内容
    • 调用显示静态框函数,传入roleArray
    • // 给多选删除按钮绑定单击事件
      $(“#batchRemoveBtn”).click(function () {

           // 创建一个数组对象,用来存放后面获得的角色对象<br />            var roleArray = [];
      
           // 遍历被勾选的内容<br />            $(".itemBox:checked").each(function () {<br />                // 通过this引用当前遍历得到的多选框的id<br />                var roleId = this.id;
      
               // 通过DOM操作获取角色名称<br />                var roleName = $(this).parent().next().text();
      
               roleArray.push({<br />                    id: roleId,<br />                    name: roleName<br />                });<br />            });
      
           // 判断roleArray的长度是否为0<br />            if (roleArray.length == 0) {<br />                layer.msg("请至少选择一个来删除");<br />                return;<br />            }
      
           // 显示确认框<br />            showConfirmModal(roleArray);
      

    后台

    Handler方法

    传入的是json数据,使用请求体来获取json字符串
    @ResponseBody
    @RequestMapping(“/role/update.json”)
    public ResultEntity updateRole(Role role) {
    roleService.updateRole(role);
    return ResultEntity.successWithoutData();
    }

    RoleService的实现

    @Override
    public void deleteRole(List roleIdList) {
    RoleExample roleExample = new RoleExample();
    RoleExample.Criteria criteria = roleExample.createCriteria();
    criteria.andIdIn(roleIdList);
    roleMapper.deleteByExample(roleExample);
    }

    菜单维护

    树形结构基础知识

    尚筹网 - 图35

    整个树形结构最多只能有三级

    在数据库中表示树形结构

    创建菜单数据库表

    创建菜单的数据库表
    create table t_menu
    (
    id int(11) not null auto_increment,
    pid int(11),
    name varchar(200),
    url varchar(200),
    icon varchar(200),
    primary key (id)
    );

    插入数据

    插入数据
    insert into t_menu (id, pid, name, icon, url) values(‘1’,NULL,’ 系统权限菜单’,’glyphicon glyphicon-th-list’,NULL);
    insert into t_menu (id, pid, name, icon, url) values(‘2’,’1’,’ 控 制 面 板 ‘,’glyphicon glyphicon-dashboard’,’main.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘3’,’1’,’权限管理’,’glyphicon glyphicon glyphicon-tasks’,NULL);
    insert into t_menu (id, pid, name, icon, url) values(‘4’,’3’,’ 用 户 维 护 ‘,’glyphicon glyphicon-user’,’user/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘5’,’3’,’ 角 色 维 护 ‘,’glyphicon glyphicon-king’,’role/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘6’,’3’,’ 菜 单 维 护 ‘,’glyphicon glyphicon-lock’,’permission/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘7’,’1’,’ 业 务 审 核 ‘,’glyphicon glyphicon-ok’,NULL);
    insert into t_menu (id, pid, name, icon, url) values(‘8’,’7’,’ 实名认证审核’,’glyphicon glyphicon-check’,’auth_cert/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘9’,’7’,’ 广 告 审 核 ‘,’glyphicon glyphicon-check’,’auth_adv/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘10’,’7’,’ 项 目 审 核 ‘,’glyphicon glyphicon-check’,’auth_project/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘11’,’1’,’ 业 务 管 理 ‘,’glyphicon glyphicon-th-large’,NULL);
    insert into t_menu (id, pid, name, icon, url) values(‘12’,’11’,’ 资 质 维 护 ‘,’glyphicon glyphicon-picture’,’cert/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘13’,’11’,’ 分 类 管 理 ‘,’glyphicon glyphicon-equalizer’,’certtype/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘14’,’11’,’ 流 程 管 理 ‘,’glyphicon glyphicon-random’,’process/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘15’,’11’,’ 广 告 管 理 ‘,’glyphicon glyphicon-hdd’,’advert/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘16’,’11’,’ 消 息 模 板 ‘,’glyphicon glyphicon-comment’,’message/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘17’,’11’,’ 项 目 分 类 ‘,’glyphicon glyphicon-list’,’projectType/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘18’,’11’,’ 项 目 标 签 ‘,’glyphicon glyphicon-tags’,’tag/index.htm’);
    insert into t_menu (id, pid, name, icon, url) values(‘19’,’1’,’ 参 数 管 理 ‘,’glyphicon glyphicon-list-alt’,’param/index.htm’);

    关联方式

    子节点通过pid字段关联到父节点的id字段,建立父子关系。
    尚筹网 - 图36

    根节点的pid为空

    在Java类中表示树形结构

    基本方式

    在 Menu 类中使用 List children 属性存储当前节点的子节点。

    为了配合zTree 所需要添加的属性
    • pid 属性:找到父节点
    • name 属性:作为节点名称
    • icon 属性:当前节点使用的图标
    • open 属性:控制节点是否默认打开
    • url 属性:点击节点时跳转的位置

    页面显示树形结构

    目标

    将数据库中查询得到的数据到页面上显示出来

    思路

    数据库查询全部→Java 对象组装→页面上使用 zTree 显示

    逆向工程

    • 修改配置文件

    • 修改Menu实体类
    • 此处省略get,set,有参,无参构造器
    • package crowd.entity;

    import java.util.ArrayList;
    import java.util.List;

    public class Menu {
    // 主键
    private Integer id;
    // 父节点id
    private Integer pid;
    // 结点名称
    private String name;
    // 结点附带的url地址,是将来点击菜单项时要跳转的路径
    private String url;
    // 结点图标样式
    private String icon;
    // 存储节点的集合,初始化是为了避免空指针异常
    private List

    children = new ArrayList<>();
    // 控制节点是否默认打开,true为打开
    private Boolean open = true;

    后端

    MenuHandler方法

    查询节点信息,将节点的id和信息封装为map返回,最终返回根节点信息,则得到整棵树
    @ResponseBody
    @RequestMapping(“/menu/get/whole/tree.json”)
    public ResultEntity

    getWholeTreeNew() {
    // 1.查询全部的Menu对象
    List menuList = menuService.getAll();
    // 2.申明一个变量来存储找到的一个根节点
    Menu root = null;
    // 3.创建 Map 对象用来存储 id 和 Menu 对象的对应关系便于查找父节点
    Map menuMap = new HashMap<>();
    // 4.遍历 menuList 填充 menuMap
    for (Menu menu : menuList) {
    Integer id = menu.getId();
    menuMap.put(id, menu);
    }
    // 5.再次遍历 menuList 查找根节点、组装父子节点
    for (Menu menu : menuList) {
    // 6.获取节点的父节点值
    Integer pid = menu.getPid();
    // 7.判断父节点为空则为根节点
    if (pid == null) {
    root = menu;
    // 8.如果当前节点没有父节点,那肯定是根节点,不循环执行
    continue;
    }
    // 9.如果 pid 不为 null,说明当前节点有父节点,那么可以根据 pid 到 menuMap 中 查找对应的 Menu 对象
    Menu father = menuMap.get(pid);
    // 10.将当前节点存入父节点的 children 集合
    father.getChildren().add(menu);
    }
    // 11.经过上面的运算,根节点包含了整个树形结构,返回根节点就是返回整个树
    return ResultEntity.successWithData(root);
    }

    Service实现

    @Override
    public List

    getAll() {
    return menuMapper.selectByExample(new MenuExample());
    }

    绑定单击事件

    前端

    引入ZTree和自定义的外部js文件



    menu主页面



    权限菜单列表







      <%— 显示树形结构依附于上面的ul —%>



    显示图标和按钮

    将显示图标和按钮封装为函数
    // 显示图标
    function myAddDiyDom(treeId, treeNode) {
    // treeId就是树形结构依附的ul的id
    // treeNode就是当前节点全部数据(包括后端查询得到的)

    // 根据zTree中每一个图标span的id的规则:<br />    // 如treeDemo_7_ico<br />    // id结构就是ul的id_当前节点序号_ico(tId就是id_当前节点序号)<br />    // 可以拼出每一个span的id:<br />    var spanId = treeNode.tId + "_ico";<br />    // 删除旧的class,增加新得到的class<br />    $("#"+spanId).removeClass().addClass(treeNode.icon);<br />}
    

    // 鼠标覆盖时,显示按钮组
    function myAddHoverDom(treeId, treeNode) {
    // 定义增加、修改、删除节点的标签字符串
    var addBtn = “  “;
    var editBtn = “  “;
    var removeBtn = “  “;

    // btn用于存放不同的节点显示的不同的按钮<br />    var btn = "";
    
    // 得到每个节点的level,根据level决定显示的按钮组的内容<br />    var level = treeNode.level;
    
    // 按照一定规则设置按钮组span的id<br />    var btnGroupId = "btnGroupTreeDemo_"+treeNode.id;
    
    // 如果此时按钮组已经有内容了,则不再往下执行<br />    if ($("#"+btnGroupId).length > 0){<br />        return ;<br />    }
    
    // 根据level决定按钮组内部显示的内容<br />    if (level === 0){<br />        btn = addBtn;<br />    } else if (level === 1){<br />        btn = addBtn + editBtn;<br />        // 判断是否子节点,有子节点则不显示删除按钮,没有子节点则显示删除按钮<br />        if (treeNode.children.length === 0){<br />            btn = btn + removeBtn;<br />        }<br />    } else {<br />        // level==3则显示删除按钮与修改按钮<br />        btn = editBtn+removeBtn;<br />    }
    
    // 拼接a标签的id(treeDemo_x_a)<br />    var aId = treeNode.tId + "_a";
    
    // 根据id,在a标签后加按钮组<br />    $("#"+aId).after("<span id='"+btnGroupId+"'>"+btn+"</span>");
    

    }

    // 鼠标移开时,隐藏按钮组
    function myRemoveHoverDom(treeId, treeNode) {
    // 按钮组span的id
    var btnGroupId = “btnGroupTreeDemo_”+treeNode.id;
    // 删除此id的标签
    $(“#”+btnGroupId).remove();
    }

    生成树形结构

    • 将树形结构生成封装为外部js的函数,方便调用
    • // 封装生成树形结构的代码
      function generateTree(){
      $.ajax({
      url:”menu/get/whole/tree.json”,
      type:”post”,
      dataType:”json”,
      success:function (response) {
      if (response.result == “SUCCESS”){
      // 成功 则设置下列属性\
      var setting = {
      view:{
      // 设置每一个标签的图标
      “addDiyDom”:myAddDiyDom,
      // 设置悬浮在标签上时的函数
      “addHoverDom”:myAddHoverDom,
      // 设置从标签上移除时的函数
      “removeHoverDom”:myRemoveHoverDom
      },
      data:{
      key:{
      // 实现“点了不跑”,也就是设置了这里的url后,会根据该url去寻找页面,如果页面找不到,则不跳转
      /
      zTree 节点数据保存节点链接的目标 URL 的属性名称。
      特殊用途:当后台数据只能生成 url 属性,又不想实现点击节点跳转的功能时,可以直接修改此属性为其他不存在的属性名称
      默认值:”url”
      /
      url: “None”
      }
      }
      };
      // 通过response得到data,就是后端传来的查询结构
      var zNodes = response.data;
      // 执行zTree的初始化函数,传参分别是依附的ul的id(通过jQuery选择器)、setting变量、查询到的树形结构
      $.fn.zTree.init($(“#treeDemo”), setting, zNodes);
      }
      if (response.result == “FAILED”)
      layer.msg(“操作失败”+response.message)
      },
      error:function (response) {
      layer.msg(“statusCode=”+response.status + “ message=”+response.statusText);
      }
      });
      }
    • menu页面调用生成函数

    添加节点

    目标

    给当前节点添加子节点,保存到数据库,并刷新页面的显示

    思路

    尚筹网 - 图37

    前端

    引入模态框

    记得刷新工程
    尚筹网 - 图38

    绑定class

    方便通过选择器找到节点
    var addBtn = “  “;
    var editBtn = “  “;
    var removeBtn = “  “;

    点击添加按钮

    // 给“+”按钮,添加单击响应函数,打开添加节点的模态框
    $(“#treeDemo”).on(“click”, “.addBtn”, function () {
    // 将当前按钮的id保存为全局变量pid,方便后面调用
    window.pid = this.id;
    // 打开模态框
    $(“#menuAddModal”).modal(“show”);
    // 关闭默认跳转行为
    return false;
    });

    点击保存按钮

    // 添加节点模态框中保存按钮的单击事件
    $(“#menuSaveBtn”).click(function () {
    // 从输入框中获得name,并去掉前后空格
    var name = $.trim($(“#menuAddModal [name=name]”).val());
    // 从输入框中获得url,并去掉前后空格
    var url = $.trim($(“#menuAddModal [name=url]”).val());
    // 下面的选项中获得被选中的icon的值
    var icon = $(“#menuAddModal [name=icon]:checked”).val();

    $.ajax({<br />        url: "menu/save.json",<br />        type: "post",<br />        "data": {<br />            name: name,<br />            url: url,<br />            icon: icon,<br />            // 从全局变量获得该节点的父节点id<br />            pid: window.pid<br />        },<br />        dataType: "json",<br />        success: function (response) {<br />            if (response.result == "SUCCESS") {<br />                layer.msg("操作成功!");
    
                // 重新生成树形结构<br />                generateTree();<br />            }<br />            if (response.result == "FAILED") {<br />                layer.msg("操作失败!");<br />            }<br />        },<br />        error: function (response) {<br />            layer.msg(response.status + " " + response.statusText);<br />        }<br />    });
    
    // 关闭模态框<br />    $("#menuAddModal").modal("hide");
    
    // 清空模态框内的数据(通过模拟用户单击“重置”按钮)<br />    $("#menuResetBtn").click();<br />});
    

    后端

    Handler方法

    @ResponseBody
    @RequestMapping(“/menu/save.json”)
    public ResultEntity saveMenu(Menu menu) {
    menuService.saveMenu(menu);
    return ResultEntity.successWithoutData();
    }

    Service实现

    @Override
    public void saveMenu(Menu menu) {
    menuMapper.insert(menu);
    }

    更新节点

    目标

    修改当前节点的基本属性,不更换父节点

    思路

    尚筹网 - 图39

    前端

    修改模态框的引入

    使用ztree提供的根据id查找结点方式getNodeByParam
    // 动态生成的修改按钮,单击打开修改的模态框
    $(“#treeDemo”).on(“click”, “.editBtn”, function () {

    // 保存此按钮的id<br />    window.id = this.id;
    
    $("#menuEditModal").modal("show");
    
    // 要实现通过id拿到整个节点的信息,需要拿到zTreeObj<br />    var zTreeObj = $.fn.zTree.getZTreeObj("treeDemo");
    
    var key = "id";<br />    var value = window.id;
    
    // getNodeByParam,通过id得到当前的整个节点<br />    // 注意:id为treeNode的id,返回的就是那个treeNode<br />    var currentNode = zTreeObj.getNodeByParam(key, value);
    
    $("#menuEditModal [name=name]").val(currentNode.name);
    
    $("#menuEditModal [name=url]").val(currentNode.url);
    
    // 这里currentNode.icon其实是数组形式,利用这个值,放在[]中,传回val,就可以使相匹配的值回显在模态框中<br />    $("#menuEditModal [name=icon]").val([currentNode.icon]);
    
    return false;<br />});
    

    绑定更新按钮的单击事件

    ajax传入修改的节点的id,而不是pid,获取icon时应该选取checked中的
    // 更新节点模态框中保存按钮的单击事件
    $(“#menuEditBtn”).click(function () {

    var name = $("#menuEditModal [name=name]").val();
    
    var url = $("#menuEditModal [name=url]").val();
    
    var icon = $("#menuEditModal [name=icon]:checked").val();<br />    $.ajax({<br />        url: "menu/update.json",<br />        type: "post",<br />        "data": {<br />            name: name,<br />            url: url,<br />            icon: icon,<br />            // 从全局变量获得该节点的节点id<br />            id: window.id<br />        },<br />        dataType: "json",<br />        success: function (response) {<br />            if (response.result == "SUCCESS") {<br />                layer.msg("操作成功!");
    
                // 重新生成树形结构<br />                generateTree();<br />            }<br />            if (response.result == "FAILED") {<br />                layer.msg("操作失败!");<br />            }<br />        },<br />        error: function (response) {<br />            layer.msg(response.status + " " + response.statusText);<br />        }<br />    });<br />    // 关闭模态框<br />    $("#menuEditModal").modal("hide");<br />});
    

    后端

    Handler方法

    @ResponseBody
    @RequestMapping(“/menu/update.json”)
    public ResultEntity updateMenu(Menu menu){
    menuService.updateMenu(menu);
    return ResultEntity.successWithoutData();
    }

    Service的实现

    有选择的进行更新
    @Override
    public void updateMenu(Menu menu) {
    menuMapper.updateByPrimaryKeySelective(menu);
    }

    删除节点

    目标

    删除当前节点

    思路

    尚筹网 - 图40

    前端

    删除静态框的引入

    主要通过获取当前节点来回显提示信息
    // 动态生成的删除按钮,单击打开删除的模态框
    $(“#treeDemo”).on(“click”, “.removeBtn”, function () {

    // 保存此按钮的id<br />    window.id = this.id;
    
    $("#menuConfirmModal").modal("show");
    
    // 要实现通过id拿到整个节点的信息,需要拿到zTreeObj<br />    var zTreeObj = $.fn.zTree.getZTreeObj("treeDemo");
    
    var key = "id";<br />    var value = window.id;
    
    // getNodeByParam,通过id得到当前的整个节点<br />    // 注意:id为treeNode的id,返回的就是那个treeNode<br />    var currentNode = zTreeObj.getNodeByParam(key, value);
    
    // 获取当前节点的icon和name来作为提示信息<br />    var icon = currentNode.icon;<br />    var name = currentNode.name;
    
    // 回显-向id=removeNodeSpan的span标签添加html语句(显示图标与节点名)<br />    $("#removeNodeSpan").html("【<i class='"+icon+"'>"+name+"】</i>");
    
    return false;<br />});
    

    绑定确定删除单击事件

    只需传入当前节点的id,根据id进行删除
    // 删除节点模态框中确定按钮的单击事件
    $(“#confirmBtn”).click(function () {

    var name = $("#menuEditModal [name=name]").val();
    
    var url = $("#menuEditModal [name=url]").val();
    
    var icon = $("#menuEditModal [name=icon]:checked").val();<br />    $.ajax({<br />        url: "menu/remove.json",<br />        type: "post",<br />        "data": {<br />            // 从全局变量获得该节点的节点id<br />            id: window.id<br />        },<br />        dataType: "json",<br />        success: function (response) {<br />            if (response.result == "SUCCESS") {<br />                layer.msg("操作成功!");
    
                // 重新生成树形结构<br />                generateTree();<br />            }<br />            if (response.result == "FAILED") {<br />                layer.msg("操作失败!");<br />            }<br />        },<br />        error: function (response) {<br />            layer.msg(response.status + " " + response.statusText);<br />        }<br />    });<br />    // 关闭模态框<br />    $("#menuConfirmModal").modal("hide");<br />});
    

    后端

    @RestController

    将@RestController替换@ResponseBody和@Controller

    Handler方法

    @RequestMapping(“/menu/remove.json”)
    public ResultEntity removeMenu(Integer id){
    menuService.remove(id);
    return ResultEntity.successWithoutData();
    }

    Service实现

    @Override
    public void remove(Integer id) {
    menuMapper.deleteByPrimaryKey(id);
    }

    权限控制

    原理

    尚筹网 - 图41

    Admin分配Role

    目标

    通过页面操作把Admin和Role之间的关联关系保存到数据库

    思路

    尚筹网 - 图42

    后端

    替换button为a标签

    class=”btn btn-success btn-xs”>

    Handler方法

    分别将已分配角色和未分配角色都传入model中,则不用前端判断
    @RequestMapping(“/assign/to/page.html”)
    public String toAssignPage(
    @RequestParam(“adminId”) Integer adminId,
    ModelMap modelMap
    ) {
    // 查询已分配角色
    List assignRoleList = roleService.getAssignedRole(adminId);
    // 查询未分配的角色
    List unAssignRoleList = roleService.getUnAssignedRole(adminId);
    // 存入模型
    modelMap.addAttribute(“assignRoleList”, assignRoleList);
    modelMap.addAttribute(“unAssignRoleList”, unAssignRoleList);
    // 转发到assign-role
    return “assign-role”;
    }

    Service实现

    @Override
    public List getAssignedRole(Integer adminId) {
    return roleMapper.selectAssignedRoleList(adminId);
    }

    @Override
    public List getUnAssignedRole(Integer adminId) {
    return roleMapper.selectUnAssignedRoleList(adminId);
    }

    sql语句

    使用子查询,在查询已分配和未分配较方便



    前端

    页面角色显示

    assign-role.jsp

































    点击按钮分配角色

    Admin执行分配

    目标

    点击保存,将角色保存在数据库中

    思路

    尚筹网 - 图43

    前端

    提交表单

    表单需要设置隐藏域,来定位pageNum和keyword以及获取正在执行的adminId





    完善表单

    让右边没有选中的角色全部选中,提交表单时才不会遗漏
    // 让右边框中元素全部为选中,提交表单则全部提交
    $(“#submitBtn”).click(function () {
    $(“select:eq(1)>option”).prop(“selected”,”selected”);
    });

    后端

    Handler方法

    在获取roleIdList时,允许为空
    @RequestMapping(“/assign/do/role/assign.html”)
    public String saveAdminRoleRelationship(
    @RequestParam(value = “adminId”) Integer adminId,
    @RequestParam(“pageNum”) Integer pageNum,
    @RequestParam(“keyword”) String keyword,
    // 管理员可以没有权限,所以可以设置roleId可以为空
    @RequestParam(value = “roleIdList”,required = false) List roleIdList,
    ModelMap modelMap
    ){

    // 调用service层方法<br />    adminService.saveAdminRoleRelationship(adminId, roleIdList);<br />    return "redirect:/admin/get/page.html?pageNum="+pageNum+"&keyword="+keyword;<br />}
    

    Service实现

    由于单个删除浪费时间,可以先将adminId的角色全部删除后再插入
    @Override
    public void saveAdminRoleRelationship(Integer adminId, List roleIdList) {
    // 为了简化操作:先将adminId的角色全部删除
    adminMapper.deleteOldRelationship(adminId);
    // 根据roleIdList和adminId保存新的关系
    if(roleIdList!=null&&roleIdList.size()>=0){
    adminMapper.saveNewRelationship(adminId,roleIdList);
    }

    AdminMapper接口

    AminMapper接口定义方法,自定义sql进行删除和插入,使用@Param注解,在sql中会使用到参数
    void deleteOldRelationship(Integer adminId);

    void saveNewRelationship(@Param(“adminId”) Integer adminId,@Param(“roleIdList”) List roleIdList);

    sql语句


    delete from inner_admin_role where admin_id=#{adminId}


    insert into inner_admin_role(admin_id,role_id) values

    (#{adminId},#{roleId})

    ## Role分配Auth ### 目标 把角色和权限的关联保存到数据库 ### 思路 尚筹网 - 图44 ### 创建权限表 # 建t_auth表
    CREATE TABLE t_auth (
    id int(11) NOT NULL AUTO_INCREMENT,
    name varchar(200) DEFAULT NULL,
    title varchar(200) DEFAULT NULL,
    category_id int(11) DEFAULT NULL, PRIMARY KEY (id)
    ); # 给t_auth表插入数据
    INSERT INTO t_auth(id,name,title,category_id) VALUES(1,’’,’用户模块’,NULL);
    INSERT INTO t_auth(id,name,title,category_id) VALUES(2,’user:delete’,’删除’,1);
    INSERT INTO t_auth(id,name,title,category_id) VALUES(3,’user:get’,’查询’,1);
    INSERT INTO t_auth(id,name,title,category_id) VALUES(4,’’,’角色模块’,NULL);
    INSERT INTO t_auth(id,name,title,category_id) VALUES(5,’role:delete’,’删除’,4);
    INSERT INTO t_auth(id,name,title,category_id) VALUES(6,’role:get’,’查询’,4);
    INSERT INTO t_auth(id,name,title,category_id) VALUES(7,’role:add’,’新增’,4); ### 逆向工程

    生成实体类后,添加有参和无参构造器,将各文件放在各自包中,最后创建AuthService接口和实现类 ### 前端 #### 给按钮添加class和id值 var checkBtn = “ #### 模态框的引入 - modal-role-assign-auth.jsp - <%@ page contentType=”text/html;charset=UTF-8” language=”java” %>
    - 静态资源引入 - <%@include file=”/WEB-INF/modal-role-assign-auth.jsp” %> #### 弹出模态框 将生成树形结构封装为函数
    // 弹出权限分配静态框
    $(“#rolePageTBody”).on(“click”, “.checkBtn”, function () {
    // 打开模态框
    $(“#assignModal”).modal(“show”);
    // 生成树形结构
    fullAuthTree();
    }); #### 生成树形结构的函数 处理树形结构不同于菜单维护,使用zTree的简单json数据来显示,而不是后台将数据封装好
    // 生成权限分配树形结构
    function fullAuthTree(){
    // 发送Ajax请求查询Auth数据
    var ajaxReturn = $.ajax({
    url: “assign/get/all/auth.json”,
    type: “post”,
    async: false,
    dataType: “json”
    }); if (ajaxReturn.status != 200){
    layer.msg(“请求出错!错误码:”+ ajaxReturn.status + “错误信息:” + ajaxReturn.statusText);
    return ;
    } var resultEntity = ajaxReturn.responseJSON; if (resultEntity.result == “FAILED”){
    layer.msg(“操作失败!”+resultEntity.message);
    } if (resultEntity.result == “SUCCESS”) {
    var authList = resultEntity.data;
    // 将服务端查询到的list交给zTree自己组装
    var setting = {
    data: {
    // 开启简单JSON功能
    simpleData: {
    enable: true,
    // 通过pIdKey属性设置父节点的属性名,而不使用默认的pId
    pIdKey: “categoryId”
    },
    key: {
    // 设置在前端显示的节点名是查询到的title,而不是使用默认的name
    name: “title”
    },
    }, check: {
    enable: true
    }
    }; // 生成树形结构信息
    $.fn.zTree.init($(“#authTreeDemo”), setting, authList); // 设置节点默认是展开的
    // 1 得到zTreeObj
    var zTreeObj = $.fn.zTree.getZTreeObj(“authTreeDemo”);
    // 2 设置默认展开
    zTreeObj.expandAll(true);
    }
    } ### 后端 #### Handler方法 获取所有Auth并封装在List,以json数据返回,方便调用
    @ResponseBody
    @RequestMapping(“/assign/get/all/auth.json”)
    public ResultEntity> getAllAuth(){
    List authList = authService.getAllAuth();
    return ResultEntity.successWithData(authList);
    } #### AuthService实现 @Override
    public List getAllAuth() {
    return authMapper.selectByExample(new AuthExample());
    } ## Role回显Auth ### 目标 生成模态框后,自动发送Ajax请求,并回显在复选框上 ### 思路 尚筹网 - 图45 ### 前端 #### 保存roleId 方便传入roleId查找Auth来回显
    // 弹出权限分配静态框
    $(“#rolePageTBody”).on(“click”, “.checkBtn”, function () {
    window.roleId = this.id;
    // 打开模态框
    $(“#assignModal”).modal(“show”);
    // 生成树形结构
    fullAuthTree();
    }); #### 回显Auth复选框 // 回显权限信息
    ajaxReturn = $.ajax({
    url: “assign/get/assigned/auth/id/by/role/id.json”,
    type: “post”,
    dataType: “json”,
    data: {
    roleId: window.roleId
    },
    async: false
    }); if (ajaxReturn.status != 200) {
    layer.msg(“请求出错!错误码:” + ajaxReturn.status + “错误信息:” + ajaxReturn.statusText);
    return;
    } // 获取返回的json信息
    resultEntity = ajaxReturn.responseJSON; if (resultEntity.result == “FAILED”) {
    layer.msg(“操作失败!” + resultEntity.message);
    } if (resultEntity.result == “SUCCESS”) {
    var authIdArray = resultEntity.data; // 遍历得到的autoId的数组
    // 根据authIdArray勾选对应的节点
    for (var i = 0; i < authIdArray.length; i++) {
    var authId = authIdArray[i]; // 通过id得到treeNode
    var treeNode = zTreeObj.getNodeByParam(“id”, authId); // checked设置为true,表示勾选节点
    var checked = true; // checkTypeFlag设置为false,表示不联动勾选,
    // 即父节点的子节点未完全勾选时不改变父节点的勾选状态
    // 否则会出现bug:前端只要选了一个子节点,传到后端后,下次再调用时,发现前端那个子节点的所有兄弟节点也被勾选了,
    // 因为在子节点勾选时,父节点也被勾选了,之后前端显示时,联动勾选,导致全部子节点被勾选
    var checkTypeFlag = false; // zTreeObj的checkNode方法 执行勾选操作
    zTreeObj.checkNode(treeNode, checked, checkTypeFlag);
    }
    } ### 后端 #### 创建inner-role-auth数据表 CREATE TABLE inner_role_auth(
    id INT NOT NULL AUTO_INCREMENT,
    role_id INT,
    auth_id INT,
    PRIMARY KEY (id)
    ); #### Hanlder方法 返回authId的List集合,在前端可以遍历authId来回显
    @ResponseBody
    @RequestMapping(“/assign/get/assigned/auth/id/by/role/id.json”)
    public ResultEntity> getAssignedAuthIdByRoleId(@RequestParam(“roleID”) Integer roleId){
    List authList = authService.getAssignedAuthIdByRoleId(roleId);
    return ResultEntity.successWithData(authList);
    } #### AuthService实现 @Override
    public List getAssignedAuthIdByRoleId(Integer roleId) {
    return authMapper.selectAuthIdByRoleId(roleId);
    } #### sql语句 通过roleId查询authId
    ## Role执行分配 ### 目标 点击执行,在数据库中保存,并显示模态框 ### 思路 尚筹网 - 图46 ### 前端 #### 绑定提交单击事件 提交后发送Ajax请求,注意在传入后端的参数中,需要将roleId也看作为数组,以请求体的方式传入,使用@RequestBody接受
    // 给提交权限修改绑定单击事件
    $(“#assignBtn”).click(function () {
    // 声明一个数组,用来存放被勾选的auth的id
    var authIdArray = []; // 拿到zTreeObj
    var zTreeObj = $.fn.zTree.getZTreeObj(“authTreeDemo”); // 通过getCheckedNodes方法拿到被选中的option信息
    var authArray = zTreeObj.getCheckedNodes(); for (var i = 0; i < authArray.length; i++) {
    // 从被选中的auth中遍历得到每一个auth的id
    var authId = authArray[i].id;
    // 通过push方法将得到的id存入authIdArray
    authIdArray.push(authId);
    }
    var requestBody = {
    // 为了后端取值方便,两个数据都用数组格式存放,后端统一用List获取
    roleId: [window.roleId],
    authIdList: authIdArray
    }
    requestBody = JSON.stringify(requestBody); // 发送Ajax请求保存
    $.ajax({
    url: “assign/do/role/assign/auth.json”,
    type: “post”,
    data: requestBody,
    contentType: “application/json;charset=UTF-8”,
    dataType: “json”,
    success: function (response) {
    if (response.result == “SUCCESS”){
    layer.msg(“操作成功!”);
    }
    if (response.result == “FAILED”){
    layer.msg(“操作失败!提示信息:”+ response.message);
    }
    },
    error: function (response) {
    layer.msg(response.status + “ “ + response.statusText);
    }
    }); // 关闭模态框
    $(“#assignModal”).modal(“hide”);
    }); ### 后端 #### Handler方法 使用Map接受参数,方便取出各数组,和admin执行分配方法一致,先将所有记录删除后再插入
    @ResponseBody
    @RequestMapping(“/assign/do/role/assign/auth.json”)
    public ResultEntity saveRoleAuthRelathinship(@RequestBody Map> map) {
    // 取出roleId进行删除
    List roleIdList = map.get(“roleId”);
    Integer roleId = roleIdList.get(0);
    // 根据roleId进行删除
    authService.deleteOldRelationship(roleId);
    // 取出新增的权限的authId
    List authIdList = map.get(“authIdList”);
    // 判断是否为空,再进行插入
    if (authIdList != null && authIdList.size() > 0) {
    authService.saveNewRelationship(roleId, authIdList);
    }
    return ResultEntity.successWithoutData();
    } #### AuthServuce实现 @Override
    public void deleteOldRelationship(Integer roleId) {
    authMapper.deleteOldRelationship(roleId);
    } @Override
    public void saveNewRelationship(Integer roleId, List authIdList) {
    authMapper.insertNewRelationship(roleId,authIdList);
    } #### AuthMapper接口定义方法 void deleteOldRelationship(Integer roleId); void insertNewRelationship(@Param(“roleId”) Integer roleId,@Param(“authIdList”) List authIdList); #### sql语句
    delete from inner_role_auth where role_id=#{roleId}

    insert into inner_role_auth (role_id, auth_id) values

    (#{roleId},#{authId})

    SpringSecurity的引入

    在springSecurity注入IOC容器中时,应该注入SpringMVC的IOC,对浏览器请求进行控制
    尚筹网 - 图47

    添加依赖

    父工程下添加,控制版本


    org.springframework.security
    spring-security-web
    ${fall.spring.security.version}



    org.springframework.security
    spring-security-config
    ${fall.spring.security.version}



    org.springframework.security
    spring-security-taglibs
    ${fall.spring.security.version}

    在component下添加


    org.springframework.security
    spring-security-web



    org.springframework.security
    spring-security-config


    org.springframework.security
    spring-security-taglibs
    ### 加入SpringSecurity的filter web.xml下配置


    springSecurityFilterChain
    org.springframework.web.filter.DelegatingFilterProxy


    springSecurityFilterChain
    /
    ### 创建配置类 在mvc.config中创建springSecurity的配置类
    @Configuration // 配置类
    @EnableWebSecurity // 开启web环境下的权限控制功能
    public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
    } ### 找不到springSecurityFilterChain 此时启动项目,在Tomcat Localhost Log下会报错
    尚筹网 - 图48 #### 报错原因 Web 组件加载顺序:Listener→Filter→Servlet - Spring IOC 容器:ContextLoaderListener 创建 - SpringMVC IOC 容器:DispatcherServlet 创建 - springSecurityFilterChain:从 IOC 容器中找到对应的 bean 尚筹网 - 图49 在ContextLoaderListener 初始化后,springSecurityFilterChain会在spring的IOC容器中找bean,但spring的IOC并未扫描springSecurity的配置类,就没有创建bean #### 解决方案一:合并IOC容器 将Spring和SpringMVC的IOC容器合二为一
    将spring的IOC容器注释掉,初始化时就不会在spring的IOC中找bean,而是放弃,在第一次请求时找SpringMVC中的bean - web.xml中注释掉 -



    <!— classpath:spring-persist-
    .xml—>




    • 在SpringMVC配置加载文件时,将spring的IOC扫描的bean加载进去


    • contextConfigLocation
      classpath:spring-*.xml

    解决方案二:修改源码

    先扫描SpringMVC的IOC容器,而不是spring的IOC容器,此处并未采用

    放行资源

    对登录页和静态资源放行
    @Override
    protected void configure(HttpSecurity security) throws Exception {
    security
    .authorizeRequests()
    .antMatchers(“/admin/to/login/page.html”) // 对登录页放行
    .permitAll() //无条件访问
    .antMatchers( // 对静态资源放行
    “/bootstrap/
    , “/crowd/

    , “/css/
    , “/fonts/

    , “/img/
    , “/jquery/

    , “/layer/
    , “/script/

    , “/ztree/**”)
    .permitAll()
    .anyRequest() // 其他未设置的全部请求
    .authenticated(); // 需要认证
    }

    登录配置

    • 修改登录页的表单提交路径
    • 在security下在配置表单登录功能
    • .and()
      .formLogin() // 开启表单登录
      .loginPage(“/admin/to/login/page.html”) // 登录页
      .loginProcessingUrl(“/admin/security/login.html”) // 登录请求的地址
      .defaultSuccessUrl(“/admin/to/main/page.html”) // 登录成功后前往的地址
      .usernameParameter(“loginAcct”) // 账号请求参数的名称
      .passwordParameter(“loginPswd”) // 密码请求参数的名称
      .and()
      .csrf() // 为了方便,本项目禁用跨站请求伪造功能
      .disable();
    • 注意禁用 CSRF 功能,实际开发时还是不要禁用
    • 创建用户配置模拟登录
    • @Override
      protected void configure(AuthenticationManagerBuilder builder) throws Exception {
      builder
      .inMemoryAuthentication()
      .withUser(“tom”)
      .password(“123”)
      .roles(“ADMIN”);
      }
    • 注意在spring-web-mvc.xml中将拦截器注释掉,不然在访问main页面时会被拦截器拦截重新登录

    退出登录配置

    • 在退出按钮设置请求地址
    • 退出系统
    • 开启退出登录功能
    • .and()
      .logout() // 开启登录退出功能
      .logoutUrl(“/admin/security/logout.html”) // 退出登录请求的地址
      .logoutSuccessUrl(“/admin/to/login/page.html”) // 退出成功后跳转的页面

    使用数据库登录

    目标

    使用SpringSecurity通过查找数据库,对用户的账号密码校验,和赋予用户的权限

    思路

    尚筹网 - 图50

    完善查询语句

    • 通过adminId查找权限操作
      • authService接口实现类
      • @Override
        public List getAssignAuthByAdminId(Integer adminId) {
        return authMapper.selectAssignAuthByAdminId(adminId);
        }
      • sql语句
    • 通过username查找admin
      • 使用QBC查询,AdminService接口实现类
      • @Override
        public Admin getAdminByLoginAcct(String username) {
        AdminExample adminExample = new AdminExample();
        AdminExample.Criteria criteria = adminExample.createCriteria();
        criteria.andLoginAcctLike(username);
        List admins = adminMapper.selectByExample(adminExample);
        Admin admin = admins.get(0);
        return admin;
        }

    创建SecurityAdmin

    SecurityAdmin封装了admin的信息和所拥有的权限,此时继承SpringSecurity提供的User类,并调用User的有参构造函数进行构造
    /*
    为了能方便地获取到原始地Admin对象,因此创建一个SecurityAdmin类,继承User。
    */
    public class SecurityAdmin extends User {

    // 原始的Admin对象,包含Admin的所有属性<br />    private Admin originalAdmin;
    
    public SecurityAdmin(<br />            // 传入原始的admin<br />            Admin originalAdmin,<br />            // 创建角色,权限的集合<br />            List<GrantedAuthority> authorities) {<br />        // 调用父类的构造器<br />        super(originalAdmin.getLoginAcct(), originalAdmin.getUserPswd(), authorities);<br />        this.originalAdmin = originalAdmin;<br />    }
    
    // 对外提供获取原始的Admin对象的get方法<br />    public Admin getOriginalAdmin() {<br />        return originalAdmin;<br />    }<br />}
    

    实现UserDetailsService接口

    使用CrowdUserDetailsServiceImpl来实现UserDetailsService接口,将用户和权限封装进SecurityAdmin中
    此时CrowdUserDetailsServiceImpl放在impl包下,CrowdUserDetails放在api包下,(并没有放在config包下),需要再实现CrowdUserDetails
    注意:在进行角色存入时,需要添加ROLE_前缀表示存入的是角色,而不是权限
    CrowdUserDetails接口
    public interface CrowdUserDetailsService {
    public UserDetails loadUserByUsername(String username);
    }
    CrowdUserDetailsServiceImpl实现类
    @Service
    public class CrowdUserDetailsServiceImpl implements CrowdUserDetailsService, UserDetailsService {

    @Autowired<br />    private AdminService adminService;
    
    @Autowired<br />    private RoleService roleService;
    
    @Autowired<br />    private AuthService authService;
    
    @Override<br />    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {<br />        // 通过用户名得到Admin对象<br />        Admin admin = adminService.getAdminByLoginAcct(username);
    
        // 通过AdminId得到角色List<br />        List<Role> roleList = roleService.getAssignedRole(admin.getId());
    
        // 通过AdminId得到权限name地List<br />        List<String> authNameList = authService.getAssignAuthByAdminId(admin.getId());
    
        // 创建List用来存放GrantedAuthority(权限信息)<br />        ArrayList<GrantedAuthority> authorities = new ArrayList<>();
    
        // 向List存放角色信息,注意角色必须要手动加上 “ROLE_” 前缀<br />        for (Role role : roleList) {<br />            String roleName = "ROLE_" + rolerole.getName();<br />            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(roleName);<br />            authorities.add(simpleGrantedAuthority);<br />        }
    
        // 向List存放权限信息<br />        for (String authority : authNameList) {<br />            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);<br />            authorities.add(simpleGrantedAuthority);<br />        }
    
        // 将Admin对象和权限信息存入SecurityAdmin中<br />        SecurityAdmin securityAdmin = new SecurityAdmin(admin, authorities);
    
        // 返回封装好的SecurityAdmin对象<br />        return securityAdmin;<br />    }
    

    }

    再配置类中使用数据库登录

    先装配UserDetailsService,修改配置类
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    builder
    .userDetailsService(userDetailsService); //使用数据库登录
    }

    使用盐值加密

    • 将BCryptPasswordEncoder注入到容器,可以再配置文件中配置,也可以使用@Bean注解
    • @Bean
      public BCryptPasswordEncoder BCryptPasswordEncoder() {
      return new BCryptPasswordEncoder();
      }
    • 配置使用盐值加密
    • @Override
      protected void configure(AuthenticationManagerBuilder builder) throws Exception {
      builder
      .userDetailsService(userDetailsService) // 使用数据库登录
      .passwordEncoder(BCryptPasswordEncoder()); // 使用盐值加密
      }
    • 修改保存管理员的方法(saveAdmin),不使用MD5加密,而是用盐值加密
    • 先注入自动装配(此时我们已将容器合并,在spring容器中不需要再配置)
    • @Autowired
      private BCryptPasswordEncoder bCryptPasswordEncoder;
    • 再使用BCryptPasswordEncoder
    • // 1.取出密码进行md5加密
      String password=admin.getUserPswd();
      // 使用盐值加密代替md5加密
      String encode = bCryptPasswordEncoder.encode(password);
      admin.setUserPswd(encode);

    页面显示用户名

    密码擦除

    • 由于SecurityAdmin的父类,User已经在登陆后封装进principal时,将password设置为空,在页面无法显示Credentials信息
    • public void eraseCredentials() {
      this.password = null;
      }
    • 但在SecurityAdmin的originalAdmin中还存在password的信息,因此将originalAdmin中的password设置为空(此处设置不会影响登录,登录时的密码验证时通过父类User来匹配的)
    • // 擦除originalAdmin中的密码,即将originalAdmin设置为空
      originalAdmin.setUserPswd(null);

    角色访问控制

    • 方法一:可以在配置类中设置访问时要求的角色
    • security
      .antMatchers(“/admin/get/page.html”) // 针对分页显示Admin数据设定访问控制
      .hasRole(“经理”) // 要求具备经理角色
    • 此时在没有权限的时候,会报403异常,此时在SpringSecurity配置类中配置异常处理机制
    • security
      .exceptionHandling()
      .accessDeniedHandler(new AccessDeniedHandler() {
      @Override
      public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
      request.setAttribute(“exception”,new Exception(CrowdConstant.MESSAGE_ACCESS_DENIED));
      request.getRequestDispatcher(“/WEB-INF/system-error.jsp”).forward(request,response);
      }
      });
    • SpringSecurity异常不会被SpringMVC捕捉(因此403需要自己在SpringSecurity配置),层次对应关系如下
    • 尚筹网 - 图51

    • 方法二:通过注解在Handler方法上设置
    • // 拥有部长的角色才可以访问
      @PreAuthorize(“hasAnyRole(‘部长’)”)
      @RequestMapping(“/role/get/page/info.json”)
      public ResultEntity> getPageInfo(){

      }
    • 此时需要在配置类中加入注解,以上注解才生效
    • @Configuration // 配置类
      @EnableWebSecurity // 开启web环境下的权限控制功能
      @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启此功能才可以使用注解来设置权限信息
      public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

      }
    • 在访问没有权限的资源时,SpringMVC会抛出异常,但没有对应的异常处理机制,因此配置异常处理机制,捕获一个大的Exception
    • // 处理springSecurity抛出的异常
      @ExceptionHandler(value = {Exception.class})
      public ModelAndView resolveException(
      Exception exception,
      HttpServletRequest request,
      HttpServletResponse response
      ) throws IOException {
      String viewName = “system-error”;
      return conmonResolver(exception, request, response, viewName);
      }

    权限访问控制

    • 在方法上加入权限,即拥有此权限才可以访问
    • @PreAuthorize(“hasAuthority(‘user:save’)”)
      @RequestMapping(“admin/save.html”)
      public String saveAdmin(Admin admin){

      }
    • 在配置类中设置,则/admin/get/page.html需要经理角色或者user:get才可以访问(如果OR 为 ADN 则需要角色和权限同时满足)
    • security
      .antMatchers(“/admin/get/page.html”) // 针对分页显示Admin数据设定访问控制
      .access(“hasAnyRole(‘经理’) OR hasAuthority(‘user:get’) “) // 要求具备经理角色和user:get权限

    页面元素的权限控制

    会员环境搭建

    总目标

    会员登录注册 、发起众筹项目 、展示众筹项目 、支持众筹项目 、订单、支付功能

    分布式架构思路图

    尚筹网 - 图52

    创建工程模块

    IDEA在Project Structure中就可以导入以前的模块
    尚筹网 - 图53

    导入后效果
    尚筹网 - 图54

    父工程打包方式为pom,其他工程打包为jar

    • 端口号约定
    • atcrowdfunding08-member-eureka 1000
    • atcrowdfunding10-member-mysql-provider 2000
    • atcrowdfunding11-member-redis-provider 3000
    • atcrowdfunding12-member-authentication-consumer 4000
    • atcrowdfunding13-member-project-consumer 5000
    • atcrowdfunding14-member-order-consumer 7000
    • atcrowdfunding15-member-pay-consumer 8000
    • atcrowdfunding16-member-zuul 80
    • 父工程导入依赖,进行版本控制





    • org.springframework.cloud
      spring-cloud-dependencies
      Hoxton.SR8
      pom
      import



      org.springframework.boot
      spring-boot-dependencies
      2.3.3.RELEASE
      pom
      import



      org.mybatis.spring.boot
      mybatis-spring-boot-starter
      2.1.3



      com.alibaba
      druid
      1.1.17


    Eureka模块

    • 引入依赖


    • org.springframework.cloud
      spring-cloud-starter-netflix-eureka-server

    • 启动类
    • @EnableEurekaServer // 开启eureka功能
      @SpringBootApplication
      public class CrowdMainClass {
      public static void main(String[] args) {
      SpringApplication.run(CrowdMainClass.class,args);
      }
      }
    • yml配置文件
    • server:
      port: 1000
      spring:
      application:
      name: atguigu-crowd-eureka
      eureka:
      instance:
      hostname: localhost
      client:
      fetch-registry: false # 自己就是注册中心,所以不需要“从注册中心取回信息”
      register-with-eureka: false # 自己就是注册中心,所以自己不注册自己
      service-url: # 客户端访问 Eureka 时使用的地址
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

    实体类模块

    实体类的划分

    • VO(View Object) 视图对象
    • 用途 1:接收浏览器发送过来的数据
    • 用途 2:把数据发送给浏览器去显示
    • PO (Persistent Object) 持久化对象
    • 用途 1:将数据封装到 PO 对象存入数据库
    • 用途 2:将数据库数据查询出来存入 PO 对象
    • 所以 PO 对象是和数据库表对应,一个数据库表对应一个 PO 对象
    • DO(Data Object) 数据对象
    • 用途 1:从 Redis 查询得到数据封装为 DO 对象
    • 用途 2:从 ElasticSearch 查询得到数据封装为 DO 对象
    • 用途 3:从 Solr 查询得到数据封装为 DO 对象
    • ……
    • 从中间件或其他第三方接口查询到的数据封装为 DO 对象
    • DTO(Data Transfer Object )数据传输对象
    • 用途 1:从 Consumer 发送数据到 Provider
    • 用途 2:Provider 返回数据给 Consumer
    • 示例
    • 尚筹网 - 图55

    • 使用 org.springframework.beans.BeanUtils.copyProperties(Object, Object)在不同实体类之间复制属性。

    创建包

    本项目只用到VO,PO
    创建com.atguigu.crowd.entity.po ,com.atguigu.crowd.entity.vo包

    使用lombok插件

    lombok在.class文件时才会自动创建,IDEA中导入依赖


    org.projectlombok
    lombok
    1.16.12

    Mysql工程

    逆向工程

    • 创建数据库表
    • create table t_member (
      id int(11) not null auto_increment,
      loginacct varchar(255) not null,
      userpswd char(200) not null,
      username varchar(255),
      email varchar(255),
      authstatus int(4) comment ‘实名认证状态 0 - 未实名认证, 1 - 实名认证申 请中, 2 - 已实名认证’,
      usertype int(4) comment ‘ 0 - 个人, 1 - 企业’,
      realname varchar(255),
      cardnum varchar(255),
      accttype int(4) comment ‘0 - 企业, 1 - 个体, 2 - 个人, 3 - 政府’,
      primary key (id) );
    • 逆向工程

    • 生成的实体类中使用lombok
    • @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class MemberPO {
    • 将各自文件归入各自的包

    整合mybatis

    • 引入依赖



    • com.alibaba
      druid


      org.mybatis.spring.boot
      mybatis-spring-boot-starter


      mysql
      mysql-connector-java


      org.springframework.cloud
      spring-cloud-starter-netflix-eureka-client


      org.springframework.boot
      spring-boot-starter-web


      org.springframework.boot
      spring-boot-starter-test
      test


      org.junit.vintage
      junit-vintage-engine




      com.atguigu.crowd
      atcrowdfunding09-member-entity
      1.0-SNAPSHOT


      com.atguigu.crowd
      atcrowdfunding04-admin-entity
      1.0-SNAPSHOT


      junit
      junit
      test


      com.atguigu.crowd
      atcrowdfunding05-common-util
      1.0-SNAPSHOT
      compile

    • 启动类
    • 注意扫描mapper接口
    • @MapperScan(basePackages = “com.atguigu.crowd.mapper”) // 扫描mapper接口所在的包
      @SpringBootApplication
      public class CrowdMainClass {
      public static void main(String[] args) {
      SpringApplication.run(CrowdMainClass.class,args);
      }
      }
    • yml配置文件
    • server:
      port: 2000
      eureka: # 注册eureka
      client:
      service-url:
      defaultZone: http://localhost:1000/eureka/
      spring:
      datasource: # 配置数据源
      name: mydb
      type: com.alibaba.druid.pool.DruidDataSource
      username: root
      password: …
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/project_crowd?serverTimezone=UTC # 注意配置时区
      application:
      name: atguigu-crowd-mysql # 客户端名
      mybatis: # mybatis配置
      mapper-locations: classpath:/mybatis/mapper/*.xml
      logging: # 打印sql日志
      level:
      com.atguigu.crowd.mapper: debug
      com.atguigu.crowd.test: debug
    • 测试类
    • @RunWith(SpringRunner.class)
      @SpringBootTest(classes = CrowdMainClass.class)
      public class SpringTest {

      @Autowired
      private DataSource dataSource;

      @Autowired
      private MemberPOMapper memberPOMapper;

      @Test
      public void connectTest() throws SQLException {
      Connection connection = dataSource.getConnection();
      System.out.println(connection.toString());
      }

      @Test
      public void mybatisTest() {
      BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
      String source = “123123”;
      String encode = passwordEncoder.encode(source);
      MemberPO memberPO = new MemberPO(null, “jack”, encode, “ 杰 克 “, “jack@qq.com”, 1, 1, “杰克”, “123123”, 2);
      memberPOMapper.insert(memberPO);
      }
      }

    Redis工程

    • 引入依赖



    • org.springframework.boot
      spring-boot-starter-data-redis


      org.springframework.cloud
      spring-cloud-starter-netflix-eureka-client


      org.springframework.boot
      spring-boot-starter-web


      org.springframework.boot
      spring-boot-starter-test
      test


      org.junit.vintage
      junit-vintage-engine




      com.atguigu.crowd
      atcrowdfunding09-member-entity
      1.0-SNAPSHOT


      com.atguigu.crowd
      atcrowdfunding05-common-util
      1.0-SNAPSHOT

    • 主启动类
    • @SpringBootApplication
      public class CrowdMainClass {
      public static void main(String[] args) {
      SpringApplication.run(CrowdMainClass.class,args);
      }
      }
    • yml配置文件
    • server:
      port: 3000
      eureka: # redis也是eureka的客户端
      client:
      service-url:
      defaultZone: http://localhost:1000/eureka/
      spring:
      application:
      name: atguigu-crowd-redis
      redis:
      host: 192.168.241.130
    • 测试连接redis
    • 先启动redis服务,测试类如下
    • @RunWith(SpringRunner.class)
      @SpringBootTest(classes = CrowdMainClass.class)
      public class RedisTest {

      @Autowired
      private StringRedisTemplate redisTemplate;

      @Test
      public void redisConnectTest(){
      ValueOperations stringStringValueOperations = redisTemplate.opsForValue();
      stringStringValueOperations.set(“k2”,”v2”);
      }
      }

    • 暴露接口
    • 在api工程创建接口,通过fegin连接
    • @FeignClient(“atguigu-crowd-redis”)
      public interface RedisRemoteService {

      @RequestMapping(“/set/redis/key/value/remote”)
      ResultEntity setRedisKeyValueRemote(
      @RequestParam(“key”) String key,
      @RequestParam(“value”) String value
      );

      @RequestMapping(“/set/redis/key/value/with/timeout/remote”)
      ResultEntity setRedisKeyValueWithTimeoutRemote(
      @RequestParam(“key”) String key,
      @RequestParam(“value”) String value,
      @RequestParam(“time”) long time,
      @RequestParam(“timeUnit”) TimeUnit timeunit
      );

      @RequestMapping(“/get/redis/value/by/key/remote”)
      ResultEntity getRedisValueByKeyRemote(
      @RequestParam(“key”) String key
      );

      @RequestMapping(“/remove/redis/key/by/key/remote”)
      ResultEntity RemoveRedisKeyByKeyRemote(
      @RequestParam(“key”) String key
      );

    }

    • provider的handler,提供设置值,超时时间,获取值,删除key的方法
    • @RestController
      public class RedisProviderHandler {

      @Autowired
      StringRedisTemplate redisTemplate;

      @RequestMapping(“/set/redis/key/value/remote”)
      ResultEntity setRedisKeyValueRemote(
      @RequestParam(“key”) String key,
      @RequestParam(“value”) String value
      ){
      try {
      ValueOperations opsForValue = redisTemplate.opsForValue();
      opsForValue.set(key, value);
      return ResultEntity.successWithoutData();
      } catch (Exception exception) {
      exception.printStackTrace();
      return ResultEntity.failed(exception.getMessage());
      }
      }

      @RequestMapping(“/set/redis/key/value/with/timeout/remote”)
      ResultEntity setRedisKeyValueWithTimeoutRemote(
      @RequestParam(“key”) String key,
      @RequestParam(“value”) String value,
      @RequestParam(“time”) long time,
      @RequestParam(“timeUnit”) TimeUnit timeunit
      ){
      try {
      ValueOperations opsForValue = redisTemplate.opsForValue();
      opsForValue.set(key, value,time,timeunit);
      return ResultEntity.successWithoutData();
      } catch (Exception exception) {
      exception.printStackTrace();
      return ResultEntity.failed(exception.getMessage());
      }
      }

      @RequestMapping(“/get/redis/value/by/key/remote”)
      ResultEntity getRedisValueByKeyRemote(
      @RequestParam(“key”) String key
      ){
      try {
      ValueOperations opsForValue = redisTemplate.opsForValue();
      String keyValue = opsForValue.get(key);
      return ResultEntity.successWithData(keyValue);
      } catch (Exception exception) {
      exception.printStackTrace();
      return ResultEntity.failed(exception.getMessage());
      }
      }

      @RequestMapping(“/remove/redis/key/by/key/remote”)
      ResultEntity RemoveRedisKeyByKeyRemote(
      @RequestParam(“key”) String key
      ){
      try {
      redisTemplate.delete(key);
      return ResultEntity.successWithoutData();
      } catch (Exception exception) {
      exception.printStackTrace();
      return ResultEntity.failed(exception.getMessage());
      }
      }

    }

    Auth工程

    • 引入依赖


    • org.springframework.cloud
      spring-cloud-starter-netflix-eureka-client


      org.springframework.boot
      spring-boot-starter-web


      org.springframework.boot
      spring-boot-starter-thymeleaf


      com.atguigu.crowd
      atcrowdfunding17-member-api
      1.0-SNAPSHOT

    • 主启动类
    • @SpringBootApplication
      public class CrowdMainClass {
      public static void main(String[] args) {
      SpringApplication.run(CrowdMainClass.class, args);
      }
      }
    • yml配置文件
    • server:
      port: 4000
      eureka:
      client:
      service-url:
      defaultZone: http://localhost:1000/eureka/
      spring:
      thymeleaf: # 配置视图解析器
      prefix: classpath:/templates/
      suffix: .html
      application:
      name: atguigu-crowd-auth
    • 编写一个controller返回主页面
    • @Controller
      public class PortalHandler {

      @RequestMapping(“/“)
      public String PortalPage(){
      return “portal”;
      }
      }

    • 引入主界面 portal.html
    • 修改为UTF-8编码,不然会出现乱码,引入thymeleaf名称空间,设置base标签,获取相对路径
    • <!DOCTYPE html>











    • 引入静态资源
    • 注意包名一定要为static,springboot默认static为静态资源
    • 尚筹网 - 图56

    Zuul工程

    • 引入依赖


    • org.springframework.cloud
      spring-cloud-starter-netflix-eureka-client


      org.springframework.cloud
      spring-cloud-starter-netflix-zuul

    • 主启动类(注意开启zuul
    • // 开启zuul
      @EnableZuulProxy
      @SpringBootApplication
      public class CrowdMainClass {
      public static void main(String[] args) {
      SpringApplication.run(CrowdMainClass.class,args);
      }
      }
    • yml配置文件
    • server:
      port: 80
      spring:
      application:
      name: atguigu-crowd-zuul
      eureka:
      client:
      service-url:
      defaultZone: http://localhost:1000/eureka/
      zuul:
      ignored-services: ““ # 忽略原本微服务名称
      sensitive-headers: “
      “ # 在zuul向其他微服务重定向时保持原本的请求体和响应头信息
      routes: # 自定义路由规则
      crowd-portal: # 自定义路由规则名称
      service-id: atguigu-crowd-auth # 微服务名称
      path: / # /表示多层路径,/*表示单层路径(此时就无法访问静态资源)
    • 测试时注意将eureka和对应的auth服务启动,不然会报错500

    会员登录和注册

    会员注册

    短信发送

    目标

    • 将验证码发送到用户手机上
    • 将验证码存入redis中

    思路

    尚筹网 - 图57

    前端

    • 配置viewcontroller,跳转注册页面
    • @Configuration
      public class CrowdWebMvcConfig implements WebMvcConfigurer {
      @Override
      public void addViewControllers(ViewControllerRegistry registry) {
      // 转发请求的url路径和视图名
      String registerUrl = “/auth/to/member/reg/page”;
      String registerViewName = “member-reg”;

      // 前往注册页面
      registry.addViewController(registerUrl).setViewName(registerViewName);
      }
      }
    • 主页面修改超链接
    • 注册
    • 修改注册页
    • <!DOCTYPE html>














    • 为获取验证码绑定单击事件
    • 此时需要添加给input添加name属性,button添加id属性和type类型




    • 单击发送ajax请求

    后端

    • 封装发送短信的工具方法
    • 需要在阿里云上购买短信API,此处我使用的是不需要导入依赖的短信API,使用JDK8即可使用
    • 尚筹网 - 图58

    • 封装方法时返回ResultEntity,并设置返回信息
    • /*
      @param host 请求地址 支持http 和 https 及 WEBSOCKET
      @param path 后缀
      @param appcode 用来吊第三方API的appcode(购买后可以查看)
      @param phone 短信接收的手机号码
      @param sign 签名ID
      @param skin 模板ID
      @return
      /
      public static ResultEntity sendShortMessage(
      String host,
      String path,
      String appcode,
      String phone,
      String sign,
      String skin) {
      // 生成验证码
      StringBuilder stringBuilder = new StringBuilder();
      for (int i = 0; i < 4; i++) {
      int random = (int) (Math.random()
      10);
      stringBuilder.append(random);
      }
      String param = stringBuilder.toString();
      String urlSend = host + path + “?param=” + param + “&phone=” + phone + “&sign=” + sign + “&skin=” + skin;
      try {
      URL url = new URL(urlSend);
      HttpURLConnection httpURLCon = (HttpURLConnection) url.openConnection();
      httpURLCon.setRequestProperty(“Authorization”, “APPCODE “ + appcode);// 格式Authorization:APPCODE (中间是英文空格)
      int httpCode = httpURLCon.getResponseCode();
      if (httpCode == 200) {
      String json = read(httpURLCon.getInputStream());
      System.out.println(“正常请求计费(其他均不计费)”);
      System.out.println(“获取返回的json:”);
      System.out.print(json);
      return ResultEntity.successWithData(param);
      } else {
      Map> map = httpURLCon.getHeaderFields();
      String error = map.get(“X-Ca-Error-Message”).get(0);
      if (httpCode == 400 && error.equals(“Invalid AppCode not exists“)) {
      return ResultEntity.failed(“AppCode错误 “);
      } else if (httpCode == 400 && error.equals(“Invalid Url”)) {
      return ResultEntity.failed(“请求的 Method、Path 或者环境错误”);
      } else if (httpCode == 400 && error.equals(“Invalid Param Location”)) {
      return ResultEntity.failed(“参数错误”);
      } else if (httpCode == 403 && error.equals(“Unauthorized”)) {
      return ResultEntity.failed(“服务未被授权(或URL和Path不正确)”);
      } else if (httpCode == 403 && error.equals(“Quota Exhausted”)) {
      return ResultEntity.failed(“套餐包次数用完 “);
      } else {
      return ResultEntity.failed(“参数名错误 或 其他错误” + error);
      }
      }

      } catch (MalformedURLException e) {
      return ResultEntity.failed(“URL格式错误”);
      } catch (UnknownHostException e) {
      return ResultEntity.failed(“URL地址错误”);
      } catch (Exception e) {
      // 打开注释查看详细报错异常信息
      e.printStackTrace();
      return ResultEntity.failed(“套餐包次数用完”);
      }
      }

    /
    读取返回结果
    */
    private static String read(InputStream is) throws IOException {
    StringBuffer sb = new StringBuffer();
    BufferedReader br = new BufferedReader(new InputStreamReader(is));
    String line = null;
    while ((line = br.readLine()) != null) {
    line = new String(line.getBytes(), “utf-8”);
    sb.append(line);
    }
    br.close();
    return sb.toString();
    }

    • 使用yml配置文件设置发送短信需要用到的固定参数
    • 导入依赖


    • org.springframework.boot
      spring-boot-configuration-processor
    • 配置实体类
    • @AllArgsConstructor
      @NoArgsConstructor
      @Data
      @Component // 需要注入IOC容器
      @ConfigurationProperties(prefix = “short.message”) // 在yml配置文件中对应的前缀
      public class ShortMessageProperties {
      private String host;
      private String path;
      private String appcode;
      private String sign;
      private String skin;
      }
    • yml配置文件
    • short:
      message:
      host: https://fsmsn.market.alicloudapi.com
      path: /fsms132
      appcode: 1d96cd2b0d044fde8fc7c5c828dd370d
      sign: 175622
      skin: 1
    • 此时可以使用IOC中的ShortMessageProperties对象
    • handler方法调用工具方法,并存入redis
    • @Controller
      public class MemberHandler {

      @Autowired
      private ShortMessageProperties shortMessageProperties;

      @Autowired
      private RedisRemoteService redisRemoteService;

      @ResponseBody
      @RequestMapping(“/auth/member/send/short/message.json”)
      public ResultEntity sendMessage(@RequestParam(“phoneNum”) String phoneNum) {
      // 发送验证码到phoneNum手机
      ResultEntity sendShortMessage = CrowdUtils.sendShortMessage(
      shortMessageProperties.getHost(),
      shortMessageProperties.getPath(),
      shortMessageProperties.getAppcode(),
      phoneNum,
      shortMessageProperties.getSign(),
      shortMessageProperties.getSkin());
      // 判断短信是否发送成功
      if (ResultEntity.SUCCESS.equals(sendShortMessage.getResult())) {
      // 如果发送成功,将验证码存入redis
      String code = sendShortMessage.getData();
      // 拼接存入redis的key值
      String key = CrowdConstant.REDIS_CODE_PREFIX + phoneNum;
      ResultEntity saveCodeResultEntity = redisRemoteService.setRedisKeyValueWithTimeoutRemote(key, code, 10, TimeUnit.MINUTES);
      // 判断redis中是否保存成功
      if (ResultEntity.SUCCESS.equals(saveCodeResultEntity.getResult())) {
      // 保存成功则发送消息即可
      return ResultEntity.successWithoutData();
      } else {
      // 失败直接返回保存的对象
      return saveCodeResultEntity;
      }
      } else {
      return sendShortMessage;
      }
      }
      }

    • 主启动类,注意需要启动fegin功能
    • // 启用fegin客户端功能
      @EnableFeignClients
      @SpringBootApplication
      public class CrowdMainClass {
      public static void main(String[] args) {
      SpringApplication.run(CrowdMainClass.class, args);
      }
      }
    • 运行时需要同时启动redis和eureka服务

    执行注册

    目标

    如果验证码和各项信息能够通过。就将Member存入数据库。

    思路

    尚筹网 - 图59

    前端

    • 在页面添加name属性(注意和MemberVO对象属性保持一致),修改表单提交地址和请求方式,此处实例部分









    后端

    mysql-provider工程实现保存操作
    • 先将loginacct字段设置为unique
    • ALTER TABLE t_member ADD UNIQUE INDEX(loginacct);
    • handler方法
    • 注意一定要用@RequestBody,在分布式架构中,Ribbon以json形式传输数据,在这里不会像SpringMVC一样自动寻找set方法配置
    • @RequestMapping(“/save/member/remote”)
      public ResultEntity saveMember(@RequestBody MemberPO memberPO){
      try {
      memberService.saveMember(memberPO);
      return ResultEntity.successWithoutData();
      } catch (Exception e) {
      if (e instanceof DuplicateKeyException){
      return ResultEntity.failed(CrowdConstant.MESSAGE_LOGIN_ACCT_ALREADY_IN_USE);
      }
      // 3.如果捕获到异常则返回失败的结果
      return ResultEntity.failed(e.getMessage());
      }
      }
    • saveMember接口实现
    • 此时将事务只读设置为false,只有查询是只读
    • @Transactional(
      propagation = Propagation.REQUIRES_NEW,
      rollbackFor = Exception.class,
      readOnly = false
      )
      @Override
      public void saveMember(MemberPO memberPO) {
      memberPOMapper.insertSelective(memberPO);
      }

    api工程
    • 使用fegin调用,注意方法必须和provider中调用的一样
    • @FeignClient(“atguigu-crowd-mysql”)
      public interface MysqlRemoteService {

      @RequestMapping(“/get/memberpo/by/login/acct/remote”)
      public ResultEntity getMemberPOByLoginAcctRemote(@RequestParam(“loginacct”) String loginacct);

      @RequestMapping(“/save/member/remote”)
      public ResultEntity saveMember(@RequestBody MemberPO memberPO);
      }

    authentication-consumer工程
    • 创建与视图交互的MemberVO对象,注意和表单的name属性值相等,才可自动注入到MemberVO
    • @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class MemberVO {

      private String loginacct;

      private String userpswd;

      private String username;

      private String email;

      private String phoneNum;

      private String code;
      }

    • handler方法
    • @Autowired
      private ShortMessageProperties shortMessageProperties;

    @Autowired
    private RedisRemoteService redisRemoteService;

    @Autowired
    private MysqlRemoteService mysqlRemoteService;

    @RequestMapping(“/auth/do/member/register.html”)
    public String register(MemberVO memberVO, ModelMap modelMap) {
    // 1.获取用户手机号
    String phoneNum = memberVO.getPhoneNum();

    // 2.拼redis中存储<br />    String redisCodeKey = CrowdConstant.REDIS_CODE_PREFIX + phoneNum;
    
    // 3.从redis读取key对应的value<br />    ResultEntity<String> redisValueByKeyRemote = redisRemoteService.getRedisValueByKeyRemote(redisCodeKey);
    
    // 4.检查查询操作是否有效<br />    // 未找到验证码<br />    if (ResultEntity.FAILED.equals(redisValueByKeyRemote.getResult())) {<br />        modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, redisValueByKeyRemote.getMessage());<br />        return "member-reg";<br />    }
    
    // 获取redis中的验证码<br />    String redisCode = redisValueByKeyRemote.getData();
    
    // redis中验证码为空<br />    if (redisCode == null) {<br />        modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_CODE_NOT_EXISTS);<br />        return "member-reg";<br />    }
    
    // redis中验证码不为空<br />    if (ResultEntity.SUCCESS.equals(redisValueByKeyRemote.getResult())) {<br />        // 5.如果从redis能够查询到value则比较表单验证码和redis验证码<br />        String formCode = memberVO.getCode();
    
        // 验证码不一致<br />        if (!Objects.equals(formCode, redisCode)) {<br />            modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_CODE_INVALID);<br />            return "member-reg";<br />        }
    
        // 验证码一致<br />        if (Objects.equals(formCode, redisCode)) {<br />            // 6.如果验证码一致,则从redis中删除<br />            redisRemoteService.RemoveRedisKeyByKeyRemote(redisCode);
    
            // 7.执行密码加密<br />            String userpswd = memberVO.getUserpswd();<br />            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();<br />            String passWord = bCryptPasswordEncoder.encode(userpswd);<br />            memberVO.setUserpswd(passWord);
    
            // 8.执行保存,使用BeanUtil工具类进行属性拷贝<br />            MemberPO memberPO = new MemberPO();<br />            BeanUtils.copyProperties(memberVO, memberPO);<br />            ResultEntity<String> saveMemberResultEntity = mysqlRemoteService.saveMember(memberPO);
    
            // 保存失败<br />            if (ResultEntity.FAILED.equals(saveMemberResultEntity.getResult())) {<br />                modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, saveMemberResultEntity.getMessage());<br />                return "member-reg";<br />            }<br />        }<br />    }<br />    // 使用重定向避免重复提交表单<br />    return "redirect:/auth/to/member/login/page";<br />}
    
    • view-controller跳转登录页面
    • @Configuration
      public class CrowdWebMvcConfig implements WebMvcConfigurer {
      @Override
      public void addViewControllers(ViewControllerRegistry registry) {
      // 转发请求的url路径和视图名
      String registerUrl = “/auth/to/member/reg/page”;
      String registerViewName = “member-reg”;
      // 登录请求的url路径和视图名
      String loginUrl = “/auth/to/member/login/page”;
      String loginViewName = “member-login”;

        // 前往注册页面<br />        registry.addViewController(registerUrl).setViewName(registerViewName);<br />        // 前往登录页面<br />        registry.addViewController(loginUrl).setViewName(loginViewName);<br />    }<br />}
      
    • 由于第一次请求redis需要建立缓存和连接,如果按照默认ribbon的工作时间来操作,第一次请求可能会导致超时,所以在yml配置文件中配置ribbon工作时间,可以避免
    • ribbon:
      ReadTimeout: 10000
      ConnectTimeout: 10000

    会员登录

    目标

    在用户输入账号密码后,在数据库中进行查询信息,有就跳转页面,没有需要回显错误消息。

    思路

    尚筹网 - 图60

    前端

    • 登录页完善表单


    • 登陆失败时显示的提示


      未登录时访问受限的提示












    • 主页面回显用户名
    • [[${session.loginMember.username}]]

    后端

    authentication-consumer工程

    • 创建登录后,回显的MemberLoginVO实体
    • @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class MemberLoginVO {
      private Integer Id;

      private String username;

      private String email;

    }

    • handler方法
    • @RequestMapping(“/auth/member/do/login.html”)
      public String doLogin(
      @RequestParam(“loginacct”) String loginacct,
      @RequestParam(“loginpswd”) String userpswdForm,
      ModelMap modelMap,
      HttpSession session) {
      // 根据loginacct查找对象
      ResultEntity memberPOResultEntity = mysqlRemoteService.getMemberPOByLoginAcctRemote(loginacct);

      // 如果没有对象
      if (CrowdConstant.MESSAGE_LOGIN_FAILED.equals(memberPOResultEntity.getResult())) {
      modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, memberPOResultEntity.getMessage());
      return “member-login”;
      } else {
      // 有对象则取出对象
      MemberPO memberPO = memberPOResultEntity.getData();

        // 如果取出对象为空<br />        if (memberPO == null) {<br />            modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_LOGIN_FAILED);<br />            return "member-login";<br />        }
      
        // 取出对象不为空,将密码进行比较<br />        // 注意此时是密码保存是使用的盐值加密,每次加密后的值都不相同,所以不能直接使用==判断,而是用matches方法<br />        String userpswdDb = memberPO.getUserpswd();<br />        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();<br />        boolean matches = bCryptPasswordEncoder.matches(userpswdForm, userpswdDb);<br />        if (matches) {<br />            // 密码正确时,将数据存入封装到MemberLoginVO,并保存到session中<br />            MemberLoginVO memberLoginVO = new MemberLoginVO(memberPO.getId(), memberPO.getUsername(), memberPO.getEmail());<br />            session.setAttribute(CrowdConstant.ATTR_NAME_LOGIN_MEMBER, memberLoginVO);<br />            return "redirect:/auth/to/member/center/page";<br />        } else {<br />            modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_LOGIN_FAILED);<br />            return "member-login";<br />        }
      

      }
      }

    • 配置view-controller,完成重定向
    • // 主页面请求的url路径和视图名
      String centerUrl = “/auth/to/member/center/page”;
      String centerViewName = “member-center”;

      // 前往主页面
      registry.addViewController(centerUrl).setViewName(centerViewName);

    mysql-provider工程

    • Mysql工程中handler的实现,提供接口
    • @RequestMapping(“/get/memberpo/by/login/acct/remote”)
      public ResultEntity getMemberPOByLoginAcctRemote(@RequestParam(“loginacct”) String loginacct) {
      try {
      // 1.调用本地 Service 完成查询
      MemberPO memberPO = memberService.getMemberPOByLoginAcct(loginacct);
      // 2.如果没有抛异常,那么就返回成功的结果
      return ResultEntity.successWithData(memberPO);
      } catch (Exception e) {
      e.printStackTrace();
      // 3.如果捕获到异常则返回失败的结果
      return ResultEntity.failed(e.getMessage());
      }
    • Mysql工程中service方法的实现
    • @Override
      public MemberPO getMemberPOByLoginAcct(String loginacct) {
      MemberPOExample memberPOExample = new MemberPOExample();
      MemberPOExample.Criteria criteria = memberPOExample.createCriteria();
      criteria.andLoginacctEqualTo(loginacct);
      List memberPOList = memberPOMapper.selectByExample(memberPOExample);
      if(memberPOList == null || memberPOList.size() == 0){
      return null;
      }
      return memberPOList.get(0);
      }

    api工程

    • API工程中使用fegin调用Mysql工程
    • @FeignClient(“atguigu-crowd-mysql”)
      public interface MysqlRemoteService {

      @RequestMapping(“/get/memberpo/by/login/acct/remote”)
      public ResultEntity getMemberPOByLoginAcctRemote(@RequestParam(“loginacct”) String loginacct);

    退出登录

    目标

    点击退出登录,返回到登录页面

    思路

    点击登录后,清除session

    前端

    修改超链接

  • 退出系统
  • 后端

    @RequestMapping(“/auth/member/logout.html”)
    public String doLoginOut(HttpSession session){
    session.invalidate();
    return “redirect:http://localhost/“;
    }

    登录检查

    目标

    把项目中必须登录才能访问的功能保护起来,如果没有登录就访问则跳转到登录页面。

    思路

    总思路

    使用网关的filter功能实现
    尚筹网 - 图61

    session问题

    在分布式架构中,不同工程在不同的tomcat上,在登录时存入的session和在其他工程之间是不互通的
    尚筹网 - 图62

    需要使用SpringSession,会将session存入redis中,工作原理如下
    尚筹网 - 图63

    后端

    解决session共享

    • zuulauthentication工程导入依赖


    • org.springframework.boot
      spring-boot-starter-data-redis



      org.springframework.session
      spring-session-data-redis
    • zuulauthentication工程的yml配置文件中配置
    • redis: # 配置redis的地址
      host: 192.168.241.130
      session: # session存储的类型
      store-type: redis

    放行静态资源

    创建工具类,放行静态资源和登录页以及登录请求,定义判断是否为静态资源的方法(使用截取的方法)
    public class AccessPassResources {

    public static Set<String> PASS_RES_SET = new HashSet<>();       // 放行的路径
    
    static {<br />        PASS_RES_SET.add("/");<br />        PASS_RES_SET.add("/auth/to/member/reg/page");<br />        PASS_RES_SET.add("/auth/to/member/login/page");<br />        PASS_RES_SET.add("/auth/member/logout");<br />        PASS_RES_SET.add("/auth/member/do/login");<br />        PASS_RES_SET.add("/auth/do/member/register");<br />        PASS_RES_SET.add("/auth/member/send/short/message.json");<br />    }
    
    public static Set<String> STATIC_RES_SET = new HashSet<>();    // 放行的静态资源
    
    static {<br />        STATIC_RES_SET.add("bootstrap");<br />        STATIC_RES_SET.add("css");<br />        STATIC_RES_SET.add("fonts");<br />        STATIC_RES_SET.add("img");<br />        STATIC_RES_SET.add("jquery");<br />        STATIC_RES_SET.add("layer");<br />        STATIC_RES_SET.add("script");<br />        STATIC_RES_SET.add("ztree");<br />    }
    
    public static boolean judgeCurrentServletPathWhetherStaticResource(String servletPath){
    
        // 字符串无效的情况<br />        if (servletPath == null || servletPath.length() == 0){<br />            throw new RuntimeException(CrowdConstant.MESSAGE_STRING_INVALIDATE);<br />        }
    
        // 以”/“截取字符串<br />        String[] split = servletPath.split("/");<br />        // 获取的字符数组中第一个/的左边为空字符串,考虑到他的索引为0,所以我们需要一级路径判断,取索引为1的字符串<br />        String splitFirst = split[1];<br />        // 判断是否在静态资源中<br />        return STATIC_RES_SET.contains(splitFirst);<br />    }
    

    }

    zuul工程

    • 引入工具类依赖


    • com.atguigu.crowd
      atcrowdfunding05-common-util
      1.0-SNAPSHOT
    • 创建zuulFilter过滤器
    • @Component
      public class CrowdAccessFilter extends ZuulFilter {
      @Override
      public String filterType() {
      // pre表示在微服务前拦截
      return “pre”;
      }

      @Override
      public int filterOrder() {
      // 只有一个filter不设置次序
      return 0;
      }

      @Override
      public boolean shouldFilter() {
      // 使用ThreadLocal线程本地化技术获取request
      RequestContext currentContext = RequestContext.getCurrentContext();
      HttpServletRequest request = currentContext.getRequest();

        // 根据request获取请求路径<br />        String servletPath = request.getServletPath();
      
        // 判断路径是否需要放行<br />        boolean containPath = AccessPassResources.PASS_RES_SET.contains(servletPath);
      
        // 如果路径在放行的set集合中,返回false放行<br />        if (containPath){<br />            return false;<br />        }
      
        // 判断路径是否为静态资源<br />        boolean currentServletPathWhetherStaticResource = AccessPassResources.judgeCurrentServletPathWhetherStaticResource(servletPath);
      
        // 为静态资源则,返回false放行,反之则返回true执行run方法<br />        return !currentServletPathWhetherStaticResource;<br />    }
      

      @Override
      public Object run() throws ZuulException {
      // 使用ThreadLocal线程本地化技术获取request
      RequestContext currentContext = RequestContext.getCurrentContext();
      HttpServletRequest request = currentContext.getRequest();

        // 从request中获取session对象<br />        HttpSession session = request.getSession();
      
       // 从session中取出保存的登录对象<br />        Object loginMember =  session.getAttribute(CrowdConstant.ATTR_NAME_LOGIN_MEMBER);
      
        // 如果没有登录对象<br />        if (loginMember == null){<br />            // 从 currentContext 对象中获取 Response 对象<br />            HttpServletResponse response = currentContext.getResponse();
      
            // 将提示消息存入session域<br />            session.setAttribute(CrowdConstant.ATTR_NAME_LOGIN_MEMBER,CrowdConstant.MESSAGE_ACCESS_FORBIDEN);
      
            // 重定向到登录页面<br />            try {<br />                // 直接重定向登录页面,而不是登录请求<br />                response.sendRedirect("/auth/to/member/login/page");<br />            } catch (IOException e) {<br />                e.printStackTrace();<br />            }<br />        }
      
        return null;<br />    }<br />}
      
    • 注意yml配置文件设置,设置sensitive-headers: “*” 防止在重定向后无法保存原来的请求体和请求体(session信息)
    • zuul:
      ignored-services: ““ # 忽略原本微服务名称
      sensitive-headers: “
      “ # 在zuul向其他微服务重定向时保持原本的请求体和响应头信息

    找不到MemberLoginVO

    zuul工程注意依赖entity,不然会报错找不到MemberLoginVO


    com.atguigu.crowd
    atcrowdfunding09-member-entity
    1.0-SNAPSHOT

    重定向问题

    分布式架构工程的端口不一致,相当于2个不同网站,在访问时会有session域不同步的问题,所以重定向统一修改问通过网关的80端口访问
    return “redirect:http://localhost/…”;
    此时需要修改MemberHandler中的重定向的地址,以后请求通过网关访问时,会拦截且统一保存session

    前端

    注意在登录页面回显zuul存入session的信息,此时的错误是在session中取得

    zuulfilter过滤后显示的登录提示

    发起项目

    OSS存储

    介绍

    使用

    • 创建bucket,注意一定要设置为公共读
    • 尚筹网 - 图64

    • 创建目录

    尚筹网 - 图65

    • 上传文件
    • 尚筹网 - 图66

    Java 程序调用OSS服务接口

    尚筹网 - 图67

    介绍

    访问密钥 AccessKey(AK)相当于登录密码,只是使用场景不同。AccessKey 用于程序方式调 用云服务 API,而登录密码用于登录控制台。如果您不需要调用 API,那么就不需要创建 AccessKey。 您可以使用 AccessKey 构造一个 API 请求(或者使用云服务 SDK)来操作资源。AccessKey 包 括 AccessKeyId 和 AccessKeySecret。
    AccessKeyId 用于标识用户,相当于账号。
    AccessKeySecret 是用来验证用户的密钥。AccessKeySecret 必须保密。
    禁止使用主账号AK,因为主账号AK泄露会威胁您所有资源的安全。请使用子账号(RAM 用户)AK 进行操作,可有效降低 AK 泄露的风险。

    创建子账号 AK

    1. 点击头像 -> 点击AccessKey管理
    2. 尚筹网 - 图68

    3. 创建用户
    • 尚筹网 - 图69

    • 创建后注意记住AccessKey secret或者下载CSV文件,后边需要连接,填写验证码,即可
    1. 添加权限
    • 尚筹网 - 图70

    1. 授权权限
    • 尚筹网 - 图71

    • 确定之后,就创建好了子账号
    1. 点击授权可以查看
    • 尚筹网 - 图72

    将OSS引入项目

    • 加入依赖


    • com.atguigu.crowd
      atcrowdfunding09-member-entity
      1.0-SNAPSHOT


      org.springframework.cloud
      spring-cloud-starter-netflix-eureka-client


      org.springframework.boot
      spring-boot-starter-web


      org.springframework.boot
      spring-boot-starter-thymeleaf


      org.springframework.boot
      spring-boot-configuration-processor


      org.springframework.boot
      spring-boot-starter-data-redis


      org.springframework.session
      spring-session-data-redis


      com.aliyun.oss
      aliyun-sdk-oss
      3.5.0


      org.springframework.boot
      spring-boot-starter-test
      test


      org.junit.vintage
      junit-vintage-engine



    • 创建OSSproperties类
    • @Data
      @NoArgsConstructor
      @AllArgsConstructor
      @Component
      @ConfigurationProperties(prefix = “aliyun.oss”)
      public class OSSProperties {
      private String endPoint;

      private String bucketName;

      private String accessKeyId;

      private String accessKeySecret;

      private String bucketDomain;
      }

    • yml配置文件
    • 注意配置OSSproperties需要的属性aliyun.oss
    • server:
      port: 5000
      spring:
      application:
      name: atguigu-crowd-project
      thymeleaf:
      prefix: classpath:/templates/
      suffix: .html
      redis: # 配置redis的地址
      host: 192.168.241.130
      session: # session存储的类型
      store-type: redis
      eureka:
      client:
      service-url:
      defaultZone: http://localhost:1000/eureka/
      aliyun:
      oss:
      access-key-id: LTAI5t7WYH9BFoPQLqciWx2D # 你创建子账户的AccessKey ID
      access-key-secret: 2gkfhCOoXcIQOdTxdDkXbXxjzXZOTk # 你创建子账户的AccessKey secret
      bucket-domain: project-atcrowdfunding.oss-cn-chengdu.aliyuncs.com # bucket中查找外网访问域名
      bucket-name: project-atcrowdfunding
      end-point: oss-cn-chengdu.aliyuncs.com
    • CrowdUtil类中添加上传工具静态方法
    • 先引入依赖

    • com.aliyun.oss
      aliyun-sdk-oss
      3.5.0
    • uploadFileToOSS方法
    • public static ResultEntity uploadFileToOSS(
      String endPoint,
      String accessKeyId,
      String accessKeySecret,
      InputStream inputStream,
      String bucketName,
      String bucketDomain,
      String originalName ){

      // 创建OSSClient实例
      OSS ossClient = new OSSClientBuilder().build(endPoint,accessKeyId,accessKeySecret);

      // 生成上传文件的目录,按照日期来划分目录
      String folderName = new SimpleDateFormat(“yyyyMMdd”).format(new Date());

      // 生成上传文件在OSS服务器上保存的文件名,通过uuid生成随机uuid,将其中的“-”删去(替换成空字符串)
      String fileMainName = UUID.randomUUID().toString().replace(“-“, “”);

      // 从原始文件名中获取文件扩展名
      String extensionName = originalName.substring(originalName.lastIndexOf(“.”));

      // 使用目录、文件主体名称、文件扩展名拼接得到对象名称
      String objectName = folderName + “/“ + fileMainName + extensionName;

      try {
      // 调用OSS客户端对象的方法上传文件并获取响应结果数据
      PutObjectResult putObjectResult = ossClient.putObject(bucketName,objectName,inputStream);

        // 从响应结果中获取具体的响应消息<br />            ResponseMessage responseMessage = putObjectResult.getResponse();
      
        // 根据响应状态判断是否成功<br />            if (responseMessage == null) {<br />                // 拼接访问刚刚上传的文件的路径<br />                String ossFileAccessPath = bucketDomain + "/" + objectName;
      
            // 返回成功,并带上访问路径<br />                return ResultEntity.successWithData(ossFileAccessPath);<br />            }else {<br />                // 获取响应状态码<br />                int statusCode = responseMessage.getStatusCode();<br />                // 没有成功 获取错误消息<br />                String errorMessage = responseMessage.getErrorResponseAsString();
      
            return ResultEntity.failed("当前响应状态码=" + statusCode + " 错误消息=" + errorMessage);<br />            }<br />        } catch (Exception e){<br />            e.printStackTrace();<br />            return ResultEntity.failed(e.getMessage());<br />        } finally {<br />            // 关闭OSSClient<br />            ossClient.shutdown();<br />        }
      

      }

    目标

    将各个表单页面提交的数据汇总到一起保存到数据库。

    思路

    尚筹网 - 图73

    后端

    逆向工程

    • 创建发起项目需要的数据库表
    • 分类表
      create table t_type (
      id int(11) not null auto_increment,
      name varchar(255) comment ‘分类名称’,
      remark varchar(255) comment ‘分类介绍’,
      primary key (id) );

      # 项目分类中间表
      create table t_project_type (
      id int not null auto_increment,
      projectid int(11),
      typeid int(11),
      primary key (id) );

    标签表
    create table t_tag (
    id int(11) not null auto_increment,
    pid int(11),
    name varchar(255),
    primary key (id) );

    # 项目标签中间表
    create table t_project_tag(
    id int(11) not null auto_increment,
    projectid int(11),
    tagid int(11),
    primary key (id) );

    项目表
    create table t_project (
    id int(11) not null auto_increment,
    project_name varchar(255) comment ‘项目名称’,
    project_description varchar(255) comment ‘项目描述’,
    money bigint (11) comment ‘筹集金额’,
    day int(11) comment ‘筹集天数’,
    status int(4) comment ‘0-即将开始,1-众筹中,2-众筹成功,3-众筹失败 ‘,
    deploydate varchar(10) comment ‘项目发起时间’,
    supportmoney bigint(11) comment ‘已筹集到的金额’,
    supporter int(11) comment ‘支持人数’,
    completion int(3) comment ‘百分比完成度’,
    memberid int(11) comment ‘发起人的会员 id’,
    createdate varchar(19) comment ‘项目创建时间’,
    follower int(11) comment ‘关注人数’,
    header_picture_path varchar(255) comment ‘头图路径’,
    primary key (id) );

    项目表项目详情图片表
    create table t_project_item_pic (
    id int(11) not null auto_increment,
    projectid int(11),
    item_pic_path varchar(255),
    primary key (id) );

    项目发起人信息表
    create table t_member_launch_info(
    id int(11) not null auto_increment,
    memberid int(11) comment ‘会员 id’,
    description_simple varchar(255) comment ‘简单介绍’,
    description_detail varchar(255) comment ‘详细介绍’,
    phone_num varchar(255) comment ‘联系电话’,
    service_num varchar(255) comment ‘客服电话’,
    primary key (id) );

    回报信息表
    create table t_return (
    id int(11) not null auto_increment,
    projectid int(11),
    type int(4) comment ‘0 - 实物回报, 1 虚拟物品回报’,
    supportmoney int(11) comment ‘支持金额’,
    content varchar(255) comment ‘回报内容’,
    count int(11) comment ‘回报产品限额,“0”为不限回报数量’,
    signalpurchase int(11) comment ‘是否设置单笔限购’,
    purchase int(11) comment ‘具体限购数量’,
    freight int(11) comment ‘运费,“0”为包邮’,
    invoice int(4) comment ‘0 - 不开发票, 1 - 开发票’,
    returndate int(11) comment ‘项目结束后多少天向支持者发送回报’,
    describ_pic_path varchar(255) comment ‘说明图片路径’,
    primary key (id) );

    发起人确认信息表
    create table t_member_confirm_info (
    id int(11) not null auto_increment, memberid int(11) comment ‘会员 id’,
    paynum varchar(200) comment ‘易付宝企业账号’,
    cardnum varchar(200) comment ‘法人身份证号’,
    primary key (id) );

    • 逆向工程








    • 将实体类放在entity包中,接口和xml文件放在mysql-provider工程对应的包中

    创建VO对象

    可通过尚筹网资料获取VO对象
    尚筹网 - 图74

    跳转页面

    我的众筹页面跳转

    • 在auth工程的wen配置类添加,跳转到众筹页面
    • // 众筹页面请求的url路径和视图名
      String crowdUrl = “/member/my/crowd”;
      String crowdViewName = “member-crowd”;
      // 前往众筹页面
      registry.addViewController(crowdUrl).setViewName(crowdViewName);

    发起众筹页面跳转

    • 在zuul的yml配置文件中配置访问project工程的path
    • crowd-project: # 自定义路由规则名称
      service-id: atguigu-crowd-project # 微服务名称
      path: /project/ # auth已经通过/访问,此时需要加个/project
    • 前端通过 Zuul 访问具体功能才能够保持 Cookie,进而保持 Session 一致,此时在project中创建web配置,设置view-controller
    • @Override
      public void addViewControllers(ViewControllerRegistry registry) {
      // 重定向跳转页面
      registry.addViewController(“/agree/portal/page”).setViewName(“project-agree”);
      registry.addViewController(“/launch/project/page”).setViewName(“project-launch”);
      registry.addViewController(“/return/project/page”).setViewName(“project-return”);
      }

    接收发起项目表单数据

    • 表单提交地址
    • handler方法
    • @Controller
      public class ProjectConsumerHandler {

      @Autowired
      private OSSProperties ossProperties;

      @RequestMapping(“/create/project/information”)
      public String saveProjectBasicInfo(
      // 接收表单的部分信息
      ProjectVO projectVO,
      // 接收上传的头图
      MultipartFile headerPicture,
      // 接收项目详情图片
      List detailPictureList,
      // 将收集的一部分数据的ProjectVO保存到session
      HttpSession session,
      // 将错误信息保存到ModelMap中
      ModelMap modelMap
      ) throws IOException {

        // 如果头图为空,返回错误信息<br />        if (headerPicture.isEmpty()) {<br />            // 返回错误消息<br />            modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_HEADER_PIC_EMPTY);<br />            return "project-launch";<br />        }
      
        // 将头图上传到OSS上
      
        ResultEntity<String> resultEntity = CrowdUtils.uploadFileToOSS(<br />                ossProperties.getEndPoint(),<br />                ossProperties.getAccessKeyId(),<br />                ossProperties.getAccessKeySecret(),<br />                headerPicture.getInputStream(),<br />                ossProperties.getBucketName(),<br />                ossProperties.getBucketDomain(),<br />                headerPicture.getOriginalFilename()<br />        );
      
        if (ResultEntity.FAILED.equals(resultEntity.getResult())) {<br />            // 上传失败则显示错误信息<br />            modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_HEADER_PIC_UPLOAD_FAILED);<br />            // 返回上一页面<br />            return "project-launch";<br />        }
      
        // 上传成功将信息保存到projectVO中<br />        String data = resultEntity.getData();<br />        projectVO.setHeaderPicturePath(data);
      
        // 将详情图片上传到OSS<br />        // 创建存放图片地址的list集合<br />        List<String> detailPicturePathList = new ArrayList<>();
      
        // 判断详情图片是否为空<br />        if (detailPictureList == null || detailPictureList.isEmpty()) {<br />            // 上传失败则显示错误信息<br />            modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_DETAIL_PIC_EMPTY);<br />            // 返回上一页面<br />            return "project-launch";<br />        }
      
        // 图片不为空,遍历详情图片<br />        for (MultipartFile detailPicture : detailPictureList) {<br />            // 判断当前图片是否有效<br />            if (detailPicture == null) {<br />                // 上传失败则显示错误信息<br />                modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_DETAIL_PIC_EMPTY);<br />                // 返回上一页面<br />                return "project-launch";<br />            }
      
            ResultEntity<String> detailPictureResultEntity = CrowdUtils.uploadFileToOSS(<br />                    ossProperties.getEndPoint(),<br />                    ossProperties.getAccessKeyId(),<br />                    ossProperties.getAccessKeySecret(),<br />                    detailPicture.getInputStream(),<br />                    ossProperties.getBucketName(),<br />                    ossProperties.getBucketDomain(),<br />                    detailPicture.getOriginalFilename()<br />            );
      
            if (ResultEntity.FAILED.equals(detailPictureResultEntity.getResult())) {<br />                // 上传失败则显示错误信息<br />                modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_DETAIL_PIC_UPLOAD_FAILED);<br />                // 返回上一页面<br />                return "project-launch";<br />            }
      
            // 保存成功将路径存入list<br />            String pictureResultEntityData = detailPictureResultEntity.getData();<br />            detailPicturePathList.add(pictureResultEntityData);<br />        }
      
        // 将图片路径list集合保存在projectVO中<br />        projectVO.setDetailPicturePathList(detailPicturePathList);
      
        // 将projectVO对象存入session域<br />        session.setAttribute(CrowdConstant.ATTR_NAME_TEMPLE_PROJECT, projectVO);
      
        // 以网关的路径来重定向到return页面,才能保持session信息<br />        return "redirect:http://localhost/project/return/project/page";<br />    }<br />}
      

    收集回报信息

    • 前端流程
    • 尚筹网 - 图75

    • ajax请求保存照片
    • @ResponseBody
      @RequestMapping(“/create/upload/return/picture.json”)
      public ResultEntity uploadReturnPicture(@RequestParam(“returnPicture”) MultipartFile returnPicture) throws IOException {
      // 判断是否是有效上传
      boolean pictureIsEmpty = returnPicture.isEmpty();
      if (pictureIsEmpty){
      // 如果上传文件为空
      ResultEntity.failed(CrowdConstant.MESSAGE_RETURN_PIC_EMPTY);
      }

      // 执行文件上传
      ResultEntity returnPictureEntity = CrowdUtils.uploadFileToOSS(
      ossProperties.getEndPoint(),
      ossProperties.getAccessKeyId(),
      ossProperties.getAccessKeySecret(),
      returnPicture.getInputStream(),
      ossProperties.getBucketName(),
      ossProperties.getBucketDomain(),
      returnPicture.getOriginalFilename()
      );

    • 保存回报表单信息
    • @ResponseBody
      @RequestMapping(“/create/save/return.json”)
      public ResultEntity saveReturn(ReturnVO returnVO, HttpSession session) {
      try {
      // 从session中取出刚才保存的projectVO,需要进行强转
      ProjectVO projectVO = (ProjectVO) session.getAttribute(CrowdConstant.ATTR_NAME_TEMPLE_PROJECT);

        // 判断return数据是否为空<br />        if (returnVO == null) {<br />            return ResultEntity.failed(CrowdConstant.MESSAGE_TEMPLE_PROJECT_MISSING);<br />        }
      
        // 获取projectVO中的renturnVO<br />        List<ReturnVO> returnVOList = projectVO.getReturnVOList();
      
        // 如果returnVOList为空<br />        if (returnVOList == null || returnVOList.size() == 0) {<br />            // 初始化List<br />            returnVOList = new ArrayList<>();<br />            projectVO.setReturnVOList(returnVOList);<br />        }
      
        // returnVOList不为空,将值追加进去<br />        returnVOList.add(returnVO);
      
        // 重新把projectVO存入session中<br />        session.setAttribute(CrowdConstant.ATTR_NAME_TEMPLE_PROJECT, projectVO);
      
        // 返回结果集<br />        return ResultEntity.successWithoutData();
      

      } catch (Exception exception) {
      exception.printStackTrace();
      // 返回失败的结果集
      return ResultEntity.failed(exception.getMessage());
      }

    }

    点击下一步跳转

    • 修改超链接标签
    • 下一步
    • 配置view-controller
    • registry.addViewController(“/create/confirm/page”).setViewName(“project-confirm”);
    • 修改project-confirm页面

    点击提交按钮确认表单

    • 在project-confirm页面修改表单提交信息,绑定提交单击事件



    • 收集表单数据
    • @RequestMapping(“/create/confirm”)
      public String saveConfirm(MemberConfirmInfoVO memberConfirmInfoVO, HttpSession session, ModelMap modelMap) {

      // 从session域读取之前的projectVO
      ProjectVO projectVO = (ProjectVO) session.getAttribute(CrowdConstant.ATTR_NAME_TEMPLE_PROJECT);

      // 判断是否为空
      if (projectVO == null) {
      throw new RuntimeException(CrowdConstant.MESSAGE_TEMPLE_PROJECT_MISSING);
      }

      // 将确认的信息保存在projectVO中
      projectVO.setMemberConfirmInfoVO(memberConfirmInfoVO);

      // 从session域读取当前访问的用户
      MemberLoginVO memberLoginVO = (MemberLoginVO) session.getAttribute(CrowdConstant.ATTR_NAME_LOGIN_MEMBER);

      // 从中取出保存进去的id值,进而保存在数据库中
      Integer memberLoginVOId = memberLoginVO.getId();

      // 调用mysql的方法将projectVO保存到数据库中
      ResultEntity resultEntity = mysqlRemoteService.saveProjectVORemote(projectVO , memberLoginVOId);

      // 保存失败,将错误信息保存到modelMap中
      if (ResultEntity.SUCCESS.equals(resultEntity.getResult())) {
      modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE,resultEntity.getMessage());
      return “project-confirm”;
      }

      // 保存好后将session域清空
      session.removeAttribute(CrowdConstant.MESSAGE_TEMPLE_PROJECT_MISSING);

      return “redirect:http://localhost/projetc/create/success/page“;
      }

    • 声明mysql的fegin接口,注意实体类一定要使用@RequestBody
    • @RequestMapping(“/save/projectvo/remote”)
      public ResultEntity saveProjectVORemote(@RequestBody ProjectVO projectVO,@RequestParam(“memberLoginVOId”) Integer memberLoginVOId);
    • 在ProjectConsumerHandler的主启动类开启fegin代理
    • @SpringBootApplication
      @EnableFeignClients
      public class CrowdMainClass {
      public static void main(String[] args) {
      SpringApplication.run(CrowdMainClass.class,args);
      }
      }

    把项目信息保存到数据库

    • mysql-provider工程ProjectProviderHandler方法
    • @RequestMapping(“/save/projectvo/remote”)
      public ResultEntity saveProjectVORemote(@RequestBody ProjectVO projectVO, @RequestParam(“memberLoginVOId”) Integer memberLoginVOId) {

      try {<br />            // 调用service方法进行保存<br />            projectService.saveProject(projectVO, memberLoginVOId);
      
          // 返回结果<br />            return ResultEntity.successWithoutData();<br />        } catch (Exception exception) {<br />            exception.printStackTrace();<br />            return ResultEntity.failed(exception.getMessage());<br />        }<br />    }
      
    • 保存信息ProjectService接口的实现
    • 注意需要修改projectPOMapper.xml文件的insertSelective方法添加 useGeneratedKeys=”true” keyProperty=”id”,来获取projectId
    • @Transactional(readOnly = true)
      @Service
      public class ProjectServiceImpl implements ProjectService {

      @Autowired
      private ReturnPOMapper returnPOMapper;

      @Autowired
      private MemberConfirmInfoPOMapper memberConfirmInfoPOMapper;

      @Autowired
      private MemberLaunchInfoPOMapper memberLaunchInfoPOMapper;

      @Autowired
      private ProjectPOMapper projectPOMapper;

      @Autowired
      private ProjectItemPicPOMapper projectItemPicPOMapper;

      @Transactional(readOnly = false, rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
      @Override
      public void saveProject(ProjectVO projectVO, Integer memberLoginVOId) {

        // 一:保存projectPO信息<br />        // 1.创建projectPO对象<br />        ProjectPO projectPO = new ProjectPO();<br />        // 2.将projectVO的值赋值给projectPO<br />        BeanUtils.copyProperties(projectVO, projectPO);<br />        // 3.把memberID设置到projectPO中<br />        projectPO.setMemberid(memberLoginVOId);<br />        // 4.生成创建的时间<br />        String createdate = new SimpleDateFormat("yyyy-MM-dd").format(new Date());<br />        projectPO.setCreatedate(createdate);<br />        // 5.status设置为0,表示即将开始<br />        projectPO.setStatus(0);<br />        // 6.保存projectPO对象,为了获取projectPO的id属性,需要修改xml配置文件添加 useGeneratedKeys="true" keyProperty="id"<br />        projectPOMapper.insertSelective(projectPO);<br />        // 7.获取id属性<br />        Integer projectId = projectPO.getId();
      
        // 二: 保存项目分类的关联的相关信息<br />        // 1.从projectVO中获取typeIdList<br />        List<Integer> typeIdList = projectVO.getTypeIdList();<br />        // 2.保存typeIdList<br />        projectPOMapper.insertTypeRelationship(typeIdList, projectId);
      
        // 三:保存项目标签的关联的相关信息<br />        // 1.从projectVO中获取typeIdList<br />        List<Integer> tagIdList = projectVO.getTagIdList();<br />        // 2.保存tagIdList<br />        projectPOMapper.insertTagRelationship(tagIdList, projectId);
      
        // 四:保存详情图片路径<br />        // 1.从projectVO中获取detailPicturePathList<br />        List<String> detailPicturePathList = projectVO.getDetailPicturePathList();<br />        // 2.保存detailPicturePathList<br />        projectItemPicPOMapper.insertPathList(projectId, detailPicturePathList);
      
        // 五:保存项目发起人信息<br />        MemberLauchInfoVO memberLauchInfoVO = projectVO.getMemberLauchInfoVO();<br />        MemberLaunchInfoPO memberLaunchInfoPO = new MemberLaunchInfoPO();<br />        BeanUtils.copyProperties(memberLauchInfoVO, memberLaunchInfoPO);<br />        memberLaunchInfoPO.setMemberid(projectId);<br />        memberLaunchInfoPOMapper.insert(memberLaunchInfoPO);
      
        // 六:保存项目回报信息<br />        List<ReturnVO> returnVOList = projectVO.getReturnVOList();<br />        ArrayList<ReturnPO> returnPOList = new ArrayList<>();<br />        for (ReturnVO returnVO : returnVOList) {<br />            ReturnPO returnPO = new ReturnPO();<br />            BeanUtils.copyProperties(returnVO , returnPO);<br />            returnPOList.add(returnPO);<br />        }<br />        returnPOMapper.insertReturnPOBatch(returnPOList , projectId);
      
        // 七:保存项目确认信息<br />        MemberConfirmInfoVO memberConfirmInfoVO = projectVO.getMemberConfirmInfoVO();<br />        MemberConfirmInfoPO memberConfirmInfoPO = new MemberConfirmInfoPO();<br />        BeanUtils.copyProperties(memberConfirmInfoVO, memberConfirmInfoPO);<br />        memberConfirmInfoPO.setMemberid(projectId);<br />        memberConfirmInfoPOMapper.insert(memberConfirmInfoPO);<br />    }<br />}
      
    • 对应需要添加的sql语句
      • ProjectPOMapper接口
      • void insertTypeRelationship(@Param(“typeIdList”) List typeIdList, @Param(“projectId”) Integer projectId);

    void insertTagRelationship(@Param(“tagIdList”) List tagIdList, @Param(“projectId”) Integer projectId);

    • ProjectPOMapper.xml文件

    • insert into t_project_type(projectid,typeid) values

      (#{projectId} , #{typeId})


    insert into t_project_tag(projectid,tagid) values

    (#{projectId} , #{tagId})

    • ProjectItemPicPOMapper接口
    • void insertPathList(@Param(“projectId”) Integer projectId,@Param(“detailPicturePathList”) List detailPicturePathList);
    • ProjectItemPicPOMapper.xml文件

    • insert into t_project_item_pic(projectid, item_pic_path) values

      (#{projectId} , #{detailPath})

    • ReturnPOMapper接口
    • void insertReturnPOBatch(@Param(“returnPOList”) ArrayList returnPOList,@Param(“projectId”) Integer projectId);
    • ReturnPOMapper.xml文件

    • insert into t_return (
      projectid,
      type,
      supportmoney,
      content,
      count,
      signalpurchase,
      purchase,
      freight,
      invoice,
      returndate,
      describ_pic_path)
      values

      (
      #{projectId},
      #{returnPO.type},
      #{returnPO.supportmoney},
      #{returnPO.content},
      #{returnPO.count},
      #{returnPO.signalpurchase},
      #{returnPO.purchase},
      #{returnPO.freight},
      #{returnPO.invoice},
      #{returnPO.returndate},
      #{returnPO.describPicPath}
      )

    前端

    • member-center页面添加超链接
    • member-crowd页面添加跳转地址,前面要写上域名(如果没有配置域名写 localhost 一样),确保通过Zuul 访问具体功能。
    • 因为必须通过 Zuul 访问具体功能才能够保持 Cookie,进而保持 Session 一致
    • project-agree页面

    展示项目

    目标

    在主页面上加载保存在数据库中真实的数据,按分类显示。

    思路

    尚筹网 - 图76

    后端

    创建实体类

    • PortalTypeVO
    • @Data
      @NoArgsConstructor
      @AllArgsConstructor
      public class PortalTypeVO {
      private Integer id;
      private String name;
      private String remark;

      private List portalProjectVOList;
      }

    • PortalProjectVO
    • @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class PortalProjectVO {
      private Integer projectId;
      private String projectName;
      private String headerPicturePath;
      private Integer money;
      private String deployDate;
      private Integer percentage;
      private Integer supporter;
      }

    sql语句查询显示信息

    • ProjectPOMapper接口
    • List selectPortalTypeVOList();
    • ProjectPOMapper.xml









    Service接口和实现

    • ProjectService
    • List selectPortalTypeVOList();
    • ProjectServiceImpl
    • @Override
      public List selectPortalTypeVOList() {
      return projectPOMapper.selectPortalTypeVOList();
      }

    Mysql工程handler方法

    • 调用service方法
    • @RequestMapping(“/get/portal/type/project/data/remote”)
      public ResultEntity> getPortalTypeVOList() {
      try {
      List portalTypeVOList = projectService.selectPortalTypeVOList();
      return ResultEntity.successWithData(portalTypeVOList);
      } catch (Exception exception) {
      exception.printStackTrace();
      return ResultEntity.failed(exception.getMessage());
      }
      }

    api工程

    • 暴露接口,注意和mysql工程中一致
    • @RequestMapping(“/get/portal/type/project/data/remote”)
      public ResultEntity> getPortalTypeVOList();

    处理页面请求handler

    • 将数据存入model中,显示到页面上
    • @RequestMapping(“/“)
      public String PortalPage(Model model) {

      // 调用远程mysql工程方法
      ResultEntity> portalTypeVOListEntity = mysqlRemoteService.getPortalTypeVOList();

      // 判断是否查询成功
      if (ResultEntity.SUCCESS.equals(portalTypeVOListEntity.getResult())) {
      // 将数据存入model用于前端显示
      List portalTypeVOList = portalTypeVOListEntity.getData();
      model.addAttribute(CrowdConstant.ATTR_NAME_PORTAL_TYPE_LIST, portalTypeVOList);
      }

      return “portal”;
      }

    前端

    • 根据modal中的数据,显示页面信息
    • 未能找到分类信息









      开启智慧未来





      未能找到当前分类的项目信息




      300x200


      活性富氢净水直饮机



      $20,000

      2017-20-20






      40%




      12345








              </div><br />            </div><br />        </div><br />    </div>
      

    显示项目详情

    目标

    点击项目,显示项目详情

    思路

    尚筹网 - 图78

    后端

    创建实体类

    • DetailProjectVO
    • @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class DetailProjectVO {

      private Integer projectId;
      private String projectName;
      private String projectDesc;
      private Integer followerCount;
      private Integer day;
      private Integer status;
      private String statusText;
      private Integer money;
      private Integer supportMoney;
      private Integer percentage;
      private String deployDate;
      private Integer lastDay;
      private Integer supporterCount;
      private String headerPicturePath;
      private List detailPicturePathList;
      private List detailReturnVOList;

    }

    • DetailReturnVO
    • /*
      每一个项目的回报信息
      */
      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class DetailReturnVO {

      // 回报主键信息
      private Integer returnId;

      // 当前栏位需要支持的金额
      private Integer supportMoney;

      // 是否限购,0时无限额,1时有限额
      private Integer signalPurchase;

      // 具体的限额数量
      private Integer purchase;

      // 当前栏位的支持者数量
      private Integer supporterCount;

      // 运费 0时表示包邮
      private Integer freight;

      // 众筹成功多少天后发货
      private Integer returnDate;

      // 回报的详细内容
      private String content;

    }

    sql语句查找详情信息

    • ProjectPOMapper接口
    • DetailProjectVO selectDetailProjectVO(@Param(“id”) Integer projectId);
    • ProjectPOMapper.xml,多张表进行查询使用collection















    Service接口和实现

    • ProjectService
    • DetailProjectVO selectDetailProjectVO(Integer projectId);
    • ProjectServiceImpl,注意计算剩余天数和设置状态内容
    • @Override
      public DetailProjectVO selectDetailProjectVO(Integer projectId) {
      DetailProjectVO detailProjectVO = projectPOMapper.selectDetailProjectVO(projectId);

      // 根据status设置当前筹集的状态
      Integer status = detailProjectVO.getStatus();
      switch (status) {
      case 0:
      detailProjectVO.setStatusText(“即将开始”);
      break;
      case 1:
      detailProjectVO.setStatusText(“众筹中”);
      break;
      case 2:
      detailProjectVO.setStatusText(“众筹成功”);
      break;
      case 3:
      detailProjectVO.setStatusText(“众筹失败”);
      break;
      default:
      break;
      }

      // 使用LocalDate计算众筹还剩余多少时间
      // 获取当前是当年的第多少天
      LocalDate todayDate = LocalDate.now();
      Integer todayDateDayOfYear = todayDate.getDayOfYear();

      // 获取众筹开始的时间
      String deployDate = detailProjectVO.getDeployDate();

      // 转化为LocalDate类型,取出的deployDate不能为空,不然会空指针
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern(“yyyy-MM-dd”);
      LocalDate date = LocalDate.parse(deployDate, formatter);
      // 获取在当年的第多少天
      Integer deployDateOfYear= date.getDayOfYear();

      // 获取筹集持续时间
      Integer day = detailProjectVO.getDay();

      // 计算众筹的剩余时间
      Integer lastDay = day - (todayDateDayOfYear - deployDateOfYear);

      // 设置lastDay剩余时间
      detailProjectVO.setLastDay(lastDay);
      return detailProjectVO;
      }

    Mysql工程handler方法

    • 调用接口返回信息
    • @RequestMapping(“/get/detail/project/remote/{projectId}”)
      public ResultEntity getDetailProjectVORemote(@PathVariable(“projectId”) Integer projectId) {
      try {
      DetailProjectVO detailProjectVO = projectService.selectDetailProjectVO(projectId);
      return ResultEntity.successWithData(detailProjectVO);
      } catch (Exception exception) {
      exception.printStackTrace();
      return ResultEntity.failed(exception.getMessage());
      }
      }

    api工程

    • 暴露接口,注意和mysql-provider中一致
    • @RequestMapping(“/get/detail/project/remote/{projectId}”)
      public ResultEntity getDetailProjectVORemote(@PathVariable(“projectId”) Integer projectId);

    处理页面请求handler

    • 路径上获取参数注意使用@PathVariable
    • @RequestMapping(“/get/detail/project/remote/{projectId}”)
      public ResultEntity getDetailProjectVORemote(@PathVariable(“projectId”) Integer projectId) {
      try {
      DetailProjectVO detailProjectVO = projectService.selectDetailProjectVO(projectId);
      return ResultEntity.successWithData(detailProjectVO);
      } catch (Exception exception) {
      exception.printStackTrace();
      return ResultEntity.failed(exception.getMessage());
      }
      }

    前端

    • 修改portal页面的a标签
    • th:href=”@{http://localhost/project/get/project/detail/} + ${project.projectId}”
      th:text=”${project.projectName}”>活性富氢净水直饮机
    • 遍历detailProviderVO对象,显示在页面上,注意修改登录和退出登录的超链接地址

    订单功能

    搭建订单环境

    • 引入依赖


    • org.springframework.cloud
      spring-cloud-starter-netflix-eureka-client


      org.springframework.boot
      spring-boot-starter-web



      org.springframework.boot
      spring-boot-starter-thymeleaf



      com.atguigu.crowd
      atcrowdfunding17-member-api
      1.0-SNAPSHOT



      org.springframework.boot
      spring-boot-configuration-processor



      org.springframework.boot
      spring-boot-starter-data-redis



      org.springframework.session
      spring-session-data-redis


      org.springframework.boot
      spring-boot-starter-test
      test


      org.junit.vintage
      junit-vintage-engine



    • application配置文件
    • server:
      port: 7000
      spring:
      application:
      name: atguigu-crowd-order
      thymeleaf:
      prefix: classpath:/templates/
      suffix: .html
      redis: # 配置redis的地址
      host: 192.168.241.130
      session: # session存储的类型
      store-type: redis
      eureka:
      client:
      service-url:
      defaultZone: http://localhost:1000/eureka/
      ribbon:
      ReadTimeout: 10000
      ConnectTimeout: 10000

    逆向工程

    • 创建数据表
    • 尚筹网 - 图79

    • 订单表
    • CREATE TABLE project_crowd.t_order (
      id INT NOT NULL AUTO_INCREMENT COMMENT ‘主键’,
      order_num CHAR(100) COMMENT ‘订单号’,
      pay_order_num CHAR(100) COMMENT ‘支付宝流水号’,
      order_amount DOUBLE(10,5) COMMENT ‘订单金额’,
      invoice INT COMMENT ‘是否开发票(0 不开,1 开)’,
      invoice_title CHAR(100) COMMENT ‘发票抬头’,
      order_remark CHAR(100) COMMENT ‘订单备注’,
      address_id CHAR(100) COMMENT ‘收货地址 id’,
      PRIMARY KEY (id)
      );
    • 收货地址表
    • CREATE TABLE project_crowd.t_address (
      id INT NOT NULL AUTO_INCREMENT COMMENT ‘主键’,
      receive_name CHAR(100) COMMENT ‘收件人’,
      phone_num CHAR(100) COMMENT ‘手机号’,
      address CHAR(200) COMMENT ‘收货地址’,
      member_id INT COMMENT ‘用户 id’,
      PRIMARY KEY (id)
      );
    • 项目信息表
    • CREATE TABLE project_crowd.t_order_project (
      id INT NOT NULL AUTO_INCREMENT COMMENT ‘主键’,
      project_name CHAR(200) COMMENT ‘项目名称’,
      launch_name CHAR(100) COMMENT ‘发起人’,
      return_content CHAR(200) COMMENT ‘回报内容’,
      return_count INT COMMENT ‘回报数量’,
      support_price INT COMMENT ‘支持单价’,
      freight INT COMMENT ‘配送费用’,
      order_id INT COMMENT ‘订单表的主键’,
      PRIMARY KEY (id)
      );
    • 逆向工程生成也对应的实体类



    • 将各自文件移动到各自包中

    确认回报内容

    目标

    确认回报信息,填写回报数量,并结算,保存在VO对象中

    思路

    尚筹网 - 图80

    后端

    创建实体类

    创建VO对象与页面相对应
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class OrderProjectVO implements Serializable {

    private static final long serialVersionUID = 1L;
    
    private Integer id;
    
    private String orderNum;
    
    private String payOrderNum;
    
    private Double orderAmount;
    
    private Integer invoice;
    
    private String invoiceTitle;
    
    private String orderRemark;
    
    private String addressId;
    
    private Integer signalPurchase;
    
    private Integer purchase;<br />}
    

    sql语句查找VO对象相关字段

    • OrderPOMapper接口
    • OrderProjectVO selectOrderProjectVO(@Param(“returnId”) Integer returnId);
    • OrderPOMapper.xml中查询语句

    Service接口的实现

    • OrderService接口
    • public interface OrderService {

      OrderProjectVO selectOrderProjectVO(Integer returnId);
      }

    • OrderServiceImpl实现类
    • @Service
      @Transactional(readOnly = false)
      public class OrderServiceImpl implements OrderService {

      @Autowired
      private OrderPOMapper orderPOMapper;

      @Autowired
      private OrderProjectPOMapper orderProjectPOMapper;

      @Autowired
      private AddressPOMapper addressPOMapper;

      @Override
      public OrderProjectVO selectOrderProjectVO(Integer returnId) {
      return orderPOMapper.selectOrderProjectVO(returnId);
      }
      }

    Mysql工程handler方法

    • 注意和api中的方法名一致
    • @RestControllerpublic class OrderProviderHandler {

      @Autowired
      private OrderService orderService;

      @RequestMapping(“/get/order/projectvo/remote”)
      public ResultEntity getOrderProjectVORemote(
      @RequestParam(“returnId”) Integer returnId,
      HttpSession session) {

        try {<br />            OrderProjectVO orderProjectVO = orderService.selectOrderProjectVO(returnId);<br />            return ResultEntity.successWithData(orderProjectVO);<br />        } catch (Exception exception) {<br />            exception.printStackTrace();<br />            return ResultEntity.failed(exception.getMessage());<br />        }<br />    }<br />}
      

    api工程

    • 暴露接口
    • @RequestMapping(“/get/order/projectvo/remote”)
      public ResultEntity getOrderProjectVORemote(@RequestParam(“returnId”) Integer returnId);

    order工程

    处理页面请求的handler
    • 使用@PathVariable接收参数,注意将数据存入session中方便使用
    • @Controller
      public class OrderHandler {

      @Autowired
      private MysqlRemoteService mysqlRemoteService;

      @RequestMapping(“/confirm/return/info/{returnId}”)
      public String showReturnConfirmInfo(
      @PathVariable(“returnId”) Integer returnId,
      HttpSession session
      ) {
      ResultEntity orderProjectVOResultEntity = mysqlRemoteService.getOrderProjectVORemote(returnId);

        if (ResultEntity.SUCCESS.equals(orderProjectVOResultEntity.getResult())) {<br />            session.setAttribute(CrowdConstant.ATTR_NAME_ORDER_PROJECT, orderProjectVOResultEntity.getData());<br />        }
      
        return "confirm_return";<br />    }
      

    }

    主启动类
    • 开启fegin代理
    • @EnableFeignClients
      @SpringBootApplication
      public class CrowdMainClass {
      public static void main(String[] args) {
      SpringApplication.run(CrowdMainClass.class , args);
      }
      }

    前端

    • 修改详情页的“支持”超链接
    • 带上returnId方便查询,注意通过网关配置的路由路径访问

    • 支持
    • 回显数据在confirm_return.html页面,注意页面上显示用户名和退出登录的路径

    • 活性富氢净水直饮机
      深圳市博实永道电子商务有限公司

      每满1750人抽取一台活性富氢净水直饮机,至少抽取一台。抽取名额(小数点后一位四舍五入)=参与人数÷1750人,由苏宁官方抽取。

      style=”width:60px;”
      th:value=”${session.orderProjectVO.returnCount}”>
      ¥[[${session.orderProjectVO.supportPrice}]]
      免运费
      th:text=”${session.orderProjectVO.freight}”>运费

    • 输入订单数时判断是否大于限购数和计算总金额的js代码

    确认订单

    目标

    点击结算,显示用户地址信息,没有即新增,显示订单的信息和总金额。

    思路

    尚筹网 - 图81

    后端

    创建实体类

    • 创建Address实体类
    • @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class AddressVO implements Serializable {
      private static final long serialVersionUID = 1L;

      private Integer id;

      private String receiveName;

      private String phoneNum;

      private String address;

      private Integer memberId;
      }

    Service接口实现

    • OrderService接口
    • List selectAddressVO(Integer memberLoginVOId);
    • OrderServiceImpl实现
    • @Override
      public List selectAddressVO(Integer memberLoginVOId) {
      AddressPOExample addressPOExample = new AddressPOExample();
      AddressPOExample.Criteria criteria = addressPOExample.createCriteria();
      criteria.andMemberIdEqualTo(memberLoginVOId);
      ArrayList addressVOList = new ArrayList<>();
      List addressPOList = addressPOMapper.selectByExample(addressPOExample);
      for (AddressPO addressPO : addressPOList) {
      AddressVO addressVO = new AddressVO();
      BeanUtils.copyProperties(addressPO, addressVO);
      addressVOList.add(addressVO);
      }
      return addressVOList;
      }

    mysql工程handler方法

    • 使用@RequestParam接收
    • @RequestMapping(“/get/order/addressvo/remote”)
      public ResultEntity> getAddressVORemote(@RequestParam(“memberLoginVOId”) Integer memberLoginVOId) {
      try {
      List addressPOList = orderService.selectAddressVO(memberLoginVOId);
      return ResultEntity.successWithData(addressPOList);
      } catch (Exception exception) {
      exception.printStackTrace();
      return ResultEntity.failed(exception.getMessage());
      }
      }

    api工程

    • 暴露接口
    • @RequestMapping(“/get/order/addressvo/remote”)
      public ResultEntity> getAddressVORemote(@RequestParam(“memberLoginVOId”) Integer memberLoginVOId);

    order工程

    • handler方法
    • @RequestMapping(“/confirm/order/{returnCount}”)
      public String showConfirmOrderInfo(
      @PathVariable(“returnCount”) Integer returnCount,
      HttpSession session
      ) {
      // 从session中取出对象
      OrderProjectVO orderProjectVO = (OrderProjectVO) session.getAttribute(CrowdConstant.ATTR_NAME_ORDER_PROJECT);

      // 将接收的returnCount赋值进去,保存到session域
      orderProjectVO.setReturnCount(returnCount);
      session.setAttribute(CrowdConstant.ATTR_NAME_ORDER_PROJECT, orderProjectVO);

      // 从session域中获取发起人的id值
      MemberLoginVO memberLoginVO = (MemberLoginVO) session.getAttribute(CrowdConstant.ATTR_NAME_LOGIN_MEMBER);
      Integer memberLoginVOId = memberLoginVO.getId();

        // 根据memberLoginVOId查找地址信息<br />        ResultEntity<List<AddressVO>> addressVOListEntity = mysqlRemoteService.getAddressVORemote(memberLoginVOId);
      
        if (ResultEntity.SUCCESS.equals(addressVOListEntity.getResult())) {<br />            // 将地址信息保存到session中<br />            session.setAttribute(CrowdConstant.ATTR_NAME_ORDER_ADDRESS, addressVOListEntity.getData());<br />    }
      

      return “confirm_order”;
      }

    前端

    • confirm-return点击结算发送请求
    • $(“#submitBtn”).click(function () {
      var returnCount = $(“#returnCountInput”).val();
      window.location.href = “/order/confirm/order/“ + returnCount;
      });
    • 将session域中的数据显示在页面上

    新增地址

    目标

    提交表单,将新增地址保存在数据库,显示在页面上。

    思路

    使用重定向,并没有使用ajax
    尚筹网 - 图82

    后端

    order工程

    • 使用实体类接收提交的表单
    • @RequestMapping(“/save/address”)
      public String saveAddressVO(AddressVO addressVO, HttpSession session) {
      // 调用远程接口保存address
      ResultEntity resultEntity = mysqlRemoteService.saveAddressVORemote(addressVO);

      // 从session域中获取returnCount重定向页面
      OrderProjectVO orderProjectVO = (OrderProjectVO) session.getAttribute(CrowdConstant.ATTR_NAME_ORDER_PROJECT);
      Integer returnCount = orderProjectVO.getReturnCount();

      // 重定向页面
      return “redirect:http://localhost/order/confirm/order/“ + returnCount;

    }

    api工程

    • 暴露接口,注意实体类使用@RequestBody
    • @RequestMapping(“/save/order/addressvo/remote”)
      public ResultEntity saveAddressVORemote(@RequestBody AddressVO addressVO);

    远程mysql工程handler

    • 和api中接口一致
    • @RequestMapping(“/save/order/addressvo/remote”)
      public ResultEntity saveAddressVORemote(@RequestBody AddressVO addressVO) {
      try {
      orderService.insertAddressVO(addressVO);
      return ResultEntity.successWithoutData();
      } catch (Exception exception) {
      exception.printStackTrace();
      return ResultEntity.failed(exception.getMessage());
      }
      }

    Service实现

    • OrderServiceji接口
    • void insertAddressVO(AddressVO addressVO);
    • OrderServiceImpl实现类,注意事务设置为只读,开启异常回滚
    • @Transactional(readOnly = false, rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
      @Override
      public void insertAddressVO(AddressVO addressVO) {
      AddressPO addressPO = new AddressPO();
      BeanUtils.copyProperties(addressVO, addressPO);
      addressPOMapper.insert(addressPO);
      }

    前端

    • 提交地址表单,注意设置memberId在隐藏域中,方便插入


























    • 点击已了解,设置立即付款的disable为空,跳转支付
    • 尚筹网 - 图83

    • $(“#knowRiskAndRuleCheckbox”).click(function () {
      var currentStatus = this.checked;
      if (currentStatus) {
      $(“#payBtn”).prop(“disabled”,””);
      } else {
      $(“#payBtn”).prop(“disabled”,”disabled”);
      }
      });

    支付功能

    使用支付宝接口

    电脑网站支付

    • 进入文档首页
    • 应用过程
    • 尚筹网 - 图84

    • 开发工程中使用开放平台提供的沙箱环境进行调试,沙箱环境中的应用已经创建好了,不需要执行创建流程。功能开发完成,项目上线时再创建应用。

    加密

    • 对称加密
    • 明文→加密→密文
    • 密文→解密→明文
    • 对称加密只需要公钥即可。
    • 非对称加密
    • 尚筹网 - 图85

    • 私钥加密的密文必须使用公钥解密。
    • 公钥加密的密文必须使用私钥解密。
    • 私钥和公钥成对出现。
    • 调用支付宝所使用的密钥
    • 在支付宝开放平台的应用中设置商户公钥 ,在支付宝开放平台的应用中获取支付宝的公钥。
    • 尚筹网 - 图86

    • 此时我们需要生成商户的私钥和支付宝公钥。
    • 使用在线生成工具
    • 尚筹网 - 图87

    • 生成密钥
    • 尚筹网 - 图88

    支付流程

    • 网站支付流程
    • 尚筹网 - 图89

    内网穿透

    介绍

    • 项目发布方式
    • 尚筹网 - 图90

    • 常规的上网方式:内网可以访问外网,而外网无法访问内网。此时内网穿透可以使外网访问内网。
    • 尚筹网 - 图91

    • 此时我们是在内开发的,如果不使用内网穿透,支付宝就无法调用我们的项目,也无法发送请求给我们。

    使用natapp

    将内网外网通过natapp隧道打通,让内网的数据让外网可以获取。

    • 登录natapp官网->注册->登录->实名认证
    • 下载客户端
    • 购买免费隧道
    • 尚筹网 - 图92

    • 尚筹网 - 图93

    • 配置config.ini
    • 将本文件放置于 natapp 同级目录 程序将读取 [default] 段
      #在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置
      #命令行参数 -config= 可以指定任意 config.ini 文件
      [default]
      authtoken= #对应一条隧道的 authtoken
      clienttoken= #对应客户端的clienttoken,将会忽略 authtoken,若无请留 空,
      log=stdout #log 日志文件,可指定本地文件, none=不做记录,stdout= 直接屏幕输出 ,默认为 none
      loglevel=DEBUG #日志等级 DEBUG, INFO, WARNING, ERROR 默认为 DEBUG
      http_proxy= #代理设置 如 http://10.123.10.10:3128 非代理上网用 户请务必留空

    • 启动natapp
    • 尚筹网 - 图94

    • 注意域名会变化

    使用沙箱环境

    由于在测试中使用,所以此时使用支付宝提供的沙箱测试。
    进入支付宝开发者后,登录认证即可使用
    尚筹网 - 图95

    • 公钥和私钥
    • 可以自定义自己生成的私钥和公钥,也可以使用默认提供的
    • 尚筹网 - 图96

    尚筹网 - 图97

    应用私钥和支付宝公钥后续调用支付宝时需要使用

    搭建order工程环境

    • 引入依赖


    • org.springframework.cloud
      spring-cloud-starter-netflix-eureka-client


      org.springframework.boot
      spring-boot-starter-web



      org.springframework.boot
      spring-boot-starter-thymeleaf



      com.atguigu.crowd
      atcrowdfunding17-member-api
      1.0-SNAPSHOT



      org.springframework.boot
      spring-boot-configuration-processor



      org.springframework.boot
      spring-boot-starter-data-redis



      org.springframework.session
      spring-session-data-redis


      org.springframework.boot
      spring-boot-starter-test
      test


      org.junit.vintage
      junit-vintage-engine




      com.alipay.sdk
      alipay-sdk-java
      3.3.49.ALL

    • 主启动类,注意开启Fegin
    • @EnableFeignClients
      @SpringBootApplication
      public class CrowdMainClass {
      public static void main(String[] args) {
      SpringApplication.run(CrowdMainClass.class, args);
      }
      }
    • application.yml配置文件,使用8000端口
    • server:
      port: 8000
      spring:
      application:
      name: atguigu-crowd-pay
      thymeleaf:
      prefix: classpath:/templates/
      suffix: .html
      redis: # 配置redis的地址
      host: 192.168.241.130
      session: # session存储的类型
      store-type: redis
      eureka:
      client:
      service-url:
      defaultZone: http://localhost:1000/eureka/
      ribbon:
      ReadTimeout: 10000
      ConnectTimeout: 10000
    • 在zuul注册路由访问
    • zuul:
      ignored-services: ““ # 忽略原本微服务名称
      sensitive-headers: “
      “ # 在zuul向其他微服务重定向时保持原本的请求体和响应头信息
      crowd-pay: # 自定义路由规则名称
      service-id: atguigu-crowd-pay # 微服务名称
      path: /pay/ # auth已经通过/访问,此时需要加个/pay

    目标

    点击支持后,跳转到支付页面,支付后保存支付的信息到数据库。

    思路

    尚筹网 - 图98

    前端

    • 点击支持,提交表单
    • 此时由于数据在多张表单,此时我们使用一个空表单,将数据全部收集在一个表单中

    • 绑定支持单击事件
    • // 支付按钮的单击响应事件
      $(“#payBtn”).click(function () {
      // 收集要提交给表单的数据
      var addressId = $(“[name=addressId]:checked”).val();
      var invoice = $(“[name=invoiceRadio]:checked”).val();
      var invoiceTitle = $.trim($(“[name=invoiceTitle]”).val());
      var remark = $.trim($(“[name=remark]”).val());
      // 提交表单
      $(“#summaryForm”)
      .append(““)
      .append(““)
      .append(““)
      .append(““)
      .submit();
      });

    后端

    创建实体类

    订单中VO对象的关系(级联关系)尚筹网 - 图99

    • 创建orderVO接收表单所有数据
    • @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class OrderVO implements Serializable {
      private static final long serialVersionUID = 1L;

      // 主键
      private Integer id;

      // 订单号
      private String orderNum;

      // 支付宝流水单号
      private String payOrderNum;

      // 订单金额
      private Double orderAmount;

      // 是否开发票
      private Integer invoice;

      // 发票抬头
      private String invoiceTitle;

      // 备注
      private String orderRemark;

      private String addressId;

      private OrderProjectVO orderProjectVO;
      }

    创建配置类

    • 配置调用支付宝时需要使用的参数,注意绑定配置文件中的前缀为ali.pay
    • @Data
      @AllArgsConstructor
      @NoArgsConstructor
      @Component
      @ConfigurationProperties(prefix = “ali.pay”)
      public class PayProperties {
      private String appId;
      private String merchantPrivateKey;
      private String alipayPublicKey;
      private String notifyUrl;
      private String returnUrl;
      private String signType;
      private String charset;
      private String gatewayUrl;
      }
    • yml配置文件配置参数值
    • merchantPrivateKey填写沙箱中的应用私钥
    • alipayPublicKey填写沙箱中的支付宝公钥
    • returnUrl是浏览器访问,所以通过网关调用我们的handler处理
    • notifyUrl是支付宝调用我们返回值,此时需要内网穿透才可以访问我们,所以需要启动natapp填写内网穿透url
    • gatewayUrl沙箱提供的支付宝网关

    • ali:
      pay:
      appId: 2021000119607739 # 沙箱提供的appId
      merchantPrivateKey: …
      alipayPublicKey: …
      notifyUrl: http://56x6a9.natappfree.cc/pay/notify # 支付宝需要访问我们,所以内网穿透启动后填写
      returnUrl: http://localhost/pay/return # return时浏览器访问,所以可以通过zuul访问
      signType: RSA2
      charset: utf-8 # 字符集编码
      gatewayUrl: https://openapi.alipaydev.com/gateway.do # 支付宝网关

    handler方法处理支付请求

    • 点击支持后处理请求,获取表单数据封装对象,并保存到session中,方便支付宝返回信息后再次封装对象,最后调用封装的方法给支付宝发送请求(注意存入和取出session保存在redis中的对象时,该对象需要序列化
    • @Controller
      public class PayHandler {

      @Autowired
      private PayProperties payProperties;

      @Autowired
      private MysqlRemoteService mysqlRemoteService;

      Logger logger = LoggerFactory.getLogger(PayHandler.class);

      // 必须加上@ResponseBody,让当前方法的返回值成为响应体,当前方法在页面上显示支付宝支付界面
      @ResponseBody
      @RequestMapping(“/generate/order”)
      public String generateOrder(OrderVO orderVO, HttpSession session) throws AlipayApiException, UnsupportedEncodingException {
      // 从seesion域中获取OrderProjectVO对象
      OrderProjectVO orderProjectVO = (OrderProjectVO) session.getAttribute(CrowdConstant.ATTR_NAME_ORDER_PROJECT);
      // 将OrderProjectVO和OrderVO组合
      orderVO.setOrderProjectVO(orderProjectVO);

        // 设置订单号<br />        // 当前的时间<br />        String nowTime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());<br />        // UUID生成用户名<br />        String user = UUID.randomUUID().toString().replace("-", "").toUpperCase();<br />        // 拼接订单号<br />        String orderNum = nowTime + user;<br />        // 将订单号设置到OrderVO中<br />        orderVO.setOrderNum(orderNum);
      
        // 计算总金额<br />        Double orderAmount = (double) (orderProjectVO.getFreight() + orderProjectVO.getReturnCount() * orderProjectVO.getSupportPrice());<br />        orderVO.setOrderAmount(orderAmount);
      
        // 先将orderVO保存到session域<br />        session.setAttribute(CrowdConstant.ATTR_NAME_ORDER, orderVO);
      
        // 调用专门封装好的方法给支付宝接口发送请求<br />        return sendRequestToAliPay(orderNum, orderAmount, orderProjectVO.getProjectName(), orderProjectVO.getReturnContent());<br />    }<br />    }
      
    • 封装的发送请求方法
    • /*
      发送请求给支付宝

      @param orderNum 订单号
      @param orderAmount 总金额
      @param subject 商品的描述,可以使用项目名称
      @param body 商品的描述,这里可以使用回报描述
      @return 返回到页面上显示支付宝页面
      @throws AlipayApiException
      @throws UnsupportedEncodingException
      */
      private String sendRequestToAliPay(String orderNum, Double orderAmount, String subject, String body) throws AlipayApiException, UnsupportedEncodingException {

      //获得初始化的AlipayClient
      AlipayClient alipayClient = new DefaultAlipayClient(
      payProperties.getGatewayUrl(),
      payProperties.getAppId(),
      payProperties.getMerchantPrivateKey(),
      “json”,
      payProperties.getCharset(),
      payProperties.getAlipayPublicKey(),
      payProperties.getSignType());

      //设置请求参数
      AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
      alipayRequest.setReturnUrl(payProperties.getReturnUrl());
      alipayRequest.setNotifyUrl(payProperties.getNotifyUrl());

      alipayRequest.setBizContent(“{\”out_trade_no\”:\”” + orderNum + “\”,”
      + “\”total_amount\”:\”” + orderAmount + “\”,”
      + “\”subject\”:\”” + subject + “\”,”
      + “\”body\”:\”” + body + “\”,”
      + “\”product_code\”:\”FAST_INSTANT_TRADE_PAY\”}”);

      //若想给BizContent增加其他可选请求参数,以增加自定义超时时间参数timeout_express来举例说明
      //alipayRequest.setBizContent(“{\”out_trade_no\”:\””+ out_trade_no +”\”,”
      // + “\”total_amount\”:\””+ total_amount +”\”,”
      // + “\”subject\”:\””+ subject +”\”,”
      // + “\”body\”:\””+ body +”\”,”
      // + “\”timeout_express\”:\”10m\”,”
      // + “\”product_code\”:\”FAST_INSTANT_TRADE_PAY\”}”);
      //请求参数可查阅【电脑网站支付的API文档-alipay.trade.page.pay-请求参数】章节

      // 返回
      return alipayClient.pageExecute(alipayRequest).getBody();
      }

    • 支付宝返回调用方法,/return请求后,获取支付宝返回信息,封装进对象后,将数据保存到数据库
    • @ResponseBody
      @RequestMapping(“/return”)
      public String returnUrlMethod(HttpServletRequest request, HttpSession session) throws UnsupportedEncodingException, AlipayApiException {
      //获取支付宝GET过来反馈信息
      Map params = new HashMap<>();
      Map requestParams = request.getParameterMap();
      for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
      String name = iter.next();
      String[] values = requestParams.get(name);
      String valueStr = “”;
      for (int i = 0; i < values.length; i++) {
      valueStr = (i == values.length - 1) ? valueStr + values[i]
      : valueStr + values[i] + “,”;
      }
      //乱码解决,这段代码在出现乱码时使用
      //valueStr = new String(valueStr.getBytes(“ISO-8859-1”), “utf-8”);
      params.put(name, valueStr);
      }

      boolean signVerified = AlipaySignature.rsaCheckV1(<br />               params,<br />               payProperties.getAlipayPublicKey(),<br />               payProperties.getCharset(),<br />               payProperties.getSignType()); //调用SDK验证签名
      
      //——请在这里编写您的程序(以下代码仅作参考)——<br />       if (signVerified) {<br />           //商户订单号<br />           String orderNum = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"), "UTF-8");
      
          //支付宝交易号<br />           String payOrderNum = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"), "UTF-8");
      
          //付款金额<br />           String orderAmount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"), "UTF-8");
      
          // 将数据保存到数据库<br />           OrderVO orderVO = (OrderVO) session.getAttribute(CrowdConstant.ATTR_NAME_ORDER);
      
          // 设置订单号和支付宝交易号<br />           orderVO.setOrderNum(orderNum);<br />           orderVO.setPayOrderNum(payOrderNum);
      
          // 调用接口执行保存<br />           ResultEntity<String> resultEntity = mysqlRemoteService.saveOrderVO(orderVO);
      
          logger.info("保存结果" + resultEntity.getResult());
      
          return "trade_no:" + orderNum + "<br/>out_trade_no:" + payOrderNum + "<br/>total_amount:" + orderAmount;<br />       } else {<br />           return "验签失败";<br />       }<br />   }
      

      @RequestMapping(“/notify”)
      public void notifyUrlMethod(HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
      //获取支付宝POST过来反馈信息
      Map params = new HashMap();
      Map requestParams = request.getParameterMap();
      for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
      String name = (String) iter.next();
      String[] values = (String[]) requestParams.get(name);
      String valueStr = “”;
      for (int i = 0; i < values.length; i++) {
      valueStr = (i == values.length - 1) ? valueStr + values[i]
      : valueStr + values[i] + “,”;
      }
      //乱码解决,这段代码在出现乱码时使用
      valueStr = new String(valueStr.getBytes(“ISO-8859-1”), “utf-8”);
      params.put(name, valueStr);
      }

      boolean signVerified = AlipaySignature.rsaCheckV1(<br />               params,<br />               payProperties.getAlipayPublicKey(),<br />               payProperties.getCharset(),<br />               payProperties.getSignType()); //调用SDK验证签名<br />       //调用SDK验证签名
      
      //——请在这里编写您的程序(以下代码仅作参考)——
      

    / 实际验证过程建议商户务必添加以下校验:
    1、需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,
    2、判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),
    3、校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email)
    4、验证app_id是否为该商户本身。
    /
    if (signVerified) {//验证成功
    //商户订单号
    String out_trade_no = new String(request.getParameter(“out_trade_no”).getBytes(“ISO-8859-1”), “UTF-8”);

           //支付宝交易号<br />           String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"), "UTF-8");
    
           //交易状态<br />           String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");
    
           logger.info("验证成功!");<br />           logger.info("out_trade_no" + out_trade_no);<br />           logger.info("trade_no" + trade_no);<br />           logger.info("trade_status" + trade_status);<br />       } else {<br />           //验证失败<br />           logger.info("验证失败!");<br />           //调试用,写文本函数记录程序运行情况是否正常<br />           // String sWord = AlipaySignature.getSignCheckContentV1(params);<br />           // AlipayConfig.logResult(sWord);<br />       }<br />   }
    

    此时浏览器显示返回的信息,需要将数据保存到数据库。

    保存信息

    尚筹网 - 图100

    api工程

    • 暴露接口,实体类注意使用@RequestBody
    • @RequestMapping(“/save/ordervo/remote”)
      ResultEntity saveOrderVO(@RequestBody OrderVO orderVO);

    mysql工程

    • handler方法
    • @RequestMapping(“/save/ordervo/remote”)
      ResultEntity saveOrderVO(@RequestBody OrderVO orderVO) {
      try {
      orderService.insertOrderVO(orderVO);
      return ResultEntity.successWithoutData();
      } catch (Exception exception) {
      exception.printStackTrace();
      return ResultEntity.failed(exception.getMessage());
      }
      }
    • OrderService接口和实现类
    • void insertOrderVO(OrderVO orderVO);
    • 注意需要先保存orderPO再取出id保存到orderProjectPO对象的orderId字段中
    • @Transactional(readOnly = false , propagation = Propagation.REQUIRES_NEW , rollbackFor = Exception.class)
      @Override
      public void insertOrderVO(OrderVO orderVO) {
      OrderPO orderPO = new OrderPO();
      BeanUtils.copyProperties(orderVO , orderPO);
      OrderProjectPO orderProjectPO = new OrderProjectPO();
      BeanUtils.copyProperties(orderVO.getOrderProjectVO() , orderProjectPO);
      orderPOMapper.insert(orderPO);
      Integer orderId = orderPO.getId();
      orderProjectPO.setOrderId(orderId);
      orderProjectPOMapper.insert(orderProjectPO);
      }
    • 保存orderPO再取出id需要在xml的insert方法中设置