一、 初识

商业模式

  • B2C

    • 管理员和普通用户
  • B2B2C

    • 商家到商家再到用户
    • 普通用户可以买京东自营也可以买其他商家

涉及技术

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

项目介绍、项目模块、设计技术

微服务架构

后端

  • springboot
  • springcloud
  • mybatisplus
  • spring security
  • redis
  • maven
  • easyExcel
  • jwt
  • OAuth2

前端

  • vue
  • element-ui
  • axios
  • nodejs

其他

  • ailyunoss
  • ailyun视频点播
  • ailyun短信服务
  • 微信支付和登录
  • docker
  • git
  • jenkins

二、mybatis-plus

初始

依赖

<!--mybatis-plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.5</version>
</dependency>
<!--lombok用来简化实体类-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>

配置

# 服务端口
server.port=8080
# 服务名
spring.application.name=service-edu

# 环境设置:dev、test、prod
spring.profiles.active=dev

# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli_edu?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.hikari.minimum-idle=3
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.max-lifetime =30000
spring.datasource.hikari.connection-test-query=SELECT 1

#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.153.222:3306/gulimall_pms?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0
# 开启日志
logging:
  level:
    com.ming.gulimall: debug

mapper

配置类添加@MapperScan(“com.ming.mapper”)

或者@Mapper

@Repository
public interface UserMapper extends BaseMapper<User> &#123;
&#125;

使用到手写sql,实体类需要@Data,@No,@All

ID

  • 自动增长

  • uuid

  • redis生成: 五台1-5,定长5

  • snow flake:

    • 是 Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bt作为机器的D(5个bt是数据中心,5个bt的机器ID) 12bt作为毫秒内的流水号(意味着每个节点在每豪秒可以产生4096个ID),最后还有一个符号位,永远是0。

注解

@TableId

描述
AUTO 数据库ID自增
NONE 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
INPUT 自己输入
ASSIGN_ID 分配ID(主键类型为Number(Long和Integer)或String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)
ASSIGN_UUID 分配UUID,主键类型为String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认default方法)
ID_WORKER 分布式全局唯一ID 长整型类型(please use ASSIGN_ID)
UUID 32位UUID字符串(please use ASSIGN_UUID)
ID_WORKER_STR 分布式全局唯一ID 字符串类型(please use ASSIGN_ID)

全局

#全局设置主键生成策略
mybatis-plus.global-config.db-config.id-type=auto

long: 2L

@TableField

属性 类型 必须指定 默认值 描述
value String “” 数据库字段名
el String “” 映射为原生 #{ ... } 逻辑,相当于写在 xml 里的 #{ ... } 部分
exist boolean true 是否为数据库表字段

扩展功能

自动填充date

字段添加@TableField(fill = FieldFill.INSERT)

实现类: 继承MetaObjectHandler

DEFAULT(默认不处理), INSERT, UPDATE, INSERT_UPDATE

create_time 对 create_time

@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;

@Component

package com.ming.handler;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler &#123;
    @Override
    public void insertFill(MetaObject metaObject) &#123;
        this.setFieldValByName("createTime", new Date(), metaObject);
        this.setFieldValByName("updateTime", new Date(), metaObject);
        this.setFieldValByName("version", 1, metaObject);
    &#125;

    @Override
    public void updateFill(MetaObject metaObject) &#123;
        this.setFieldValByName("updateTime", new Date(), metaObject);
    &#125;
&#125;

逻辑删除

  • 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除数据
  • 逻辑删除:假删除,将对应数据中代表是否被删除字段状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录

开启功能

MybatisPlusConfig,新版本3.2.0不需要

@Bean
public ISqlInjector sqlInjector() &#123;
    return new LogicSqlInjector();
&#125;

逻辑删除字段

@TableLogic

需要有初始值

@TableLogic
@TableField(fill = FieldFill.INSERT)
private Integer deleted;
  • @TableLogic描述:表字段逻辑处理注解(逻辑删除)
属性 类型 必须指定 默认值 描述
value String “” 逻辑未删除值
delval String “” 逻辑删除值

application.properties

# 默认值
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0

如果想查询出逻辑删除的,需要自己写xml

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag  # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

分页插件

流程

控制器封装Page对象传入service,service使用querywrapper将数据处理返回map,控制器返回data(map);

MyBatis Plus自带分页插件,只要简单的配置即可实现分页功能

联表查询分页, 自己写mapper,mapper第一个参数是Page,第二个参数是查询对象(需要带名字,不会分解),mapper结果返回list,service使用page.setRecods(list)存入结果,返回IPage

// mapper
List<CourseInfo> getCourseQueryList(Page<?> page, @Param("courseQuery") CourseQuery courseQuery);


// servcie
@Override
public IPage<CourseInfo> getCourseQueryList(Long size, Long currentPage, CourseQuery courseQuery) &#123;
    Page<CourseInfo> page = new Page<>(currentPage, size);
    return page.setRecords(eduCourseMapper.getCourseQueryList(page, courseQuery)); //page对象,Ipage是其父类
&#125;

(1)创建配置类

此时可以删除主类中的 @MapperScan 扫描注解

// 分页插件
@Bean
public PaginationInterceptor paginationInterceptor() &#123;
    return new PaginationInterceptor();
&#125;

(2)测试selectPage分页

测试:最终通过page对象获取相关数据

@Test
public void testSelectPage() &#123;
    Page<User> page = new Page<>(1,5);
    userMapper.selectPage(page, null);
    // myService.page(page, queryWrapper);
    page.getRecords().forEach(System.out::println); // 每页数据list集合
    System.out.println(page.getCurrent());            // 当前页
    System.out.println(page.getPages());              // 总页数
    System.out.println(page.getSize());             // 每页显示记录数
    System.out.println(page.getTotal());             // 总记录数
    System.out.println(page.hasNext());              // 有下一页
    System.out.println(page.hasPrevious());         // 有上一页
&#125;

控制台sql语句打印:SELECT id,name,age,email,create_time,update_time FROM user LIMIT 0,5

(3)测试selectMapsPage分页:结果集是Map

@Test
public void testSelectMapsPage() &#123;
    Page<User> page = new Page<>(1, 5);
    IPage<Map<String, Object>> mapIPage = userMapper.selectMapsPage(page, null);
    // 必须使用 mapIPage 获取记录列表,否则会有数据类型转换错误
    mapIPage.getRecords().forEach(System.out::println);
    System.out.println(page.getCurrent());
    System.out.println(page.getPages());
    System.out.println(page.getSize());
    System.out.println(page.getTotal());
    System.out.println(page.hasNext());
    System.out.println(page.hasPrevious());
&#125;

乐观锁

乐观锁顾名思义就是在操作时很乐观,认为操作不会产生并发问题(不会有其他线程对数据进行修改,因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制CAS( compare and swap)算法实现
简单理解:这里的数据,别想太多,你尽管用,出问题了算我怂,即操作失败后事务回滚、提示。

悲观锁

总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加(恚观)锁。一旦加锁,不同线程同时执行时只能有一个线程执行,其他的线程在入口处等待,直到锁被释放。

@Version
private Integer version;
@Configuration
@MapperScan("com.ming.mapper")
public class MpConfig &#123;
    // 乐观锁插件
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() &#123;
        return new OptimisticLockerInterceptor();
    &#125;
&#125;

需要先将user取出来更新,update时会在where中对比version

MapperCRUD

@Autowired
UserMapper userMapper;

insert(entity);

selectById(1L)

selectBatchIds(Arrays.asList(1, 2, 3))

selectByMap(hashMap)

userMapper.selectPage(Page对象, null);

deleteById(8L);

deleteBatchIds(Arrays.asList(8, 9, 10));

deleteByMap(map);

delete

// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

update

// 根据 whereEntity 条件,更新记录
int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);
// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);

select

// 根据 ID 查询
T selectById(Serializable id);
// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

id查询记录

User user = userMapper.selectById(1L);

id批量查询

完成了动态sql的foreach的功能

List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
users.forEach(System.out::println);

map条件查询

通过map封装查询条件

@Test
public void testSelectByMap()&#123;
    HashMap<String, Object> map = new HashMap<>();
    map.put("name", "Helen");
    map.put("age", 18);
    List<User> users = userMapper.selectByMap(map);
    users.forEach(System.out::println);
&#125;

注意:map中的key对应的是数据库中的列名。例如数据库user_id,实体类是userId,这时map的key需要填写user_id

id删除

@Test
public void testDeleteById()&#123;
    int result = userMapper.deleteById(8L);
    System.out.println(result);
&#125;

id批量删除

    @Test
    public void testDeleteBatchIds() &#123;
        int result = userMapper.deleteBatchIds(Arrays.asList(8, 9, 10));
        System.out.println(result);
    &#125;

map查询删除

@Test
public void testDeleteByMap() &#123;
    HashMap<String, Object> map = new HashMap<>();
    map.put("name", "Helen");
    map.put("age", 18);
    int result = userMapper.deleteByMap(map);
    System.out.println(result);
&#125;

ServiceCRUD

save

// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量)
boolean saveBatch(Collection<T> entityList, int batchSize);

SaveOrUpdate

// TableId 注解存在更新记录,否插入一条记录
boolean saveOrUpdate(T entity);
// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);

Remove

// 根据 entity 条件,删除记录
boolean remove(Wrapper<T> queryWrapper);
// 根据 ID 删除
boolean removeById(Serializable id);
// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);
// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);

Update

// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereEntity 条件,更新记录
boolean update(T entity, Wrapper<T> updateWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);

Get

// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper)

List

// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

Page

// 无条件分页查询
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);

Count

// 查询总记录数
int count();
// 根据 Wrapper 条件,查询总记录数
int count(Wrapper<T> queryWrapper);

性能分析

性能分析拦截器,用于输出每条 SQL 语句及其执行时间

SQL 性能执行分析,开发环境使用,超过指定时间,停止运行。有助于发现问题

配置插件

(1)参数说明

参数:maxTime: SQL 执行最大时长,超过自动停止运行,有助于发现问题。

参数:format: SQL是否格式化,默认false。

(2)在 MybatisPlusConfig 中配置

/**
 * SQL 执行性能分析插件
 * 开发环境使用,线上不推荐。 maxTime 指的是 sql 最大执行时长
 */
@Bean
@Profile(&#123;"dev","test"&#125;)// 设置 dev test 环境开启
public PerformanceInterceptor performanceInterceptor() &#123;
    PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
    performanceInterceptor.setMaxTime(100);//ms,超过此处设置的ms则sql不执行
    performanceInterceptor.setFormat(true);
    return performanceInterceptor;
&#125;

(3)Spring Boot 中设置dev环境

#环境设置:dev、test、prod
spring.profiles.active=dev

可以针对各环境新建不同的配置文件application-dev.properties、application-test.properties`application-prod.properties

也可以自定义环境名称:如test1、test2

条件构造器

如果想进行复杂条件查询,那么需要使用条件构造器 Wapper,涉及到如下方法

1、delete

2、selectOne

3、selectCount

4、selectList

5、selectMaps

6、selectObjs

7、update

wapper介绍

Wrapper : 条件构造抽象类,最顶端父类

AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件

​ QueryWrapper : Entity 对象封装操作类,不是用lambda语法

​ UpdateWrapper : Update 条件封装,用于Entity对象更新操作

AbstractLambdaWrapper : Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。

​ LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper

​ LambdaUpdateWrapper : Lambda 更新封装Wrapper

Wrapper

注意:以下条件构造器的方法入参中的 column均表示数据库字段

1、ge、gt、le、lt、isNull、isNotNull

ge: 大于等于

@Test
public void testDelete() &#123;
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper
        .isNull("name")
        .ge("age", 12)
        .isNotNull("email");
    int result = userMapper.delete(queryWrapper);
    System.out.println("delete return count = " + result);
&#125;

SQL:UPDATE user SET deleted=1 WHERE deleted=0 AND name IS NULL AND age >= ? AND email IS NOT NULL

2、eq、ne

注意:seletOne返回的是一条实体记录,当出现多条时会报错

@Test
public void testSelectOne() &#123;
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("name", "Tom");
    User user = userMapper.selectOne(queryWrapper);
    System.out.println(user);
&#125;

SELECT id,name,age,email,create_time,update_time,deleted,version FROM user WHERE deleted=0 AND name = ?

3、between、notBetween

包含大小边界

@Test
public void testSelectCount() &#123;
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.between("age", 20, 30);
    Integer count = userMapper.selectCount(queryWrapper);
    System.out.println(count);
&#125;

SELECT COUNT(1) FROM user WHERE deleted=0 AND age BETWEEN ? AND ?

4、allEq

@Test
public void testSelectList() &#123;
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    Map<String, Object> map = new HashMap<>();
    map.put("id", 2);
    map.put("name", "Jack");
    map.put("age", 20);
    queryWrapper.allEq(map);
    List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
&#125;

SELECT id,name,age,email,create_time,update_time,deleted,version

FROM user WHERE deleted=0 AND name = ? AND id = ? AND age = ?

5、like、notLike、likeLeft、likeRight

selectMaps返回Map集合列表

@Test
public void testSelectMaps() &#123;
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper
        .notLike("name", "e")
        .likeRight("email", "t");
    List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);//返回值是Map列表
    maps.forEach(System.out::println);
&#125;

SELECT id,name,age,email,create_time,update_time,deleted,version

FROM user WHERE deleted=0 AND name NOT LIKE ? AND email LIKE ?

6、in、notIn、inSql、notinSql、exists、notExists

in、notIn:

notIn("age",{1,2,3})--->age not in (1,2,3)
notIn("age", 1, 2, 3)--->age not in (1,2,3)

inSql、notinSql:可以实现子查询

  • 例: inSql("age", "1,2,3,4,5,6")—>age in (1,2,3,4,5,6)
  • 例: inSql("id", "select id from table where id < 3")—>id in (select id from table where id < 3)
@Test
public void testSelectObjs() &#123;
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    //queryWrapper.in("id", 1, 2, 3);
    queryWrapper.inSql("id", "select id from user where id < 3");
    List<Object> objects = userMapper.selectObjs(queryWrapper);//返回值是Object列表
    objects.forEach(System.out::println);
&#125;

SELECT id,name,age,email,create_time,update_time,deleted,version

FROM user WHERE deleted=0 AND id IN (select id from user where id < 3)

7、or、and

注意:这里使用的是 UpdateWrapper

不调用or则默认为使用 and

@Test
public void testUpdate1() &#123;
    //修改值
    User user = new User();
    user.setAge(99);
    user.setName("Andy");
    //修改条件
    UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<>();
    userUpdateWrapper
        .like("name", "h")
        .or()
        .between("age", 20, 30);
    int result = userMapper.update(user, userUpdateWrapper);
    System.out.println(result);


    QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>()
                    .eq("catelog_id", catelogId);
            if(!StringUtils.isEmpty(key)) &#123;
                wrapper.and((obj) -> &#123;
                    obj
                        .eq("attr_group_id", key)
                        .or()
                        .like("attr_group_name", key);
                &#125;);
            &#125;
&#125;

UPDATE user SET name=?, age=?, update_time=? WHERE deleted=0 AND name LIKE ? OR age BETWEEN ? AND ?

8、嵌套or、嵌套and

这里使用了lambda表达式,or中的表达式最后翻译成sql时会被加上圆括号

@Test
public void testUpdate2() &#123;
    //修改值
    User user = new User();
    user.setAge(99);
    user.setName("Andy");
    //修改条件
    UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<>();
    userUpdateWrapper
        .like("name", "h")
        .or( i -> i
                .eq("name", "李白")
                .ne("age", 20)
         );
    int result = userMapper.update(user, userUpdateWrapper);
    System.out.println(result);
&#125;

UPDATE user SET name=?, age=?, update_time=?

WHERE deleted=0 AND name LIKE ?

OR ( name = ? AND age <> ? )

9、orderBy、orderByDesc、orderByAsc

@Test
public void testSelectListOrderBy() &#123;
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.orderByDesc("id");
    List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
&#125;

SELECT id,name,age,email,create_time,update_time,deleted,version

FROM user WHERE deleted=0 ORDER BY id DESC

10、last

直接拼接到 sql 的最后

注意:只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用

@Test
public void testSelectListLast() &#123;
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.last("limit 1");
    List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
&#125;

SELECT id,name,age,email,create_time,update_time,deleted,version

FROM user WHERE deleted=0 limit 1

11、select限定列

@Test
public void testSelectListColumn() &#123;
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.select("id", "name", "age");
    List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
&#125;

SELECT id,name,age FROM user WHERE deleted=0

12、set、setSql

最终的sql会合并 user.setAge(),以及 userUpdateWrapper.set() 和 setSql() 中 的字段

@Test
public void testUpdateSet() &#123;
    //修改值
    User user = new User();
    user.setAge(99);
    //修改条件
    UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<>();
    userUpdateWrapper
        .like("name", "h")
        .set("name", "老李头")//除了可以查询还可以使用set设置修改的字段
        .setSql(" email = '123@qq.com'");//可以有子查询
    int result = userMapper.update(user, userUpdateWrapper);
&#125;

UPDATE user SET age=?, update_time=?, name=?, email = ‘123@qq.com’ WHERE deleted=0 AND name LIKE ?

代码生成器

<!-- 代码生成模板引擎 -->
<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.0</version>
</dependency>

快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码

package com.atguigu.eduservice;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.Test;

public class CodeGenerator &#123;
    @Test
    public void main1() &#123;

        // 1、创建代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 2、全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        System.out.println(projectPath);
        gc.setOutputDir("D:\\个人\\大学期间\\大二下\\课程学习\\web\\code\\guli-parent\\service\\service-edu" + "/src/main/java"); /*1*/
        gc.setAuthor("mingyue");
        gc.setOpen(false); //生成后是否打开资源管理器
        gc.setFileOverride(false); //重新生成时文件是否覆盖
        /*
         * mp生成service层代码,默认接口名称第一个字母有 I
         * UcenterService
         * */
        gc.setServiceName("%sService");    //去掉Service接口的首字母I
        gc.setIdType(IdType.ID_WORKER_STR); //主键策略
        gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型
        gc.setSwagger2(true);//开启Swagger2模式

        mpg.setGlobalConfig(gc);

        // 3、数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/guli_edu?serverTimezone=GMT%2B8&useUnicode=true&useSSL=false&characterEncoding=utf8"); /*2*/
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root"); /*3*/
        dsc.setPassword("root"); /*4*/
        dsc.setDbType(DbType.MYSQL);
        mpg.setDataSource(dsc);

        // 4、包配置
        PackageConfig pc = new PackageConfig();
        pc.setModuleName("eduservice"); //模块名 /*5*/
        pc.setParent("com.ming"); /*6*/
        pc.setController("controller");
        pc.setEntity("entity");
        pc.setService("service");
        pc.setMapper("mapper");
        mpg.setPackageInfo(pc);

        // 5、策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setInclude("edu_teacher"); // 可以一次写多个生成  /*6表名*/
        strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
        strategy.setTablePrefix(pc.getModuleName() + "_"); //生成实体时去掉表前缀

        strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
        strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作

        strategy.setRestControllerStyle(true); //restful api风格控制器
        strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符

        mpg.setStrategy(strategy);

        // 6、执行
        mpg.execute();
    &#125;
&#125;

三、项目

模块

结构

guli-parent:在线教学根目录(父工程),管理四个子模块:

    1. canal-client:canal数据库表同步模块(统计同步数据)

    2. common:公共模块父节点

            common-util:工具类模块,所有模块都可以依赖于它

            service-base:service服务的base包,包含service服务的公共配置类,所有service模块依赖于它

            spring-security:认证与授权模块,需要认证授权的service服务依赖于它

    3. infrastructure:基础服务模块父节点

            api-gateway:api网关服务

    4. service:api接口服务父节点

            service-acl:用户权限管理api接口服务(用户管理、角色管理和权限管理等)

            service-cms:cms api接口服务

            service-edu:教学相关api接口服务

            service-msm:短信api接口服务

            service-order:订单相关api接口服务

            service-oss:阿里云oss api接口服务

            service-statistics:统计报表api接口服务

            service-ucenter:会员api接口服务

            service-vod:视频点播api接口服务

maven

父模块

<artifactId>guli-parent</artifactId>
<packaging>pom</packaging>

限制依赖版本

<properties>
    <java.version>1.8</java.version>
    <guli.version>0.0.1-SNAPSHOT</guli.version>
    <mybatis-plus.version>3.0.5</mybatis-plus.version>
    <velocity.version>2.0</velocity.version>
    <swagger.version>2.7.0</swagger.version>
    <aliyun.oss.version>2.8.3</aliyun.oss.version>
    <jodatime.version>2.10.1</jodatime.version>
    <poi.version>3.17</poi.version>
    <commons-fileupload.version>1.3.1</commons-fileupload.version>
    <commons-io.version>2.6</commons-io.version>
    <httpclient.version>4.5.1</httpclient.version>
    <jwt.version>0.7.0</jwt.version>
    <aliyun-java-sdk-core.version>4.3.3</aliyun-java-sdk-core.version>
    <aliyun-sdk-oss.version>3.1.0</aliyun-sdk-oss.version>
    <aliyun-java-sdk-vod.version>2.15.2</aliyun-java-sdk-vod.version>
    <aliyun-java-vod-upload.version>1.4.11</aliyun-java-vod-upload.version>
    <aliyun-sdk-vod-upload.version>1.4.11</aliyun-sdk-vod-upload.version>
    <fastjson.version>1.2.28</fastjson.version>
    <gson.version>2.8.2</gson.version>
    <json.version>20170516</json.version>
    <commons-dbutils.version>1.7</commons-dbutils.version>
    <canal.client.version>1.1.0</canal.client.version>
    <docker.image.prefix>zx</docker.image.prefix>
    <cloud-alibaba.version>0.2.2.RELEASE</cloud-alibaba.version>
</properties>

导入依赖

<dependencyManagement>
    <dependencies>
        <!--Spring Cloud-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>$&#123;cloud-alibaba.version&#125;</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--mybatis-plus 持久层-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>$&#123;mybatis-plus.version&#125;</version>
        </dependency>
        <!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>$&#123;velocity.version&#125;</version>
        </dependency>
        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>$&#123;swagger.version&#125;</version>
        </dependency>
        <!--swagger ui-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>$&#123;swagger.version&#125;</version>
        </dependency>
        <!--aliyunOSS-->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>$&#123;aliyun.oss.version&#125;</version>
        </dependency>
        <!--日期时间工具-->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>$&#123;jodatime.version&#125;</version>
        </dependency>
        <!--xls-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>$&#123;poi.version&#125;</version>
        </dependency>
        <!--xlsx-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>$&#123;poi.version&#125;</version>
        </dependency>
        <!--文件上传-->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>$&#123;commons-fileupload.version&#125;</version>
        </dependency>
        <!--commons-io-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>$&#123;commons-io.version&#125;</version>
        </dependency>
        <!--httpclient-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>$&#123;httpclient.version&#125;</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>$&#123;gson.version&#125;</version>
        </dependency>
        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>$&#123;jwt.version&#125;</version>
        </dependency>
        <!--aliyun-->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>$&#123;aliyun-java-sdk-core.version&#125;</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>$&#123;aliyun-sdk-oss.version&#125;</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-vod</artifactId>
            <version>$&#123;aliyun-java-sdk-vod.version&#125;</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-vod-upload</artifactId>
            <version>$&#123;aliyun-java-vod-upload.version&#125;</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-sdk-vod-upload</artifactId>
            <version>$&#123;aliyun-sdk-vod-upload.version&#125;</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>$&#123;fastjson.version&#125;</version>
        </dependency>
        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
            <version>$&#123;json.version&#125;</version>
        </dependency>
        <dependency>
            <groupId>commons-dbutils</groupId>
            <artifactId>commons-dbutils</artifactId>
            <version>$&#123;commons-dbutils.version&#125;</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>$&#123;canal.client.version&#125;</version>
        </dependency>
    </dependencies>
</dependencyManagement>

统一结果

返回码接口

创建包com.atguigu.commonutils,创建接口 ResultCode.java

package com.atguigu.commonutils;
public interface ResultCode &#123;
    public static Integer SUCCESS = 20000;
    public static Integer ERROR = 20001;
&#125;

Result工具类

R.java

@Data
public class R &#123;
    @ApiModelProperty(value = "是否成功")
    private Boolean success;
    @ApiModelProperty(value = "返回码")
    private Integer code;
    @ApiModelProperty(value = "返回消息")
    private String message;
    @ApiModelProperty(value = "返回数据")
    private Map<String, Object> data = new HashMap<String, Object>();
    private R()&#123;&#125;
    public static R ok()&#123;
        R r = new R();
        r.setSuccess(true);
        r.setCode(ResultCode.SUCCESS);
        r.setMessage("成功");
        return r;
    &#125;
    public static R error()&#123;
        R r = new R();
        r.setSuccess(false);
        r.setCode(ResultCode.ERROR);
        r.setMessage("失败");
        return r;
    &#125;
    public R success(Boolean success)&#123;
        this.setSuccess(success);
        return this;
    &#125;
    public R message(String message)&#123;
        this.setMessage(message);
        return this;
    &#125;
    public R code(Integer code)&#123;
        this.setCode(code);
        return this;
    &#125;
    public R data(String key, Object value)&#123;
        this.data.put(key, value);
        return this;
    &#125;
    public R data(Map<String, Object> map)&#123;
        this.setData(map);
        return this;
    &#125;
&#125;

其他项目依赖引入

在底模块中引入另一个底模块,这样才能使用其中的类

<dependency>
    <groupId>com.ming</groupId>
    <artifactId>common-utils</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

使用

@ApiOperation(value = "所有讲师列表")
@GetMapping
public R list()&#123;
    List<Teacher> list = teacherService.list(null);
    return R.ok().data("items", list);
&#125;

异常处理

统一异常处理器

在service-base中创建统一异常处理类GlobalExceptionHandler.java,在servic子模块pom中需要引入

/**
 * 统一异常处理类
 */
@ControllerAdvice
public class GlobalExceptionHandler &#123;
    // 什么异常执行
    @ExceptionHandler(Exception.class)  // 修改即可指定异常
    @ResponseBody
    public R error(Exception e) &#123;
        e.printStackTrace();
        return R.error().message(e.getMessage());
    &#125;
&#125;

自定义异常

异常类

package com.ming.baseservice.exceptionHandler;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("异常类")
public class MingException extends RuntimeException&#123;
    @ApiModelProperty("异常码")
    private Integer code;
    private String msg;
&#125;

处理异常

// 统一异常
@ControllerAdvice
public class GlobalExceptionHandler &#123;
    // 什么异常执行
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public R error(Exception e) &#123;
        e.printStackTrace();
        return R.error().message(e.getMessage());
    &#125;

    @ExceptionHandler(MingException.class)
    @ResponseBody
    public R error(MingException e) &#123;
        return R.error()
                .code(e.getCode())
                .message(e.getMsg());
    &#125;

&#125;

日志

配置日志级别

日志记录器(Logger)的行为是分等级的。如下表所示:

分为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL

默认情况下,spring boot从控制台打印出来的日志级别只有INFO及以上级别,可以配置日志级别

# 设置日志级别
logging.level.root=WARN

这种方式只能将日志打印在控制台上

Logback日志

spring boot内部使用Logback作为日志实现的框架。

Logbacklog4j非常相似

logback相对于log4j的一些优点:https://blog.csdn.net/caisini_vc/article/details/48551287

1、配置logback日志

删除application.properties中的日志配置

安装idea彩色日志插件:grep-console

resources 中创建 logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration  scan="true" scanPeriod="10 seconds">
    <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
    <!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
    <!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
    <!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
    <contextName>logback</contextName>
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“$&#123;&#125;”来使用变量。 -->
    <property name="log.path" value="D:/guli_log/edu" />
    <!-- 彩色日志 -->
    <!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
    <!-- magenta:洋红 -->
    <!-- boldMagenta:粗红-->
    <!-- cyan:青色 -->
    <!-- white:白色 -->
    <!-- magenta:洋红 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="%yellow(%date&#123;yyyy-MM-dd HH:mm:ss&#125;) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>
    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日志,也不会被输出 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <Pattern>$&#123;CONSOLE_LOG_PATTERN&#125;</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    <!--输出到文件-->
    <!-- 时间滚动输出 level为 INFO 日志 -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>$&#123;log.path&#125;/log_info.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d&#123;yyyy-MM-dd HH:mm:ss.SSS&#125; [%thread] %-5level %logger&#123;50&#125; - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>$&#123;log.path&#125;/info/log-info-%d&#123;yyyy-MM-dd&#125;.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- 时间滚动输出 level为 WARN 日志 -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>$&#123;log.path&#125;/log_warn.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d&#123;yyyy-MM-dd HH:mm:ss.SSS&#125; [%thread] %-5level %logger&#123;50&#125; - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>$&#123;log.path&#125;/warn/log-warn-%d&#123;yyyy-MM-dd&#125;.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- 时间滚动输出 level为 ERROR 日志 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>$&#123;log.path&#125;/log_error.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d&#123;yyyy-MM-dd HH:mm:ss.SSS&#125; [%thread] %-5level %logger&#123;50&#125; - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>$&#123;log.path&#125;/error/log-error-%d&#123;yyyy-MM-dd&#125;.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--
        <logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
        <logger>仅有一个name属性,
        一个可选的level和一个可选的addtivity属性。
        name:用来指定受此logger约束的某一个包或者具体的某一个类。
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
              如果未设置此属性,那么当前logger将会继承上级的级别。
    -->
    <!--
        使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
        第一种把<root level="INFO">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
        第二种就是单独给mapper下目录配置DEBUG模式,代码如下,这样配置sql语句会打印,其他还是正常DEBUG级别:
     -->
    <!--开发环境:打印控制台-->
    <springProfile name="dev">
        <!--可以输出项目中的debug日志,包括mybatis的sql日志-->
        <logger name="com.guli" level="INFO" />
        <!--
            root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
            level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,默认是DEBUG
            可以包含零个或多个appender元素。
        -->
        <root level="INFO">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
        </root>
    </springProfile>
    <!--生产环境:输出到文件-->
    <springProfile name="pro">
        <root level="INFO">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="DEBUG_FILE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="ERROR_FILE" />
            <appender-ref ref="WARN_FILE" />
        </root>
    </springProfile>
</configuration>

2、将错误日志输出到文件

GlobalExceptionHandler.java 中

类上添加注解

 @Slf4j

异常输出语句

 log.error(e.getMessage());

3、将日志堆栈信息输出到文件

定义工具类

guli-framework-common下创建util包,创建ExceptionUtil.java工具类

package com.guli.common.util;
public class ExceptionUtil &#123;
    public static String getMessage(Exception e) &#123;
        StringWriter sw = null;
        PrintWriter pw = null;
        try &#123;
            sw = new StringWriter();
            pw = new PrintWriter(sw);
            // 将出错的栈信息输出到printWriter中
            e.printStackTrace(pw);
            pw.flush();
            sw.flush();
        &#125; finally &#123;
            if (sw != null) &#123;
                try &#123;
                    sw.close();
                &#125; catch (IOException e1) &#123;
                    e1.printStackTrace();
                &#125;
            &#125;
            if (pw != null) &#123;
                pw.close();
            &#125;
        &#125;
        return sw.toString();
    &#125;
&#125;

调用

log.error(ExceptionUtil.getMessage(e));

GuliException中创建toString方法

@Override
public String toString() &#123;
    return "GuliException&#123;" +
        "message=" + this.getMessage() +
        ", code=" + code +
        '&#125;';
&#125;

数据校验

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.17.Final</version>
    <scope>compile</scope>
</dependency>

实体类

import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

@Validated
public class RegisterVo &#123;
    @NotNull(message = "昵称不能为空")
    @ApiModelProperty(value = "昵称")
    private String nickname;

    @NotNull(message = "手机号不能为空")
    @Pattern(regexp  = "^\\d&#123;11&#125;$", message = "手机号格式不正确")
    @ApiModelProperty(value = "手机号")
    private String mobile;

    @NotNull(message = "密码不能为空")
    @ApiModelProperty(value = "密码")
    private String password;

    @NotNull(message = "验证码不能为空")
    @ApiModelProperty(value = "验证码")
    private String code;
&#125;

控制器

// 注册
@ApiOperation("注册")
@PostMapping("/register")
public R register(@Valid @RequestBody RegisterVo registerVo) &#123;
    ucenterMemberService.register(registerVo);
    return R.ok();
&#125;

异常处理

package com.ming.baseservice.exceptionHandler;

import com.ming.commonUtils.R;
import com.ming.commonUtils.ResultCode;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Objects;

// 统一异常
@ControllerAdvice
public class GlobalExceptionHandler &#123;
    // 表单数据验证异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public R error(MethodArgumentNotValidException e) &#123;
        return R.error()
                .message(Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
    &#125;
&#125;
@RequestMapping("/update")
    // @RequiresPermissions("product:brand:update")
    public R update(@Valid @RequestBody BrandEntity brand, BindingResult result)&#123;
        if(result.hasErrors()) &#123;
            Map<String, String> map = new HashMap<>();
            result.getFieldErrors().forEach((item) -> &#123;
                map.put(
                        item.getField(),
                        item.getDefaultMessage()
                );
            &#125;);
            return R.error(400, "数据不合法").put("data", map);
        &#125;
        brandService.updateById(brand);

        return R.ok();
    &#125;

四、前端

ES6

es6是规范,js遵循它

变量

let有作用域,作用域内只能声明一次

const常量必须赋值,不能改变

解构

// 1、数组解构
let [x, y, z] = [1, 2, 3];

// 2、对象解构
let user = &#123;name: 'Helen', age: 18&#125;
let &#123; name, age &#125; =  user  //注意:结构的变量必须是user中的属性

模板字符串

模板字符串相当于加强版的字符串,用反引号 ` ,除了作为普通字符串,还可以用来定义多行字符串,还可以在字符串中加入变量和表达式

// 多行字符串
let string1 =  `Hey,
can you stop angry now?`;
// 变量
let name = "Mike";
let age = 27;
let info = `My Name is $&#123;name&#125;,I am $&#123;age+1&#125; years old next year.`;
// 函数
let string2 = `Game start,$&#123;f()&#125;`

对象简写

const age = 12
const name = "Amy"

// 传统
const person1 = &#123;age: age, name: name&#125;
console.log(person1)

// ES6
const person2 = &#123;age, name&#125;
console.log(person2) //&#123;age: 12, name: "Amy"&#125;

方法简写

// 传统
const person1 = &#123;
    sayHi: function() &#123;
        console.log("Hi")
    &#125;
&#125;
// ES6
const person2 = &#123;
    sayHi() &#123;
        console.log("Hi")
    &#125;
&#125;

对象拓展运算符

拓展运算符(…)用于取出参数对象所有可遍历属性然后拷贝到当前对象。

// 1、拷贝对象
let person1 = &#123;name: "Amy", age: 15&#125;
let someone = &#123; ...person1 &#125;
console.log(someone)  //&#123;name: "Amy", age: 15&#125;

// 2、合并对象
let age = &#123;age: 15&#125;
let name = &#123;name: "Amy"&#125;
let person2 = &#123;...age, ...name&#125;
console.log(person2)  //&#123;age: 15, name: "Amy"&#125;

箭头函数

箭头函数提供了一种更加简洁的函数书写方式。基本语法是:参数 => 函数体

// 传统
var f1 = function(a) &#123;
    return a
&#125;
// ES6
var f2 = a => a
// 当箭头函数没有参数或者有多个参数,要用 () 括起来。
// 当箭头函数函数体有多行语句,用 &#123;&#125; 包裹起来,表示代码块,
// 当只有一行语句,并且需要返回结果时,可以省略 &#123;&#125; , 结果会自动返回。
var f3 = (a,b) => &#123;
    let result = a+b
    return result
&#125;
// 相当于:
var f4 = (a,b) => a+b

箭头函数多用于匿名函数的定义

vue

debuger 添加断点调试;

http://mingyuefusu.gitee.io/blog/2020/05/20/vue/

https://cn.vuejs.org

Vue是一套用于构建用户界面的渐进式框架。

Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

入门

在vs code中创建代码片段:

文件 => 首选项 => 用户代码片段 => 新建全局代码片段/或文件夹代码片段:vue-html.code-snippets

&#123;
    "vue htm": &#123;
        "scope": "html",
        "prefix": "vuehtml",
        "body": [
            "<!DOCTYPE html>",
            "<html lang=\"en\">",
            "",
            "<head>",
            "    <meta charset=\"UTF-8\">",
            "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">",
            "    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">",
            "    <title>Document</title>",
            "</head>",
            "",
            "<body>",
            "    <div id=\"app\">",
            "",
            "    </div>",
            "    <script src=\"vue.min.js\"></script>",
            "    <script>",
            "        new Vue(&#123;",
            "            el: '#app',",
            "            data: &#123;",
            "                $1",
            "            &#125;",
            "        &#125;)",
            "    </script>",
            "</body>",
            "",
            "</html>",
        ],
        "description": "my vue template in html"
    &#125;
&#125;

指令

  • v-bind :

  • v-model

  • v-on @

  • v-if v-show v-for="(value, key, index) in class"

  • v-for

  • .prevent 修饰符,阻止事件原本的默认行为

组件

组件(Component)是 Vue.js 最强大的功能之一。

组件可以扩展 HTML 元素,封装可重用的代码。

生命周期

//===创建时的四个事件
beforeCreate() &#123; // 第一个被执行的钩子方法:实例被创建出来之前执行
    console.log(this.message) //undefined
    this.show() //TypeError: this.show is not a function
    // beforeCreate执行时,data 和 methods 中的 数据都还没有没初始化
&#125;,
created() &#123; // 第二个被执行的钩子方法
    console.log(this.message) //床前明月光
    this.show() //执行show方法
    // created执行时,data 和 methods 都已经被初始化好了!
    // 如果要调用 methods 中的方法,或者操作 data 中的数据,最早,只能在 created 中操作
&#125;,
beforeMount() &#123; // 第三个被执行的钩子方法
    console.log(document.getElementById('h3').innerText) //&#123;&#123; message &#125;&#125;
    // beforeMount执行时,模板已经在内存中编辑完成了,尚未被渲染到页面中
&#125;,
mounted() &#123; // 第四个被执行的钩子方法
    console.log(document.getElementById('h3').innerText) //床前明月光
    // 内存中的模板已经渲染到页面,用户已经可以看见内容
&#125;,


//===运行中的两个事件
beforeUpdate() &#123; // 数据更新的前一刻
    console.log('界面显示的内容:' + document.getElementById('h3').innerText)
    console.log('data 中的 message 数据是:' + this.message)
    // beforeUpdate执行时,内存中的数据已更新,但是页面尚未被渲染
&#125;,
updated() &#123;
    console.log('界面显示的内容:' + document.getElementById('h3').innerText)
    console.log('data 中的 message 数据是:' + this.message)
    // updated执行时,内存中的数据已更新,并且页面已经被渲染
&#125;

路由

Vue.js 路由允许我们通过不同的 URL 访问不同的内容。

通过 Vue.js 可以实现多视图的单页Web应用(single page web application,SPA)。

Vue.js 路由需要载入 vue-router 库

<route-link to=""></route-link>  跳转
<route-view></route-view>          显示的地方 

routes放入路由规则,router实例中添加routers,将router加入Vue

new Vue({router}) <------ new VueRouter(router) <-------- routes

<script>
    // 1. 定义(路由)组件。
    // 可以从其他文件 import 进来
    const Welcome = &#123; template: '<div>欢迎</div>' &#125;
    const Student = &#123; template: '<div>student list</div>' &#125;
    const Teacher = &#123; template: '<div>teacher list</div>' &#125;
    // 2. 定义路由
    // 每个路由应该映射一个组件。
    const routes = [
        &#123; path: '/', redirect: '/welcome' &#125;, //设置默认指向的路径
        &#123; path: '/welcome', component: Welcome &#125;,
        &#123; path: '/student', component: Student &#125;,
        &#123; path: '/teacher', component: Teacher &#125;
    ]
    // 3. 创建 router 实例,然后传 `routes` 配置
    const router = new VueRouter(&#123;
        routes // (缩写)相当于 routes: routes
    &#125;)
    // 4. 创建和挂载根实例。
    // 从而让整个应用都有路由功能
    const app = new Vue(&#123;
        el: '#app',
        router
    &#125;)
    // 现在,应用已经启动了!
</script>

模板

vue: &#123;&#123; message &#125;&#125;  
mybatis: #&#123;&#125; $&#123;&#125; 
thymeleaf: $&#123;&#125;   [[ $&#123;&#125; ]]
php: &#123;$name&#125;
vue-value: /:id
js: `$&#123;&#125;`
springboot-$Value: &#123;$ming.url&#125;

axios

代码片段

自定义代码片段

参数

npm install qs

qs.stringify转换为参数name=hehe&age=10

JSON.stringify转换为json{"name":"hehe","age":10}

简介

axios是独立于vue的一个项目,基于promise用于浏览器和node.js的http客户端

  • 在浏览器中可以帮助我们完成 ajax请求的发送
  • 在node.js中可以向远程接口发送请求

注意:测试时需要开启后端服务器,并且后端开启跨域访问权限

axios.get('http://localhost:8081/admin/ucenter/member')
            .then(response => &#123;
                console.log(response)
                this.memberList = response.data.data.items
            &#125;)
            .catch(error => &#123;
                console.log(error)
            &#125;)

reqeust工具类

import cookie from 'js-cookie';
import &#123; Message &#125; from 'element-ui'
// const &#123; default: Axios &#125; = require("axios");

import axios from 'axios';
const service = axios.create(&#123;
    baseURL: 'http://127.0.0.1:81',
    timeout: 10000
&#125;)

// 请求
service.interceptors.request.use(
    config => &#123;
        let token = cookie.get("token");
        if(token) &#123;
            config.headers['token'] = token;
        &#125;
        return config;
    &#125;,
    error => &#123;
        return Promise.reject(error)
    &#125;
)
// 响应
service.interceptors.response.use(
    res => &#123;
      if(res.data.code === 26000) &#123;
        Message.error("未登录");
        let allCookies = cookie.get();
        Object.keys(allCookies).forEach( item =>&#123;
          cookie.remove(item);
        &#125;)
      &#125;
      return res.data;
    &#125;,
    error => &#123;
        return Promise.reject(error)
    &#125;
)

export default service;

eleme-ui

http://element-cn.eleme.io/#/zh-CN

<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

node

官网:https://nodejs.org/en/

中文网:http://nodejs.cn/

LTS:长期支持版本

Current:最新版

简单的说 Node.js 就是运行在服务端的 JavaScript。

Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。

如果你是一个前端程序员,你不懂得像PHP、Python或Ruby等动态编程语言,然后你想创建自己的服务,那么Node.js是一个非常好的选择。

Node.js 是运行在服务端的 JavaScript,如果你熟悉Javascript,那么你将会很容易的学会Node.js。

当然,如果你是后端程序员,想部署一些高性能的服务,那么学习Node.js也是一个非常好的选择。

node 控制台程序.js

浏览器的内核包括两部分核心

  • DOM渲染引擎;
  • js解析器(js引擎)
  • js运行在浏览器中的内核中的js引擎内部

Node.js是脱离浏览器环境运行的JavaScript程序,基于V8 引擎(Chrome 的 JavaScript的引擎)

const http = require('http');
http.createServer(function (request, response) &#123;
    // 发送 HTTP 头部 
    // HTTP 状态值: 200 : OK
    // 内容类型: text/plain
    response.writeHead(200, &#123;'Content-Type': 'text/plain'&#125;);
    // 发送响应数据 "Hello World"
    response.end('Hello Server');
&#125;).listen(8888);
// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8888/');

npm

介绍

NPM全称Node Package Manager,是Node.js包管理工具,是全球最大的模块生态系统,里面所有的模块都是开源免费的;也是Node.js的包管理工具,相当于前端的Maven

初始化

#建立一个空文件夹,在命令提示符进入该文件夹  执行命令初始化
npm init
#按照提示输入相关信息,如果是用默认值则直接回车即可。
#name: 项目名称
#version: 项目版本号
#description: 项目描述
#keywords: &#123;Array&#125;关键词,便于用户搜索到我们的项目
#最后会生成package.json文件,这个是包的配置文件,相当于maven的pom.xml
#我们之后也可以根据需要进行修改。
#如果想直接生成 package.json 文件,那么可以使用命令
npm init -y

NPM官方的管理的包都是从 http://npmjs.com下载的,但是这个网站在国内速度很慢。

这里推荐使用淘宝 NPM 镜像 http://npm.taobao.org/ ,淘宝 NPM 镜像是一个完整 npmjs.com 镜像,同步频率目前为 10分钟一次,以保证尽量与官方服务同步。

设置镜像

#经过下面的配置,以后所有的 npm install 都会经过淘宝的镜像地址下载
npm config set registry https://registry.npm.taobao.org  

#查看npm配置信息
npm config list 

命令

#使用 npm install 安装依赖包的最新版,
#模块安装的位置:项目目录\node_modules
#安装会自动在项目目录下添加 package-lock.json文件,这个文件帮助锁定安装包的版本
#同时package.json 文件中,依赖包会被添加到dependencies节点下,类似maven中的 <dependencies>
npm install  
npm install jquery

#npm管理的项目在备份和传输的时候一般不携带node_modules文件夹
npm install #根据package.json中的配置下载依赖,初始化项目

#如果安装时想指定特定的版本
npm install jquery@2.1.x

#devDependencies节点:开发时的依赖包,项目打包到生产环境的时候不包含的依赖
#使用 -D参数将依赖添加到devDependencies节点
npm install --save-dev eslint
#或
npm install -D eslint

#全局安装
#Node.js全局安装的npm包和工具的位置:用户目录\AppData\Roaming\npm\node_modules
#一些命令行工具常使用全局安装的方式
npm install -g webpack
#更新包(更新到最新版本)
npm update 包名

#全局更新
npm update -g 包名

#卸载包
npm uninstall 包名

#全局卸载
npm uninstall -g 包名

babel

Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而在现有环境执行执行。

这意味着,你可以现在就用 ES6 编写程序,而不用担心现有环境是否支持。

安装

Babel提供babel-cli工具,用于命令行转码。它的安装命令如下:

npm install --global babel-cli
#查看是否安装成功
babel --version

初始化项目

npm init -y

配置.babelrc

Babel的配置文件是.babelrc,存放在项目的根目录下,该文件用来设置转码规则和插件,基本格式如下。

&#123;
    "presets": [],
    "plugins": []
&#125;

presets字段设定转码规则,将es2015规则加入 .babelrc:

&#123;
    "presets": ["es2015"],
    "plugins": []
&#125;

安装转码器

在项目中安装

npm install --save-dev babel-preset-es2015

转码

# 转码结果写入一个文件
    mkdir dist1
    # --out-file 或 -o 参数指定输出文件
    babel src/example.js --out-file dist1/compiled.js
    # 或者
    babel src/example.js -o dist1/compiled.js

# 整个目录转码
    mkdir dist2
    # --out-dir 或 -d 参数指定输出目录
    babel src --out-dir dist2
    # 或者
    babel src -d dist2

模块化

随着网站逐渐变成”互联网应用程序”,嵌入网页的Javascript代码越来越庞大,越来越复杂。

后端: controller、service、mapper,controller注入service、service注入mapper、类与类之间的调用成为后端模块化操作

前端: js与js之间调用

CommonJS

ES5

CommonJS使用 exports 和require 来导出、导入模块,能直接运行

module.exports =

// 定义成员:
const sum = function(a,b)&#123;
    return parseInt(a) + parseInt(b)
&#125;
const subtract = function(a,b)&#123;
    return parseInt(a) - parseInt(b)
&#125;
// 导出成员:
module.exports = &#123;
    sum: sum,
    subtract: subtract
&#125;
//简写
module.exports = &#123;
    sum,
    subtract,
&#125;

= require()

//引入模块,注意:当前路径必须写 ./
const m = require('./四则运算.js')
const result1 = m.sum(1, 2)
const result2 = m.subtract(1, 2)
console.log(result1, result2)

ES6模块化

无法直接运行,需要使用babel编译成ES5

babel src -d dist2

多个export

export function getList() &#123;
    console.log('获取数据列表')
&#125;

export function save() &#123;
    console.log('保存数据')
&#125;

import {x, y} from

//只取需要的方法即可,多个方法用逗号分隔
import &#123; getList, save &#125; from "./userApi.js"
getList()
save()

ES6另一种

export default

export default &#123;
    getList() &#123;
        console.log('获取数据列表2')
    &#125;,

    save() &#123;
        console.log('保存数据2')
    &#125;
&#125;

import XX from

import user from "./userApi2.js"
user.getList()
user.save()

webpack

Webpack 是一个前端资源加载/打包工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。

从图中我们可以看出,Webpack 可以将多种静态资源 js、css、less 转换成一个静态文件,减少了页面的请求。

安装

npm install -g webpack webpack-cli

JS打包

1、webpack.config.js

读取src文件夹中的main.js内容,分析资源依赖,把相关的js文件打包放入当前目录的dist文件夹下的bundle.js

const path = require("path"); //Node.js内置模块
module.exports = &#123;
    entry: './src/main.js', //配置入口文件
    output: &#123;
        path: path.resolve(__dirname, './dist'), //输出路径,__dirname:当前文件所在路径
        filename: 'bundle.js' //输出文件
    &#125;
&#125;

2、执行命令

webpack #有黄色警告
webpack --mode=development #没有警告
#执行后查看bundle.js 里面包含了上面两个js文件的内容并惊醒了代码压缩

也可以配置项目的npm运行命令,修改package.json文件

"scripts": &#123;
    //...,
    "dev": "webpack --mode=development"
 &#125;

运行npm命令执行打包

npm run dev

CSS打包

1、安装

安装style-loader和 css-loader

Webpack 本身只能处理 JavaScript 模块,如果要处理其他类型的文件,就需要使用 loader 进行转换。

Loader 可以理解为是模块和资源的转换器。

相关Loader插件

  • css-loader 是将 css 装载到 javascript
  • style-loader 是让 javascript 认识css
npm install --save-dev style-loader css-loader 

2、webpack.config.js

const path = require("path"); //Node.js内置模块
module.exports = &#123;
    const path = require("path"); //Node.js内置模块
    module.exports = &#123;
        entry: './src/main.js', //配置入口文件
        output: &#123;
            path: path.resolve(__dirname, './dist'), //输出路径,__dirname:当前文件所在路径
            filename: 'bundle.js' //输出文件
        &#125;
    &#125;,

    //...,
    output:&#123;&#125;,
    module: &#123;
        rules: [  
            &#123;  
                test: /\.css$/,    //打包规则应用到以css结尾的文件上
                use: ['style-loader', 'css-loader']
            &#125;  
        ]  
    &#125;
&#125;

模板

vue-element-admin

而vue-element-admin是基于element-ui 的一套后台管理系统集成方案

功能:https://panjiachen.github.io/vue-element-admin-site/zh/guide/#功能

GitHub地址:https://github.com/PanJiaChen/vue-element-admin

项目在线预览:https://panjiachen.gitee.io/vue-element-admin

# 解压压缩包
# 进入目录
cd vue-element-admin-master
# 安装依赖
npm install
# 启动。执行后,浏览器自动弹出并访问http://localhost:9527/
npm run dev

vue-admin-template

vueAdmin-template是基于vue-element-admin的一套后台管理系统基础模板(最少精简版),可作为模板进行二次开发。

GitHub地址:https://github.com/PanJiaChen/vue-admin-template

**建议在 vue-admin-template 的基础上进行二次开发,把 vue-element-admin当做工具箱,想要什么功能或者组件就复制过来。

# 解压压缩包
# 进入目录
cd vue-admin-template-master
# 安装依赖
npm install
# 启动。执行后,浏览器自动弹出并访问http://localhost:9528/
npm run dev

问题

跨域

访问协议、ip地址、端口号有不同

控制器添加: @CrossOrigin

两次请求: 浏览器的原因,一次测试联通,一直真正请求

admin

表格插槽

<el-table-column label="头衔" width="80">
    <template slot-scope="scope">
        &#123;&#123; scope.row.level===1?'高级讲师':'首席讲师' &#125;&#125;
    </template>
</el-table-column>

slot-scope ,可以接收传递给插槽的 prop

我的

分页

// 获取分页范围
export function getPageRange(index, pages, showCount) &#123;
  var leftSize = 0 ;
  var oddFlag = showCount % 2 === 1; // 是奇数
  // 确认左右大小
  if(oddFlag) &#123; // 奇数
    leftSize = parseInt(showCount - 1) / 2;
  &#125; else &#123; // 偶数
    leftSize = showCount/2;
  &#125;
  var left = 0, right = 0;
  left = (index - leftSize) < 1?     1   :  (index - leftSize); // 获取最左侧
  right = (left + showCount - 1) > pages?     pages : (left+showCount-1); // 获取最右侧
  // 保证有showCount
  if(right-showCount+1 >= 1) &#123;
    left = right-showCount+1;
  &#125;
  return &#123;min: left, max: right&#125;;
&#125;

前端级联

流程:

后端传回树形结构,设置多个index下标确定对应层数据,第二级的数据分出来,选出第一级获得第二级的数据,更新第一层需要更新第二层。

五、OSS

依赖

<!-- 阿里云oss依赖 -->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
</dependency>

<!-- 日期工具栏依赖 -->
<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
</dependency>
new DateTime().toString("yyyy/MM/dd");

error

不使用数据库

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

用其他的模块

@ComponentScan(basePackages = &#123;"com.ming"&#125;)

使用

配置

#服务端口
server.port=8082
#服务名
spring.application.name=service-oss

#环境设置:dev、test、prod
spring.profiles.active=dev

#阿里云 OSS
#不同的服务器,地址不同
aliyun.oss.file.endpoint=oss-cn-beijing.aliyuncs.com
aliyun.oss.file.keyid=LTAI4GDbFK19tVZ5t7YAGgYk
aliyun.oss.file.keysecret=mBQ1V3jZnqHAhPt87SdIANBvOFELhqYue
#bucket可以在控制台创建,也可以使用java代码创建
aliyun.oss.file.bucketname=ming-edu

获取常量配置

@Value("${myProperties.data.name}")

package com.ming.oss.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 常量类,读取配置文件application.properties中的配置
 */
@Component
//@PropertySource("classpath:application.properties")
public class ConstantPropertiesUtil implements InitializingBean &#123;

    @Value("$&#123;aliyun.oss.file.endpoint&#125;")
    private String endpoint;

    @Value("$&#123;aliyun.oss.file.keyid&#125;")
    private String keyId;

    @Value("$&#123;aliyun.oss.file.keysecret&#125;")
    private String keySecret;

    @Value("$&#123;aliyun.oss.file.bucketname&#125;")
    private String bucketName;

    public static String END_POINT;
    public static String ACCESS_KEY_ID;
    public static String ACCESS_KEY_SECRET;
    public static String BUCKET_NAME;

    @Override
    public void afterPropertiesSet() throws Exception &#123;
        END_POINT = endpoint;
        ACCESS_KEY_ID = keyId;
        ACCESS_KEY_SECRET = keySecret;
        BUCKET_NAME = bucketName;
    &#125;
&#125;

ServiceImpl

  • MutipartFile file

  • UUID.randomUUID()

  • file.getOriginalFilename()

  • new Datetime().toString(“yyyy/MM/dd”)

package com.ming.oss.service.impl;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.ming.oss.service.OssService;
import com.ming.oss.utils.ConstantPropertiesUtil;
import org.joda.time.DateTime;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

@Service
public class OssServiceImpl implements OssService &#123;
    @Override
    public String uploadFileAvatar(MultipartFile file)&#123;
        // Endpoint以杭州为例,其它Region请按实际情况填写。
        String endpoint = ConstantPropertiesUtil.END_POINT;
        // 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
        String accessKeyId = ConstantPropertiesUtil.ACCESS_KEY_ID;
        String accessKeySecret = ConstantPropertiesUtil.ACCESS_KEY_SECRET;
        String bucketName = ConstantPropertiesUtil.BUCKET_NAME;
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try&#123;
            // 上传文件流。
            InputStream  inputStream = file.getInputStream();

            // 获取文件名称
            // String fileName = file.getOriginalFilename();
            String fileName = UUID.randomUUID().toString().replaceAll("-", "") +file.getOriginalFilename();
            String dateTime = new DateTime().toString("yyyy/MM/dd");
            String filePath = dateTime +"/" +fileName;
            // 路径 名称
            ossClient.putObject(bucketName, filePath, inputStream);

            // 关闭OSSClient。
            ossClient.shutdown();
            String url = "https://" +bucketName +"." +endpoint +"/" +filePath;
            return url;
        &#125;catch (Exception e) &#123;
            e.printStackTrace();
            return null;
        &#125;

    &#125;
&#125;

前端上传

ImageCropper

PanThumb

<!-- 讲师头像:TODO -->
<!-- 讲师头像 -->
<el-form-item label="讲师头像">

    <!-- 头衔缩略图 -->
    <pan-thumb :image="teacher.avatar"/>
    <!-- 文件上传按钮 -->
    <el-button type="primary" icon="el-icon-upload" @click="imagecropperShow=true">更换头像
    </el-button>

    <!--
    v-show:是否显示上传组件
    :key:类似于id,如果一个页面多个图片上传控件,可以做区分
    :url:后台上传的url地址
    @close:关闭上传组件
    @crop-upload-success:上传成功后的回调 
    -->
    <image-cropper
                   v-show="imagecropperShow"
                   :width="300"
                   :height="300"
                   :key="imagecropperKey"
                   :url="BASE_API+'/eduoss/fileoss/uploadImg'"
                   field="file"
                   @close="close"
                   @crop-upload-success="cropSuccess"/>

</el-form-item>
<!-- end 头像 -->

return &#123;
      BASE_API: process.env.VUE_APP_BASE_API,
      saveBtnDisabled: false ,// 保存按钮是否禁用,
      imagecropperShow: false, // 是否显示上传组件
      imagecropperKey: 0 //上传组件id
    &#125;


// 上传成功后的回调函数
cropSuccess(data) &#123;
    console.log(data)
    this.imagecropperShow = false
    this.teacher.avatar = data.url
    // 上传成功后,重新打开上传组件时初始化组件,否则显示上一次的上传结果
    this.imagecropperKey = this.imagecropperKey + 1
&#125;

// 关闭上传组件
close() &#123;
    this.imagecropperShow = false
    // 上传失败后,重新打开上传组件时初始化组件,否则显示上一次的上传结果
    this.imagecropperKey = this.imagecropperKey + 1
&#125;

六、nginx

请求转发、负载均衡、动静分离

请求转发:根据请求路径,转发到相应的服务器

负载均衡:请求到来,根据一定的规则(如:轮循)分配给不同的服务器

发现两个nginx

nginx.exe -s stop
nginx.exe -s reload

配置解析

########### 每个指令必须有分号结束。#################
#user administrator administrators;  #配置用户或者组,默认为nobody nobody。
#worker_processes 2;  #允许生成的进程数,默认为1
#pid /nginx/pid/nginx.pid;   #指定nginx进程运行文件存放地址
error_log log/error.log debug;  #制定日志路径,级别。这个设置可以放入全局块,http块,server块,级别以此为:debug|info|notice|warn|error|crit|alert|emerg
events &#123;
    accept_mutex on;   #设置网路连接序列化,防止惊群现象发生,默认为on
    multi_accept on;  #设置一个进程是否同时接受多个网络连接,默认为off
    #use epoll;      #事件驱动模型,select|poll|kqueue|epoll|resig|/dev/poll|eventport
    worker_connections  1024;    #最大连接数,默认为512
&#125;
http &#123;
    include       mime.types;   #文件扩展名与文件类型映射表
    default_type  application/octet-stream; #默认文件类型,默认为text/plain
    #access_log off; #取消服务日志    
    log_format myFormat '$remote_addr–$remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for'; #自定义格式
    access_log log/access.log myFormat;  #combined为日志格式的默认值
    sendfile on;   #允许sendfile方式传输文件,默认为off,可以在http块,server块,location块。
    sendfile_max_chunk 100k;  #每个进程每次调用传输数量不能大于设定的值,默认为0,即不设上限。
    keepalive_timeout 65;  #连接超时时间,默认为75s,可以在http,server,location块。

    upstream mysvr &#123;   
      server 127.0.0.1:7878;
      server 192.168.10.121:3333 backup;  #热备
    &#125;
    error_page 404 https://www.baidu.com; #错误页
    server &#123;
        keepalive_requests 120; #单连接请求上限次数。
        listen       4545;   #监听端口
        server_name  127.0.0.1;   #监听地址       
        location  ~*^.+$ &#123;       #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。
           #root path;  #根目录
           #index vv.txt;  #设置默认页
           proxy_pass  http://mysvr;  #请求转向mysvr 定义的服务器列表
           deny 127.0.0.1;  #拒绝的ip
           allow 172.18.5.54; #允许的ip           
        &#125; 
    &#125;
&#125;

项目配置

    # 转发配置
    location ~ /eduservice/ &#123;
            proxy_pass http://localhost:8081;
    &#125;
    location ~ /eduoss/ &#123;
            proxy_pass http://localhost:8082;
    &#125;

匹配规则

  1. 留空,在留空的情况下,配置表示请求路径由 location_match 开始。
  2. = ,表示精确匹配。只有请求的url路径与后面的字符串完全相等时,才会命中。
  3. ~,表示区分大小写的正则匹配。
  4. ~*,表示不区分大小写的正则匹配。
  5. ^~ ,表示如果该符号后面的字符是最佳匹配,采用该规则,不再进行后续的查找

当有多条 location 规则时,nginx 有一套比较复杂的规则,优先级如下:

  • 精确匹配 =
  • 前缀匹配 ^~(立刻停止后续的正则搜索)
  • 按文件中顺序的正则匹配 ~~*
  • 匹配不带任何修饰的前缀匹配。

正则匹配,顺序有关

七、EasyExcel

1、数据导入:减轻录入工作量

2、数据导出:统计信息归档

3、数据传输:异构系统之间数据传输

特点

  • Java领域解析、生成Excel比较有名的框架有Apache poi、jxl等。但他们都存在一个严重的问题就是非常的耗内存。如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的full gc。
  • EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
  • EasyExcel采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)。

依赖

需要poi依赖

<dependencies>
    <!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>2.1.1</version>
    </dependency>
</dependencies>

测试

实体类 —-》AnalysisEventListener —-》EasyExcel.read

@ExcelProperty

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student &#123;
    @ExcelProperty(value = "编号", index = 0) 
    private String id;
    @ExcelProperty(value = "姓名",index = 1) 
    private String name;
&#125;

EasyExcel.write(fileUrl, Student.class).sheet("表单号").doWrite(dataList);

public static void main(String[] args) &#123;
    String url = "F:\\laji\\excelData.xlsx";
    EasyExcel.write(url, Student.class).sheet("表单1").doWrite(getStudent());

&#125;

public static List<Student> getStudent() &#123;
    List<Student> list = new ArrayList<>();
    for( int i = 0; i< 10; i++) &#123;
        Student student = new Student(Integer.toString(i), "ming" +i);
        list.add(student);
    &#125;
    return list;
&#125;

读取

EasyExcel.read(url, Student.class, new ExcelListener()).sheet().doRead();

监听器

AnalysisEventListener

public class ExcelListener extends AnalysisEventListener<Student> &#123;
    private List<Student> list = new ArrayList<>();
    // 一行
    @Override
    public void invoke(Student student, AnalysisContext analysisContext) &#123;
        System.out.println(student.toString());
        list.add(student);
    &#125;

    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) &#123;
        System.out.println(headMap.toString());
    &#125;

    // 结束后
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) &#123;

    &#125;
&#125;

添加课程分类

controll -> service -> EasyExcel -> AnalysisEventListener

Service

  • 获取MultipartFile转换为InputStream
  • 使用EasyExcel 进行write

EasyExcel

  • 需要传入AnalysisEvnetListener

AnalysisEventListener

  • 要进行数据库操作,所以需要传入service对象或者mapper对象
  • invoke对表格进行行遍历
    • 通过tilte和pid=0判断标题是否存在,不存在,添加

前端上传

<template>
  <div class="app-container">
    <el-form label-width="120px">
      <el-form-item label="信息描述">
        <el-tag type="info">excel模版说明</el-tag>
        <el-tag>
          <i class="el-icon-download"/>
          <a href="/public/testExcel.xlsx">点击下载模版</a>
        </el-tag>

      </el-form-item>

      <el-form-item label="选择Excel">
        <el-upload
          ref="upload"
          :auto-upload="false"
          :on-success="fileUploadSuccess"
          :on-error="fileUploadError"
          :disabled="importBtnDisabled"
          :limit="1"
          :action="BASE_API+'/eduservice/subject/addSubject'"
          name="file"
          accept="application/vnd.ms-excel">
          <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
          <el-button
            :loading="loading"
            style="margin-left: 10px;"
            size="small"
            type="success"
            @click="submitUpload">&#123;&#123; fileUploadBtnText &#125;&#125;</el-button>
        </el-upload>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default &#123;
      data() &#123;
        return &#123;
          BASE_API: process.env.VUE_APP_BASE_API, // 接口API地址
          fileUploadBtnText: '上传到服务器', // 按钮文字
          importBtnDisabled: false, // 按钮是否禁用,
          loading: false
        &#125;
    &#125;,
    methods: &#123;
        // 点击上传
        submitUpload() &#123;
          this.fileUploadBtnText = '正在上传'
          this.importBtnDisabled = true
          this.loading = true
          this.$refs.upload.submit()
        &#125;,
        // 上传成功
        fileUploadSuccess(response) &#123;
          if (response.success === true) &#123;
            this.fileUploadBtnText = '导入成功'
            this.loading = false
            this.$message(&#123;
              type: 'success',
              message: response.message
            &#125;)
          &#125; 
        &#125;,
        // 上传失败
        fileUploadError(response) &#123;
          this.fileUploadBtnText = '导入失败'
          this.loading = false
          this.$message(&#123;
            type: 'error',
            message: '导入失败'
          &#125;)
        &#125;
    &#125;
&#125;
</script>

<style>

</style>

树形数据显示

BeanUtils.copyProperties 将对象相同属性赋值

后端

  • 使用两个vo类

  • 取出所有分类,双层for循环遍历取出即可

<template>
  <div class="app-container">
    <el-input v-model="filterText" placeholder="Filter keyword" style="margin-bottom:30px;" />

    <el-tree
      ref="subjectTree"
      :data="subjectList"
      :props="defaultProps"
      :filter-node-method="filterNode"
      class="filter-tree"
      default-expand-all
    />

  </div>
</template>

<script>
import subjectApi from '@/api/edu/subject'
export default &#123;

  data() &#123;
    return &#123;
      filterText: '',
      subjectList: [],
      defaultProps: &#123;
        children: 'children',
        label: 'title'
      &#125;
    &#125;
  &#125;,
  watch: &#123;
    filterText(val) &#123;
      this.$refs.subjectTree.filter(val)
    &#125;
  &#125;,

  created() &#123;
    this.fetchNodeList()
  &#125;,

  methods: &#123;
    fetchNodeList() &#123;
      subjectApi.getAllSubject().then(response => &#123;
        if (response.success === true) &#123;
          this.subjectList = response.data.list;
        &#125;
      &#125;)
    &#125;,
    filterNode(value, data) &#123;
      if (!value) return true
      return data.title.indexOf(value) !== -1
    &#125;
  &#125;
&#125;
</script>

八、技术点

富文本

尝试用 ::v-deep 替换 /deep/

public目录用/访问、

视频点播

https://help.aliyun.com/document_detail/52200.html?spm=a2c4g.11186623.6.1053.f172381523pd9O

api 接口,sdk封装了api请求调用。

视频点播(ApsaraVideo for VoD)是集音视频采集、编辑、上传、自动化转码处理、媒体资源管理、分发加速于一体的一站式音视频点播解决方案。

nginx配置

location ~ /vod/ &#123;           
    proxy_pass http://localhost:8003;
&#125;
# 上传大小
client_max_body_size 1024m;


nginx -s reload

安装jar包

mvn install:install-file -DgroupId=com.aliyun -DartifactId=aliyun-sdk-vod-upload -Dversion=1.4.11 -Dpackaging=jar -Dfile=aliyun-java-vod-upload-1.4.11.jar

上传demo

package com.ming;

import com.aliyun.vod.upload.impl.UploadImageImpl;
import com.aliyun.vod.upload.impl.UploadVideoImpl;
import com.aliyun.vod.upload.req.UploadImageRequest;
import com.aliyun.vod.upload.req.UploadVideoRequest;
import com.aliyun.vod.upload.resp.UploadImageResponse;
import com.aliyun.vod.upload.resp.UploadVideoResponse;

public class UploadVideoDemo &#123;
    //账号AK信息请填写(必选)
    private static final String accessKeyId = "LTAI4GDbFK19tVZ5t7YAGgYk";
    //账号AK信息请填写(必选)
    private static final String accessKeySecret = "MingmBQ1V3jZnqHAhPt87SdIANBvOFELhq";

    public static void main(String[] args) &#123;
        //1.音视频上传-本地文件上传
        //视频标题(必选)
        String title = "测试标题";
        //本地文件上传和文件流上传时,文件名称为上传文件绝对路径,如:/User/sample/文件名称.mp4 (必选)
        //文件名必须包含扩展名
        String fileName = "D:\\个人\\大学期间\\大三上\\项目\\项目资料\\1-阿里云上传测试视频\\test01.mp4";
        //本地文件上传
        testUploadVideo(accessKeyId, accessKeySecret, title, fileName);

        //2.图片上传-本地文件上传
        //testUploadImageLocalFile(accessKeyId, accessKeySecret);
    &#125;

    /**
     * 本地文件上传接口
     *
     * @param accessKeyId
     * @param accessKeySecret
     * @param title
     * @param fileName
     */
    private static void testUploadVideo(String accessKeyId, String accessKeySecret, String title, String fileName) &#123;
        UploadVideoRequest request = new UploadVideoRequest(accessKeyId, accessKeySecret, title, fileName);
        /* 可指定分片上传时每个分片的大小,默认为1M字节 */
        request.setPartSize(1 * 1024 * 1024L);
        /* 可指定分片上传时的并发线程数,默认为1,(注:该配置会占用服务器CPU资源,需根据服务器情况指定)*/
        request.setTaskNum(1);
        /* 是否开启断点续传, 默认断点续传功能关闭。当网络不稳定或者程序崩溃时,再次发起相同上传请求,可以继续未完成的上传任务,适用于超时3000秒仍不能上传完成的大文件。
        注意: 断点续传开启后,会在上传过程中将上传位置写入本地磁盘文件,影响文件上传速度,请您根据实际情况选择是否开启*/
        request.setEnableCheckpoint(false);
        /* OSS慢请求日志打印超时时间,是指每个分片上传时间超过该阈值时会打印debug日志,如果想屏蔽此日志,请调整该阈值。单位: 毫秒,默认为300000毫秒*/
        //request.setSlowRequestsThreshold(300000L);
        /* 可指定每个分片慢请求时打印日志的时间阈值,默认为300s*/
        //request.setSlowRequestsThreshold(300000L);
        /* 是否使用默认水印(可选),指定模板组ID时,根据模板组配置确定是否使用默认水印*/
        //request.setIsShowWaterMark(true);
        /* 自定义消息回调设置(可选),参数说明参考文档 https://help.aliyun.com/document_detail/86952.html#UserData */
        // request.setUserData("&#123;\"Extend\":&#123;\"test\":\"www\",\"localId\":\"xxxx\"&#125;,\"MessageCallback\":&#123;\"CallbackURL\":\"http://test.test.com\"&#125;&#125;");

        /* 视频分类ID(可选) */
        //request.setCateId(0);
        /* 视频标签,多个用逗号分隔(可选) */
        //request.setTags("标签1,标签2");
        /* 视频描述(可选) */
        //request.setDescription("视频描述");
        /* 封面图片(可选) */
        //request.setCoverURL("http://cover.sample.com/sample.jpg");
        /* 模板组ID(可选) */
        //request.setTemplateGroupId("8c4792cbc8694*****d5330e56a33d");
        /* 存储区域(可选) */
        //request.setStorageLocation("in-2017032*****18266-5sejdln9o.oss-cn-shanghai.aliyuncs.com");
        /* 开启默认上传进度回调 */
        // request.setPrintProgress(true);
        /* 设置自定义上传进度回调 (必须继承 ProgressListener) */
        // request.setProgressListener(new PutObjectProgressListener());
        UploadVideoImpl uploader = new UploadVideoImpl();
        UploadVideoResponse response = uploader.uploadVideo(request);
        System.out.print("RequestId=" + response.getRequestId() + "\n");  //请求视频点播服务的请求ID
        if (response.isSuccess()) &#123;
            System.out.print("VideoId=" + response.getVideoId() + "\n");
        &#125; else &#123;
            /* 如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因 */
            System.out.print("VideoId=" + response.getVideoId() + "\n");
            System.out.print("ErrorCode=" + response.getCode() + "\n");
            System.out.print("ErrorMessage=" + response.getMessage() + "\n");
        &#125;
    &#125;

    /**
     * 图片上传接口,本地文件上传示例
     * 参数参考文档 https://help.aliyun.com/document_detail/55619.html
     *
     * @param accessKeyId
     * @param accessKeySecret
     */
    private static void testUploadImageLocalFile(String accessKeyId, String accessKeySecret) &#123;
        // 图片类型(必选)取值范围:default(默认),cover(封面),watermark(水印)
        String imageType = "cover";
        UploadImageRequest request = new UploadImageRequest(accessKeyId, accessKeySecret, imageType);
        /* 图片文件扩展名(可选)取值范围:png,jpg,jpeg */
        //request.setImageExt("png");
        /* 图片标题(可选)长度不超过128个字节,UTF8编码 */
        //request.setTitle("图片标题");
        /* 图片标签(可选)单个标签不超过32字节,最多不超过16个标签,多个用逗号分隔,UTF8编码F */
        //request.setTags("标签1,标签2");
        /* 存储区域(可选)*/
        //request.setStorageLocation("out-4f3952f78c021*****013e7.oss-cn-shanghai.aliyuncs.com");
        /* 流式上传时,InputStream为必选,fileName为源文件名称,如:文件名称.png(可选)*/
        //request.setFileName("测试文件名称.png");
        /* 开启默认上传进度回调 */
        // request.setPrintProgress(true);
        /* 设置自定义上传进度回调 (必须继承 ProgressListener) */
        // request.setProgressListener(new PutObjectProgressListener());

        UploadImageImpl uploadImage = new UploadImageImpl();
        UploadImageResponse response = uploadImage.upload(request);
        System.out.print("RequestId=" + response.getRequestId() + "\n");
        if (response.isSuccess()) &#123;
            System.out.print("ImageId=" + response.getImageId() + "\n");
            System.out.print("ImageURL=" + response.getImageURL() + "\n");
        &#125; else &#123;
            System.out.print("ErrorCode=" + response.getCode() + "\n");
            System.out.print("ErrorMessage=" + response.getMessage() + "\n");
        &#125;


    &#125;
&#125;

地址demo

package com.ming;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.vod.model.v20170321.GetPlayInfoRequest;
import com.aliyuncs.vod.model.v20170321.GetPlayInfoResponse;
import com.aliyuncs.vod.model.v20170321.GetVideoPlayAuthRequest;
import com.aliyuncs.vod.model.v20170321.GetVideoPlayAuthResponse;
import org.junit.Test;

import java.util.List;


public class VoidTest &#123;
    public static DefaultAcsClient initVodClient() throws ClientException &#123;
        String regionId = "cn-shanghai";  // 点播服务接入区域
        String accessKeyId = "LTAI4GDbFK19tVZ5t7YAGgYkMing";
        String accessKeySecret = "mBQ1V3jZnqHAhPt87SdIANBvOFELhq";
        DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
        DefaultAcsClient client = new DefaultAcsClient(profile);
        return client;
    &#125;

    public static GetPlayInfoResponse getPlayInfo(DefaultAcsClient client) throws Exception &#123;
        GetPlayInfoRequest request = new GetPlayInfoRequest();
        request.setVideoId("d99a1edced324490a5f91ce4da434858");
        return client.getAcsResponse(request);
    &#125;
    /*获取地址*/
    public static void main(String[] argv) throws ClientException &#123;
        DefaultAcsClient client = initVodClient();
        GetPlayInfoResponse response = new GetPlayInfoResponse();
        try &#123;
            response = getPlayInfo(client);
            List<GetPlayInfoResponse.PlayInfo> playInfoList = response.getPlayInfoList();
            //播放地址
            for (GetPlayInfoResponse.PlayInfo playInfo : playInfoList) &#123;
                System.out.print("PlayInfo.PlayURL = " + playInfo.getPlayURL() + "\n");
            &#125;
            //Base信息
            System.out.print("VideoBase.Title = " + response.getVideoBase().getTitle() + "\n");
        &#125; catch (Exception e) &#123;
            System.out.print("ErrorMessage = " + e.getLocalizedMessage());
        &#125;
        System.out.print("RequestId = " + response.getRequestId() + "\n");


    &#125;

    /*获取令牌*/
    @Test
    public void test() throws ClientException &#123;
        DefaultAcsClient client = initVodClient();
        GetVideoPlayAuthResponse response = new GetVideoPlayAuthResponse();
        try &#123;
            response = getVideoPlayAuth(client);
            //播放凭证
            System.out.print("PlayAuth = " + response.getPlayAuth() + "\n");
            //VideoMeta信息
            System.out.print("VideoMeta.Title = " + response.getVideoMeta().getTitle() + "\n");
        &#125; catch (Exception e) &#123;
            System.out.print("ErrorMessage = " + e.getLocalizedMessage());
        &#125;
        System.out.print("RequestId = " + response.getRequestId() + "\n");
    &#125;

    /*获取播放凭证函数*/
    public static GetVideoPlayAuthResponse getVideoPlayAuth(DefaultAcsClient client) throws Exception &#123;
        GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
        request.setVideoId("d99a1edced324490a5f91ce4da434858");
        return client.getAcsResponse(request);
    &#125;

&#125;

上传前端

<el-form-item label="上传视频">
    <el-upload
           :on-success="handleVodUploadSuccess"
           :on-remove="handleVodRemove"
           :before-remove="beforeVodRemove"
           :on-exceed="handleUploadExceed"
           :file-list="fileList"
           :action="BASE_API+'/admin/vod/video/upload'"
           :limit="1"
           class="upload-demo">
    <el-button size="small" type="primary">上传视频</el-button>
    <el-tooltip placement="right-end">
        <div slot="content">最大支持1G,<br>
            支持3GP、ASF、AVI、DAT、DV、FLV、F4V、<br>
            GIF、M2T、M4V、MJ2、MJPEG、MKV、MOV、MP4、<br>
            MPE、MPG、MPEG、MTS、OGG、QT、RM、RMVB、<br>
            SWF、TS、VOB、WMV、WEBM 等视频格式上传</div>
        <i class="el-icon-question"/>
    </el-tooltip>
    </el-upload>
</el-form-item>

微服务/nacos

一、什么是微服务

1、微服务的由来

微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。

2、为什么需要微服务

在传统的IT行业软件大多都是各种独立系统的堆砌,这些系统的问题总结来说就是扩展性差,可靠性不高,维护成本高。到后面引入了SOA服务化,但是,由于 SOA 早期均使用了总线模式,这种总线模式是与某种技术栈强绑定的,比如:J2EE。这导致很多企业的遗留系统很难对接,切换时间太长,成本太高,新系统稳定性的收敛也需要一些时间。

3、微服务与单体架构区别

(1)单体架构所有的模块全都耦合在一块,代码量大,维护困难。

​ 微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。

(2)单体架构所有的模块都共用一个数据库,存储方式比较单一。

​ 微服务每个模块都可以使用不同的存储方式(比如有的用redis,有的用mysql等),数据库也是单个模块对应自己的数据库。

(3)单体架构所有的模块开发所使用的技术一样。

​ 微服务每个模块都可以使用不同的开发技术,开发模式更灵活。

4、微服务本质

(1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。

(2)微服务的目的是有效的拆分应用,实现敏捷开发和部署 。

(3)微服务提倡的理念团队间应该是 inter-operate, not integrate 。inter-operate是定义好系统的边界和接口,在一个团队内全栈,让团队自治,原因就是因为如果团队按照这样的方式组建,将沟通的成本维持在系统内部,每个子系统就会更加内聚,彼此的依赖耦合能变弱,跨系统的沟通成本也就能降低。

5、什么样的项目适合微服务

微服务可以按照业务功能本身的独立性来划分,如果系统提供的业务是非常底层的,如:操作系统内核、存储系统、网络系统、数据库系统等等,这类系统都偏底层,功能和功能之间有着紧密的配合关系,如果强制拆分为较小的服务单元,会让集成工作量急剧上升,并且这种人为的切割无法带来业务上的真正的隔离,所以无法做到独立部署和运行,也就不适合做成微服务了。

6、微服务开发框架

目前微服务的开发框架,最常用的有以下四个:

Spring Cloud:http://projects.spring.io/spring-cloud(现在非常流行的微服务架构)

Dubbo:http://dubbo.io

Dropwizard:http://www.dropwizard.io (关注单个微服务的开发)

Consul、etcd&etc.(微服务的模块)

7、什么是Spring Cloud

Spring Cloud是一系列框架的集合。它利用Spring Boot的开发便利性简化了分布式系统基础设施的开发,如服务发现、服务注册、配置中心、消息总线、负载均衡、 熔断器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过SpringBoot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包

8、Spring Cloud和Spring Boot是什么关系

Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的开发工具;Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架; Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring Boot来实现,必须基于Spring Boot开发。可以单独使用Spring Boot开发项目,但是Spring Cloud离不开 Spring Boot。

9、Spring Cloud相关基础服务组件

服务发现——Netflix Eureka (Nacos)

服务调用——Netflix Feign

熔断器——Netflix Hystrix

服务网关——Spring Cloud GateWay

分布式配置——Spring Cloud Config (Nacos)

消息总线 —— Spring Cloud Bus (Nacos)

10、Spring Cloud的版本

Spring Cloud并没有熟悉的数字版本号,而是对应一个开发代号。

Cloud代号 Boot版本(train) Boot版本(tested) lifecycle
Angle 1.2.x incompatible with 1.3 EOL in July 2017
Brixton 1.3.x 1.4.x 2017-07卒
Camden 1.4.x 1.5.x -
Dalston 1.5.x not expected 2.x -
Edgware 1.5.x not expected 2.x -
Finchley 2.0.x not expected 1.5.x -
Greenwich 2.1.x
Hoxton 2.2.x

开发代号看似没有什么规律,但实际上首字母是有顺序的,比如:Dalston版本,我们可以简称 D 版本,对应的 Edgware 版本我们可以简称 E 版本。

小版本

Spring Cloud 小版本分为:

SNAPSHOT: 快照版本,随时可能修改

M: MileStone,M1表示第1个里程碑版本,一般同时标注PRE,表示预览版版。

SR: Service Release,SR1表示第1个正式版本,一般同时标注GA:(GenerallyAvailable),表示稳定版本。

二、nacos

https://github.com/alibaba/nacos/releases/tag/1.1.4

http://192.168.244.1:8848/

centos

sh startup.sh -m standalone

步骤

  • 引入依赖,服务注册、调用
  • 开启@EnableDiscoveryClient注册 @EnableFeignClients调用远程
  • 配置文件添加nacos地址spring.cloud.nacos.discovery.server-addr
  • 接口@FeignClient(“service-vod”)使用调用,@Component

详细

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

<!--hystrix依赖,主要是用  @HystrixCommand -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<!--服务注册-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--服务调用-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
# 主应用
@EnableDiscoveryClient  # 注册
@EnableFeignClients # 调用

# nacos服务地址
spring.cloud.nacos.discovery.server-addr=192.168.244.1:8848

添加远程调用的接口

注意,list要指定泛型,路径要齐全,要加上@PathVariable(“名称”),@Component

@Component
@FeignClient("service-vod")
public interface VodClient &#123;
    // 删除video
    @ApiOperation("删除video")
    @DeleteMapping("/eduvideo/deleteVideo/&#123;id&#125;")
    R deleteVideo(@PathVariable("id") String id) ;
&#125;

三、hystrix

(1)接口化请求调用当调用被@FeignClient注解修饰的接口时,在框架内部,将请求转换成Feign的请求实例feign.Request,交由Feign框架处理。
(2)Feign :转化请求Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,封装了Http调用流程。

(3)Hystrix:熔断处理机制 Feign的调用关系,会被Hystrix代理拦截,对每一个Feign调用请求,Hystrix都会将其包装成HystrixCommand,参与Hystrix的流控和熔断规则。如果请求判断需要熔断,则Hystrix直接熔断,抛出异常或者使用FallbackFactory返回熔断Fallback结果;如果通过,则将调用请求传递给Ribbon组件。

(4)Ribbon:服务地址选择 当请求传递到Ribbon之后,Ribbon会根据自身维护的服务列表,根据服务的服务质量,如平均响应时间,Load等,结合特定的规则,从列表中挑选合适的服务实例,选择好机器之后,然后将机器实例的信息请求传递给Http Client客户端,HttpClient客户端来执行真正的Http接口调用;

(5)HttpClient :Http客户端,真正执行Http调用根据上层Ribbon传递过来的请求,已经指定了服务地址,则HttpClient开始执行真正的Http请求

依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

<!--hystrix依赖,主要是用  @HystrixCommand -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

service-edu配置

#开启熔断机制
feign.hystrix.enabled=true
# 设置hystrix超时时间,默认1000ms
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=6000

使用

VodFileDegradeFeignClient

@Component
implements VodClient

VodClient

@FeignClient(name = "service-vod", fallback = VodFileDegradeFeignClient.class)

为什么删除课程时什么会为空???因为只select查询了一列,为空,对象也为空?

四、nacos配置中心

1、介绍

Spring Cloud Config 为分布式系统的外部配置提供了服务端和客户端的支持方案。在配置的服务端您可以在所有环境中为应用程序管理外部属性的中心位置。客户端和服务端概念上的Spring Environment 和 PropertySource 抽象保持同步, 它们非常适合Spring应用程序,但是可以与任何语言中运行的应用程序一起使用。当应用程序在部署管道中从一个开发到测试直至进入生产时,您可以管理这些环境之间的配置,并确保应用程序在迁移时具有它们需要运行的所有内容。服务器存储后端的默认实现使用git,因此它很容易支持标记版本的配置环境,并且能够被管理内容的各种工具访问。很容易添加替代的实现,并用Spring配置将它们插入。

Spring Cloud Config 包含了Client和Server两个部分,server提供配置文件的存储、以接口的形式将配置文件的内容提供出去,client通过接口获取数据、并依据此数据初始化自己的应用。Spring cloud使用git或svn存放配置文件,默认情况下使用git。

2、Nacos替换Config

Nacos 可以与 Spring, Spring Boot, Spring Cloud 集成,并能代替 Spring Cloud Eureka, Spring Cloud Config。通过 Nacos Server 和 spring-cloud-starter-alibaba-nacos-config 实现配置的动态变更。

(1)应用场景

在系统开发过程中,开发者通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成。配置变更是调整系统运行时的行为的有效手段。

如果微服务架构中没有使用统一配置中心时,所存在的问题:

  • 配置文件分散在各个项目里,不方便维护

  • 配置内容安全与权限

  • 更新配置后,项目需要重启

nacos配置中心:系统配置的集中管理(编辑、存储、分发)、动态更新不重启、回滚配置(变更管理、历史版本管理、变更审计)等所有与配置相关的活动。

3、创建

Data Id: ${prefix}-${spring.profile.active}.${file-extension}

service-edu.properties

4、依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

5、bootstrap.properties配置文件

#配置中心地址
spring.cloud.nacos.config.server-addr=106.75.103.21:8848
#spring.profiles.active=dev  当有这行,需要service-edu-dev.properties
# 该配置影响统一配置中心中的dataId
spring.application.name=service-edu

bootstrap.yml 和application.yml 都可以用来配置参数。

bootstrap.yml 可以理解成系统级别的一些参数配置,这些参数一般是不会变动的。
application.yml 可以用来定义应用级别的。

6、命名空间

spring.cloud.nacos.config.namespace=13b5c197-de5b-47e7-9903-ec0538c9db01

7、读取多个配置文件

spring.cloud.nacos.config.server-addr=127.0.0.1:8848

spring.profiles.active=dev

# 该配置影响统一配置中心中的dataId,之前已经配置过
spring.application.name=service-statistics

spring.cloud.nacos.config.namespace=13b5c197-de5b-47e7-9903-ec0538c9db01

spring.cloud.nacos.config.ext-config[0].data-id=redis.properties
# 开启动态刷新配置,否则配置文件修改,工程无法感知
spring.cloud.nacos.config.ext-config[0].refresh=true
spring.cloud.nacos.config.ext-config[1].data-id=jdbc.properties
spring.cloud.nacos.config.ext-config[1].refresh=true

NUXT

一、概念

服务端渲染

后端渲染:在服务器端直接将页面生成发送给服务器
前端渲染: 返回json给前端,通过js将数据绑定到页面上

服务端渲染又称SSR (Server Side Render)是在服务端完成页面的内容,而不是在客户端通过AJAX获取数据。

服务器端渲染(SSR)的优势主要在于:更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。

如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再进行页面内容的抓取。也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题。

另外,使用服务器端渲染,我们可以获得更快的内容到达时间(time-to-content),无需等待所有的 JavaScript 都完成下载并执行,产生更好的用户体验,对于那些「内容到达时间(time-to-content)与转化率直接相关」的应用程序而言,服务器端渲染(SSR)至关重要。

NUXT

Nuxt.js 是一个基于 Vue.js 的轻量级应用框架,可用来创建服务端渲染 (SSR) 应用,也可充当静态站点引擎生成静态站点应用,具有优雅的代码结构分层和热加载等特性。

https://zh.nuxtjs.org/

安装

轮播图

"vue-awesome-swiper": "^3.1.3"

二、目录结构

(1)资源目录 assets

用于组织未编译的静态资源如 LESS、SASS 或 JavaScript。

(2)组件目录 components

用于组织应用的 Vue.js 组件。Nuxt.js 不会扩展增强该目录下 Vue.js 组件,即这些组件不会像页面组件那样有 asyncData 方法的特性。

(3)布局目录 layouts

用于组织应用的布局组件。 layout: 'default'

(4)页面目录 pages

用于组织应用的路由及视图。Nuxt.js 框架读取该目录下所有的 .vue 文件并自动生成对应的路由配置。 layout: 'default'

(5)插件目录 plugins

用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。

(6)nuxt.config.js 文件

nuxt.config.js 文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。

三、异步请求

如果在created中获取数据,貌似会渲染失败

在create前运行,无法获取this对象,不携带完整cookie

asyncData( &#123;params, error&#125;) &#123;
    return teacherApi.getPageTeacher(8, 1)
      .then(resp => &#123;
        console.log(resp.data);
        return &#123;
          data: resp.data
        &#125;
      &#125;)

redis

1.简介

Redis是当前比较热门的NOSQL系统之一,它是一个开源的使用ANSI c语言编写的key-value存储系统(区别于MySQL的二维表格的形式存储。)。和Memcache类似,但很大程度补偿了Memcache的不足。和Memcache一样,Redis数据都是缓存在计算机内存中,不同的是,Memcache只能将数据缓存到内存中,无法自动定期写入硬盘,这就表示,一断电或重启,内存清空,数据丢失。所以Memcache的应用场景适用于缓存无需持久化的数据。而Redis不同的是它会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,实现数据的持久化。

Redis的特点:

1,Redis读取的速度是110000次/s,写的速度是81000次/s;

2,原子 。Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。

3,支持多种数据结构:string(字符串);list(列表);hash(哈希),set(集合);zset(有序集合)

4,持久化,集群部署

5,支持过期时间,支持事务,消息订阅

2.使用

ps -ef | grep redis # 显示所有进程信息,连同命令行
kill -9 3434

启动redis

#bind 127.0.0.1只监听这个ip,可以在后面添加

保护模式

protected-mode no

守护进程

daemonize no

依赖

<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.0</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

初始配置类

service-base添加配置类

package com.ming.baseservice.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport &#123;

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) &#123;
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashKeySerializer(redisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    &#125;

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) &#123;
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    &#125;
&#125;

配置文件

# redis
spring.redis.host=121.89.163.232
spring.redis.port=6379
spring.redis.database= 1
spring.redis.password=root
spring.redis.timeout=1800000ms
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=1
<!--配置lettuce.pool.min-idle需要增加的依赖-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.4.2</version>
</dependency>

结果缓存

方法中添加

springframe

@Cacheable(value = "banner", key = "'selectIndexList'")

数据缓存

 @Autowired
private RedisTemplate<String, String> redisTemplate;
// 取
redisTemplate.opsForValue().get(phone);
// 存
redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);

3.缓存注解

(1)缓存@Cacheable

根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。

查看源码,属性值如下:

属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key

(2)缓存@CachePut

使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。

查看源码,属性值如下:

属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key

(3)缓存@CacheEvict

使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法

查看源码,属性值如下:

属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key
allEntries 是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存
beforeInvocation 是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存

单点登录SSO

一、简介

方式:

  • session广播机制(session复制,默认 30分钟过期)
  • cookie + redis
  • token

二、JWT

简介

JWT头

JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。

&#123;
  "alg": "HS256",
  "typ": "JWT"
&#125;

在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。

有效载荷

有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,如下例

&#123;
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
&#125;

请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。

JSON对象也使用Base64 URL算法转换为字符串保存。

签名哈希

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)

在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用”.”分隔,就构成整个JWT对象。

Base64URL算法

如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。

作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是”+”,”/“和”=”,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:”=”去掉,”+”用”-“替换,”/“用”_”替换,这就是Base64URL算法。

原则

JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。

&#123;
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
&#125;

之后,当用户与服务器通信时,客户在请求中发回JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名。

服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。

用法

客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。

此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header Authorization字段中。当跨域时,也可以将JWT被放置于POST请求的数据主体中。

问题和趋势

  • JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。

  • 生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库

  • 存储在客户端,不占用服务端的内存资源

  • JWT默认不加密,但可以加密。生成原始令牌后,可以再次对其进行加密。

  • 当JWT未加密时,一些私密数据无法通过JWT传输。

  • JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。

  • JWT本身包含认证信息,token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长对于某些重要操作,用户在使用时应该每次都进行进行身份验证

  • 为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。

使用

依赖

common_utils添加

<dependencies>
    <!-- JWT-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
    </dependency>
</dependencies>
工具类
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * @author
 */
public class JwtUtils &#123;

    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    public static String getJwtToken(String id, String nickname)&#123;

        String JwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("guli-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("id", id)
                .claim("nickname", nickname)
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();

        return JwtToken;
    &#125;

    /**
     * 判断token是否存在与有效
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) &#123;
        if(StringUtils.isEmpty(jwtToken)) return false;
        try &#123;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        &#125; catch (Exception e) &#123;
            e.printStackTrace();
            return false;
        &#125;
        return true;
    &#125;

    /**
     * 判断token是否存在与有效
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) &#123;
        try &#123;
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        &#125; catch (Exception e) &#123;
            e.printStackTrace();
            return false;
        &#125;
        return true;
    &#125;

    /**
     * 根据token获取会员id
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) &#123;
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    &#125;
&#125;

service-msm模块

properties、主运行程序(component扫描)

ucenter模块

java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-core</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>

异常处理

// 表单数据验证异常
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public R error(MethodArgumentNotValidException e) &#123;
    return R.error()
        .message(Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
&#125;

// jwt异常
@ExceptionHandler(ExpiredJwtException.class)
@ResponseBody
public R error(ExpiredJwtException e) &#123;
    return R.error()
        .message("登录过期");
&#125;

@ExceptionHandler(JwtException.class)
@ResponseBody
public R error(JwtException e) &#123;
    return R.error()
        .message("token无效");
&#125;

三、MD5

package com.ming.ucenterservice.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


public final class MD5 &#123;

    public static String encrypt(String strSrc) &#123;
        try &#123;
            char hexChars[] = &#123; '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' &#125;;
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) &#123;
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            &#125;
            return new String(chars);
        &#125; catch (NoSuchAlgorithmException e) &#123;
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        &#125;
    &#125;

    public static void main(String[] args) &#123;
        System.out.println(MD5.encrypt("111111"));
    &#125;

&#125;

短信服务

https://help.aliyun.com/document_detail/101893.html?spm=a2c4g.11186623.6.650.608f50a48m8naa

dependency

<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
    </dependency>
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-core</artifactId>
    </dependency>
</dependencies>

controller

package com.ming.msmservice.controller;

import com.ming.commonUtils.R;
import com.ming.msmservice.service.MsmService;
import com.ming.msmservice.utils.RandomUtil;
import io.swagger.annotations.Api;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@RestController
@Api(tags = "短信服务")
@CrossOrigin
@RequestMapping("/api/msm")
public class MsmApiController &#123;
    @Autowired
    MsmService msmService;
    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @GetMapping("/send/&#123;phoneNumber&#125;")
    public R sendCode(@PathVariable("phoneNumber") String phoneNumber) &#123;
        // redis中是否存在
        String code = redisTemplate.opsForValue().get(phoneNumber);
        if(!StringUtils.isEmpty(code)) &#123;
            return R.ok();
        &#125;
        // 获取随机值
        code = RandomUtil.getFourBitRandom();
        Map<String, Object> param = new HashMap<>();
        param.put("code", code);
        // 阿里云发送短信
        boolean isSend = msmService.sendCode(param, phoneNumber);
        // 存入redis
        if(isSend) &#123;
            redisTemplate.opsForValue().set(phoneNumber, code);
            redisTemplate.opsForValue().set(phoneNumber, code, 5, TimeUnit.MINUTES);
            return R.ok();
        &#125;
        return R.error().message("短信发送失败");
    &#125;

&#125;

service

package com.ming.msmservice.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.ming.msmservice.service.MsmService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
@PropertySource("classpath:/config.properties")
public class MsmServiceImpl implements MsmService &#123;
    @Autowired
    Environment environment;

    // 阿里云发送短信
    @Override
    public boolean sendCode(Map<String, Object> param, String phoneNumber) &#123;
        DefaultProfile profile = DefaultProfile.getProfile("default", environment.getProperty("msm.accessKeyId"),
                environment.getProperty("msm.secret"));
        IAcsClient client = new DefaultAcsClient(profile);

        CommonRequest request = new CommonRequest();
        request.setSysMethod(MethodType.POST);
        request.setSysDomain("dysmsapi.aliyuncs.com");
        request.setSysVersion("2017-05-25");
        request.setSysAction("SendSms");
        request.putQueryParameter("PhoneNumbers", phoneNumber);
        request.putQueryParameter("SignName", environment.getProperty("msm.SignName"));
        request.putQueryParameter("TemplateCode", environment.getProperty("msm.TemplateCode"));
        request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param));
        try &#123;
            CommonResponse response = client.getCommonResponse(request);
            // 进一步判断,是否发送成功
            int res = StringUtils.indexOf(response.getData(), "\"Code\":\"OK\"");
            return response.getHttpResponse().isSuccess() && res > -1;
        &#125; catch (ServerException e) &#123;
            e.printStackTrace();
        &#125; catch (ClientException e) &#123;
            e.printStackTrace();
        &#125;
        return false;
    &#125;
&#125;

整合前端

npm install element-ui
npm install vue-qriously
npm install js-cookie
# 添加、修改
cookie.set("eduUser", this.loginInfo, &#123;domain: "localhost"&#125;);
# 获取
let userStr = cookie.get("eduUser")
this.userInfo = JSON.parse(userStr);
# 删除
cookie.remove("eduUser")

nginx

location ~ /test/ &#123;
    proxy_pass http://localhost:8003;
&#125;   

寻找存在/test/的URL,匹配则转发到。。

element-ui

# message
this.$message.error(err.message);
# 验证器

    

request.js

import cookie from 'js-cookie';
const &#123; default: Axios &#125; = require("axios");

import axios from 'axios';
const service = axios.create(&#123;
    baseURL: 'http://127.0.0.1:81',
    timeout: 5000
&#125;)

// 请求
service.interceptors.request.use(
    config => &#123;
        let token = cookie.get("token");
        if(token) &#123;
            config.headers['token'] = token;
        &#125;
        return config;
    &#125;, 
    error => &#123;
        return Promise.reject(error)
    &#125;
)
// 响应
service.interceptors.response.use(
    res => &#123;
        return res.data;
    &#125;,
    error => &#123;
        return Promise.reject(error)
    &#125;
)

export default service;

微信登录

https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html

一、依赖

 <!--httpclient-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>
<!--commons-io-->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
</dependency>
<!--gson-->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>

二、 Gson

Gson gson = new Gson();
HashMap tokenDataMap = gson.fromJson(tokenDataStr, HashMap.class);

三、 技巧

Stirng.format()

String.format(formatStr, 
 one, two, three
)
String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
    "?appid=%s" +
    "&secret=%s" +
    "&code=%s" +
    "&grant_type=authorization_code";
String accessTokenUrl = String.format(
    baseAccessTokenUrl,
    ConstantPropertiesUtil.WX_OPEN_APP_ID,
    ConstantPropertiesUtil.WX_OPEN_APP_SECRET,
    code
);

类保存参数

implements InitializingBean afterPropertiesSet

@Component
@PropertySource("classpath:wechat.properties")
public class ConstantPropertiesUtil  implements InitializingBean &#123; // !!
    @Value("$&#123;wx.open.app_id&#125;")
    private String appId;

    @Value("$&#123;wx.open.app_secret&#125;")
    private String appSecret;

    @Value("$&#123;wx.open.redirect_url&#125;")
    private String redirectUrl;

    public static String WX_OPEN_APP_ID;
    public static String WX_OPEN_APP_SECRET;
    public static String WX_OPEN_REDIRECT_URL;

    @Override
    public void afterPropertiesSet() throws Exception &#123;
        WX_OPEN_APP_ID = appId;
        WX_OPEN_APP_SECRET = appSecret;
        WX_OPEN_REDIRECT_URL = redirectUrl;
    &#125;
&#125;

四、 Http工具类

package com.ming.ucenterservice.utils;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 *  依赖的jar包有:commons-lang-2.6.jar、httpclient-4.3.2.jar、httpcore-4.3.1.jar、commons-io-2.4.jar
 * @author zhaoyb
 *
 */
public class HttpClientUtils &#123;

    public static final int connTimeout=10000;
    public static final int readTimeout=10000;
    public static final String charset="UTF-8";
    private static HttpClient client = null;

    static &#123;
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(128);
        cm.setDefaultMaxPerRoute(128);
        client = HttpClients.custom().setConnectionManager(cm).build();
    &#125;

    public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception&#123;
        return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
    &#125;

    public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception&#123;
        return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
    &#125;

    public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,
            SocketTimeoutException, Exception &#123;
        return postForm(url, params, null, connTimeout, readTimeout);
    &#125;

    public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
            SocketTimeoutException, Exception &#123;
        return postForm(url, params, null, connTimeout, readTimeout);
    &#125;

    public static String get(String url) throws Exception &#123;
        return get(url, charset, null, null);
    &#125;

    public static String get(String url, String charset) throws Exception &#123;
        return get(url, charset, connTimeout, readTimeout);
    &#125;

    /**
     * 发送一个 Post 请求, 使用指定的字符集编码.
     *
     * @param url
     * @param body RequestBody
     * @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3
     * @param charset 编码
     * @param connTimeout 建立链接超时时间,毫秒.
     * @param readTimeout 响应超时时间,毫秒.
     * @return ResponseBody, 使用指定的字符集编码.
     * @throws ConnectTimeoutException 建立链接超时异常
     * @throws SocketTimeoutException  响应超时
     * @throws Exception
     */
    public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)
            throws ConnectTimeoutException, SocketTimeoutException, Exception &#123;
        HttpClient client = null;
        HttpPost post = new HttpPost(url);
        String result = "";
        try &#123;
            if (StringUtils.isNotBlank(body)) &#123;
                HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
                post.setEntity(entity);
            &#125;
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) &#123;
                customReqConf.setConnectTimeout(connTimeout);
            &#125;
            if (readTimeout != null) &#123;
                customReqConf.setSocketTimeout(readTimeout);
            &#125;
            post.setConfig(customReqConf.build());

            HttpResponse res;
            if (url.startsWith("https")) &#123;
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(post);
            &#125; else &#123;
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(post);
            &#125;
            result = IOUtils.toString(res.getEntity().getContent(), charset);
        &#125; finally &#123;
            post.releaseConnection();
            if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) &#123;
                ((CloseableHttpClient) client).close();
            &#125;
        &#125;
        return result;
    &#125;


    /**
     * 提交form表单
     *
     * @param url
     * @param params
     * @param connTimeout
     * @param readTimeout
     * @return
     * @throws ConnectTimeoutException
     * @throws SocketTimeoutException
     * @throws Exception
     */
    public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
            SocketTimeoutException, Exception &#123;

        HttpClient client = null;
        HttpPost post = new HttpPost(url);
        try &#123;
            if (params != null && !params.isEmpty()) &#123;
                List<NameValuePair> formParams = new ArrayList<NameValuePair>();
                Set<Entry<String, String>> entrySet = params.entrySet();
                for (Entry<String, String> entry : entrySet) &#123;
                    formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
                &#125;
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
                post.setEntity(entity);
            &#125;

            if (headers != null && !headers.isEmpty()) &#123;
                for (Entry<String, String> entry : headers.entrySet()) &#123;
                    post.addHeader(entry.getKey(), entry.getValue());
                &#125;
            &#125;
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) &#123;
                customReqConf.setConnectTimeout(connTimeout);
            &#125;
            if (readTimeout != null) &#123;
                customReqConf.setSocketTimeout(readTimeout);
            &#125;
            post.setConfig(customReqConf.build());
            HttpResponse res = null;
            if (url.startsWith("https")) &#123;
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(post);
            &#125; else &#123;
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(post);
            &#125;
            return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
        &#125; finally &#123;
            post.releaseConnection();
            if (url.startsWith("https") && client != null
                    && client instanceof CloseableHttpClient) &#123;
                ((CloseableHttpClient) client).close();
            &#125;
        &#125;
    &#125;




    /**
     * 发送一个 GET 请求
     *
     * @param url
     * @param charset
     * @param connTimeout  建立链接超时时间,毫秒.
     * @param readTimeout  响应超时时间,毫秒.
     * @return
     * @throws ConnectTimeoutException   建立链接超时
     * @throws SocketTimeoutException   响应超时
     * @throws Exception
     */
    public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)
            throws ConnectTimeoutException,SocketTimeoutException, Exception &#123;

        HttpClient client = null;
        HttpGet get = new HttpGet(url);
        String result = "";
        try &#123;
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) &#123;
                customReqConf.setConnectTimeout(connTimeout);
            &#125;
            if (readTimeout != null) &#123;
                customReqConf.setSocketTimeout(readTimeout);
            &#125;
            get.setConfig(customReqConf.build());

            HttpResponse res = null;

            if (url.startsWith("https")) &#123;
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(get);
            &#125; else &#123;
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(get);
            &#125;

            result = IOUtils.toString(res.getEntity().getContent(), charset);
        &#125; finally &#123;
            get.releaseConnection();
            if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) &#123;
                ((CloseableHttpClient) client).close();
            &#125;
        &#125;
        return result;
    &#125;


    /**
     * 从 response 里获取 charset
     *
     * @param ressponse
     * @return
     */
    @SuppressWarnings("unused")
    private static String getCharsetFromResponse(HttpResponse ressponse) &#123;
        // Content-Type:text/html; charset=GBK
        if (ressponse.getEntity() != null  && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) &#123;
            String contentType = ressponse.getEntity().getContentType().getValue();
            if (contentType.contains("charset=")) &#123;
                return contentType.substring(contentType.indexOf("charset=") + 8);
            &#125;
        &#125;
        return null;
    &#125;



    /**
     * 创建 SSL连接
     * @return
     * @throws GeneralSecurityException
     */
    private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException &#123;
        try &#123;
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() &#123;
                public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException &#123;
                    return true;
                &#125;
            &#125;).build();

            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() &#123;

                @Override
                public boolean verify(String arg0, SSLSession arg1) &#123;
                    return true;
                &#125;

                @Override
                public void verify(String host, SSLSocket ssl)
                        throws IOException &#123;
                &#125;

                @Override
                public void verify(String host, X509Certificate cert)
                        throws SSLException &#123;
                &#125;

                @Override
                public void verify(String host, String[] cns,
                                   String[] subjectAlts) throws SSLException &#123;
                &#125;

            &#125;);

            return HttpClients.custom().setSSLSocketFactory(sslsf).build();

        &#125; catch (GeneralSecurityException e) &#123;
            throw e;
        &#125;
    &#125;

    public static void main(String[] args) &#123;
        try &#123;
            String str= post("https://localhost:443/ssl/test.shtml","name=12&page=34","application/x-www-form-urlencoded", "UTF-8", 10000, 10000);
            //String str= get("https://localhost:443/ssl/test.shtml?name=12&page=34","GBK");
            /*Map<String,String> map = new HashMap<String,String>();
            map.put("name", "111");
            map.put("page", "222");
            String str= postForm("https://localhost:443/ssl/test.shtml",map,null, 10000, 10000);*/
            System.out.println(str);
        &#125; catch (ConnectTimeoutException e) &#123;
            // TODO Auto-generated catch block
            e.printStackTrace();
        &#125; catch (SocketTimeoutException e) &#123;
            // TODO Auto-generated catch block
            e.printStackTrace();
        &#125; catch (Exception e) &#123;
            // TODO Auto-generated catch block
            e.printStackTrace();
        &#125;
    &#125;

&#125;

五、实现

// 获取access_token等
    @Override
    public HashMap getTokenData(String code) throws Exception &#123;
        String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
                "?appid=%s" +
                "&secret=%s" +
                "&code=%s" +
                "&grant_type=authorization_code";
        String accessTokenUrl = String.format(baseAccessTokenUrl,
                ConstantPropertiesUtil.WX_OPEN_APP_ID,
                ConstantPropertiesUtil.WX_OPEN_APP_SECRET,
                code
        );
        String tokenData = HttpClientUtils.get(accessTokenUrl);
        Gson gson = new Gson();
        HashMap tokenDataMap = gson.fromJson(tokenData, HashMap.class);
        String access_token = tokenDataMap.get("access_token").toString();
        return tokenDataMap;
    &#125;

    // 获取用户信息
    @Override
    public HashMap getUserInfo(String accessToken, String openid) throws Exception &#123;
        String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
                "?access_token=%s" +
                "&openid=%s";
        String userInfoUrl = String.format(baseUserInfoUrl,
                accessToken,
                openid
        );
        String userInfoStr = HttpClientUtils.get(userInfoUrl);
        Gson gson = new Gson();
        return gson.fromJson(userInfoStr, HashMap.class);
    &#125;

阿里云播放器

集成文档:https://help.aliyun.com/document_detail/51991.html?spm=a2c4g.11186623.2.39.478e192b8VSdEn

在线配置:https://player.alicdn.com/aliplayer/setting/setting.html

功能展示:https://player.alicdn.com/aliplayer/presentation/index.html

service获取凭证

DefaultAcsClient defaultAcsClient = AliyunVodSDKUtils.initVodClient();
//请求
GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
request.setVideoId(videoId);
//响应
GetVideoPlayAuthResponse response = defaultAcsClient.getAcsResponse(request);
//得到播放凭证
playAuth = response.getPlayAuth();

播放

<!-- 阿里云视频播放器样式 -->
    <link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.8.1/skins/default/aliplayer-min.css" >
    <!-- 阿里云视频播放器脚本 -->
    <script charset="utf-8" type="text/javascript" src="https://g.alicdn.com/de/prismplayer/2.8.1/aliplayer-min.js" />
mounted() &#123;
  new Aliplayer(&#123;
    id: 'J_prismPlayer',
    vid: this.vid, // 视频id
    playauth: this.playAuth, // 播放凭证
    encryptType: '1', // 如果播放加密视频,则需设置encryptType=1,非加密视频无需设置此项
    width: '100%',
    height: '800px',
    // 以下可选设置
    cover: 'http://guli.shop/photo/banner/1525939573202.jpg', // 封面
    qualitySort: 'asc', // 清晰度排序

    mediaType: 'video', // 返回音频还是视频
    autoplay: false, // 自动播放
    isLive: false, // 直播
    rePlay: false, // 循环播放 
    preload: true,
    controlBarVisibility: 'hover', // 控制条的显示方式:鼠标悬停
    useH5Prism: true, // 播放器类型:html5
  &#125;, function(player) &#123;
    console.log('播放器创建成功')
  &#125;)
&#125;

微信支付

1.依赖

<dependencies>
    <dependency>
        <groupId>com.github.wxpay</groupId>
        <artifactId>wxpay-sdk</artifactId>
        <version>0.0.3</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
    </dependency>
</dependencies>

2.HttpClient

package com.ming.orderservice.utils;

import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * http请求客户端
 * 
 * @author qy
 * 
 */
public class HttpClient &#123;
    private String url;
    private Map<String, String> param;
    private int statusCode;
    private String content;
    private String xmlParam;
    private boolean isHttps;

    public boolean isHttps() &#123;
        return isHttps;
    &#125;

    public void setHttps(boolean isHttps) &#123;
        this.isHttps = isHttps;
    &#125;

    public String getXmlParam() &#123;
        return xmlParam;
    &#125;

    public void setXmlParam(String xmlParam) &#123;
        this.xmlParam = xmlParam;
    &#125;

    public HttpClient(String url, Map<String, String> param) &#123;
        this.url = url;
        this.param = param;
    &#125;

    public HttpClient(String url) &#123;
        this.url = url;
    &#125;

    public void setParameter(Map<String, String> map) &#123;
        param = map;
    &#125;

    public void addParameter(String key, String value) &#123;
        if (param == null)
            param = new HashMap<String, String>();
        param.put(key, value);
    &#125;

    public void post() throws ClientProtocolException, IOException &#123;
        HttpPost http = new HttpPost(url);
        setEntity(http);
        execute(http);
    &#125;

    public void put() throws ClientProtocolException, IOException &#123;
        HttpPut http = new HttpPut(url);
        setEntity(http);
        execute(http);
    &#125;

    public void get() throws ClientProtocolException, IOException &#123;
        if (param != null) &#123;
            StringBuilder url = new StringBuilder(this.url);
            boolean isFirst = true;
            for (String key : param.keySet()) &#123;
                if (isFirst)
                    url.append("?");
                else
                    url.append("&");
                url.append(key).append("=").append(param.get(key));
            &#125;
            this.url = url.toString();
        &#125;
        HttpGet http = new HttpGet(url);
        execute(http);
    &#125;

    /**
     * set http post,put param
     */
    private void setEntity(HttpEntityEnclosingRequestBase http) &#123;
        if (param != null) &#123;
            List<NameValuePair> nvps = new LinkedList<NameValuePair>();
            for (String key : param.keySet())
                nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
            http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
        &#125;
        if (xmlParam != null) &#123;
            http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
        &#125;
    &#125;

    private void execute(HttpUriRequest http) throws ClientProtocolException,
            IOException &#123;
        CloseableHttpClient httpClient = null;
        try &#123;
            if (isHttps) &#123;
                SSLContext sslContext = new SSLContextBuilder()
                        .loadTrustMaterial(null, new TrustStrategy() &#123;
                            // 信任所有
                            public boolean isTrusted(X509Certificate[] chain,
                                    String authType)
                                    throws CertificateException &#123;
                                return true;
                            &#125;
                        &#125;).build();
                SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                        sslContext);
                httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
                        .build();
            &#125; else &#123;
                httpClient = HttpClients.createDefault();
            &#125;
            CloseableHttpResponse response = httpClient.execute(http);
            try &#123;
                if (response != null) &#123;
                    if (response.getStatusLine() != null)
                        statusCode = response.getStatusLine().getStatusCode();
                    HttpEntity entity = response.getEntity();
                    // 响应内容
                    content = EntityUtils.toString(entity, Consts.UTF_8);
                &#125;
            &#125; finally &#123;
                response.close();
            &#125;
        &#125; catch (Exception e) &#123;
            e.printStackTrace();
        &#125; finally &#123;
            httpClient.close();
        &#125;
    &#125;

    public int getStatusCode() &#123;
        return statusCode;
    &#125;

    public String getContent() throws ParseException, IOException &#123;
        return content;
    &#125;

&#125;

3.service

package com.ming.orderservice.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.github.wxpay.sdk.WXPayUtil;
import com.ming.baseservice.exceptionHandler.MingException;
import com.ming.commonUtils.ResultCode;
import com.ming.orderservice.entity.Order;
import com.ming.orderservice.entity.PayLog;
import com.ming.orderservice.mapper.PayLogMapper;
import com.ming.orderservice.service.OrderService;
import com.ming.orderservice.service.PayLogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ming.orderservice.utils.HttpClient;
import com.ming.orderservice.utils.WechatPayConst;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * 支付日志表 服务实现类
 * </p>
 *
 * @author mingyue
 * @since 2020-09-27
 */
@Service
public class PayLogServiceImpl extends ServiceImpl<PayLogMapper, PayLog> implements PayLogService &#123;
    @Autowired
    OrderService orderService;

    // 生成二维码
    @Override
    public Map createQRCode(String orderNo) &#123;
        try &#123;
            //根据订单id获取订单信息
            QueryWrapper<Order> wrapper = new QueryWrapper<>();
            wrapper.eq("order_no",orderNo);
            Order order = orderService.getOne(wrapper);

            Map m = new HashMap();
            //1、设置支付参数
            m.put("appid", WechatPayConst.APPID);
            m.put("mch_id", WechatPayConst.PARTNER);
            m.put("nonce_str", WXPayUtil.generateNonceStr());
            m.put("body", order.getCourseTitle());
            m.put("out_trade_no", orderNo);
            m.put("total_fee", order.getTotalFee().multiply(new BigDecimal("100")).longValue()+"");
            m.put("spbill_create_ip", WechatPayConst.BILL_CREATE_IP);
            m.put("notify_url", WechatPayConst.NOTIFY_URL);
            m.put("trade_type", "NATIVE");

            //2、HTTPClient来根据URL访问第三方接口并且传递参数
            HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");

            //client设置参数
            client.setXmlParam(WXPayUtil.generateSignedXml(m, WechatPayConst.PARTNER_KEY));
            client.setHttps(true);
            client.post();
            //3、返回第三方的数据
            String xml = client.getContent();
            Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
            //4、封装返回结果集
            System.out.println(resultMap);
            Map map = new HashMap<>();
            map.put("out_trade_no", orderNo);
            map.put("course_id", order.getCourseId());
            map.put("total_fee", order.getTotalFee());
            map.put("result_code", resultMap.get("result_code"));
            map.put("code_url", resultMap.get("code_url"));

            //微信支付二维码2小时过期,可采取2小时未支付取消订单
            //redisTemplate.opsForValue().set(orderNo, map, 120, TimeUnit.MINUTES);
            return map;
        &#125; catch (Exception e) &#123;
            e.printStackTrace();
            throw new MingException(ResultCode.ERROR, "生成二维码失败");
        &#125;
    &#125;

    // 调用查询接口
    @Override
    public Map<String, String> queryPayStatus(String orderNo) &#123;
        try &#123;
            //1、封装参数
            Map m = new HashMap<>();
            m.put("appid", WechatPayConst.APPID);
            m.put("mch_id", WechatPayConst.PARTNER);
            m.put("out_trade_no", orderNo);
            m.put("nonce_str", WXPayUtil.generateNonceStr());

            //2、设置请求
            HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
            client.setXmlParam(WXPayUtil.generateSignedXml(m, WechatPayConst.PARTNER_KEY));
            client.setHttps(true);
            client.post();
            //3、返回第三方的数据
            String xml = client.getContent();
            Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
            //6、转成Map
            //7、返回
            return resultMap;
        &#125; catch (Exception e) &#123;
            e.printStackTrace();
        &#125;
        return null;
    &#125;

    // 更改订单状态
    @Override
    public void updateOrderStatus(Map<String, String> map) &#123;
        //获取订单id
        String orderNo = map.get("out_trade_no");
        //根据订单id查询订单信息
        QueryWrapper<Order> wrapper = new QueryWrapper<>();
        wrapper.eq("order_no",orderNo);
        Order order = orderService.getOne(wrapper);

        if(order.getStatus().intValue() == 1) return;
        order.setStatus(1);
        orderService.updateById(order);

        //记录支付日志
        PayLog payLog = new PayLog();
        payLog.setOrderNo(order.getOrderNo());//支付订单号
        payLog.setPayTime(new Date());
        payLog.setPayType(1);//支付类型
        payLog.setTotalFee(order.getTotalFee());//总金额(分)
        payLog.setTradeState(map.get("trade_state"));//支付状态
        payLog.setTransactionId(map.get("transaction_id"));
        payLog.setAttr(JSONObject.toJSONString(map));
        baseMapper.insert(payLog);//插入到支付日志表
    &#125;
&#125;

echarts

基础

npm install --save echarts@4.1.0

import echarts from 'echarts'

setChart() &#123;
    // 基于准备好的dom,初始化echarts实例
    this.chart = echarts.init(document.getElementById('chart'))
    // console.log(this.chart)

    // 指定图表的配置项和数据
    var option = &#123;
        // x轴是类目轴(离散数据),必须通过data设置类目数据
        xAxis: &#123;
            type: 'category',
            data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
        &#125;,
        // y轴是数据轴(连续数据)
        yAxis: &#123;
            type: 'value'
        &#125;,
        // 系列列表。每个系列通过 type 决定自己的图表类型
        series: [&#123;
            // 系列中的数据内容数组
            data: [820, 932, 901, 934, 1290, 1330, 1320],
            // 折线图
            type: 'line'
        &#125;]
    &#125;

    this.chart.setOption(option)
&#125;

x轴触发/标题

title: &#123;
    text: this.title
&#125;,
tooltip: &#123;
    trigger: 'axis'
&#125;,

区域缩放

dataZoom: [&#123;
  show: true,
  height: 30,
  xAxisIndex: [
    0
  ],
  bottom: 30,
  start: 10,
  end: 80,
  handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
  handleSize: '110%',
  handleStyle: &#123;
    color: '#d3dee5'

  &#125;,
  textStyle: &#123;
    color: '#fff'
  &#125;,
  borderColor: '#90979c'
&#125;,
&#123;
  type: 'inside',
  show: true,
  height: 15,
  start: 1,
  end: 35
&#125;]

Canal

一、简介

我们采取了服务调用获取统计数据,这样耦合度高,效率相对较低,目前我采取另一种实现方式,通过实时同步数据库表的方式实现,例如我们要统计每天注册与登录人数,我们只需把会员表同步到统计库中,实现本地统计就可以了,这样效率更高,耦合度更低,Canal就是一个很好的数据库同步工具。canal是阿里巴巴旗下的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL。

mysql准备

service mysql start canal的原理是基于mysql binlog技术,所以这里一定需要开启mysql的binlog写入功能

show variables like 'log_bin';  # OFF表示该功能未开启
#1,修改 mysql 的配置文件 my.cnf
vi /etc/my.cnf 
#追加内容:
log-bin=mysql-bin     #binlog文件名
binlog_format=ROW     #选择row模式
server_id=1           #mysql实例id,不能和canal的slaveId重复

# docker中
vim /etc/mysql/mysql.conf.d/mysqld.cnf
log-bin=mysql-bin     
binlog_format=ROW     
server_id=1           

#2,重启 mysql:
service mysql restart    

#3,登录 mysql 客户端,查看 log_bin 变量
mysql> show variables like 'log_bin';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin       | ON|
+---------------+-------+
1 row in set (0.00 sec)
#————————————————
#如果显示状态为ON表示该功能已开启

添加权限

CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
GRANT SHOW VIEW, SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;

cannal准备

https://github.com/alibaba/canal/releases

开放端口11111

cd /usr/local/canal

canal.deployer-1.1.4.tar.gz

tar zxvf canal.deployer-1.1.4.tar.gz

vi conf/example/instance.properties

#需要改成自己的数据库信息
canal.instance.master.address=192.168.44.132:3306

#需要改成自己的数据库用户名与密码

canal.instance.dbUsername=canal
canal.instance.dbPassword=canal

#需要改成同步的数据库表规则,例如只是同步一下表
#canal.instance.filter.regex=.*\\..*
canal.instance.filter.regex=guli_ucenter.ucenter_member

注:

mysql 数据解析关注的表,Perl正则表达式.
多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\) 
常见例子:
1.  所有表:.*   or  .*\\..*
2.  canal schema下所有表: canal\\..*
3.  canal下的以canal打头的表:canal\\.canal.*
4.  canal schema下的一张表:canal.test1
5.  多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔)
注意:此过滤条件只针对row模式的数据有效(ps. mixed/statement因为不解析sql,所以无法准确提取tableName进行过滤)

启动

sh bin/startup.sh

maven

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>commons-dbutils</groupId>
        <artifactId>commons-dbutils</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.otter</groupId>
        <artifactId>canal.client</artifactId>
    </dependency>
</dependencies>

util

import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.*;
import com.alibaba.otter.canal.protocol.Message;
import com.google.protobuf.InvalidProtocolBufferException;
import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.net.InetSocketAddress;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

@Component
public class CanalClient &#123;

    //sql队列
    private Queue<String> SQL_QUEUE = new ConcurrentLinkedQueue<>();

    @Resource
    private DataSource dataSource;

    /**
     * canal入库方法
     */
    public void run() &#123;

        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.44.132",
                11111), "example", "", "");
        int batchSize = 1000;
        try &#123;
            connector.connect();
            connector.subscribe(".*\\..*");
            connector.rollback();
            try &#123;
                while (true) &#123;
                    //尝试从master那边拉去数据batchSize条记录,有多少取多少
                    Message message = connector.getWithoutAck(batchSize);
                    long batchId = message.getId();
                    int size = message.getEntries().size(); 
                    if (batchId == -1 || size == 0) &#123;
                        Thread.sleep(1000);
                    &#125; else &#123;
                        dataHandle(message.getEntries());
                    &#125;
                    connector.ack(batchId);

                    //当队列里面堆积的sql大于一定数值的时候就模拟执行
                    if (SQL_QUEUE.size() >= 1) &#123;
                        executeQueueSql();
                    &#125;
                &#125;
            &#125; catch (InterruptedException e) &#123;
                e.printStackTrace();
            &#125; catch (InvalidProtocolBufferException e) &#123;
                e.printStackTrace();
            &#125;
        &#125; finally &#123;
            connector.disconnect();
        &#125;
    &#125;

    /**
     * 模拟执行队列里面的sql语句
     */
    public void executeQueueSql() &#123;
        int size = SQL_QUEUE.size();
        for (int i = 0; i < size; i++) &#123;
            String sql = SQL_QUEUE.poll();
            System.out.println("[sql]----> " + sql);

            this.execute(sql.toString());
        &#125;
    &#125;

    /**
     * 数据处理
     *
     * @param entrys
     */
    private void dataHandle(List<Entry> entrys) throws InvalidProtocolBufferException &#123;
        for (Entry entry : entrys) &#123;
            if (EntryType.ROWDATA == entry.getEntryType()) &#123;
                RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
                EventType eventType = rowChange.getEventType();
                if (eventType == EventType.DELETE) &#123;
                    saveDeleteSql(entry);
                &#125; else if (eventType == EventType.UPDATE) &#123;
                    saveUpdateSql(entry);
                &#125; else if (eventType == EventType.INSERT) &#123;
                    saveInsertSql(entry);
                &#125;
            &#125;
        &#125;
    &#125;

    /**
     * 保存更新语句
     *
     * @param entry
     */
    private void saveUpdateSql(Entry entry) &#123;
        try &#123;
            RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
            List<RowData> rowDatasList = rowChange.getRowDatasList();
            for (RowData rowData : rowDatasList) &#123;
                List<Column> newColumnList = rowData.getAfterColumnsList();
                StringBuffer sql = new StringBuffer("update " + entry.getHeader().getTableName() + " set ");
                for (int i = 0; i < newColumnList.size(); i++) &#123;
                    sql.append(" " + newColumnList.get(i).getName()
                            + " = '" + newColumnList.get(i).getValue() + "'");
                    if (i != newColumnList.size() - 1) &#123;
                        sql.append(",");
                    &#125;
                &#125;
                sql.append(" where ");
                List<Column> oldColumnList = rowData.getBeforeColumnsList();
                for (Column column : oldColumnList) &#123;
                    if (column.getIsKey()) &#123;
                        //暂时只支持单一主键
                        sql.append(column.getName() + "=" + column.getValue());
                        break;
                    &#125;
                &#125;
                SQL_QUEUE.add(sql.toString());
            &#125;
        &#125; catch (InvalidProtocolBufferException e) &#123;
            e.printStackTrace();
        &#125;
    &#125;

    /**
     * 保存删除语句
     *
     * @param entry
     */
    private void saveDeleteSql(Entry entry) &#123;
        try &#123;
            RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
            List<RowData> rowDatasList = rowChange.getRowDatasList();
            for (RowData rowData : rowDatasList) &#123;
                List<Column> columnList = rowData.getBeforeColumnsList();
                StringBuffer sql = new StringBuffer("delete from " + entry.getHeader().getTableName() + " where ");
                for (Column column : columnList) &#123;
                    if (column.getIsKey()) &#123;
                        //暂时只支持单一主键
                        sql.append(column.getName() + "=" + column.getValue());
                        break;
                    &#125;
                &#125;
                SQL_QUEUE.add(sql.toString());
            &#125;
        &#125; catch (InvalidProtocolBufferException e) &#123;
            e.printStackTrace();
        &#125;
    &#125;

    /**
     * 保存插入语句
     *
     * @param entry
     */
    private void saveInsertSql(Entry entry) &#123;
        try &#123;
            RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
            List<RowData> rowDatasList = rowChange.getRowDatasList();
            for (RowData rowData : rowDatasList) &#123;
                List<Column> columnList = rowData.getAfterColumnsList();
                StringBuffer sql = new StringBuffer("insert into " + entry.getHeader().getTableName() + " (");
                for (int i = 0; i < columnList.size(); i++) &#123;
                    sql.append(columnList.get(i).getName());
                    if (i != columnList.size() - 1) &#123;
                        sql.append(",");
                    &#125;
                &#125;
                sql.append(") VALUES (");
                for (int i = 0; i < columnList.size(); i++) &#123;
                    sql.append("'" + columnList.get(i).getValue() + "'");
                    if (i != columnList.size() - 1) &#123;
                        sql.append(",");
                    &#125;
                &#125;
                sql.append(")");
                SQL_QUEUE.add(sql.toString());
            &#125;
        &#125; catch (InvalidProtocolBufferException e) &#123;
            e.printStackTrace();
        &#125;
    &#125;

    /**
     * 入库
     * @param sql
     */
    public void execute(String sql) &#123;
        Connection con = null;
        try &#123;
            if(null == sql) return;
            con = dataSource.getConnection();
            QueryRunner qr = new QueryRunner();
            int row = qr.execute(con, sql);
            System.out.println("update: "+ row);
        &#125; catch (SQLException e) &#123;
            e.printStackTrace();
        &#125; finally &#123;
            DbUtils.closeQuietly(con);
        &#125;
    &#125;
&#125;

启动类

@SpringBootApplication
public class CanalApplication implements CommandLineRunner &#123;
    @Resource
    private CanalClient canalClient;

    public static void main(String[] args) &#123;
        SpringApplication.run(CanalApplication.class, args);
    &#125;

    @Override
    public void run(String... strings) throws Exception &#123;
        //项目启动,执行canal客户端监听
        canalClient.run();
    &#125;
&#125;

递归查询

// 通过pid递归获取菜单,第一层
List<Permission> result = buildTree(permissionList, 1, "0");

private List<Permission> buildTree(List<Permission> permissionList, int level, String pid) &#123;
    List<Permission> childNodeList = new ArrayList<>();
    // 每个节点都判断
    for (Permission permission : permissionList) &#123;
        // 父节点id与子节点pid 相等
        if(permission.getPid().equals( pid )) &#123;
            // 用该节点的id获取子节点
            List<Permission> childrenNodes = buildTree(permissionList, level+1, permission.getId());
            // 添加该节点的子节点
            if(childrenNodes.size() != 0) &#123;
                permission.setChildren(childrenNodes);
            &#125;
            permission.setLevel(level);
            childNodeList.add(permission);
        &#125;
    &#125;
    return childNodeList;
&#125;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@TableField(exist = false)
private List<CategoryEntity> childCategoryEntity;

九、权限控制

网关

一. 介绍

请求转发、负载均衡、权限控制

API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:

(1)客户端会多次请求不同的微服务,增加了客户端的复杂性。

(2)存在跨域请求,在一定场景下处理相对复杂。

(3)认证复杂,每个服务都需要独立认证。

(4)难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。

(5)某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。

以上这些问题可以借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性

二. springcloud gateway

Spring cloud gateway是spring官方基于Spring 5.0、Spring Boot2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供简单、有效和统一的API路由管理方式,Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filer链的方式提供了网关基本的功能,例如:安全、监控/埋点、限流等。

网关提供API全托管服务,丰富的API管理功能,辅助企业管理大规模的API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等贡呢。一般来说网关对外暴露的URL或者接口信息,我们统称为路由信息。如果研发过网关中间件或者使用过Zuul的人,会知道网关的核心是Filter以及Filter Chain(Filter责任链)。Sprig Cloud Gateway也具有路由和Filter的概念。下面介绍一下Spring Cloud Gateway中几个重要的概念。

(1)路由。路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter组成。如果断言路由为真,则说明请求的URL和配置匹配

(2)断言。Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于http request中的任何信息,比如请求头和参数等。

(3)过滤器。一个标准的Spring webFilter。Spring cloud gateway中的filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理

三 .依赖

<dependencies>
    <dependency>
        <groupId>com.atguigu</groupId>
        <artifactId>common_utils</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <!--gson-->
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
    </dependency>

    <!--服务调用-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

四. properties

# 服务端口
server.port=8222
# 服务名
spring.application.name=service-gateway

# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

#使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true
#服务路由名小写
#spring.cloud.gateway.discovery.locator.lower-case-service-id=true

#设置路由id
spring.cloud.gateway.routes[0].id=service-acl
#设置路由的uri
spring.cloud.gateway.routes[0].uri=lb://service-acl
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[0].predicates= Path=/*/acl/**

#配置service-edu服务
spring.cloud.gateway.routes[1].id=service-edu
spring.cloud.gateway.routes[1].uri=lb://service-edu
spring.cloud.gateway.routes[1].predicates= Path=/eduservice/**

#配置service-ucenter服务
spring.cloud.gateway.routes[2].id=service-ucenter
spring.cloud.gateway.routes[2].uri=lb://service-ucenter
spring.cloud.gateway.routes[2].predicates= Path=/ucenterservice/**

#配置service-ucenter服务
spring.cloud.gateway.routes[3].id=service-cms
spring.cloud.gateway.routes[3].uri=lb://service-cms
spring.cloud.gateway.routes[3].predicates= Path=/cmsservice/**

spring.cloud.gateway.routes[4].id=service-msm
spring.cloud.gateway.routes[4].uri=lb://service-msm
spring.cloud.gateway.routes[4].predicates= Path=/edumsm/**

spring.cloud.gateway.routes[5].id=service-order
spring.cloud.gateway.routes[5].uri=lb://service-order
spring.cloud.gateway.routes[5].predicates= Path=/orderservice/**

spring.cloud.gateway.routes[6].id=service-order
spring.cloud.gateway.routes[6].uri=lb://service-order
spring.cloud.gateway.routes[6].predicates= Path=/orderservice/**

spring.cloud.gateway.routes[7].id=service-oss
spring.cloud.gateway.routes[7].uri=lb://service-oss
spring.cloud.gateway.routes[7].predicates= Path=/eduoss/**

spring.cloud.gateway.routes[8].id=service-statistic
spring.cloud.gateway.routes[8].uri=lb://service-statistic
spring.cloud.gateway.routes[8].predicates= Path=/staservice/**

spring.cloud.gateway.routes[9].id=service-vod
spring.cloud.gateway.routes[9].uri=lb://service-vod
spring.cloud.gateway.routes[9].predicates= Path=/eduvod/**

spring.cloud.gateway.routes[10].id=service-edu
spring.cloud.gateway.routes[10].uri=lb://service-edu
spring.cloud.gateway.routes[10].predicates= Path=/eduuser/**

五.跨域

package com.ming.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

@Configuration
public class CorsConfig &#123;
    @Bean
    public CorsWebFilter corsFilter() &#123;
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    &#125;
&#125;

security

一、介绍

Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证Authentication)和用户授权(Authorization)两个部分。

(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。

(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

Spring Security其实就是用filter,多请求的路径进行过滤。

(1)如果是基于Session,那么Spring-security会对cookie里的sessionid进行解析,找到服务器存储的sesion信息,然后判断当前用户是否符合请求的要求。

(2)如果是token,则是解析出token,然后将当前请求加入到Spring-security管理的权限信息中去

二、依赖

<dependencies>
    <dependency>
        <groupId>com.ming</groupId>
        <artifactId>common-utils</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <!-- Spring Security依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
    </dependency>
</dependencies>

三、思路

如果系统的模块众多,每个模块都需要就行授权与认证,所以我们选择基于token的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为key,权限列表为value的形式存入redis缓存中,根据用户名相关信息生成token返回,浏览器将token记录到cookie中,每次调用api接口都默认将token携带到header请求头中,Spring-security解析header头获取token信息,解析token获取当前用户名,根据用户名就可以从redis中获取权限列表,这样Spring-security就能够判断当前请求是否有权限访问如果系统的模块众多,每个模块都需要就行授权与认证,所以我们选择基于token的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为key,权限列表为value的形式存入redis缓存中,根据用户名相关信息生成token返回,浏览器将token记录到cookie中,每次调用api接口都默认将token携带到header请求头中,Spring-security解析header头获取token信息,解析token获取当前用户名,根据用户名就可以从redis中获取权限列表,这样Spring-security就能够判断当前请求是否有权限访问

十、 注意

mybatis

联表查询

实体类构造方法,get,set

@Mapper 添加了@Mapper注解之后这个接口在编译时会生成相应的实现类,注解开发

@Repository

这两个好像不需要。。。

#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 取别名
mybatis-plus.type-aliases-package=com.ming.eduservice.entity 
# 获取mapper
mybatis-plus.mapper-locations=classpath:com/ming/eduservice/mapper/xml/*.xml  
<select id="getAllChapterVideo" resultMap="ChapterVideo">
    select *,'' as eduVideoList
    from guli_edu.edu_chapter
    where course_id = #&#123;id&#125;
</select>

<resultMap id="ChapterVideo" type="ChapterVideoVo" >
    <id property="id" column="id"/>
    <collection property="eduVideoList" column="id" select="selectVideo" javaType="ArrayList" ofType="EduChapter" />
</resultMap>

<select id="selectVideo" resultType="EduChapter">
    select * from guli_edu.edu_video
    where guli_edu.edu_video.chapter_id = #&#123;id&#125;
</select>

property对应实体参数,column对应sql列,能传值

id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。 这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接映射)的时候。

一对多关联时,不标注会为null?传入的值,需要另有result或者id

加载异常,alias给实体取别名,location注册mapper文件,@Repository注册接口,关联时给id赋值,否则会null,collection集合,association多对一,property实体属性,column数据库查询列名可传递值。

<build>
    <resources>
        <!--引入静态文件-->
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
        </resource>
        <!--引入mapper对应的xml文件-->
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
    </resources>
</build>

使用page

联表查询分页, 自己写mapper,mapper第一个参数是Page,第二个参数是查询对象(需要带名字,不会分解),mapper结果返回list,service使用page.setRecods(list)存入结果,返回IPage

// mapper
List<CourseInfo> getCourseQueryList(Page<?> page, @Param("courseQuery") CourseQuery courseQuery);


// servcie
@Override
public IPage<CourseInfo> getCourseQueryList(Long size, Long currentPage, CourseQuery courseQuery) &#123;
    Page<CourseInfo> page = new Page<>(currentPage, size);
    return page.setRecords(eduCourseMapper.getCourseQueryList(page, courseQuery)); //page对象,Ipage是其父类
&#125;

旧不连接

Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl

# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://106.75.103.69:3306/guli_edu?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=
spring.datasource.hikari.minimum-idle=3
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.max-lifetime =30000
spring.datasource.hikari.connection-test-query=SELECT 1

js

深拷贝Object.assign({}, chapter))

eslint webpack.base.conf.js

vue

表格显示标签

<el-table-column label="更改内容">
    <template slot-scope="scope">
        <p v-html='scope.row.name'></p>
    </template>
</el-table-column>

项目存档

表单

显示

<template>
  <div class="app-container">
    <!--查询表单-->
    <el-form :inline="true" class="demo-form-inline">
      <el-form-item>
        <el-input v-model.trim="queryParam.title" placeholder="课程名"/>
      </el-form-item>
      <el-form-item>
        <el-input v-model.trim="queryParam.teacherName" placeholder="讲师名"/>
      </el-form-item>

      <el-form-item>
        <el-select v-model="queryParam.status" clearable placeholder="状态" @change="getList()">
          <el-option value="Draft" label="未发布" />
          <el-option value="Normal" label="已发布" />
        </el-select>
      </el-form-item>

      <el-button type="primary" icon="el-icon-search" @click="getList()">查询</el-button>
      <el-button type="default" @click="resetQueryParam()">清空</el-button>
    </el-form>
    <!-- 表单 -->
    <el-table :data="courseData.list" border style="width: 100%">
      <el-table-column prop="id" label="编号" width="180" />
      <el-table-column prop="title" label="标题" width="180" />
      <el-table-column prop="teacherName" label="教师名" width="100" />
      <el-table-column prop="price" label="价格" width="100" />
      <el-table-column prop="subjectTitle" label="分类名" width="100" />
      <el-table-column prop="status" label="状态" width="100" >
        <template slot-scope="scope">
            &#123;&#123;scope.row.status == "Normal"? "已发布" : "未发布"&#125;&#125;
        </template>
      </el-table-column>
      <el-table-column prop="description" label="简介">
        <template slot-scope="scope">
          <p v-html="scope.row.description"></p>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" width="200">
        <template slot-scope="scope">
          <router-link :to="'/course/update/' +scope.row.id">
            <el-button type="primary" size="mini" icon="el-icon-edit">修改信息</el-button>
          </router-link>
          <el-button
            type="danger"
            size="mini"
            icon="el-icon-delete"
            @click="deleteCourseById(scope.row.id)"
          >删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页 -->
    <el-pagination
      :current-page="currentPage"
      :page-size="size"
      :page-sizes="[3,10,20,30,90]"
      :total="courseData.count"
      style="padding: 30px 0; text-align: center;"
      layout="sizes,total, prev, pager, next, jumper"
      @current-change="getList"
      @size-change="sizeChange"
    />
  </div>
</template>


<script>
import courseApi from "@/api/edu/course";

let defaultCourseParam = &#123;
    count: null,
    pageCount: null,
    courseList: [],
&#125;;

let defaultQueryParam = &#123;
    teacherName: null,
    title: null,
    status: null
&#125;

export default &#123;
  data() &#123;
    return &#123;
      // 课程数据
      courseData: defaultCourseParam, 
      // 查询参数
      queryParam: Object.assign(&#123;&#125;, defaultQueryParam),
      size: 10,
      currentPage: 1
    &#125;;
  &#125;,
  created() &#123;
    this.getList();
  &#125;,
  computed: &#123;
    title() &#123;
        return this.queryParam.title;
    &#125;,
    teacherName() &#123;
        return this.queryParam.teacherName;
    &#125;
  &#125;,
  watch: &#123;
    // 输入变化,进行刷新
    title() &#123;
        this.getList();
    &#125;,
    teacherName() &#123;
        this.getList();
    &#125;
  &#125;,
  methods: &#123;
    // 获取数据
    getList(currentPage = 1) &#123;
        this.currentPage = currentPage;
      courseApi.getQueryList(this.size, this.currentPage, this.queryParam).then((response) => &#123;
        this.courseData = response.data;
      &#125;);
    &#125;,
    // 重置查询
    resetQueryParam() &#123;
        this.queryParam = Object.assign(&#123;&#125;, defaultQueryParam);
        this.getList();
    &#125;,
    // 每页容量改变
    sizeChange(size) &#123;
        this.size = size;
        this.getList();
    &#125;,
    deleteCourseById(id) &#123;
        this.$confirm('这将删除该课程所有信息, 是否确定?', '提示', &#123;
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        &#125;).then(() => &#123;
          courseApi.deleteCourseById(id)
            .then(response => &#123;
                this.$message.success("删除成功");
                this.getList();
            &#125;)
        &#125;)
    &#125;
  &#125;,
&#125;;
</script>

<style>
</style>

API

import request from '@/utils/request';

export default &#123;
    // 提交表单
    saveCourseInfo(courseInfo) &#123;
        return request(&#123;
            url: "/eduservice/course/addCourse",
            method: "post",
            data: courseInfo
        &#125;)
    &#125;,
    // 获取分类表
    // 回显课程信息
    getCourseById(id) &#123;
        return request(&#123;
            url: "/eduservice/course/getCourseInfo/" +id,
            method: "get"
        &#125;)
    &#125;,
    // 更新课程数据
    updateCourseInfo(courseInfo) &#123;
        return request(&#123;
            url: "/eduservice/course/updateCourseInfo",
            method: "post",
            data: courseInfo
        &#125;)
    &#125;,
    // 发布:获取课程相关信息
    getPublishCourseInfo(id) &#123;
        return request(&#123;
            url: "/eduservice/course/getPublishCourseInfo/" +id,
            method: "get",
        &#125;)
    &#125;,
    // 发布课程
    publishCourseById(id) &#123;
        return request(&#123;
            url: "/eduservice/course/publishCourseById/" +id,
            method: "get",
        &#125;) 
    &#125;,
    // 获取课程所有信息 &#123;
    getQueryList(size, currentPage, query) &#123;
        return request(&#123;
            url: `/eduservice/course/getQueryList/$&#123;size&#125;/$&#123;currentPage&#125;`,
            method: "post",
            data: query
        &#125;) 
    &#125;,
    // 删除课程相关信息
    deleteCourseById(id) &#123;
        return request(&#123;
            url: `/eduservice/course/deleteCourseById/$&#123;id&#125;`,
            method: "delete",
        &#125;) 
    &#125;

&#125;

Controller

package com.ming.eduservice.controller;


        import com.baomidou.mybatisplus.core.metadata.IPage;
        import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
        import com.ming.commonUtils.R;
        import com.ming.eduservice.entity.EduCourse;
        import com.ming.eduservice.entity.vo.CourseInfo;
        import com.ming.eduservice.entity.vo.CourseInfoForm;
        import com.ming.eduservice.entity.vo.CourseQuery;
        import com.ming.eduservice.entity.vo.PublishCourseInfo;
        import com.ming.eduservice.mapper.EduCourseMapper;
        import com.ming.eduservice.service.EduCourseService;
        import io.swagger.annotations.Api;
        import io.swagger.annotations.ApiOperation;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.transaction.annotation.Transactional;
        import org.springframework.validation.annotation.Validated;
        import org.springframework.web.bind.annotation.*;

        import springfox.documentation.service.Tags;

/**
 * <p>
 * 课程 前端控制器
 * </p>
 *
 * @author mingyue
 * @since 2020-08-15
 */
@RestController
@CrossOrigin
@Api(tags = "课程")
@RequestMapping("/eduservice/course")
public class EduCourseController &#123;
    @Autowired
    EduCourseService eduCourseService;

    /**
     * 添加课程
     * @param courseInfoForm
     * @return
     */
    @ApiOperation("添加课程")
    @PostMapping("/addCourse")
    public R addCourseInfo(@RequestBody CourseInfoForm courseInfoForm) &#123;
        String id = eduCourseService.addCourseInfo(courseInfoForm);
        return R.ok().data("id", id);
    &#125;

    @ApiOperation("编辑课程")
    @PostMapping("/updateCourseInfo")
    public R updateCourseInfo(@RequestBody CourseInfoForm courseInfoForm) &#123;
        boolean res = eduCourseService.updateCourseInfo(courseInfoForm);
        return R.ok();
    &#125;

    // 获取课程信息
    @ApiOperation("通过id获取课程信息")
    @GetMapping("/getCourseInfo/&#123;id&#125;")
    public R GetCourseInfo(@PathVariable String id) &#123;
        CourseInfoForm courseInfoForm = eduCourseService.getCourseInfoById(id);
        return R.ok().data("courseInfo", courseInfoForm);
    &#125;

    // 获取课程确认所有信息
    @ApiOperation("通过id获取课程信息")
    @GetMapping("/getPublishCourseInfo/&#123;id&#125;")
    public R getPublishCourseInfo(@PathVariable String id) &#123;
        PublishCourseInfo publishCourseInfo = eduCourseService.getPublishCourseInfoById(id);
        return R.ok().data("courseInfo", publishCourseInfo);
    &#125;

    // 发布课程
    @ApiOperation("通过id发布课程")
    @GetMapping("/publishCourseById/&#123;id&#125;")
    public R publishCourseById(@PathVariable String id) &#123;
        boolean res = eduCourseService.publishCourseById(id);
        return res? R.ok() : R.error();
    &#125;

    @ApiOperation("条件分页获取课程列表")
    @PostMapping("/getQueryList/&#123;size&#125;/&#123;currentPage&#125;")
    public R getCourseQueryList(@PathVariable("size") Long size, @PathVariable("currentPage") Long currentPage, @RequestBody(required = false) CourseQuery courseQuery) &#123;
        IPage<CourseInfo> page = eduCourseService.getCourseQueryList(size, currentPage, courseQuery);
        return R.ok()
                .data("list", page.getRecords())
                .data("count", page.getTotal())
                .data("pageCount", page.getPages());
    &#125;

    @ApiOperation("通过id删除课程")
    @DeleteMapping("/deleteCourseById/&#123;id&#125;")
    public R deleteCourseInfoById(@PathVariable String id) &#123;
        boolean res = eduCourseService.deleteCourseInfoById(id);
        return res? R.ok() : R.error();
    &#125;
&#125;

serviceImpl

package com.ming.eduservice.service.impl;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ming.baseservice.exceptionHandler.MingException;
import com.ming.eduservice.entity.EduCourse;
import com.ming.eduservice.entity.EduCourseDescription;
import com.ming.eduservice.entity.EduTeacher;
import com.ming.eduservice.entity.vo.CourseInfo;
import com.ming.eduservice.entity.vo.CourseInfoForm;
import com.ming.eduservice.entity.vo.CourseQuery;
import com.ming.eduservice.entity.vo.PublishCourseInfo;
import com.ming.eduservice.mapper.EduChapterMapper;
import com.ming.eduservice.mapper.EduCourseDescriptionMapper;
import com.ming.eduservice.mapper.EduCourseMapper;
import com.ming.eduservice.mapper.EduVideoMapper;
import com.ming.eduservice.service.EduCourseDescriptionService;
import com.ming.eduservice.service.EduCourseService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * 课程 服务实现类
 * </p>
 *
 * @author mingyue
 * @since 2020-08-15
 */
@Service
public class EduCourseServiceImpl extends ServiceImpl<EduCourseMapper, EduCourse> implements EduCourseService &#123;
    @Autowired
    EduCourseDescriptionService eduCourseDescriptionService;

    @Autowired
    EduCourseMapper eduCourseMapper;

    @Autowired
    EduChapterMapper eduChapterMapper;

    @Autowired
    EduVideoMapper eduVideoMapper;

    // 添加课程
    @Override
    public String addCourseInfo(CourseInfoForm courseInfoForm) &#123;
        // 分成两个表
        EduCourse eduCourse = new EduCourse();
        EduCourseDescription eduCourseDescription = new EduCourseDescription();
        BeanUtils.copyProperties(courseInfoForm, eduCourse);
        BeanUtils.copyProperties(courseInfoForm, eduCourseDescription);
        // 插入课程
        int successInsertCourse = baseMapper.insert(eduCourse);
        if( successInsertCourse <=0 ) &#123;
            throw new MingException(20001, "插入课程失败");

        &#125;
        // 获取id
        String id = eduCourse.getId();
        eduCourseDescription.setId(id);
        // 添加课程简介
        boolean successInsertDesc = eduCourseDescriptionService.save(eduCourseDescription);
        if( !successInsertDesc ) &#123;
            throw new MingException(20001, "插入课程简介失败");
        &#125;
        // 插入成功
        return eduCourse.getId();
    &#125;

    // 获取课程信息
    @Override
    public CourseInfoForm getCourseInfoById(String id) &#123;
        // 分别获取
        EduCourse eduCourse = baseMapper.selectById(id);
        EduCourseDescription eduCourseDescription = eduCourseDescriptionService.getById(id);
        // 合并
        CourseInfoForm courseInfoForm = new CourseInfoForm();
        BeanUtils.copyProperties(eduCourse, courseInfoForm);
        BeanUtils.copyProperties(eduCourseDescription, courseInfoForm);
        return courseInfoForm;
    &#125;

    // 编辑课程
    @Override
    public boolean updateCourseInfo(CourseInfoForm courseInfoForm) &#123;
        // 分离
        EduCourse eduCourse = new EduCourse();
        EduCourseDescription eduCourseDescription = new EduCourseDescription();
        BeanUtils.copyProperties(courseInfoForm, eduCourse);
        BeanUtils.copyProperties(courseInfoForm, eduCourseDescription);
        // 更新
        if (baseMapper.updateById(eduCourse) == 1  && eduCourseDescriptionService.updateById(eduCourseDescription)) &#123;
            return true;
        &#125; else&#123;
            throw new MingException(20001, "更新失败");
        &#125;
    &#125;

    // 获取课程确认所有信息
    @Override
    public PublishCourseInfo getPublishCourseInfoById(String id) &#123;
        return  baseMapper.getPublishCourseInfoById(id);
    &#125;

    // 发布课程
    @Override
    public boolean publishCourseById(String id) &#123;
        EduCourse eduCourse = new EduCourse();
        eduCourse.setId(id);
        eduCourse.setStatus("Normal");
        int res = baseMapper.updateById(eduCourse);
        return res > 0;
    &#125;

    // 条件分页获取课程列表
    @Override
    public IPage<CourseInfo> getCourseQueryList(Long size, Long currentPage, CourseQuery courseQuery) &#123;
        Page<CourseInfo> page = new Page<>(currentPage, size);
        return page.setRecords(eduCourseMapper.getCourseQueryList(page, courseQuery));
    &#125;

    // 通过id删除课程
    @Override
    @Transactional
    public boolean deleteCourseInfoById(String id) &#123;
        Map<String, Object> map = new HashMap<>();
        map.put("course_id", id);
        // 删除小节
        int successDeleteVideo = eduVideoMapper.deleteByMap(map);

        int successDeleteChapter = eduChapterMapper.deleteByMap(map);
        // 删除描述
        boolean successDeleteChapterDesc = eduCourseDescriptionService.removeById(id);
        // 删除课程
        int successDeleteCourse = eduCourseMapper.deleteById(id);
        // 删除章节
        if(successDeleteVideo >= 0 && successDeleteChapter >= 0 && successDeleteChapterDesc && successDeleteCourse >= 0) &#123;
            return true;
        &#125; else &#123;
            throw new MingException(20001, "删除失败");
        &#125;

    &#125;
&#125;

mapper接口

@Repository
public interface EduCourseMapper extends BaseMapper<EduCourse> &#123;

    // 获取课程确认所有信息
    PublishCourseInfo getPublishCourseInfoById(@Param("id") String id);

    // 获取课程所有信息
    List<CourseInfo> getCourseQueryList(Page<?> page, @Param("courseQuery") CourseQuery courseQuery);
&#125;

mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ming.eduservice.mapper.EduCourseMapper">
    <select id="getPublishCourseInfoById" resultType="PublishCourseInfo">
        select c.id ,
               c.title,
               t.name as teacherName ,
               c.price ,
               c.lesson_num ,
               s1.title as subjectParentTitle ,
               s2.title as subjectTitle,
               c.cover
        from edu_course as c
          left join edu_teacher as t on c.teacher_id = t.id
          left join edu_subject as s1 on c.subject_parent_id = s1.id
          left join edu_subject as s2 on c.subject_id = s2.id
        where c.id = #&#123;id&#125;
    </select>

    <select id="getCourseQueryList" resultType="CourseInfo" parameterType="CourseQuery">
        select course.*,
               teacher.name as teacherName,
               subject.title as subjectTitle,
               parentSubject.title as  parentSubjectTitle,
               description.description as description
        from guli_edu.edu_course as course
               left outer join guli_edu.edu_teacher as teacher
                 on course.teacher_id = teacher.id
               left outer join guli_edu.edu_subject as subject
                 on course.subject_id = subject.id
               left outer join guli_edu.edu_subject as parentSubject
                 on course.subject_parent_id = parentSubject.id
               left outer join guli_edu.edu_course_description as description
                 on course.id = description.id
         <where>
             <if test="courseQuery.teacherName != null and courseQuery.teacherName != ''">
                 and teacher.name like concat('%', #&#123;courseQuery.teacherName&#125;, '%')
             </if>
             <if test="courseQuery.status != null and courseQuery.status != ''">
                 and course.status = #&#123;courseQuery.status&#125;
             </if>
             <if test="courseQuery.title != null and courseQuery.title != ''">
                 and course.title like concat('%', #&#123;courseQuery.title&#125;, '%')
             </if>
         </where>

    </select>

</mapper>

项目问题

路由切换,组件不清空,使用vue监听watch

前端ES6模块运行需要babel,转为ES5

mp生成19位id值,js只能处理到15位,String,@TableID(value="id", type = IdType.ID_WORKER_STR)

跨域 403

  • 访问协议,ip地址值,端口号有不一样,产生跨域
  • 解决: nginx反向代理、网关、@CrossOrigin

413
上传视频大小限制,nginx配置
302,重定向

maven
xml不加载,build resource

其他

替换字符串区分

vue: &#123;&#123; message &#125;&#125;  
mybatis: #&#123;&#125; $&#123;&#125; 
thymeleaf: $&#123;&#125;   [[ $&#123;&#125; ]]
php: &#123;$name&#125;
vue-value: /:id
js: `$&#123;&#125;`
springboot-$Value: &#123;$ming.url&#125;
springboot-param: /&#123;id&#125;

经常卡死

# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://106.75.103.69:3306/guli_edu?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
#jdbc:mysql://localhost:3306/guli_edu?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=
spring.datasource.hikari.minimum-idle=3
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.max-lifetime =30000
spring.datasource.hikari.connection-test-query=SELECT 1

十一、部署

手动部署

一、打包

<packaging>jar</packaging>

<build>
    <finalName>demojenkins</finalName>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

maven: mvn clean package 环境变量
springboot工程 => jar

二、运行

jar -jar demo.jar

jenkins

一、环境安装

maven

第一步:上传或下载安装包

wget https://downloads.apache.org/maven/maven-3/3.8.2/binaries/apache-maven-3.8.2-bin.tar.gz
cd/usr/local
apache-maven-3.6.1-bin.tar.gz

第二步:解压安装包

tar -zxvf apache-maven-3.6.1-bin.tar.gz

第三步:建立软连接

ln -s /usr/local/apache-maven-3.6.1/ /usr/local/maven

ln -s /usr/local/apache-maven-3.8.2/ /usr/local/maven

第四步:修改环境变量

vim /etc/profile
export MAVEN_HOME=/usr/local/maven
export PATH=$PATH:$MAVEN_HOME/bin

通过命令source /etc/profile让profile文件立即生效

source /etc/profile

第五步、测试是否安装成功

mvn –v

二、安装Jenkins

wget https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat-stable/jenkins-2.249.1-1.1.noarch.rpm
yum install -y jenkins-2.249.1-1.1.noarch.rpm
systemctl start jenkins
# java yum -y install java-1.8.0-openjdk
vi /etc/sysconfig/jenkins
# 找到文件中JENKINS_PORT=“8080”  新版本jenkins的配置文件在/etc/sysconfig/jenkins

cat /var/lib/jenkins/secrets/initialAdminPassword

cd /var/lib/jenkins/updates
sed -i 's/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' default.json && sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' default.json

kill -9 id 
systemctl restart jenkins.service 
# 没权限
chown -R jenkins:jenkins tomcat9

第一步:上传或下载安装包

https://www.jenkins.io/download/
wget https://get.jenkins.io/war-stable/2.289.3/jenkins.war
cd /usr/local/jenkin
mv ~/software/jenkins.war ./

jenkins.war

第二步:启动

环境centos8,放到tomcat中,报错-Djava.awt.headless=true

yum install libgcc.i686 --setopt=protected_multilib=false

或者

nohup java -jar /usr/local/jenkins/jenkins.war >/usr/local/jenkins/jenkins.out &

# 因为jenkins使用了hudson,所以这个war包既可以java -jar的方式启动,也可以放到web容器中启动
nohup java -Xms$&#123;opt_xms&#125; -Xmx$&#123;opt_xmx&#125; -Xmx$&#123;opt_xmx&#125; -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/jenkins.hprof -jar $&#123;jar_dir&#125;/$&#123;jar_name&#125; --httpPort=$&#123;opt_port&#125; --prefix=/jenkins >> $&#123;log_path&#125;/$&#123;project_name&#125;.log &

# 403
-Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true

nohup java -jar -Dhudson.security.csrf.GlobalCrmaveumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true /usr/local/jenkins/jenkins.war >/usr/local/jenkins/jenkins.out &

第三步:访问

http://ip:8080

解锁

cat /root/.jenkins/secrets/initialAdminPassword

三、配置镜像

官方下载插件慢 更新下载地址

cd &#123;Jenkins工作目录&#125;/updates #进入更新配置位置
cd root/.jenkins/updates # 默认jenkins的工作目录是~/jenkins目录
# 修改默认目录
vim /etc/profile #增加JENKINS_HOME 这个环境变量。
JENKINS_HOME=/data/jenkins_data/
export JENKINS_HOME

# 修改镜像
cd /data/jenkins_data/updates
sed -i 's/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' default.json && sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' default.json
kill -9 id 
# 重启
nohup java -jar /usr/local/jenkins/jenkins.war >/usr/local/jenkins/jenkins.out &

四、安装插件

https://blog.csdn.net/qq_45062377/article/details/113181197

配置jdk、git、maven

Maven Integration plugin、gitee、GitHub Branch Source

Git Parameter
这是一个参数构建扩展,可以在构建的时候选择git的某一个分支来构建服务。

Publish Over SSH
通过SSH拷贝文件到目标机器,同时可以在目标机器上执行脚本

Publish Over SSH
事先要在设置中添加目标机器的访问方式。

maven

clean package -Dmaven.test.skip=true
#需要在脚本开始时添加export BUILD_ID=dontKillMe。
#原因:因为Jenkins执行完当前任务之后需要执行下一个任务,此时Jenkins会直接把tomcat进程杀掉
export BUILD_ID=dontKillMe
cp -rf target/library.war /opt/tomcat9/webapps/
sh /opt/tomcat9/bin/startup.sh
netstat -anp | grep 8080

五、docker

FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY ./target/demojenkins.jar demojenkins.jar
ENTRYPOINT ["java","-jar","/demojenkins.jar", "&"]
#!/bin/bash
#maven打包
mvn clean package
echo 'package ok!'
echo 'build start!'
cd ./infrastructure/eureka_server
service_name="eureka-server"
service_prot=8761
#查看镜像id
IID=$(docker images | grep "$service_name" | awk '{print $3}')
echo "IID $IID"
if [ -n "$IID" ]
then
    echo "exist $SERVER_NAME image,IID=$IID"
    #删除镜像
    docker rmi -f $service_name
    echo "delete $SERVER_NAME image"
    #构建
    docker build -t $service_name .
    echo "build $SERVER_NAME image"
else
    echo "no exist $SERVER_NAME image,build docker"
    #构建
    docker build -t $service_name .
    echo "build $SERVER_NAME image"
fi
#查看容器id
CID=$(docker ps | grep "$SERVER_NAME" | awk '{print $1}')
echo "CID $CID"
if [ -n "$CID" ]
then
    echo "exist $SERVER_NAME container,CID=$CID"
    #停止
    docker stop $service_name
    #删除容器
    docker rm $service_name
else
    echo "no exist $SERVER_NAME container"
fi
#启动
docker run -d --name $service_name --net=host -p $service_prot:$service_prot $service_name
#查看启动日志
#docker logs -f  $service_name

service docker start

/bin/systemctl start docker.service