寫在前面
之前也一直很少有寫SpringBoot項目相關的文章,今天 准備整理一個我自己初始化SpringBoot項目時的一個腳手架,便於自己后面查閱。因為SpringBoot的約定大於配置,在整合各個組件的時候,我們僅僅寫很少的代碼就能 整合 跑起來。
本文,也僅僅是一個簡單的整合,更多個性化配置,更多調優,這個也是自己在工作中慢慢摸索的。如果你有什么更多好的建議或者意見,也可以留言交流。謝謝~
我們開始吧
新建SpringBoot
標題1:AOP 切面統一打印請求日志
意圖:可以看到,每個對於每個請求,開始與結束一目了然,並且打印了以下參數:
URL: 請求接口地址;
HTTP Method: 請求的方法,是 POST, GET, 還是 DELETE 等;
Class Method: 對應 Controller 的全路徑以及調用的哪個方法;
IP: 請求 IP 地址;
Request Args: 請求入參,以 JSON 格式輸出;
Response Args: 響應出參,以 JSON 格式輸出;
Time-Consuming: 請求耗時;
步驟一:添加依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 用於日志切面中,以 json 格式打印出入參 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
步驟二:新建一個包aspect
自定義一個注解:
import java.lang.annotation.*;
/**
* Description: TODO
*
* @Author: 留歌36
* @Date: 2019-11-27 15:43
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface WebLog {
/** 日志描述信息 */
String description() default "";
}
新建注解類:
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
* Description: 查看 https://www.cnblogs.com/quanxiaoha/p/10414681.html
*
* @Author: 留歌36
* @Date: 2019-11-08 11:00
*/
@Aspect
@Component
@Slf4j
public class WebLogAspect {
/** 換行符 */
private static final String LINE_SEPARATOR = System.lineSeparator();
/** 以自定義 @WebLog 注解為切點 */
@Pointcut("@annotation(com.csylh.boot2all.aspect.WebLog)")
public void webLog() {}
/**
* 在切點之前織入
* @param joinPoint
* @throws Throwable
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 開始打印請求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 獲取 @WebLog 注解的描述信息
String methodDescription = getAspectLogDescription(joinPoint);
// 打印請求相關參數
log.info("========================================== Start ==========================================");
// 打印請求 url
log.info("URL : {}", request.getRequestURL().toString());
// 打印描述信息
log.info("Description : {}", methodDescription);
// 打印 Http method
log.info("HTTP Method : {}", request.getMethod());
// 打印調用 controller 的全路徑以及執行方法
log.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// 打印請求的 IP
log.info("IP : {}", request.getRemoteAddr());
// 打印請求入參
log.info("Request Args : {}", new Gson().toJson(joinPoint.getArgs()));
}
/**
* 在切點之后織入
* @throws Throwable
*/
@After("webLog()")
public void doAfter() throws Throwable {
// 接口結束后換行,方便分割查看
log.info("=========================================== End ===========================================" + LINE_SEPARATOR);
}
/**
* 環繞
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 打印出參
log.info("Response Args : {}", new Gson().toJson(result));
// 執行耗時
log.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
return result;
}
/**
* 獲取切面注解的描述
*
* @param joinPoint 切點
* @return 描述信息
* @throws Exception
*/
public String getAspectLogDescription(JoinPoint joinPoint)
throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
StringBuilder description = new StringBuilder("");
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
description.append(method.getAnnotation(WebLog.class).description());
break;
}
}
}
return description.toString();
}
}
就這樣就OK。測試:
標題2:Swagger 整合
意圖:生成文檔形式的API並提供給不同的團隊使用
便於自己單測
無需過多冗余的word文檔,這一點很重要,因為我在工作中就遇到這么一個情況,由於開發使用的文檔和最新文檔版本導致不一致,導致后期很煩人
步驟一:添加依賴
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.4.0</version>
</dependency>
步驟2:新建swagger2配置類
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
/**
* Description:
*
* @author: 留歌36
* Date:2018/9/14 16:29
*/
@Configuration
@EnableSwagger2
public class Swagger2 {
/**
* @Description:swagger2的配置文件,這里可以配置swagger2的一些基本的內容,比如掃描的包等等
*/
@Bean
public Docket createRestApi() {
// 為swagger添加header參數可供輸入
// ParameterBuilder userTokenHeader = new ParameterBuilder();
// ParameterBuilder userIdHeader = new ParameterBuilder();
// List<Parameter> pars = new ArrayList<Parameter>();
// userTokenHeader.name("headerUserToken").description("userToken")
// .modelRef(new ModelRef("string")).parameterType("header")
// .required(false).build();
// userIdHeader.name("headerUserId").description("userId")
// .modelRef(new ModelRef("string")).parameterType("header")
// .required(false).build();
// pars.add(userTokenHeader.build());
// pars.add(userIdHeader.build());
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
// 注意修改這里 .apis(RequestHandlerSelectors.basePackage("com.zd.tongnan.controller"))
.paths(PathSelectors.any()).build()
.globalOperationParameters(setHeaderToken());
// .globalOperationParameters(pars);
}
private List<Parameter> setHeaderToken() {
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<>();
tokenPar.name("token").description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
pars.add(tokenPar.build());
return pars;
}
/**
* @Description: 構建 api文檔的信息
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 設置頁面標題
.title("xxx系統-接口數據文檔")
// 描述
.description("xxx接口數據文檔")
// 設置聯系人
.contact(new Contact("留歌36","https://blog.csdn.net/liuge36",""))
// .contact(new Contact("留歌36", "http://csylh.cn", "csylh36@163.com"))
// 定義版本號
.version("V-1.0.0").build();
}
}
步驟三:使用注解 ,主要是配置 在 controller類名,controller方法 和 實體類這三個地方
demo:
controller 類名上
@Api(value = “用戶注冊登錄接口”,tags = {“登錄注冊注銷的controller”})
public class UserController{}
controller類 方法名上
@ApiOperation:用在請求的方法上,說明方法的用途、作用
- value=“說明方法的用途、作用”
- notes=“方法的備注說明”
案例:
@ApiOperation(value = “用戶注冊接口”, notes=“這是用戶注冊的接口,隨便寫都可以”)
public ServerResponse register(@RequestBody Users user){
return iUserService.register(user);
}
controller 類方法參數上
重點 兩大類:
1.@RequestParam ⇒ @ApiImplicitParams
使用@ApiImplicitParams來定義參數
@ApiImplicitParams({
@ApiImplicitParam(name="name",value="內存名",dataType="string", paramType = "query"),
})
2.@RequestBody ⇒ @ApiModelProperty(value = "用戶名",name = "username",example = "admin",required = true) :注:這里是在對應的實體類上的各個屬性上添加注解
區別:一個是在實體類上添加注解@ApiModelProperty
一個是在方法 參數上面添加注解@ApiImplicitParams
更多使用,參考 這里
標題3:Mybatis 整合
意圖:這個是常用的持久層框架,雖然spring-data-jpa也是很優秀的。但是我自己在工作中這個用的比較多一點。
SpringBoot 整合 Mybatis 有兩種常用的方式,一種就是我們常見的 xml 的方式 ,還有一種是全注解的方式。
如何選擇:在 SQL 語句不太長的情況下,我覺得全注解的方式一定是比較清晰簡潔的。但是,復雜的 SQL 確實不太適合和代碼寫在一起,那么就使用xml文件的形式。其實這兩個方法也沒差。
步驟1:添加依賴
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
步驟2:配置 application.properties
server.port=9099
# 暫時使用SpringBoot2 自帶的 HikariCP 連接池,后面結合Druid
spring.datasource.url=jdbc:mysql://192.168.1.200:3306/test2?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=db
spring.datasource.password=xxx
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#Mybatis 配置
mybatis.config-location=classpath:mybatis-config.xml
mybatis.mapper-locations=classpath*:/mappers/**.xml
mybatis.type-aliases-package=com.liuge36.emr.entity
步驟3:resources 下新建mybatis-config.xml ,並建立自己的entity包
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置全局屬性 -->
<settings>
<!-- 使用jdbc的getGeneratedKeys獲取數據庫自增主鍵值 -->
<setting name="useGeneratedKeys" value="true" />
<!-- 使用列標簽替換列別名 默認:true -->
<setting name="useColumnLabel" value="true" />
<!-- 開啟駝峰命名轉換:Table{create_time} -> Entity{createTime} -->
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
</configuration>
步驟4:測試
新建dao包,新建MemoryDao接口
import cn.com.zdmedical.emr.entity.Memory;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* Description: TODO
*
* @Author: 留歌36
* @Date: 2019-11-28 09:10
*/
@Mapper
public interface MemoryDao {
/** 根據名字查找內存信息 */
Memory findMemoryByName(@Param("name") String name);
}
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.liuge36.emr.dao.MemoryDao">
<select id="findMemoryByName" parameterType="String" resultType="com.liuge36.emr.entity.Memory">
SELECT * FROM memory WHERE name = #{name}
</select>
</mapper>
其余的就是基本的常規業務操作了。
注解的方式:
@Mapper
public interface UserDao {
/**
* 通過名字查詢用戶信息
*/
@Select("SELECT * FROM user WHERE name = #{name}")
User findUserByName(@Param("name") String name);
/**
* 查詢所有用戶信息
*/
@Select("SELECT * FROM user")
List<User> findAllUser();
/**
* 插入用戶信息
*/
@Insert("INSERT INTO user(name, age,money) VALUES(#{name}, #{age}, #{money})")
void insertUser(@Param("name") String name, @Param("age") Integer age, @Param("money") Double money);
/**
* 根據 id 更新用戶信息
*/
@Update("UPDATE user SET name = #{name},age = #{age},money= #{money} WHERE id = #{id}")
void updateUser(@Param("name") String name, @Param("age") Integer age, @Param("money") Double money,
@Param("id") int id);
/**
* 根據 id 刪除用戶信息
*/
@Delete("DELETE from user WHERE id = #{id}")
void deleteUser(@Param("id") int id);
}
所以,其實SpringBoot整合這些框架的 基本 使用還是很簡單的。
標題4:Druid 數據庫連接池 整合
https://github.com/alibaba/druid
阿里巴巴數據庫事業部出品,為監控而生的數據庫連接池
Druid是Java語言中最好的數據庫連接池。Druid能夠提供強大的監控和擴展功能。
步驟1:添加依賴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
步驟2:配置 application.properties
#spring.datasource.url=jdbc:mysql://192.168.1.200:3306/test2?useUnicode=true&characterEncoding=UTF-8&useSSL=false
#spring.datasource.username=root
#spring.datasource.password=xx
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 這4個參數key里不帶druid也可以,即可以還用上面的這個4個參數
spring.datasource.druid.url=jdbc:mysql://192.168.1.200:3306/test2?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.druid.username=root
spring.datasource.druid.password=xx
spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver
# 初始化時建立物理連接的個數
spring.datasource.druid.initial-size=5
# 最大連接池數量
spring.datasource.druid.max-active=30
# 最小連接池數量
spring.datasource.druid.min-idle=5
# 獲取連接時最大等待時間,單位毫秒
spring.datasource.druid.max-wait=60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 連接保持空閑而不被驅逐的最小時間
spring.datasource.druid.min-evictable-idle-time-millis=300000
# 用來檢測連接是否有效的sql,要求是一個查詢語句
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
# 建議配置為true,不影響性能,並且保證安全性。申請連接的時候檢測,如果空閑時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效。
spring.datasource.druid.test-while-idle=true
# 申請連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能。
spring.datasource.druid.test-on-borrow=false
# 歸還連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能。
spring.datasource.druid.test-on-return=false
# 是否緩存preparedStatement,也就是PSCache。PSCache對支持游標的數據庫性能提升巨大,比如說oracle。在mysql下建議關閉。
spring.datasource.druid.pool-prepared-statements=true
# 要啟用PSCache,必須配置大於0,當大於0時,poolPreparedStatements自動觸發修改為true。
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=50
# 配置監控統計攔截的filters,去掉后監控界面sql無法統計
spring.datasource.druid.filters=stat,wall
# 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
spring.datasource.druid.connection-properties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# 合並多個DruidDataSource的監控數據
spring.datasource.druid.use-global-data-source-stat=true
步驟3:訪問 http://127.0.0.1:9099/druid/index.html
打開mysql客戶端navicat的sql窗口,執行show full processlist,顯示如下內容:
可以看到,啟動項目后,直接創建5個數據連接,這是由application.properties配置文件中spring.datasource.druid.initial-size=5控制的。
步驟4:druid監控
在步驟3我們可以看到,瀏覽器輸入http://127.0.0.1:9099/druid/index.html直接就能看到druid控制台界面,在這里面可以看到很多項目信息,如果任憑用戶隨意訪問,非常危險。我們可以通過配置,設置只有通過登錄認證才可以訪問。
在application.properties配置文件中增加:
# druid連接池監控
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin
# 排除一些靜態資源,以提高效率
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
再次訪問:http://127.0.0.1:9099/druid/login.html
輸入 admin /admin 進去
標題5:通用工具類+通用返回
4個常用JSON類庫分別為:Gson,FastJson,Jackson,Json-lib
步驟1:添加依賴
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.13</version>
</dependency>
步驟2:修改配置文件
# 屬性為 空(””) 或者為 NULL 都不序列化
spring.jackson.default-property-inclusion=non_empty
步驟3:新建 common 包
在包下新建:ResponseCode
/**
* Description:
*
* @author: 留歌36
* Date:2018/11/4 16:04
*/
public enum ResponseCode {
SUCCESS(200,"成功"),
ERROR(1,"錯誤"),
NEED_REGISTER(10,"需要注冊,請授權登錄!"),
NEED_LOGIN(12,"需要登錄,請登錄!"),
TOMANYLOGIN(11,"賬號被擠出."),
ILLEGAL_ARGUMENT(2,"ILLEGAL_ARGUMENT");
private final int code;
private final String desc;
ResponseCode(int code, String desc){
this.code=code;
this.desc=desc;
}
public int getCode(){
return code;
}
public String getDesc(){
return desc;
}
}
新建通用返回對象:
import org.codehaus.jackson.annotate.JsonIgnore;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import java.io.Serializable;
/**
* Description:
*
* @author: 留歌36
* Date:2018/11/4 16:03
*/
@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
//保證序列化json的時候,如果是null的對象,key也會消失
public class ServerResponse<T> implements Serializable{
private int status;
private String msg;
private T data;//可以指定泛型里面的內容,也可以不指定,而且里面的類型可以是多種,map,list,string
//編寫外部訪問的Public方法,之前需要寫一個枚舉類
//這樣外部的顯示的就是這幾個值啦
public int getStatus(){
return status;
}
public String getMsg(){
return msg;
}
public T getData(){
return data;
}
//判斷是否登陸成功
@JsonIgnore
public boolean isSuccess(){
return this.status == ResponseCode.SUCCESS.getCode();
}
//編寫 私有 的構造方法,外部是不能new的
// 開放供外部使用的Public方法
private ServerResponse(int status){
this.status=status;
}
private ServerResponse(int status, T data){
this.status=status;
this.data=data;
}
private ServerResponse(int status, String msg){
this.status=status;
this.msg=msg;
}
private ServerResponse(int status, String msg, T data){
this.status=status;
this.msg=msg;
this.data=data;
}
//編寫成功靜態的方法供外部的調用
public static <T> ServerResponse<T> createBySuccess(){
return new ServerResponse<T>(ResponseCode.SUCCESS.getCode());
}
public static <T> ServerResponse<T> createBySuccess(T data){
return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),data);
}
public static <T> ServerResponse<T> createBySuccess(String msg,T data){
return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),msg,data);
}
public static <T> ServerResponse<T> createBySuccessMessage(String msg){
return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),msg);
}
//編寫失敗的方法
public static <T> ServerResponse<T> createByError(){
return new ServerResponse<T>(ResponseCode.ERROR.getCode(),ResponseCode.ERROR.getDesc());
}
public static <T> ServerResponse<T> createByErrorMessage(String errorMessage) {
return new ServerResponse<T>(ResponseCode.ERROR.getCode(),errorMessage);
}
public static <T> ServerResponse<T> createByErrorCodeMessage(int errorcode,String erroeMessage){
return new ServerResponse<T>(errorcode,erroeMessage);
}
public static <T> ServerResponse<T> createByErrorNeeDLogin(String erroeMessage){
return new ServerResponse<T>(ResponseCode.NEED_REGISTER.getCode(),erroeMessage);
}
}
允許全局跨域:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* Description: 配置全局跨域
*
* @Author: 留歌36
* @Date: 2019-11-28 11:45
*/
@Configuration
public class GlobalCorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
SQL樣例:
create database imooc_homepage_sc;
-- 用戶信息表
create table if not exists `imooc_homepage_sc`.`homepage_user` (
`id` bigint(20) not null auto_increment comment '自增ID',
`username` varchar(128) not null default '' comment '用戶名',
`email` varchar(128) not null default '' comment '用戶郵箱',
`create_time` datetime not null default '1970-01-01 08:00:00' comment '創建時間',
`update_time` datetime not null default '1970-01-01 08:00:00' comment '更新時間',
primary key(`id`),
unique key `key_username` (`username`)
)engine=InnoDB auto_increment=1 default charset=utf8 row_format=compact comment='用戶信息表';
-- 用戶課程表
create table if not exists `imooc_homepage_sc`.`homepage_user_course` (
`id` bigint(20) not null auto_increment comment '自增ID',
`user_id` bigint(20) not null default 0 comment '用戶 ID',
`course_id` bigint(20) not null default 0 comment '課程 ID',
`create_time` datetime not null default '1970-01-01 08:00:00' comment '創建時間',
`update_time` datetime not null default '1970-01-01 08:00:00' comment '更新時間',
primary key(`id`),
unique key `key_user_course` (`user_id`, `course_id`)
)engine=InnoDB auto_increment=1 default charset=utf8 row_format=compact comment='用戶課程表';
-- 課程表
create table if not exists `imooc_homepage_sc`.`homepage_course` (
`id` bigint(20) not null auto_increment comment '自增ID',
`course_name` varchar(128) not null default '' comment '課程名稱',
`course_type` varchar(128) not null default '' comment '課程類型',
`course_icon` varchar(128) not null default '' comment '課程圖標',
`course_intro` varchar(128) not null default '' comment '課程介紹',
`create_time` datetime not null default '1970-01-01 08:00:00' comment '創建時間',
`update_time` datetime not null default '1970-01-01 08:00:00' comment '更新時間',
primary key(`id`),
unique key `key_course_name` (`course_name`)
)engine=InnoDB auto_increment=1 default charset=utf8 row_format=compact comment='課程表';
未完待續~