├── vue-admin ├── .eslintignore ├── babel.config.js ├── tests │ └── unit │ │ ├── .eslintrc.js │ │ ├── components │ │ ├── Hamburger.spec.js │ │ ├── SvgIcon.spec.js │ │ └── Breadcrumb.spec.js │ │ └── utils │ │ ├── validate.spec.js │ │ ├── parseTime.spec.js │ │ └── formatTime.spec.js ├── public │ ├── favicon.ico │ └── index.html ├── .travis.yml ├── src │ ├── assets │ │ ├── 403_images │ │ │ └── 403.gif │ │ └── 404_images │ │ │ ├── 404.png │ │ │ └── 404_cloud.png │ ├── views │ │ ├── nested │ │ │ ├── menu2 │ │ │ │ └── index.vue │ │ │ └── menu1 │ │ │ │ ├── menu1-3 │ │ │ │ └── index.vue │ │ │ │ ├── index.vue │ │ │ │ ├── menu1-2 │ │ │ │ ├── menu1-2-1 │ │ │ │ │ └── index.vue │ │ │ │ ├── menu1-2-2 │ │ │ │ │ └── index.vue │ │ │ │ └── index.vue │ │ │ │ └── menu1-1 │ │ │ │ └── index.vue │ │ ├── dashboard │ │ │ └── index.vue │ │ ├── tree │ │ │ └── index.vue │ │ ├── table │ │ │ └── index.vue │ │ ├── 403.vue │ │ ├── form │ │ │ └── index.vue │ │ ├── login │ │ │ └── index.vue │ │ └── 404.vue │ ├── layout │ │ ├── components │ │ │ ├── index.js │ │ │ ├── Sidebar │ │ │ │ ├── Item.vue │ │ │ │ ├── Link.vue │ │ │ │ ├── FixiOSBug.js │ │ │ │ ├── index.vue │ │ │ │ ├── Logo.vue │ │ │ │ └── SidebarItem.vue │ │ │ ├── AppMain.vue │ │ │ └── Navbar.vue │ │ ├── mixin │ │ │ └── ResizeHandler.js │ │ └── index.vue │ ├── App.vue │ ├── api │ │ ├── table.js │ │ └── user.js │ ├── store │ │ ├── getters.js │ │ ├── index.js │ │ └── modules │ │ │ ├── settings.js │ │ │ ├── app.js │ │ │ └── user.js │ ├── icons │ │ ├── svg │ │ │ ├── link.svg │ │ │ ├── user.svg │ │ │ ├── example.svg │ │ │ ├── table.svg │ │ │ ├── password.svg │ │ │ ├── nested.svg │ │ │ ├── eye.svg │ │ │ ├── skill.svg │ │ │ ├── eye-open.svg │ │ │ ├── pdf.svg │ │ │ ├── tree.svg │ │ │ ├── dashboard.svg │ │ │ ├── form.svg │ │ │ └── qq.svg │ │ ├── index.js │ │ └── svgo.yml │ ├── utils │ │ ├── get-page-title.js │ │ ├── validate.js │ │ ├── auth.js │ │ ├── index.js │ │ └── request.js │ ├── settings.js │ ├── styles │ │ ├── mixin.scss │ │ ├── variables.scss │ │ ├── element-ui.scss │ │ ├── transition.scss │ │ ├── index.scss │ │ └── sidebar.scss │ ├── main.js │ ├── components │ │ ├── Hamburger │ │ │ └── index.vue │ │ ├── SvgIcon │ │ │ └── index.vue │ │ └── Breadcrumb │ │ │ └── index.vue │ ├── permission.js │ └── router │ │ └── index.js ├── .env.production ├── .env.staging ├── postcss.config.js ├── .gitignore ├── .editorconfig ├── .env.development ├── mock │ ├── table.js │ ├── user.js │ ├── index.js │ └── mock-server.js ├── jest.config.js ├── build │ └── index.js ├── LICENSE ├── package.json ├── README.md ├── README-zh.md ├── vue.config.js └── .eslintrc.js ├── viewModels ├── User.go ├── article.go └── emun │ └── articleStatus.go ├── conf └── app.ini ├── pkg ├── util │ └── pagination.go ├── e │ ├── code.go │ └── msg.go └── setting │ └── setting.go ├── go.mod ├── models ├── Claims.go ├── auth.go ├── models.go ├── tag.go └── article.go ├── main.go ├── README.md ├── routers ├── api │ ├── v1 │ │ ├── auth.go │ │ ├── tag.go │ │ └── article.go │ └── v2 │ │ └── article.go └── router.go ├── LICENSE ├── middleware ├── myjwt │ ├── myjwt.go │ └── gin_jwt.go └── cors │ └── cors.go ├── docs └── sql │ └── mysql.sql └── go.sum /vue-admin/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /vue-admin/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /vue-admin/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /vue-admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rockyzsu/gin-vue/master/vue-admin/public/favicon.ico -------------------------------------------------------------------------------- /vue-admin/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /vue-admin/src/assets/403_images/403.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rockyzsu/gin-vue/master/vue-admin/src/assets/403_images/403.gif -------------------------------------------------------------------------------- /vue-admin/src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rockyzsu/gin-vue/master/vue-admin/src/assets/404_images/404.png -------------------------------------------------------------------------------- /vue-admin/.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = 'http://bj939496716:8000' 6 | 7 | -------------------------------------------------------------------------------- /vue-admin/src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rockyzsu/gin-vue/master/vue-admin/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /vue-admin/.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /viewModels/User.go: -------------------------------------------------------------------------------- 1 | package viewModels 2 | 3 | type User struct { 4 | Roles []string 5 | Introduction string 6 | Avatar string 7 | Name string 8 | } 9 | -------------------------------------------------------------------------------- /vue-admin/src/views/nested/menu2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /vue-admin/src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | -------------------------------------------------------------------------------- /vue-admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /vue-admin/src/views/nested/menu1/menu1-3/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /vue-admin/src/views/nested/menu1/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /vue-admin/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /vue-admin/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /viewModels/article.go: -------------------------------------------------------------------------------- 1 | package viewModels 2 | 3 | type Article struct { 4 | Id int 5 | Title string 6 | Status string 7 | Author string 8 | DisplayTime string 9 | Pageviews int 10 | } 11 | -------------------------------------------------------------------------------- /vue-admin/src/api/table.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList(params) { 4 | return request({ 5 | url: '/api/v1/table/list', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /vue-admin/src/views/nested/menu1/menu1-1/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /vue-admin/src/views/nested/menu1/menu1-2/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /vue-admin/postcss.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | 'plugins': { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | 'autoprefixer': {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vue-admin/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | token: state => state.user.token, 5 | avatar: state => state.user.avatar, 6 | name: state => state.user.name 7 | } 8 | export default getters 9 | -------------------------------------------------------------------------------- /vue-admin/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | tests/**/coverage/ 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-admin/src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Vue Admin Template' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /vue-admin/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /vue-admin/src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /conf/app.ini: -------------------------------------------------------------------------------- 1 | #debug or release 2 | RUN_MODE = debug 3 | 4 | [app] 5 | PAGE_SIZE = 10 6 | IDENTITY_KEY = idname 7 | 8 | 9 | [server] 10 | HTTP_PORT = 8000 11 | READ_TIMEOUT = 60 12 | WRITE_TIMEOUT = 60 13 | 14 | [database] 15 | TYPE = mysql 16 | USER = root 17 | PASSWORD = root 18 | #127.0.0.1:3306 19 | HOST = 192.168.129.251:3306 20 | NAME = blog 21 | TABLE_PREFIX = blog_ -------------------------------------------------------------------------------- /pkg/util/pagination.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/unknwon/com" 6 | 7 | "gin-vue/pkg/setting" 8 | ) 9 | 10 | func GetPage(c *gin.Context) int { 11 | result := 0 12 | page, _ := com.StrTo(c.Query("page")).Int() 13 | if page > 0 { 14 | result = (page - 1) * setting.PageSize 15 | } 16 | 17 | return result 18 | } 19 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /vue-admin/src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: 'Vue Admin Template', 4 | 5 | /** 6 | * @type {boolean} true | false 7 | * @description Whether fix the header 8 | */ 9 | fixedHeader: false, 10 | 11 | /** 12 | * @type {boolean} true | false 13 | * @description Whether show the logo in sidebar 14 | */ 15 | sidebarLogo: false 16 | } 17 | -------------------------------------------------------------------------------- /viewModels/emun/articleStatus.go: -------------------------------------------------------------------------------- 1 | package emun 2 | 3 | var ArticleStatus = map[int]string{ 4 | -1: "error", //出现未定义状态 5 | 0: "published", //已发布 6 | 1: "draft", //编辑中 7 | 2: "deleted", //已删除 8 | } 9 | 10 | func GetArticleStatus(code int) string { 11 | msg, ok := ArticleStatus[code] 12 | if ok { 13 | return msg 14 | } 15 | 16 | return ArticleStatus[-1] 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gin-vue 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/appleboy/gin-jwt v2.5.0+incompatible 7 | github.com/appleboy/gin-jwt/v2 v2.6.4 8 | github.com/astaxie/beego v1.12.2 9 | github.com/gin-gonic/gin v1.6.3 10 | github.com/go-ini/ini v1.57.0 11 | github.com/jinzhu/gorm v1.9.15 12 | github.com/unknwon/com v1.0.1 13 | gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /vue-admin/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import app from './modules/app' 5 | import settings from './modules/settings' 6 | import user from './modules/user' 7 | 8 | Vue.use(Vuex) 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | app, 13 | settings, 14 | user 15 | }, 16 | getters 17 | }) 18 | 19 | export default store 20 | -------------------------------------------------------------------------------- /models/Claims.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Claims struct { 4 | ID int `gorm:"primary_key" json:"claim_id"` 5 | AuthID int `json:"auth_id"` 6 | Type string `json:"type"` 7 | Value string `json:"value"` 8 | } 9 | 10 | func GetUserClaims(userName string) (claims []Claims) { 11 | var auth Auth 12 | db.Where("username = ?", userName).First(&auth) 13 | db.Where("auth_id = ?", auth.ID).Find(&claims) 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/e/code.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | const ( 4 | SUCCESS = 200 5 | ERROR = 500 6 | INVALID_PARAMS = 400 7 | 8 | ERROR_EXIST_TAG = 10001 9 | ERROR_NOT_EXIST_TAG = 10002 10 | ERROR_NOT_EXIST_ARTICLE = 10003 11 | 12 | ERROR_AUTH_CHECK_TOKEN_FAIL = 20001 13 | ERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002 14 | ERROR_AUTH_TOKEN = 20003 15 | ERROR_AUTH = 20004 16 | 17 | PAGE_NOT_FOUND = 40001 18 | ) 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "gin-vue/pkg/setting" 8 | "gin-vue/routers" 9 | ) 10 | 11 | func main() { 12 | router := routers.InitRouter() 13 | 14 | s := &http.Server{ 15 | Addr: fmt.Sprintf(":%d", setting.HTTPPort), 16 | Handler: router, 17 | ReadTimeout: setting.ReadTimeout, 18 | WriteTimeout: setting.WriteTimeout, 19 | MaxHeaderBytes: 1 << 20, 20 | } 21 | 22 | s.ListenAndServe() 23 | } 24 | -------------------------------------------------------------------------------- /vue-admin/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | 13 | /** 14 | * @param {string} str 15 | * @returns {Boolean} 16 | */ 17 | export function validUsername(str) { 18 | const valid_map = ['admin', 'editor'] 19 | return valid_map.indexOf(str.trim()) >= 0 20 | } 21 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gin-vue 2 | 3 | 该项目是gin+vue的前后端分离项目,使用gorm访问MySQL 4 | 5 | 使用jwt,对API接口进行权限控制。[教程](https://www.cnblogs.com/FireworksEasyCool/p/11455834.html) 6 | 7 | 在token过期后的半个小时内,用户再次操作会自动刷新token 8 | 9 | ### go后台程序运行方式 10 | 11 | 1.在MySQL中运行文件夹/docs/sql中的mysql.sql脚本 12 | 13 | 2.在文件夹/conf中修改配置文件api.ini中的数据库连接配置 14 | 15 | 3.在gin-vue目录下运行`go run main.go` 16 | 17 | ### vue运行方式请看文件夹/vue-admin中的README.md 18 | 19 | 喜欢请star 20 | 21 | 推荐一个项目([gin-vue-admin](https://github.com/Bingjian-Zhu/gin-vue-admin)),它是基于这个项目的基础上改进的,用依赖注入的方式对项目进行解耦 22 | -------------------------------------------------------------------------------- /vue-admin/src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /vue-admin/src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /vue-admin/src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getInfo() { 12 | return request({ 13 | url: '/user/info', 14 | method: 'get' 15 | }) 16 | } 17 | 18 | export function refreshToken() { 19 | return request({ 20 | url: '/auth/refresh_token', 21 | method: 'get' 22 | }) 23 | } 24 | 25 | export function logout() { 26 | return request({ 27 | url: '/user/logout', 28 | method: 'post' 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-admin/.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = 'http://localhost:8000' 6 | 7 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, 8 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled. 9 | # It only does one thing by converting all import() to require(). 10 | # This configuration can significantly increase the speed of hot updates, 11 | # when you have a large number of pages. 12 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js 13 | 14 | VUE_CLI_BABEL_TRANSPILE_MODULES = true 15 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-admin/src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /vue-admin/mock/table.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const data = Mock.mock({ 4 | 'items|30': [{ 5 | id: '@id', 6 | title: '@sentence(10, 20)', 7 | 'status|1': ['published', 'draft', 'deleted'], 8 | author: 'name', 9 | display_time: '@datetime', 10 | pageviews: '@integer(300, 5000)' 11 | }] 12 | }) 13 | 14 | export default [ 15 | { 16 | url: '/table/list', 17 | type: 'get', 18 | response: config => { 19 | const items = data.items 20 | return { 21 | code: 20000, 22 | data: { 23 | total: items.length, 24 | items: items 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /vue-admin/src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings 4 | 5 | const state = { 6 | showSettings: showSettings, 7 | fixedHeader: fixedHeader, 8 | sidebarLogo: sidebarLogo 9 | } 10 | 11 | const mutations = { 12 | CHANGE_SETTING: (state, { key, value }) => { 13 | if (state.hasOwnProperty(key)) { 14 | state[key] = value 15 | } 16 | } 17 | } 18 | 19 | const actions = { 20 | changeSetting({ commit }, data) { 21 | commit('CHANGE_SETTING', data) 22 | } 23 | } 24 | 25 | export default { 26 | namespaced: true, 27 | state, 28 | mutations, 29 | actions 30 | } 31 | 32 | -------------------------------------------------------------------------------- /vue-admin/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= webpackConfig.name %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /vue-admin/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'jwt-token' 4 | const TokenExpireKey = 'token-expire' 5 | 6 | export function getToken() { 7 | return Cookies.get(TokenKey) 8 | } 9 | 10 | export function setToken(token) { 11 | return Cookies.set(TokenKey, token) 12 | } 13 | 14 | export function removeToken() { 15 | return Cookies.remove(TokenKey) 16 | } 17 | 18 | export function getTokenExpire() { 19 | return Cookies.get(TokenExpireKey) 20 | } 21 | 22 | export function setTokenExpire(tokenExpire) { 23 | return Cookies.set(TokenExpireKey, tokenExpire) 24 | } 25 | 26 | export function removeTokenExpire() { 27 | return Cookies.remove(TokenExpireKey) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/e/msg.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | var MsgFlags = map[int]string{ 4 | SUCCESS: "ok", 5 | ERROR: "fail", 6 | INVALID_PARAMS: "请求参数错误", 7 | ERROR_EXIST_TAG: "已存在该标签名称", 8 | ERROR_NOT_EXIST_TAG: "该标签不存在", 9 | ERROR_NOT_EXIST_ARTICLE: "该文章不存在", 10 | ERROR_AUTH_CHECK_TOKEN_FAIL: "Token鉴权失败", 11 | ERROR_AUTH_CHECK_TOKEN_TIMEOUT: "Token已超时", 12 | ERROR_AUTH_TOKEN: "Token生成失败", 13 | ERROR_AUTH: "Token错误", 14 | PAGE_NOT_FOUND: "Page not found", 15 | } 16 | 17 | func GetMsg(code int) string { 18 | msg, ok := MsgFlags[code] 19 | if ok { 20 | return msg 21 | } 22 | 23 | return MsgFlags[ERROR] 24 | } 25 | -------------------------------------------------------------------------------- /vue-admin/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText:#bfcbd9; 3 | $menuActiveText:#409EFF; 4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 5 | 6 | $menuBg:#304156; 7 | $menuHover:#263445; 8 | 9 | $subMenuBg:#1f2d3d; 10 | $subMenuHover:#001528; 11 | 12 | $sideBarWidth: 210px; 13 | 14 | // the :export directive is the magic sauce for webpack 15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 16 | :export { 17 | menuText: $menuText; 18 | menuActiveText: $menuActiveText; 19 | subMenuActiveText: $subMenuActiveText; 20 | menuBg: $menuBg; 21 | menuHover: $menuHover; 22 | subMenuBg: $subMenuBg; 23 | subMenuHover: $subMenuHover; 24 | sideBarWidth: $sideBarWidth; 25 | } 26 | -------------------------------------------------------------------------------- /vue-admin/tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /vue-admin/tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /vue-admin/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 37 | -------------------------------------------------------------------------------- /vue-admin/src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /vue-admin/tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, isExternal } from '@/utils/validate.js' 2 | 3 | describe('Utils:validate', () => { 4 | it('validUsername', () => { 5 | expect(validUsername('admin')).toBe(true) 6 | expect(validUsername('editor')).toBe(true) 7 | expect(validUsername('xxxx')).toBe(false) 8 | }) 9 | it('isExternal', () => { 10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) 13 | expect(isExternal('/dashboard')).toBe(false) 14 | expect(isExternal('./dashboard')).toBe(false) 15 | expect(isExternal('dashboard')).toBe(false) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-admin/src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | 19 | // to fixed https://github.com/ElemeFE/element/issues/2461 20 | .el-dialog { 21 | transform: none; 22 | left: 0; 23 | position: relative; 24 | margin: 0 auto; 25 | } 26 | 27 | // refine element ui upload 28 | .upload-container { 29 | .el-upload { 30 | width: 100%; 31 | 32 | .el-upload-dragger { 33 | width: 100%; 34 | height: 200px; 35 | } 36 | } 37 | } 38 | 39 | // dropdown 40 | .el-dropdown-menu { 41 | a { 42 | display: block 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /vue-admin/src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /routers/api/v1/auth.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "net/http" 5 | 6 | jwt "github.com/appleboy/gin-jwt" 7 | "github.com/gin-gonic/gin" 8 | 9 | "gin-vue/models" 10 | "gin-vue/pkg/e" 11 | "gin-vue/viewModels" 12 | ) 13 | 14 | func GetUserInfo(c *gin.Context) { 15 | claims := jwt.ExtractClaims(c) 16 | userName := claims["userName"].(string) 17 | avatar := models.GetUserID(userName) 18 | 19 | code := e.SUCCESS 20 | userRoles := models.GetRoles(userName) 21 | data := viewModels.User{Roles: userRoles, Introduction: "", Avatar: avatar, Name: userName} 22 | 23 | c.JSON(http.StatusOK, gin.H{ 24 | "code": code, 25 | "msg": e.GetMsg(code), 26 | "data": data, 27 | }) 28 | } 29 | 30 | func Logout(c *gin.Context) { 31 | code := e.SUCCESS 32 | c.JSON(http.StatusOK, gin.H{ 33 | "code": code, 34 | "msg": e.GetMsg(code), 35 | "data": "success", 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /vue-admin/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-admin/src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /vue-admin/build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /routers/api/v2/article.go: -------------------------------------------------------------------------------- 1 | package v2 2 | 3 | import ( 4 | "gin-vue/pkg/e" 5 | "gin-vue/pkg/setting" 6 | "gin-vue/pkg/util" 7 | "net/http" 8 | 9 | "github.com/gin-gonic/gin" 10 | 11 | "gin-vue/models" 12 | "gin-vue/viewModels" 13 | "gin-vue/viewModels/emun" 14 | ) 15 | 16 | //获取多个文章 17 | func GetArticles(c *gin.Context) { 18 | maps := make(map[string]interface{}) 19 | code := e.SUCCESS 20 | var viewArticles []viewModels.Article 21 | var viewArticle viewModels.Article 22 | articles := models.GetArticles(util.GetPage(c), setting.PageSize, maps) 23 | for _, articles := range articles { 24 | viewArticle.Id = articles.ID 25 | viewArticle.Author = articles.CreatedBy 26 | viewArticle.DisplayTime = articles.ModifiedOn.String() 27 | viewArticle.Pageviews = 3474 28 | viewArticle.Status = emun.GetArticleStatus(articles.State) 29 | viewArticle.Title = articles.Title 30 | viewArticles = append(viewArticles, viewArticle) 31 | } 32 | c.JSON(http.StatusOK, gin.H{ 33 | "code": code, 34 | "msg": e.GetMsg(code), 35 | "data": viewArticles, 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /models/auth.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Auth struct { 4 | ID int `gorm:"primary_key" json:"id"` 5 | Username string `json:"username"` 6 | Password string `json:"password"` 7 | Avatar string `json:"avatar"` 8 | } 9 | 10 | // User demo 11 | type User struct { 12 | UserName string 13 | UserClaims []Claims 14 | } 15 | 16 | func CheckAuth(username, password string) bool { 17 | var auth Auth 18 | db.Select("id").Where(Auth{Username: username, Password: password}).First(&auth) 19 | if auth.ID > 0 { 20 | return true 21 | } 22 | 23 | return false 24 | } 25 | 26 | func GetUserID(username string) string { 27 | var auth Auth 28 | db.Select("avatar").Where(Auth{Username: username}).First(&auth) 29 | return auth.Avatar 30 | } 31 | 32 | func GetRoles(username string) []string { 33 | var auth Auth 34 | db.Select("id").Where(Auth{Username: username}).First(&auth) 35 | var claims []Claims 36 | db.Select("value").Where(Claims{AuthID: auth.ID}).Find(&claims) 37 | var roles []string 38 | for _, claim := range claims { 39 | roles = append(roles, claim.Value) 40 | } 41 | return roles 42 | } 43 | -------------------------------------------------------------------------------- /vue-admin/tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | it('ten digits timestamp', () => { 9 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 10 | }) 11 | it('new Date', () => { 12 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 13 | }) 14 | it('format', () => { 15 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 16 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 17 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 18 | }) 19 | it('get the day of the week', () => { 20 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 21 | }) 22 | it('get the day of the week', () => { 23 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 24 | }) 25 | it('empty argument', () => { 26 | expect(parseTime()).toBeNull() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /vue-admin/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 4 | 5 | import ElementUI from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | import locale from 'element-ui/lib/locale/lang/en' // lang i18n 8 | 9 | import '@/styles/index.scss' // global css 10 | 11 | import App from './App' 12 | import store from './store' 13 | import router from './router' 14 | 15 | import '@/icons' // icon 16 | import '@/permission' // permission control 17 | 18 | /** 19 | * If you don't want to use mock-server 20 | * you want to use MockJs for mock api 21 | * you can execute: mockXHR() 22 | * 23 | * Currently MockJs will be used in the production environment, 24 | * please remove it before going online! ! ! 25 | */ 26 | import { mockXHR } from '../mock' 27 | if (process.env.NODE_ENV === 'production') { 28 | mockXHR() 29 | } 30 | 31 | // set ElementUI lang to EN 32 | Vue.use(ElementUI, { locale }) 33 | 34 | Vue.config.productionTip = false 35 | 36 | new Vue({ 37 | el: '#app', 38 | router, 39 | store, 40 | render: h => h(App) 41 | }) 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 zhubingjian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vue-admin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present PanJiaChen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/skill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-admin/tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | 3 | describe('Utils:formatTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | const retrofit = 5 * 1000 6 | 7 | it('ten digits timestamp', () => { 8 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 9 | }) 10 | it('test now', () => { 11 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 12 | }) 13 | it('less two minute', () => { 14 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 15 | }) 16 | it('less two hour', () => { 17 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 18 | }) 19 | it('less one day', () => { 20 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 21 | }) 22 | it('more than one day', () => { 23 | expect(formatTime(d)).toBe('7月13日17时54分') 24 | }) 25 | it('format', () => { 26 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 27 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 28 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /vue-admin/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a:focus, 35 | a:active { 36 | outline: none; 37 | } 38 | 39 | a, 40 | a:focus, 41 | a:hover { 42 | cursor: pointer; 43 | color: inherit; 44 | text-decoration: none; 45 | } 46 | 47 | div:focus { 48 | outline: none; 49 | } 50 | 51 | .clearfix { 52 | &:after { 53 | visibility: hidden; 54 | display: block; 55 | font-size: 0; 56 | content: " "; 57 | clear: both; 58 | height: 0; 59 | } 60 | } 61 | 62 | // main-container global css 63 | .app-container { 64 | padding: 20px; 65 | } 66 | -------------------------------------------------------------------------------- /middleware/myjwt/myjwt.go: -------------------------------------------------------------------------------- 1 | package myjwt 2 | 3 | import ( 4 | "gin-vue/models" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | //IAuthorizator 授权规则接口 10 | type IAuthorizator interface { 11 | HandleAuthorizator(data interface{}, c *gin.Context) bool 12 | } 13 | 14 | //AdminAuthorizator 管理员授权规则 15 | type AdminAuthorizator struct { 16 | } 17 | 18 | //HandleAuthorizator 处理管理员授权规则 19 | func (*AdminAuthorizator) HandleAuthorizator(data interface{}, c *gin.Context) bool { 20 | if v, ok := data.(*models.User); ok { 21 | for _, itemClaim := range v.UserClaims { 22 | if itemClaim.Type == "role" && itemClaim.Value == "admin" { 23 | return true 24 | } 25 | } 26 | } 27 | return false 28 | } 29 | 30 | //TestAuthorizator 测试用户授权规则 31 | type TestAuthorizator struct { 32 | } 33 | 34 | //HandleAuthorizator 处理测试用户授权规则 35 | func (*TestAuthorizator) HandleAuthorizator(data interface{}, c *gin.Context) bool { 36 | if v, ok := data.(*models.User); ok && v.UserName == "test" { 37 | return true 38 | } 39 | return false 40 | } 41 | 42 | //AllUserAuthorizator 普通用户授权规则 43 | type AllUserAuthorizator struct { 44 | } 45 | 46 | //HandleAuthorizator 处理普通用户授权规则 47 | func (*AllUserAuthorizator) HandleAuthorizator(data interface{}, c *gin.Context) bool { 48 | return true 49 | } 50 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-admin/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop' 9 | } 10 | 11 | const mutations = { 12 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | Cookies.set('sidebarStatus', 1) 17 | } else { 18 | Cookies.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | } 30 | 31 | const actions = { 32 | toggleSideBar({ commit }) { 33 | commit('TOGGLE_SIDEBAR') 34 | }, 35 | closeSideBar({ commit }, { withoutAnimation }) { 36 | commit('CLOSE_SIDEBAR', withoutAnimation) 37 | }, 38 | toggleDevice({ commit }, device) { 39 | commit('TOGGLE_DEVICE', device) 40 | } 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | mutations, 47 | actions 48 | } 49 | -------------------------------------------------------------------------------- /vue-admin/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /pkg/setting/setting.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/go-ini/ini" 8 | ) 9 | 10 | var ( 11 | Cfg *ini.File 12 | 13 | RunMode string 14 | 15 | HTTPPort int 16 | ReadTimeout time.Duration 17 | WriteTimeout time.Duration 18 | 19 | PageSize int 20 | IdentityKey string 21 | ) 22 | 23 | func init() { 24 | var err error 25 | Cfg, err = ini.Load("conf/app.ini") 26 | if err != nil { 27 | log.Fatalf("Fail to parse 'conf/app.ini': %v", err) 28 | } 29 | 30 | LoadBase() 31 | LoadServer() 32 | LoadApp() 33 | } 34 | 35 | func LoadBase() { 36 | RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug") 37 | } 38 | 39 | func LoadServer() { 40 | sec, err := Cfg.GetSection("server") 41 | if err != nil { 42 | log.Fatalf("Fail to get section 'server': %v", err) 43 | } 44 | 45 | RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug") 46 | 47 | HTTPPort = sec.Key("HTTP_PORT").MustInt(8000) 48 | ReadTimeout = time.Duration(sec.Key("READ_TIMEOUT").MustInt(60)) * time.Second 49 | WriteTimeout = time.Duration(sec.Key("WRITE_TIMEOUT").MustInt(60)) * time.Second 50 | } 51 | 52 | func LoadApp() { 53 | sec, err := Cfg.GetSection("app") 54 | if err != nil { 55 | log.Fatalf("Fail to get section 'app': %v", err) 56 | } 57 | 58 | IdentityKey = sec.Key("IDENTITY_KEY").MustString("!@)*#)!@U#@*!@!)") 59 | PageSize = sec.Key("PAGE_SIZE").MustInt(10) 60 | } 61 | -------------------------------------------------------------------------------- /vue-admin/src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/jinzhu/gorm" 9 | _ "github.com/jinzhu/gorm/dialects/mysql" 10 | 11 | "gin-vue/pkg/setting" 12 | ) 13 | 14 | var db *gorm.DB 15 | 16 | type Model struct { 17 | ID int `gorm:"primary_key" json:"id"` 18 | CreatedOn time.Time `json:"created_on"` 19 | ModifiedOn time.Time `json:"modified_on"` 20 | } 21 | 22 | func init() { 23 | var ( 24 | err error 25 | dbType, dbName, user, password, host, tablePrefix string 26 | ) 27 | 28 | sec, err := setting.Cfg.GetSection("database") 29 | if err != nil { 30 | log.Fatal(2, "Fail to get section 'database': %v", err) 31 | } 32 | 33 | dbType = sec.Key("TYPE").String() 34 | dbName = sec.Key("NAME").String() 35 | user = sec.Key("USER").String() 36 | password = sec.Key("PASSWORD").String() 37 | host = sec.Key("HOST").String() 38 | tablePrefix = sec.Key("TABLE_PREFIX").String() 39 | 40 | db, err = gorm.Open(dbType, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", 41 | user, 42 | password, 43 | host, 44 | dbName)) 45 | 46 | if err != nil { 47 | log.Println(err) 48 | } 49 | 50 | gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string { 51 | return tablePrefix + defaultTableName 52 | } 53 | 54 | db.SingularTable(true) 55 | db.DB().SetMaxIdleConns(10) 56 | db.DB().SetMaxOpenConns(100) 57 | } 58 | 59 | func CloseDB() { 60 | defer db.Close() 61 | } 62 | -------------------------------------------------------------------------------- /vue-admin/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-admin/src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 57 | -------------------------------------------------------------------------------- /models/tag.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | type Tag struct { 10 | Model 11 | 12 | Name string `json:"name"` 13 | CreatedBy string `json:"created_by"` 14 | ModifiedBy string `json:"modified_by"` 15 | State int `json:"state"` 16 | } 17 | 18 | func GetTags(pageNum int, pageSize int, maps interface{}) (tags []Tag) { 19 | db.Where(maps).Offset(pageNum).Limit(pageSize).Find(&tags) 20 | 21 | return 22 | } 23 | 24 | func GetTagTotal(maps interface{}) (count int) { 25 | db.Model(&Tag{}).Where(maps).Count(&count) 26 | 27 | return 28 | } 29 | 30 | func ExistTagByName(name string) bool { 31 | var tag Tag 32 | db.Select("id").Where("name = ?", name).First(&tag) 33 | if tag.ID > 0 { 34 | return true 35 | } 36 | 37 | return false 38 | } 39 | 40 | func AddTag(name string, state int, createdBy string) bool { 41 | db.Create(&Tag{ 42 | Name: name, 43 | State: state, 44 | CreatedBy: createdBy, 45 | }) 46 | 47 | return true 48 | } 49 | 50 | func (tag *Tag) BeforeCreate(scope *gorm.Scope) error { 51 | scope.SetColumn("CreatedOn", time.Now().Unix()) 52 | 53 | return nil 54 | } 55 | 56 | func (tag *Tag) BeforeUpdate(scope *gorm.Scope) error { 57 | scope.SetColumn("ModifiedOn", time.Now().Unix()) 58 | 59 | return nil 60 | } 61 | 62 | func ExistTagByID(id int) bool { 63 | var tag Tag 64 | db.Select("id").Where("id = ?", id).First(&tag) 65 | if tag.ID > 0 { 66 | return true 67 | } 68 | 69 | return false 70 | } 71 | 72 | func DeleteTag(id int) bool { 73 | db.Where("id = ?", id).Delete(&Tag{}) 74 | 75 | return true 76 | } 77 | 78 | func EditTag(id int, data interface{}) bool { 79 | db.Model(&Tag{}).Where("id = ?", id).Updates(data) 80 | 81 | return true 82 | } 83 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-admin/src/views/tree/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 78 | 79 | -------------------------------------------------------------------------------- /vue-admin/mock/user.js: -------------------------------------------------------------------------------- 1 | 2 | const tokens = { 3 | admin: { 4 | token: 'admin-token' 5 | }, 6 | editor: { 7 | token: 'editor-token' 8 | } 9 | } 10 | 11 | const users = { 12 | 'admin-token': { 13 | roles: ['admin'], 14 | introduction: 'I am a super administrator', 15 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 16 | name: 'Super Admin' 17 | }, 18 | 'editor-token': { 19 | roles: ['editor'], 20 | introduction: 'I am an editor', 21 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 22 | name: 'Normal Editor' 23 | } 24 | } 25 | 26 | export default [ 27 | // user login 28 | { 29 | url: '/user/login', 30 | type: 'post', 31 | response: config => { 32 | const { username } = config.body 33 | const token = tokens[username] 34 | 35 | // mock error 36 | if (!token) { 37 | return { 38 | code: 60204, 39 | message: 'Account and password are incorrect.' 40 | } 41 | } 42 | 43 | return { 44 | code: 20000, 45 | data: token 46 | } 47 | } 48 | }, 49 | 50 | // get user info 51 | { 52 | url: '/user/info\.*', 53 | type: 'get', 54 | response: config => { 55 | const { token } = config.query 56 | const info = users[token] 57 | 58 | // mock error 59 | if (!info) { 60 | return { 61 | code: 50008, 62 | message: 'Login failed, unable to get user details.' 63 | } 64 | } 65 | 66 | return { 67 | code: 20000, 68 | data: info 69 | } 70 | } 71 | }, 72 | 73 | // user logout 74 | { 75 | url: '/user/logout', 76 | type: 'post', 77 | response: _ => { 78 | return { 79 | code: 20000, 80 | data: 'success' 81 | } 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /vue-admin/mock/index.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import { param2Obj } from '../src/utils' 3 | 4 | import user from './user' 5 | import table from './table' 6 | 7 | const mocks = [ 8 | ...user, 9 | ...table 10 | ] 11 | 12 | // for front mock 13 | // please use it cautiously, it will redefine XMLHttpRequest, 14 | // which will cause many of your third-party libraries to be invalidated(like progress event). 15 | export function mockXHR() { 16 | // mock patch 17 | // https://github.com/nuysoft/Mock/issues/300 18 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 19 | Mock.XHR.prototype.send = function() { 20 | if (this.custom.xhr) { 21 | this.custom.xhr.withCredentials = this.withCredentials || false 22 | 23 | if (this.responseType) { 24 | this.custom.xhr.responseType = this.responseType 25 | } 26 | } 27 | this.proxy_send(...arguments) 28 | } 29 | 30 | function XHR2ExpressReqWrap(respond) { 31 | return function(options) { 32 | let result = null 33 | if (respond instanceof Function) { 34 | const { body, type, url } = options 35 | // https://expressjs.com/en/4x/api.html#req 36 | result = respond({ 37 | method: type, 38 | body: JSON.parse(body), 39 | query: param2Obj(url) 40 | }) 41 | } else { 42 | result = respond 43 | } 44 | return Mock.mock(result) 45 | } 46 | } 47 | 48 | for (const i of mocks) { 49 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) 50 | } 51 | } 52 | 53 | // for mock server 54 | const responseFake = (url, type, respond) => { 55 | return { 56 | url: new RegExp(`/mock${url}`), 57 | type: type || 'get', 58 | response(req, res) { 59 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) 60 | } 61 | } 62 | } 63 | 64 | export default mocks.map(route => { 65 | return responseFake(route.url, route.type, route.response) 66 | }) 67 | -------------------------------------------------------------------------------- /vue-admin/src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | const whiteList = ['/login'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const hasToken = getToken() 22 | 23 | if (hasToken) { 24 | if (to.path === '/login') { 25 | // if is logged in, redirect to the home page 26 | next({ path: '/' }) 27 | NProgress.done() 28 | } else { 29 | const hasGetUserInfo = store.getters.name 30 | if (hasGetUserInfo) { 31 | next() 32 | } else { 33 | try { 34 | // get user info 35 | await store.dispatch('user/getInfo') 36 | next() 37 | } catch (error) { 38 | // remove token and go to login page to re-login 39 | await store.dispatch('user/resetToken') 40 | Message.error(error || 'Has Error') 41 | next(`/login?redirect=${to.path}`) 42 | NProgress.done() 43 | } 44 | } 45 | } 46 | } else { 47 | /* has no token*/ 48 | 49 | if (whiteList.indexOf(to.path) !== -1) { 50 | // in the free login whitelist, go directly 51 | next() 52 | } else { 53 | // other pages that do not have permission to access are redirected to the login page. 54 | next(`/login?redirect=${to.path}`) 55 | NProgress.done() 56 | } 57 | } 58 | }) 59 | 60 | router.afterEach(() => { 61 | // finish progress bar 62 | NProgress.done() 63 | }) 64 | -------------------------------------------------------------------------------- /vue-admin/src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /vue-admin/mock/mock-server.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const bodyParser = require('body-parser') 3 | const chalk = require('chalk') 4 | const path = require('path') 5 | 6 | const mockDir = path.join(process.cwd(), 'mock') 7 | 8 | function registerRoutes(app) { 9 | let mockLastIndex 10 | const { default: mocks } = require('./index.js') 11 | for (const mock of mocks) { 12 | app[mock.type](mock.url, mock.response) 13 | mockLastIndex = app._router.stack.length 14 | } 15 | const mockRoutesLength = Object.keys(mocks).length 16 | return { 17 | mockRoutesLength: mockRoutesLength, 18 | mockStartIndex: mockLastIndex - mockRoutesLength 19 | } 20 | } 21 | 22 | function unregisterRoutes() { 23 | Object.keys(require.cache).forEach(i => { 24 | if (i.includes(mockDir)) { 25 | delete require.cache[require.resolve(i)] 26 | } 27 | }) 28 | } 29 | 30 | module.exports = app => { 31 | // es6 polyfill 32 | require('@babel/register') 33 | 34 | // parse app.body 35 | // https://expressjs.com/en/4x/api.html#req.body 36 | app.use(bodyParser.json()) 37 | app.use(bodyParser.urlencoded({ 38 | extended: true 39 | })) 40 | 41 | const mockRoutes = registerRoutes(app) 42 | var mockRoutesLength = mockRoutes.mockRoutesLength 43 | var mockStartIndex = mockRoutes.mockStartIndex 44 | 45 | // watch files, hot reload mock server 46 | chokidar.watch(mockDir, { 47 | ignored: /mock-server/, 48 | ignoreInitial: true 49 | }).on('all', (event, path) => { 50 | if (event === 'change' || event === 'add') { 51 | try { 52 | // remove mock routes stack 53 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 54 | 55 | // clear routes cache 56 | unregisterRoutes() 57 | 58 | const mockRoutes = registerRoutes(app) 59 | mockRoutesLength = mockRoutes.mockRoutesLength 60 | mockStartIndex = mockRoutes.mockStartIndex 61 | 62 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) 63 | } catch (error) { 64 | console.log(chalk.redBright(error)) 65 | } 66 | } 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /vue-admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-admin-template", 3 | "version": "4.2.1", 4 | "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", 5 | "author": "Pan ", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "vue-cli-service serve", 9 | "build:prod": "vue-cli-service build", 10 | "build:stage": "vue-cli-service build --mode staging", 11 | "preview": "node build/index.js --preview", 12 | "lint": "eslint --ext .js,.vue src", 13 | "test:unit": "jest --clearCache && vue-cli-service test:unit", 14 | "test:ci": "npm run lint && npm run test:unit", 15 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" 16 | }, 17 | "dependencies": { 18 | "axios": "0.18.1", 19 | "element-ui": "2.7.2", 20 | "js-cookie": "2.2.0", 21 | "normalize.css": "7.0.0", 22 | "nprogress": "0.2.0", 23 | "path-to-regexp": "2.4.0", 24 | "vue": "2.6.10", 25 | "vue-router": "3.0.6", 26 | "vuex": "3.1.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "7.0.0", 30 | "@babel/register": "7.0.0", 31 | "@vue/cli-plugin-babel": "3.6.0", 32 | "@vue/cli-plugin-eslint": "^3.9.1", 33 | "@vue/cli-plugin-unit-jest": "3.6.3", 34 | "@vue/cli-service": "3.6.0", 35 | "@vue/test-utils": "1.0.0-beta.29", 36 | "autoprefixer": "^9.5.1", 37 | "babel-core": "7.0.0-bridge.0", 38 | "babel-eslint": "10.0.1", 39 | "babel-jest": "23.6.0", 40 | "chalk": "2.4.2", 41 | "connect": "3.6.6", 42 | "eslint": "5.15.3", 43 | "eslint-plugin-vue": "5.2.2", 44 | "html-webpack-plugin": "3.2.0", 45 | "mockjs": "1.0.1-beta3", 46 | "node-sass": "^4.9.0", 47 | "runjs": "^4.3.2", 48 | "sass-loader": "^7.1.0", 49 | "script-ext-html-webpack-plugin": "2.1.3", 50 | "script-loader": "0.7.2", 51 | "serve-static": "^1.13.2", 52 | "svg-sprite-loader": "4.1.3", 53 | "svgo": "1.2.2", 54 | "vue-template-compiler": "2.6.10" 55 | }, 56 | "engines": { 57 | "node": ">=8.9", 58 | "npm": ">= 3.0.0" 59 | }, 60 | "browserslist": [ 61 | "> 1%", 62 | "last 2 versions" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /models/article.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | type Article struct { 10 | Model 11 | 12 | State int `json:"state"` 13 | TagId int `json:"tag_id"` 14 | Title string `json:"title"` 15 | Desc string `json:"desc"` 16 | Content string `json:"Content"` 17 | CoverImageUrl string `json:"cover_image_url"` 18 | CreatedBy string `json:"created_by"` 19 | Tag Tag `json:"tag"` 20 | } 21 | 22 | func (tag *Article) BeforeCreate(scope *gorm.Scope) error { 23 | scope.SetColumn("CreatedOn", time.Now().Unix()) 24 | 25 | return nil 26 | } 27 | 28 | func (tag *Article) BeforeUpdate(scope *gorm.Scope) error { 29 | scope.SetColumn("ModifiedOn", time.Now().Unix()) 30 | 31 | return nil 32 | } 33 | 34 | func GetArticle(id int) (article Article) { 35 | db.Where("id = ?", id).First(&article) 36 | db.Where("id = ?", article.TagId).First(&article.Tag) 37 | //db.Model(&article).Related(&article.Tag) 38 | return 39 | } 40 | 41 | func GetArticles(PageNum int, PageSize int, maps interface{}) (article []Article) { 42 | db.Where(maps).Offset(PageNum).Limit(PageSize).Find(&article) 43 | return 44 | } 45 | 46 | func AddArticle(data map[string]interface{}) bool { 47 | db.Create(&Article{ 48 | TagId: data["tag_id"].(int), 49 | Title: data["title"].(string), 50 | Desc: data["desc"].(string), 51 | Content: data["content"].(string), 52 | CreatedBy: data["created_by"].(string), 53 | State: data["state"].(int), 54 | }) 55 | 56 | return true 57 | } 58 | 59 | func EditArticle(id int, maps interface{}) bool { 60 | db.Model(&Article{}).Where("id = ?", id).Update(maps) 61 | return true 62 | } 63 | 64 | func DeleteArticle(id int) bool { 65 | db.Where("id = ?", id).Delete(&Article{}) 66 | return true 67 | } 68 | 69 | func ExistArticleByID(id int) bool { 70 | var article Article 71 | db.Select("id").Where("id = ?", id).First(&article) 72 | if article.ID > 0 { 73 | return true 74 | } 75 | return false 76 | } 77 | 78 | func GetArticleTotal(maps interface{}) (count int) { 79 | db.Model(&Article{}).Where(maps).Count(&count) 80 | return 81 | } 82 | -------------------------------------------------------------------------------- /routers/router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "gin-vue/middleware/cors" 7 | "gin-vue/middleware/myjwt" 8 | "gin-vue/pkg/e" 9 | "gin-vue/pkg/setting" 10 | v1 "gin-vue/routers/api/v1" 11 | v2 "gin-vue/routers/api/v2" 12 | ) 13 | 14 | func InitRouter() *gin.Engine { 15 | r := gin.New() 16 | r.Use(gin.Logger()) 17 | r.Use(cors.CorsHandler()) 18 | r.Use(gin.Recovery()) 19 | gin.SetMode(setting.RunMode) 20 | var authMiddleware = myjwt.GinJWTMiddlewareInit(&myjwt.AllUserAuthorizator{}) 21 | r.POST("/login", authMiddleware.LoginHandler) 22 | //404 handler 23 | r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) { 24 | code := e.PAGE_NOT_FOUND 25 | c.JSON(404, gin.H{"code": code, "message": e.GetMsg(code)}) 26 | }) 27 | auth := r.Group("/auth") 28 | { 29 | // Refresh time can be longer than token timeout 30 | auth.GET("/refresh_token", authMiddleware.RefreshHandler) 31 | } 32 | 33 | api := r.Group("/user") 34 | api.Use(authMiddleware.MiddlewareFunc()) 35 | { 36 | api.GET("/info", v1.GetUserInfo) 37 | api.POST("/logout", v1.Logout) 38 | } 39 | 40 | var adminMiddleware = myjwt.GinJWTMiddlewareInit(&myjwt.AdminAuthorizator{}) 41 | apiv1 := r.Group("/api/v1") 42 | //使用AdminAuthorizator中间件,只有admin权限的用户才能获取到接口 43 | apiv1.Use(adminMiddleware.MiddlewareFunc()) 44 | { 45 | //vue获取table信息 46 | apiv1.GET("/table/list", v2.GetArticles) 47 | //获取标签列表 48 | apiv1.GET("/tags", v1.GetTags) 49 | //新建标签 50 | apiv1.POST("/tags", v1.AddTag) 51 | //更新指定标签 52 | apiv1.PUT("/tags/:id", v1.EditTag) 53 | //删除指定标签 54 | apiv1.DELETE("/tags/:id", v1.DeleteTag) 55 | 56 | //获取文章列表 57 | apiv1.GET("/articles", v1.GetArticles) 58 | //获取指定文章 59 | apiv1.GET("/articles/:id", v1.GetArticle) 60 | //新建文章 61 | apiv1.POST("/articles", v1.AddArticle) 62 | //更新指定文章 63 | apiv1.PUT("/articles/:id", v1.EditArticle) 64 | //删除指定文章 65 | apiv1.DELETE("/articles/:id", v1.DeleteArticle) 66 | } 67 | 68 | var testMiddleware = myjwt.GinJWTMiddlewareInit(&myjwt.TestAuthorizator{}) 69 | apiv2 := r.Group("/api/v2") 70 | apiv2.Use(testMiddleware.MiddlewareFunc()) 71 | { 72 | //获取文章列表 73 | apiv2.GET("/articles", v2.GetArticles) 74 | } 75 | 76 | return r 77 | } 78 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-admin/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 52 | 53 | 94 | -------------------------------------------------------------------------------- /vue-admin/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /vue-admin/src/views/table/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 80 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /middleware/cors/cors.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func CorsHandler() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | method := c.Request.Method //请求方法 14 | origin := c.Request.Header.Get("Origin") //请求头部 15 | var headerKeys []string // 声明请求头keys 16 | for k, _ := range c.Request.Header { 17 | headerKeys = append(headerKeys, k) 18 | } 19 | headerStr := strings.Join(headerKeys, ", ") 20 | if headerStr != "" { 21 | headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr) 22 | } else { 23 | headerStr = "access-control-allow-origin, access-control-allow-headers" 24 | } 25 | if origin != "" { 26 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 27 | c.Header("Access-Control-Allow-Origin", "*") // 这是允许访问所有域 28 | c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") //服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求 29 | // header的类型 30 | c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma") 31 | // 允许跨域设置 可以返回其他子段 32 | c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域关键设置 让浏览器可以解析 33 | c.Header("Access-Control-Max-Age", "172800") // 缓存请求信息 单位为秒 34 | c.Header("Access-Control-Allow-Credentials", "false") // 跨域请求是否需要带cookie信息 默认设置为true 35 | c.Set("content-type", "application/json") // 设置返回格式是json 36 | } 37 | 38 | //放行所有OPTIONS方法 39 | if method == "OPTIONS" { 40 | c.JSON(http.StatusOK, "Options Request!") 41 | } 42 | // 处理请求 43 | c.Next() // 处理请求 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /vue-admin/src/views/403.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 60 | 61 | 100 | -------------------------------------------------------------------------------- /vue-admin/src/views/form/index.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 79 | 80 | 85 | 86 | -------------------------------------------------------------------------------- /vue-admin/src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * Parse the time to string 7 | * @param {(Object|string|number)} time 8 | * @param {string} cFormat 9 | * @returns {string} 10 | */ 11 | export function parseTime(time, cFormat) { 12 | if (arguments.length === 0) { 13 | return null 14 | } 15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 16 | let date 17 | if (typeof time === 'object') { 18 | date = time 19 | } else { 20 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { 21 | time = parseInt(time) 22 | } 23 | if ((typeof time === 'number') && (time.toString().length === 10)) { 24 | time = time * 1000 25 | } 26 | date = new Date(time) 27 | } 28 | const formatObj = { 29 | y: date.getFullYear(), 30 | m: date.getMonth() + 1, 31 | d: date.getDate(), 32 | h: date.getHours(), 33 | i: date.getMinutes(), 34 | s: date.getSeconds(), 35 | a: date.getDay() 36 | } 37 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 38 | let value = formatObj[key] 39 | // Note: getDay() returns 0 on Sunday 40 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 41 | if (result.length > 0 && value < 10) { 42 | value = '0' + value 43 | } 44 | return value || 0 45 | }) 46 | return time_str 47 | } 48 | 49 | /** 50 | * @param {number} time 51 | * @param {string} option 52 | * @returns {string} 53 | */ 54 | export function formatTime(time, option) { 55 | if (('' + time).length === 10) { 56 | time = parseInt(time) * 1000 57 | } else { 58 | time = +time 59 | } 60 | const d = new Date(time) 61 | const now = Date.now() 62 | 63 | const diff = (now - d) / 1000 64 | 65 | if (diff < 30) { 66 | return '刚刚' 67 | } else if (diff < 3600) { 68 | // less 1 hour 69 | return Math.ceil(diff / 60) + '分钟前' 70 | } else if (diff < 3600 * 24) { 71 | return Math.ceil(diff / 3600) + '小时前' 72 | } else if (diff < 3600 * 24 * 2) { 73 | return '1天前' 74 | } 75 | if (option) { 76 | return parseTime(time, option) 77 | } else { 78 | return ( 79 | d.getMonth() + 80 | 1 + 81 | '月' + 82 | d.getDate() + 83 | '日' + 84 | d.getHours() + 85 | '时' + 86 | d.getMinutes() + 87 | '分' 88 | ) 89 | } 90 | } 91 | 92 | /** 93 | * @param {string} url 94 | * @returns {Object} 95 | */ 96 | export function param2Obj(url) { 97 | const search = url.split('?')[1] 98 | if (!search) { 99 | return {} 100 | } 101 | return JSON.parse( 102 | '{"' + 103 | decodeURIComponent(search) 104 | .replace(/"/g, '\\"') 105 | .replace(/&/g, '","') 106 | .replace(/=/g, '":"') 107 | .replace(/\+/g, ' ') + 108 | '"}' 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /vue-admin/src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /vue-admin/tests/unit/components/Breadcrumb.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, createLocalVue } from '@vue/test-utils' 2 | import VueRouter from 'vue-router' 3 | import ElementUI from 'element-ui' 4 | import Breadcrumb from '@/components/Breadcrumb/index.vue' 5 | 6 | const localVue = createLocalVue() 7 | localVue.use(VueRouter) 8 | localVue.use(ElementUI) 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | children: [{ 15 | path: 'dashboard', 16 | name: 'dashboard' 17 | }] 18 | }, 19 | { 20 | path: '/menu', 21 | name: 'menu', 22 | children: [{ 23 | path: 'menu1', 24 | name: 'menu1', 25 | meta: { title: 'menu1' }, 26 | children: [{ 27 | path: 'menu1-1', 28 | name: 'menu1-1', 29 | meta: { title: 'menu1-1' } 30 | }, 31 | { 32 | path: 'menu1-2', 33 | name: 'menu1-2', 34 | redirect: 'noredirect', 35 | meta: { title: 'menu1-2' }, 36 | children: [{ 37 | path: 'menu1-2-1', 38 | name: 'menu1-2-1', 39 | meta: { title: 'menu1-2-1' } 40 | }, 41 | { 42 | path: 'menu1-2-2', 43 | name: 'menu1-2-2' 44 | }] 45 | }] 46 | }] 47 | }] 48 | 49 | const router = new VueRouter({ 50 | routes 51 | }) 52 | 53 | describe('Breadcrumb.vue', () => { 54 | const wrapper = mount(Breadcrumb, { 55 | localVue, 56 | router 57 | }) 58 | it('dashboard', () => { 59 | router.push('/dashboard') 60 | const len = wrapper.findAll('.el-breadcrumb__inner').length 61 | expect(len).toBe(1) 62 | }) 63 | it('normal route', () => { 64 | router.push('/menu/menu1') 65 | const len = wrapper.findAll('.el-breadcrumb__inner').length 66 | expect(len).toBe(2) 67 | }) 68 | it('nested route', () => { 69 | router.push('/menu/menu1/menu1-2/menu1-2-1') 70 | const len = wrapper.findAll('.el-breadcrumb__inner').length 71 | expect(len).toBe(4) 72 | }) 73 | it('no meta.title', () => { 74 | router.push('/menu/menu1/menu1-2/menu1-2-2') 75 | const len = wrapper.findAll('.el-breadcrumb__inner').length 76 | expect(len).toBe(3) 77 | }) 78 | // it('click link', () => { 79 | // router.push('/menu/menu1/menu1-2/menu1-2-2') 80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 81 | // const second = breadcrumbArray.at(1) 82 | // console.log(breadcrumbArray) 83 | // const href = second.find('a').attributes().href 84 | // expect(href).toBe('#/menu/menu1') 85 | // }) 86 | // it('noRedirect', () => { 87 | // router.push('/menu/menu1/menu1-2/menu1-2-1') 88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 89 | // const redirectBreadcrumb = breadcrumbArray.at(2) 90 | // expect(redirectBreadcrumb.contains('a')).toBe(false) 91 | // }) 92 | it('last breadcrumb', () => { 93 | router.push('/menu/menu1/menu1-2/menu1-2-1') 94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 95 | const redirectBreadcrumb = breadcrumbArray.at(3) 96 | expect(redirectBreadcrumb.contains('a')).toBe(false) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /vue-admin/src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { login, logout, getInfo, refreshToken } from '@/api/user' 2 | import { getToken, setToken, removeToken, getTokenExpire, setTokenExpire, removeTokenExpire } from '@/utils/auth' 3 | import { resetRouter } from '@/router' 4 | 5 | const state = { 6 | token: getToken(), 7 | name: '', 8 | avatar: '', 9 | tokenExpire: getTokenExpire() 10 | } 11 | 12 | const mutations = { 13 | SET_TOKEN: (state, token) => { 14 | state.token = token 15 | }, 16 | SET_NAME: (state, name) => { 17 | state.name = name 18 | }, 19 | SET_AVATAR: (state, avatar) => { 20 | state.avatar = avatar 21 | }, 22 | SET_TOKENEXPIRE: (state, token) => { 23 | state.tokenExpire = token 24 | } 25 | } 26 | 27 | const actions = { 28 | // user login 29 | login({ commit }, userInfo) { 30 | const { username, password } = userInfo 31 | return new Promise((resolve, reject) => { 32 | login({ username: username.trim(), password: password }).then(response => { 33 | const data = response 34 | commit('SET_TOKEN', data.token) 35 | commit('SET_TOKENEXPIRE', data.expire) 36 | setToken(data.token) 37 | setTokenExpire(data.expire) 38 | resolve() 39 | }).catch(error => { 40 | reject(error) 41 | }) 42 | }) 43 | }, 44 | 45 | refreshToken({ commit }) { 46 | return new Promise((resolve, reject) => { 47 | refreshToken().then(response => { 48 | const data = response 49 | commit('SET_TOKEN', data.token) 50 | commit('SET_TOKENEXPIRE', data.expire) 51 | setToken(data.token) 52 | setTokenExpire(data.expire) 53 | resolve() 54 | }).catch(error => { 55 | reject(error) 56 | }) 57 | }) 58 | }, 59 | 60 | // get user info 61 | getInfo({ commit }) { 62 | return new Promise((resolve, reject) => { 63 | getInfo().then(response => { 64 | const data = response.data 65 | if (!data) { 66 | reject('Verification failed, please Login again.') 67 | } 68 | const { Name, Avatar } = data 69 | commit('SET_NAME', Name) 70 | commit('SET_AVATAR', Avatar) 71 | resolve(data) 72 | }).catch(error => { 73 | reject(error) 74 | }) 75 | }) 76 | }, 77 | 78 | // user logout 79 | logout({ commit, state }) { 80 | return new Promise((resolve, reject) => { 81 | logout(state.token).then(() => { 82 | commit('SET_TOKEN', '') 83 | commit('SET_TOKENEXPIRE', '') 84 | removeToken() 85 | removeTokenExpire() 86 | resetRouter() 87 | resolve() 88 | }).catch(error => { 89 | reject(error) 90 | }) 91 | }) 92 | }, 93 | 94 | // remove token 95 | resetToken({ commit }) { 96 | return new Promise(resolve => { 97 | commit('SET_TOKEN', '') 98 | commit('SET_TOKENEXPIRE', '') 99 | removeToken() 100 | removeTokenExpire() 101 | resolve() 102 | }) 103 | } 104 | } 105 | 106 | export default { 107 | namespaced: true, 108 | state, 109 | mutations, 110 | actions 111 | } 112 | 113 | -------------------------------------------------------------------------------- /middleware/myjwt/gin_jwt.go: -------------------------------------------------------------------------------- 1 | package myjwt 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "time" 7 | 8 | jwt "github.com/appleboy/gin-jwt/v2" 9 | "github.com/gin-gonic/gin" 10 | 11 | "gin-vue/models" 12 | "gin-vue/pkg/setting" 13 | ) 14 | 15 | var identityKey = setting.IdentityKey 16 | 17 | func GinJWTMiddlewareInit(jwtAuthorizator IAuthorizator) (authMiddleware *jwt.GinJWTMiddleware) { 18 | authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{ 19 | Realm: "test zone", 20 | Key: []byte("secret key"), 21 | Timeout: time.Minute * 5, 22 | MaxRefresh: time.Hour, 23 | IdentityKey: identityKey, 24 | PayloadFunc: func(data interface{}) jwt.MapClaims { 25 | if v, ok := data.(*models.User); ok { 26 | //get claims from username 27 | v.UserClaims = models.GetUserClaims(v.UserName) 28 | jsonClaim, _ := json.Marshal(v.UserClaims) 29 | //maps the claims in the JWT 30 | return jwt.MapClaims{ 31 | "userName": v.UserName, 32 | "userClaims": string(jsonClaim), 33 | } 34 | } 35 | return jwt.MapClaims{} 36 | }, 37 | IdentityHandler: func(c *gin.Context) interface{} { 38 | claims := jwt.ExtractClaims(c) 39 | //extracts identity from claims 40 | jsonClaim := claims["userClaims"].(string) 41 | var userClaims []models.Claims 42 | json.Unmarshal([]byte(jsonClaim), &userClaims) 43 | //Set the identity 44 | return &models.User{ 45 | UserName: claims["userName"].(string), 46 | UserClaims: userClaims, 47 | } 48 | }, 49 | Authenticator: func(c *gin.Context) (interface{}, error) { 50 | //handles the login logic. On success LoginResponse is called, on failure Unauthorized is called 51 | var loginVals models.Auth 52 | if err := c.ShouldBind(&loginVals); err != nil { 53 | return "", jwt.ErrMissingLoginValues 54 | } 55 | userID := loginVals.Username 56 | password := loginVals.Password 57 | 58 | if models.CheckAuth(userID, password) { 59 | return &models.User{ 60 | UserName: userID, 61 | }, nil 62 | } 63 | 64 | return nil, jwt.ErrFailedAuthentication 65 | }, 66 | //receives identity and handles authorization logic 67 | Authorizator: jwtAuthorizator.HandleAuthorizator, 68 | //handles unauthorized logic 69 | Unauthorized: func(c *gin.Context, code int, message string) { 70 | c.JSON(code, gin.H{ 71 | "code": code, 72 | "message": message, 73 | }) 74 | }, 75 | // TokenLookup is a string in the form of ":" that is used 76 | // to extract token from the request. 77 | // Optional. Default value "header:Authorization". 78 | // Possible values: 79 | // - "header:" 80 | // - "query:" 81 | // - "cookie:" 82 | // - "param:" 83 | TokenLookup: "header: Authorization, query: token, cookie: jwt", 84 | // TokenLookup: "query:token", 85 | // TokenLookup: "cookie:token", 86 | 87 | // TokenHeadName is a string in the header. Default value is "Bearer" 88 | TokenHeadName: "Bearer", 89 | 90 | // TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens. 91 | TimeFunc: time.Now, 92 | }) 93 | 94 | if err != nil { 95 | log.Fatal("JWT Error:" + err.Error()) 96 | } 97 | return 98 | } 99 | -------------------------------------------------------------------------------- /vue-admin/README.md: -------------------------------------------------------------------------------- 1 | # vue-admin-template 2 | 3 | English | [简体中文](./README-zh.md) 4 | 5 | > A minimal vue admin template with Element UI & axios & iconfont & permission control & lint 6 | 7 | **Live demo:** http://panjiachen.github.io/vue-admin-template 8 | 9 | 10 | **The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`** 11 | 12 | ## Build Setup 13 | 14 | 15 | ```bash 16 | # clone the project 17 | git clone https://github.com/PanJiaChen/vue-admin-template.git 18 | 19 | # enter the project directory 20 | cd vue-admin-template 21 | 22 | # install dependency 23 | npm install 24 | 25 | # develop 26 | npm run dev 27 | ``` 28 | 29 | This will automatically open http://localhost:9528 30 | 31 | ## Build 32 | 33 | ```bash 34 | # build for test environment 35 | npm run build:stage 36 | 37 | # build for production environment 38 | npm run build:prod 39 | ``` 40 | 41 | ## Advanced 42 | 43 | ```bash 44 | # preview the release environment effect 45 | npm run preview 46 | 47 | # preview the release environment effect + static resource analysis 48 | npm run preview -- --report 49 | 50 | # code format check 51 | npm run lint 52 | 53 | # code format check and auto fix 54 | npm run lint -- --fix 55 | ``` 56 | 57 | Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information 58 | 59 | ## Demo 60 | 61 | ![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) 62 | 63 | ## Extra 64 | 65 | If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) 66 | 67 | For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour)) 68 | 69 | ## Related Project 70 | 71 | [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 72 | 73 | [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) 74 | 75 | [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) 76 | 77 | ## Browsers support 78 | 79 | Modern browsers and Internet Explorer 10+. 80 | 81 | | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | 82 | | --------- | --------- | --------- | --------- | 83 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions 84 | 85 | ## License 86 | 87 | [MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. 88 | 89 | Copyright (c) 2017-present PanJiaChen 90 | -------------------------------------------------------------------------------- /vue-admin/README-zh.md: -------------------------------------------------------------------------------- 1 | # vue-admin-template 2 | 3 | > 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。 4 | 5 | [线上地址](http://panjiachen.github.io/vue-admin-template) 6 | 7 | [国内访问](https://panjiachen.gitee.io/vue-admin-template) 8 | 9 | 目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。 10 | 11 | ## Extra 12 | 13 | 如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) 14 | 15 | ## 相关项目 16 | 17 | [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 18 | 19 | [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) 20 | 21 | [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) 22 | 23 | 写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目: 24 | 25 | - [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2) 26 | - [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac) 27 | - [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35) 28 | - [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56) 29 | - [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836) 30 | 31 | ## Build Setup 32 | 33 | ```bash 34 | # 克隆项目 35 | git clone https://github.com/PanJiaChen/vue-admin-template.git 36 | 37 | # 进入项目目录 38 | cd vue-admin-template 39 | 40 | # 安装依赖 41 | npm install 42 | 43 | # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 44 | npm install --registry=https://registry.npm.taobao.org 45 | 46 | # 启动服务 47 | npm run dev 48 | ``` 49 | 50 | 浏览器访问 [http://localhost:9528](http://localhost:9528) 51 | 52 | ## 发布 53 | 54 | ```bash 55 | # 构建测试环境 56 | npm run build:stage 57 | 58 | # 构建生产环境 59 | npm run build:prod 60 | ``` 61 | 62 | ## 其它 63 | 64 | ```bash 65 | # 预览发布环境效果 66 | npm run preview 67 | 68 | # 预览发布环境效果 + 静态资源分析 69 | npm run preview -- --report 70 | 71 | # 代码格式检查 72 | npm run lint 73 | 74 | # 代码格式检查并自动修复 75 | npm run lint -- --fix 76 | ``` 77 | 78 | 更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/) 79 | 80 | ## Demo 81 | 82 | ![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) 83 | 84 | ## Browsers support 85 | 86 | Modern browsers and Internet Explorer 10+. 87 | 88 | | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | 89 | | --------- | --------- | --------- | --------- | 90 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions 91 | 92 | ## License 93 | 94 | [MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. 95 | 96 | Copyright (c) 2017-present PanJiaChen 97 | -------------------------------------------------------------------------------- /vue-admin/src/icons/svg/qq.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-admin/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import router from '@/router' 5 | import { getToken, getTokenExpire } from '@/utils/auth' 6 | 7 | // create an axios instance 8 | const service = axios.create({ 9 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 10 | // withCredentials: true, // send cookies when cross-domain requests 11 | timeout: 5000 // request timeout 12 | }) 13 | 14 | // request interceptor 15 | service.interceptors.request.use( 16 | config => { 17 | // do something before request is sent 18 | 19 | if (store.getters.token) { 20 | // let each request carry token 21 | // ['X-Token'] is a custom headers key 22 | // please modify it according to the actual situation 23 | // config.headers['X-Token'] = getToken() 24 | config.headers['Authorization'] = 'Bearer ' + getToken() 25 | } 26 | return config 27 | }, 28 | error => { 29 | // do something with request error 30 | console.log(error) // for debug 31 | return Promise.reject(error) 32 | } 33 | ) 34 | 35 | // response interceptor 36 | service.interceptors.response.use( 37 | /** 38 | * If you want to get http information such as headers or status 39 | * Please return response => response 40 | */ 41 | 42 | /** 43 | * Determine the request status by custom code 44 | * Here is just an example 45 | * You can also judge the status by HTTP Status Code 46 | */ 47 | response => { 48 | const res = response.data 49 | // if the custom code is not 200, it is judged as an error. 50 | if (res.code !== 200) { 51 | Message({ 52 | message: res.message || 'Error', 53 | type: 'error', 54 | duration: 5 * 1000 55 | }) 56 | 57 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 58 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 59 | // to re-login 60 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { 61 | confirmButtonText: 'Re-Login', 62 | cancelButtonText: 'Cancel', 63 | type: 'warning' 64 | }).then(() => { 65 | store.dispatch('user/resetToken').then(() => { 66 | location.reload() 67 | }) 68 | }) 69 | } 70 | return Promise.reject(new Error(res.message || 'Error')) 71 | } else { 72 | return res 73 | } 74 | }, 75 | error => { 76 | // if code is 401, refresh the token 77 | if (error.response && error.response.status === 401) { 78 | var curTime = new Date() 79 | var tokenExpire = new Date(getTokenExpire()) 80 | var allowTime = new Date(curTime.setMinutes(curTime.getMinutes() + 30)) 81 | // if token expire time small than current time add 30 minute, allow to refresh the token 82 | if (tokenExpire < allowTime) { 83 | store.dispatch('user/refreshToken').then(() => { 84 | location.reload() 85 | }) 86 | } else { 87 | store.dispatch('user/resetToken').then(() => { 88 | location.reload() 89 | }) 90 | } 91 | } else if (error.response && error.response.status === 403) { 92 | router.push({ path: '/403' }) 93 | } else { 94 | console.log('err' + error) // for debug 95 | Message({ 96 | message: error.message, 97 | type: 'error', 98 | duration: 5 * 1000 99 | }) 100 | return Promise.reject(error) 101 | } 102 | } 103 | ) 104 | 105 | export default service 106 | -------------------------------------------------------------------------------- /routers/api/v1/tag.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "gin-vue/models" 5 | "gin-vue/pkg/e" 6 | "gin-vue/pkg/setting" 7 | "gin-vue/pkg/util" 8 | "net/http" 9 | 10 | "github.com/astaxie/beego/validation" 11 | "github.com/gin-gonic/gin" 12 | "github.com/unknwon/com" 13 | ) 14 | 15 | //获取多个文章标签 16 | func GetTags(c *gin.Context) { 17 | name := c.Query("name") 18 | maps := make(map[string]interface{}) 19 | data := make(map[string]interface{}) 20 | if name != "" { 21 | maps["name"] = name 22 | } 23 | 24 | var state int = -1 25 | if arg := c.Query("state"); arg != "" { 26 | state = com.StrTo(arg).MustInt() 27 | maps["state"] = state 28 | } 29 | code := e.SUCCESS 30 | 31 | data["lists"] = models.GetTags(util.GetPage(c), setting.PageSize, maps) 32 | data["total"] = models.GetTagTotal(maps) 33 | 34 | c.JSON(http.StatusOK, gin.H{ 35 | "code": code, 36 | "msg": e.GetMsg(code), 37 | "data": data, 38 | }) 39 | 40 | } 41 | 42 | //新增文章标签 43 | func AddTag(c *gin.Context) { 44 | name := c.Query("name") 45 | state := com.StrTo(c.DefaultQuery("state", "0")).MustInt() 46 | createdBy := c.Query("created_by") 47 | 48 | valid := validation.Validation{} 49 | valid.Required(name, "name").Message("名称不能为空") 50 | valid.MaxSize(name, 100, "name").Message("名称最长为100字符") 51 | valid.Required(createdBy, "created_by").Message("创建人不能为空") 52 | valid.MaxSize(createdBy, 100, "created_by").Message("创建人最长为100字符") 53 | valid.Range(state, 0, 1, "state").Message("状态只允许0或1") 54 | 55 | code := e.INVALID_PARAMS 56 | if !valid.HasErrors() { 57 | if !models.ExistTagByName(name) { 58 | code = e.SUCCESS 59 | models.AddTag(name, state, createdBy) 60 | } else { 61 | code = e.ERROR_EXIST_TAG 62 | } 63 | } 64 | 65 | c.JSON(http.StatusOK, gin.H{ 66 | "code": code, 67 | "msg": e.GetMsg(code), 68 | "data": make(map[string]string), 69 | }) 70 | 71 | } 72 | 73 | //修改文章标签 74 | func EditTag(c *gin.Context) { 75 | id := com.StrTo(c.Param("id")).MustInt() 76 | name := c.Query("name") 77 | modifiedBy := c.Query("modified_by") 78 | 79 | valid := validation.Validation{} 80 | 81 | var state int = -1 82 | if arg := c.Query("state"); arg != "" { 83 | state = com.StrTo(arg).MustInt() 84 | valid.Range(state, 0, 1, "state").Message("状态只允许0或1") 85 | } 86 | 87 | valid.Required(id, "id").Message("ID不能为空") 88 | valid.Required(modifiedBy, "modified_by").Message("修改人不能为空") 89 | valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符") 90 | valid.MaxSize(name, 100, "name").Message("名称最长为100字符") 91 | 92 | code := e.INVALID_PARAMS 93 | if !valid.HasErrors() { 94 | code = e.SUCCESS 95 | if models.ExistTagByID(id) { 96 | data := make(map[string]interface{}) 97 | data["modified_by"] = modifiedBy 98 | if name != "" { 99 | data["name"] = name 100 | } 101 | if state != -1 { 102 | data["state"] = state 103 | } 104 | 105 | models.EditTag(id, data) 106 | } else { 107 | code = e.ERROR_NOT_EXIST_TAG 108 | } 109 | } 110 | 111 | c.JSON(http.StatusOK, gin.H{ 112 | "code": code, 113 | "msg": e.GetMsg(code), 114 | "data": make(map[string]string), 115 | }) 116 | } 117 | 118 | //删除文章标签 119 | func DeleteTag(c *gin.Context) { 120 | id := com.StrTo(c.Param("id")).MustInt() 121 | 122 | valid := validation.Validation{} 123 | valid.Min(id, 1, "id").Message("ID必须大于0") 124 | 125 | code := e.INVALID_PARAMS 126 | if !valid.HasErrors() { 127 | code = e.SUCCESS 128 | if models.ExistTagByID(id) { 129 | models.DeleteTag(id) 130 | } else { 131 | code = e.ERROR_NOT_EXIST_TAG 132 | } 133 | } 134 | 135 | c.JSON(http.StatusOK, gin.H{ 136 | "code": code, 137 | "msg": e.GetMsg(code), 138 | "data": make(map[string]string), 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /vue-admin/src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 61 | 62 | 140 | -------------------------------------------------------------------------------- /vue-admin/src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | 3 | .main-container { 4 | min-height: 100%; 5 | transition: margin-left .28s; 6 | margin-left: $sideBarWidth; 7 | position: relative; 8 | } 9 | 10 | .sidebar-container { 11 | transition: width 0.28s; 12 | width: $sideBarWidth !important; 13 | background-color: $menuBg; 14 | height: 100%; 15 | position: fixed; 16 | font-size: 0px; 17 | top: 0; 18 | bottom: 0; 19 | left: 0; 20 | z-index: 1001; 21 | overflow: hidden; 22 | 23 | // reset element-ui css 24 | .horizontal-collapse-transition { 25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 26 | } 27 | 28 | .scrollbar-wrapper { 29 | overflow-x: hidden !important; 30 | } 31 | 32 | .el-scrollbar__bar.is-vertical { 33 | right: 0px; 34 | } 35 | 36 | .el-scrollbar { 37 | height: 100%; 38 | } 39 | 40 | &.has-logo { 41 | .el-scrollbar { 42 | height: calc(100% - 50px); 43 | } 44 | } 45 | 46 | .is-horizontal { 47 | display: none; 48 | } 49 | 50 | a { 51 | display: inline-block; 52 | width: 100%; 53 | overflow: hidden; 54 | } 55 | 56 | .svg-icon { 57 | margin-right: 16px; 58 | } 59 | 60 | .el-menu { 61 | border: none; 62 | height: 100%; 63 | width: 100% !important; 64 | } 65 | 66 | // menu hover 67 | .submenu-title-noDropdown, 68 | .el-submenu__title { 69 | &:hover { 70 | background-color: $menuHover !important; 71 | } 72 | } 73 | 74 | .is-active>.el-submenu__title { 75 | color: $subMenuActiveText !important; 76 | } 77 | 78 | & .nest-menu .el-submenu>.el-submenu__title, 79 | & .el-submenu .el-menu-item { 80 | min-width: $sideBarWidth !important; 81 | background-color: $subMenuBg !important; 82 | 83 | &:hover { 84 | background-color: $subMenuHover !important; 85 | } 86 | } 87 | } 88 | 89 | .hideSidebar { 90 | .sidebar-container { 91 | width: 54px !important; 92 | } 93 | 94 | .main-container { 95 | margin-left: 54px; 96 | } 97 | 98 | .submenu-title-noDropdown { 99 | padding: 0 !important; 100 | position: relative; 101 | 102 | .el-tooltip { 103 | padding: 0 !important; 104 | 105 | .svg-icon { 106 | margin-left: 20px; 107 | } 108 | } 109 | } 110 | 111 | .el-submenu { 112 | overflow: hidden; 113 | 114 | &>.el-submenu__title { 115 | padding: 0 !important; 116 | 117 | .svg-icon { 118 | margin-left: 20px; 119 | } 120 | 121 | .el-submenu__icon-arrow { 122 | display: none; 123 | } 124 | } 125 | } 126 | 127 | .el-menu--collapse { 128 | .el-submenu { 129 | &>.el-submenu__title { 130 | &>span { 131 | height: 0; 132 | width: 0; 133 | overflow: hidden; 134 | visibility: hidden; 135 | display: inline-block; 136 | } 137 | } 138 | } 139 | } 140 | } 141 | 142 | .el-menu--collapse .el-menu .el-submenu { 143 | min-width: $sideBarWidth !important; 144 | } 145 | 146 | // mobile responsive 147 | .mobile { 148 | .main-container { 149 | margin-left: 0px; 150 | } 151 | 152 | .sidebar-container { 153 | transition: transform .28s; 154 | width: $sideBarWidth !important; 155 | } 156 | 157 | &.hideSidebar { 158 | .sidebar-container { 159 | pointer-events: none; 160 | transition-duration: 0.3s; 161 | transform: translate3d(-$sideBarWidth, 0, 0); 162 | } 163 | } 164 | } 165 | 166 | .withoutAnimation { 167 | 168 | .main-container, 169 | .sidebar-container { 170 | transition: none; 171 | } 172 | } 173 | } 174 | 175 | // when menu collapsed 176 | .el-menu--vertical { 177 | &>.el-menu { 178 | .svg-icon { 179 | margin-right: 16px; 180 | } 181 | } 182 | 183 | .nest-menu .el-submenu>.el-submenu__title, 184 | .el-menu-item { 185 | &:hover { 186 | // you can use $subMenuHover 187 | background-color: $menuHover !important; 188 | } 189 | } 190 | 191 | // the scroll bar appears when the subMenu is too long 192 | >.el-menu--popup { 193 | max-height: 100vh; 194 | overflow-y: auto; 195 | 196 | &::-webkit-scrollbar-track-piece { 197 | background: #d3dce6; 198 | } 199 | 200 | &::-webkit-scrollbar { 201 | width: 6px; 202 | } 203 | 204 | &::-webkit-scrollbar-thumb { 205 | background: #99a9bf; 206 | border-radius: 20px; 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /vue-admin/vue.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const defaultSettings = require('./src/settings.js') 4 | 5 | function resolve(dir) { 6 | return path.join(__dirname, dir) 7 | } 8 | 9 | const name = defaultSettings.title || 'vue Admin Template' // page title 10 | 11 | // If your port is set to 80, 12 | // use administrator privileges to execute the command line. 13 | // For example, Mac: sudo npm run 14 | // You can change the port by the following methods: 15 | // port = 9528 npm run dev OR npm run dev --port = 9528 16 | const port = process.env.port || process.env.npm_config_port || 9528 // dev port 17 | 18 | // All configuration item explanations can be find in https://cli.vuejs.org/config/ 19 | module.exports = { 20 | /** 21 | * You will need to set publicPath if you plan to deploy your site under a sub path, 22 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/, 23 | * then publicPath should be set to "/bar/". 24 | * In most cases please use '/' !!! 25 | * Detail: https://cli.vuejs.org/config/#publicpath 26 | */ 27 | publicPath: '/', 28 | outputDir: 'dist', 29 | assetsDir: 'static', 30 | lintOnSave: process.env.NODE_ENV === 'development', 31 | productionSourceMap: false, 32 | devServer: { 33 | port: port, 34 | open: true, 35 | overlay: { 36 | warnings: false, 37 | errors: true 38 | }, 39 | proxy: { 40 | // change xxx-api/login => mock/login 41 | // detail: https://cli.vuejs.org/config/#devserver-proxy 42 | [process.env.VUE_APP_BASE_API]: { 43 | target: `http://127.0.0.1:${port}/mock`, 44 | changeOrigin: true, 45 | pathRewrite: { 46 | ['^' + process.env.VUE_APP_BASE_API]: '' 47 | } 48 | } 49 | }, 50 | after: require('./mock/mock-server.js') 51 | }, 52 | configureWebpack: { 53 | // provide the app's title in webpack's name field, so that 54 | // it can be accessed in index.html to inject the correct title. 55 | name: name, 56 | resolve: { 57 | alias: { 58 | '@': resolve('src') 59 | } 60 | } 61 | }, 62 | chainWebpack(config) { 63 | config.plugins.delete('preload') // TODO: need test 64 | config.plugins.delete('prefetch') // TODO: need test 65 | 66 | // set svg-sprite-loader 67 | config.module 68 | .rule('svg') 69 | .exclude.add(resolve('src/icons')) 70 | .end() 71 | config.module 72 | .rule('icons') 73 | .test(/\.svg$/) 74 | .include.add(resolve('src/icons')) 75 | .end() 76 | .use('svg-sprite-loader') 77 | .loader('svg-sprite-loader') 78 | .options({ 79 | symbolId: 'icon-[name]' 80 | }) 81 | .end() 82 | 83 | // set preserveWhitespace 84 | config.module 85 | .rule('vue') 86 | .use('vue-loader') 87 | .loader('vue-loader') 88 | .tap(options => { 89 | options.compilerOptions.preserveWhitespace = true 90 | return options 91 | }) 92 | .end() 93 | 94 | config 95 | // https://webpack.js.org/configuration/devtool/#development 96 | .when(process.env.NODE_ENV === 'development', 97 | config => config.devtool('cheap-source-map') 98 | ) 99 | 100 | config 101 | .when(process.env.NODE_ENV !== 'development', 102 | config => { 103 | config 104 | .plugin('ScriptExtHtmlWebpackPlugin') 105 | .after('html') 106 | .use('script-ext-html-webpack-plugin', [{ 107 | // `runtime` must same as runtimeChunk name. default is `runtime` 108 | inline: /runtime\..*\.js$/ 109 | }]) 110 | .end() 111 | config 112 | .optimization.splitChunks({ 113 | chunks: 'all', 114 | cacheGroups: { 115 | libs: { 116 | name: 'chunk-libs', 117 | test: /[\\/]node_modules[\\/]/, 118 | priority: 10, 119 | chunks: 'initial' // only package third parties that are initially dependent 120 | }, 121 | elementUI: { 122 | name: 'chunk-elementUI', // split elementUI into a single package 123 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app 124 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm 125 | }, 126 | commons: { 127 | name: 'chunk-commons', 128 | test: resolve('src/components'), // can customize your rules 129 | minChunks: 3, // minimum common number 130 | priority: 5, 131 | reuseExistingChunk: true 132 | } 133 | } 134 | }) 135 | config.optimization.runtimeChunk('single') 136 | } 137 | ) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /vue-admin/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | /* Layout */ 7 | import Layout from '@/layout' 8 | 9 | /** 10 | * Note: sub-menu only appear when route children.length >= 1 11 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html 12 | * 13 | * hidden: true if set true, item will not show in the sidebar(default is false) 14 | * alwaysShow: true if set true, will always show the root menu 15 | * if not set alwaysShow, when item has more than one children route, 16 | * it will becomes nested mode, otherwise not show the root menu 17 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb 18 | * name:'router-name' the name is used by (must set!!!) 19 | * meta : { 20 | roles: ['admin','editor'] control the page roles (you can set multiple roles) 21 | title: 'title' the name show in sidebar and breadcrumb (recommend set) 22 | icon: 'svg-name' the icon show in the sidebar 23 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) 24 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set 25 | } 26 | */ 27 | 28 | /** 29 | * constantRoutes 30 | * a base page that does not have permission requirements 31 | * all roles can be accessed 32 | */ 33 | export const constantRoutes = [ 34 | { 35 | path: '/login', 36 | component: () => import('@/views/login/index'), 37 | hidden: true 38 | }, 39 | 40 | { 41 | path: '/404', 42 | component: () => import('@/views/404'), 43 | hidden: true 44 | }, 45 | 46 | { 47 | path: '/403', 48 | component: () => import('@/views/403'), 49 | hidden: true 50 | }, 51 | 52 | { 53 | path: '/', 54 | component: Layout, 55 | redirect: '/dashboard', 56 | children: [{ 57 | path: 'dashboard', 58 | name: 'Dashboard', 59 | component: () => import('@/views/dashboard/index'), 60 | meta: { title: 'Dashboard', icon: 'dashboard' } 61 | }] 62 | }, 63 | 64 | { 65 | path: '/example', 66 | component: Layout, 67 | redirect: '/example/table', 68 | name: 'Example', 69 | meta: { title: 'Example', icon: 'example' }, 70 | children: [ 71 | { 72 | path: 'table', 73 | name: 'Table', 74 | component: () => import('@/views/table/index'), 75 | meta: { title: 'Table', icon: 'table' } 76 | }, 77 | { 78 | path: 'tree', 79 | name: 'Tree', 80 | component: () => import('@/views/tree/index'), 81 | meta: { title: 'Tree', icon: 'tree' } 82 | } 83 | ] 84 | }, 85 | 86 | { 87 | path: '/form', 88 | component: Layout, 89 | children: [ 90 | { 91 | path: 'index', 92 | name: 'Form', 93 | component: () => import('@/views/form/index'), 94 | meta: { title: 'Form', icon: 'form' } 95 | } 96 | ] 97 | }, 98 | 99 | { 100 | path: '/nested', 101 | component: Layout, 102 | redirect: '/nested/menu1', 103 | name: 'Nested', 104 | meta: { 105 | title: 'Nested', 106 | icon: 'nested' 107 | }, 108 | children: [ 109 | { 110 | path: 'menu1', 111 | component: () => import('@/views/nested/menu1/index'), // Parent router-view 112 | name: 'Menu1', 113 | meta: { title: 'Menu1' }, 114 | children: [ 115 | { 116 | path: 'menu1-1', 117 | component: () => import('@/views/nested/menu1/menu1-1'), 118 | name: 'Menu1-1', 119 | meta: { title: 'Menu1-1' } 120 | }, 121 | { 122 | path: 'menu1-2', 123 | component: () => import('@/views/nested/menu1/menu1-2'), 124 | name: 'Menu1-2', 125 | meta: { title: 'Menu1-2' }, 126 | children: [ 127 | { 128 | path: 'menu1-2-1', 129 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'), 130 | name: 'Menu1-2-1', 131 | meta: { title: 'Menu1-2-1' } 132 | }, 133 | { 134 | path: 'menu1-2-2', 135 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'), 136 | name: 'Menu1-2-2', 137 | meta: { title: 'Menu1-2-2' } 138 | } 139 | ] 140 | }, 141 | { 142 | path: 'menu1-3', 143 | component: () => import('@/views/nested/menu1/menu1-3'), 144 | name: 'Menu1-3', 145 | meta: { title: 'Menu1-3' } 146 | } 147 | ] 148 | }, 149 | { 150 | path: 'menu2', 151 | component: () => import('@/views/nested/menu2/index'), 152 | meta: { title: 'menu2' } 153 | } 154 | ] 155 | }, 156 | 157 | { 158 | path: 'external-link', 159 | component: Layout, 160 | children: [ 161 | { 162 | path: 'https://panjiachen.github.io/vue-element-admin-site/#/', 163 | meta: { title: 'External Link', icon: 'link' } 164 | } 165 | ] 166 | }, 167 | 168 | // 404 page must be placed at the end !!! 169 | { path: '*', redirect: '/404', hidden: true } 170 | ] 171 | 172 | const createRouter = () => new Router({ 173 | // mode: 'history', // require service support 174 | scrollBehavior: () => ({ y: 0 }), 175 | routes: constantRoutes 176 | }) 177 | 178 | const router = createRouter() 179 | 180 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 181 | export function resetRouter() { 182 | const newRouter = createRouter() 183 | router.matcher = newRouter.matcher // reset router 184 | } 185 | 186 | export default router 187 | -------------------------------------------------------------------------------- /vue-admin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true, 11 | }, 12 | extends: ['plugin:vue/recommended', 'eslint:recommended'], 13 | 14 | // add your custom rules here 15 | //it is base on https://github.com/vuejs/eslint-config-vue 16 | rules: { 17 | "vue/max-attributes-per-line": [2, { 18 | "singleline": 10, 19 | "multiline": { 20 | "max": 1, 21 | "allowFirstLine": false 22 | } 23 | }], 24 | "vue/singleline-html-element-content-newline": "off", 25 | "vue/multiline-html-element-content-newline":"off", 26 | "vue/name-property-casing": ["error", "PascalCase"], 27 | "vue/no-v-html": "off", 28 | 'accessor-pairs': 2, 29 | 'arrow-spacing': [2, { 30 | 'before': true, 31 | 'after': true 32 | }], 33 | 'block-spacing': [2, 'always'], 34 | 'brace-style': [2, '1tbs', { 35 | 'allowSingleLine': true 36 | }], 37 | 'camelcase': [0, { 38 | 'properties': 'always' 39 | }], 40 | 'comma-dangle': [2, 'never'], 41 | 'comma-spacing': [2, { 42 | 'before': false, 43 | 'after': true 44 | }], 45 | 'comma-style': [2, 'last'], 46 | 'constructor-super': 2, 47 | 'curly': [2, 'multi-line'], 48 | 'dot-location': [2, 'property'], 49 | 'eol-last': 2, 50 | 'eqeqeq': ["error", "always", {"null": "ignore"}], 51 | 'generator-star-spacing': [2, { 52 | 'before': true, 53 | 'after': true 54 | }], 55 | 'handle-callback-err': [2, '^(err|error)$'], 56 | 'indent': [2, 2, { 57 | 'SwitchCase': 1 58 | }], 59 | 'jsx-quotes': [2, 'prefer-single'], 60 | 'key-spacing': [2, { 61 | 'beforeColon': false, 62 | 'afterColon': true 63 | }], 64 | 'keyword-spacing': [2, { 65 | 'before': true, 66 | 'after': true 67 | }], 68 | 'new-cap': [2, { 69 | 'newIsCap': true, 70 | 'capIsNew': false 71 | }], 72 | 'new-parens': 2, 73 | 'no-array-constructor': 2, 74 | 'no-caller': 2, 75 | 'no-console': 'off', 76 | 'no-class-assign': 2, 77 | 'no-cond-assign': 2, 78 | 'no-const-assign': 2, 79 | 'no-control-regex': 0, 80 | 'no-delete-var': 2, 81 | 'no-dupe-args': 2, 82 | 'no-dupe-class-members': 2, 83 | 'no-dupe-keys': 2, 84 | 'no-duplicate-case': 2, 85 | 'no-empty-character-class': 2, 86 | 'no-empty-pattern': 2, 87 | 'no-eval': 2, 88 | 'no-ex-assign': 2, 89 | 'no-extend-native': 2, 90 | 'no-extra-bind': 2, 91 | 'no-extra-boolean-cast': 2, 92 | 'no-extra-parens': [2, 'functions'], 93 | 'no-fallthrough': 2, 94 | 'no-floating-decimal': 2, 95 | 'no-func-assign': 2, 96 | 'no-implied-eval': 2, 97 | 'no-inner-declarations': [2, 'functions'], 98 | 'no-invalid-regexp': 2, 99 | 'no-irregular-whitespace': 2, 100 | 'no-iterator': 2, 101 | 'no-label-var': 2, 102 | 'no-labels': [2, { 103 | 'allowLoop': false, 104 | 'allowSwitch': false 105 | }], 106 | 'no-lone-blocks': 2, 107 | 'no-mixed-spaces-and-tabs': 2, 108 | 'no-multi-spaces': 2, 109 | 'no-multi-str': 2, 110 | 'no-multiple-empty-lines': [2, { 111 | 'max': 1 112 | }], 113 | 'no-native-reassign': 2, 114 | 'no-negated-in-lhs': 2, 115 | 'no-new-object': 2, 116 | 'no-new-require': 2, 117 | 'no-new-symbol': 2, 118 | 'no-new-wrappers': 2, 119 | 'no-obj-calls': 2, 120 | 'no-octal': 2, 121 | 'no-octal-escape': 2, 122 | 'no-path-concat': 2, 123 | 'no-proto': 2, 124 | 'no-redeclare': 2, 125 | 'no-regex-spaces': 2, 126 | 'no-return-assign': [2, 'except-parens'], 127 | 'no-self-assign': 2, 128 | 'no-self-compare': 2, 129 | 'no-sequences': 2, 130 | 'no-shadow-restricted-names': 2, 131 | 'no-spaced-func': 2, 132 | 'no-sparse-arrays': 2, 133 | 'no-this-before-super': 2, 134 | 'no-throw-literal': 2, 135 | 'no-trailing-spaces': 2, 136 | 'no-undef': 2, 137 | 'no-undef-init': 2, 138 | 'no-unexpected-multiline': 2, 139 | 'no-unmodified-loop-condition': 2, 140 | 'no-unneeded-ternary': [2, { 141 | 'defaultAssignment': false 142 | }], 143 | 'no-unreachable': 2, 144 | 'no-unsafe-finally': 2, 145 | 'no-unused-vars': [2, { 146 | 'vars': 'all', 147 | 'args': 'none' 148 | }], 149 | 'no-useless-call': 2, 150 | 'no-useless-computed-key': 2, 151 | 'no-useless-constructor': 2, 152 | 'no-useless-escape': 0, 153 | 'no-whitespace-before-property': 2, 154 | 'no-with': 2, 155 | 'one-var': [2, { 156 | 'initialized': 'never' 157 | }], 158 | 'operator-linebreak': [2, 'after', { 159 | 'overrides': { 160 | '?': 'before', 161 | ':': 'before' 162 | } 163 | }], 164 | 'padded-blocks': [2, 'never'], 165 | 'quotes': [2, 'single', { 166 | 'avoidEscape': true, 167 | 'allowTemplateLiterals': true 168 | }], 169 | 'semi': [2, 'never'], 170 | 'semi-spacing': [2, { 171 | 'before': false, 172 | 'after': true 173 | }], 174 | 'space-before-blocks': [2, 'always'], 175 | 'space-before-function-paren': [2, 'never'], 176 | 'space-in-parens': [2, 'never'], 177 | 'space-infix-ops': 2, 178 | 'space-unary-ops': [2, { 179 | 'words': true, 180 | 'nonwords': false 181 | }], 182 | 'spaced-comment': [2, 'always', { 183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 184 | }], 185 | 'template-curly-spacing': [2, 'never'], 186 | 'use-isnan': 2, 187 | 'valid-typeof': 2, 188 | 'wrap-iife': [2, 'any'], 189 | 'yield-star-spacing': [2, 'both'], 190 | 'yoda': [2, 'never'], 191 | 'prefer-const': 2, 192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 193 | 'object-curly-spacing': [2, 'always', { 194 | objectsInObjects: false 195 | }], 196 | 'array-bracket-spacing': [2, 'never'] 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /vue-admin/src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 117 | 118 | 164 | 165 | 228 | -------------------------------------------------------------------------------- /routers/api/v1/article.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "gin-vue/pkg/e" 5 | "gin-vue/pkg/setting" 6 | "gin-vue/pkg/util" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/astaxie/beego/validation" 11 | "github.com/gin-gonic/gin" 12 | "github.com/unknwon/com" 13 | 14 | "gin-vue/models" 15 | ) 16 | 17 | //获取单个文章 18 | func GetArticle(c *gin.Context) { 19 | id := com.StrTo(c.Param("id")).MustInt() 20 | 21 | valid := validation.Validation{} 22 | valid.Min(id, 1, "id").Message("ID必须大于0") 23 | 24 | code := e.INVALID_PARAMS 25 | var data interface{} 26 | if !valid.HasErrors() { 27 | if models.ExistArticleByID(id) { 28 | data = models.GetArticle(id) 29 | code = e.SUCCESS 30 | } else { 31 | code = e.ERROR_NOT_EXIST_ARTICLE 32 | } 33 | } else { 34 | for _, err := range valid.Errors { 35 | log.Printf("err.key: %s, err.message: %s", err.Key, err.Message) 36 | } 37 | } 38 | 39 | c.JSON(http.StatusOK, gin.H{ 40 | "code": code, 41 | "msg": e.GetMsg(code), 42 | "data": data, 43 | }) 44 | } 45 | 46 | //获取多个文章 47 | func GetArticles(c *gin.Context) { 48 | data := make(map[string]interface{}) 49 | maps := make(map[string]interface{}) 50 | valid := validation.Validation{} 51 | 52 | var state int = -1 53 | if arg := c.Query("state"); arg != "" { 54 | state = com.StrTo(arg).MustInt() 55 | maps["state"] = state 56 | 57 | valid.Range(state, 0, 1, "state").Message("状态只允许0或1") 58 | } 59 | 60 | var tagId int = -1 61 | if arg := c.Query("tag_id"); arg != "" { 62 | tagId = com.StrTo(arg).MustInt() 63 | maps["tag_id"] = tagId 64 | 65 | valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0") 66 | } 67 | 68 | code := e.INVALID_PARAMS 69 | if !valid.HasErrors() { 70 | code = e.SUCCESS 71 | 72 | data["lists"] = models.GetArticles(util.GetPage(c), setting.PageSize, maps) 73 | data["total"] = models.GetArticleTotal(maps) 74 | 75 | } else { 76 | for _, err := range valid.Errors { 77 | log.Printf("err.key: %s, err.message: %s", err.Key, err.Message) 78 | } 79 | } 80 | 81 | c.JSON(http.StatusOK, gin.H{ 82 | "code": code, 83 | "msg": e.GetMsg(code), 84 | "data": data, 85 | }) 86 | } 87 | 88 | //新增文章 89 | func AddArticle(c *gin.Context) { 90 | tagId := com.StrTo(c.Query("tag_id")).MustInt() 91 | title := c.Query("title") 92 | desc := c.Query("desc") 93 | content := c.Query("content") 94 | createdBy := c.Query("created_by") 95 | state := com.StrTo(c.DefaultQuery("state", "0")).MustInt() 96 | 97 | valid := validation.Validation{} 98 | valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0") 99 | valid.Required(title, "title").Message("标题不能为空") 100 | valid.Required(desc, "desc").Message("简述不能为空") 101 | valid.Required(content, "content").Message("内容不能为空") 102 | valid.Required(createdBy, "created_by").Message("创建人不能为空") 103 | valid.Range(state, 0, 1, "state").Message("状态只允许0或1") 104 | 105 | code := e.INVALID_PARAMS 106 | if !valid.HasErrors() { 107 | if models.ExistTagByID(tagId) { 108 | data := make(map[string]interface{}) 109 | data["tag_id"] = tagId 110 | data["title"] = title 111 | data["desc"] = desc 112 | data["content"] = content 113 | data["created_by"] = createdBy 114 | data["state"] = state 115 | 116 | models.AddArticle(data) 117 | code = e.SUCCESS 118 | } else { 119 | code = e.ERROR_NOT_EXIST_TAG 120 | } 121 | } else { 122 | for _, err := range valid.Errors { 123 | log.Printf("err.key: %s, err.message: %s", err.Key, err.Message) 124 | } 125 | } 126 | 127 | c.JSON(http.StatusOK, gin.H{ 128 | "code": code, 129 | "msg": e.GetMsg(code), 130 | "data": make(map[string]interface{}), 131 | }) 132 | } 133 | 134 | //修改文章 135 | func EditArticle(c *gin.Context) { 136 | valid := validation.Validation{} 137 | 138 | id := com.StrTo(c.Param("id")).MustInt() 139 | tagId := com.StrTo(c.Query("tag_id")).MustInt() 140 | title := c.Query("title") 141 | desc := c.Query("desc") 142 | content := c.Query("content") 143 | modifiedBy := c.Query("modified_by") 144 | 145 | var state int = -1 146 | if arg := c.Query("state"); arg != "" { 147 | state = com.StrTo(arg).MustInt() 148 | valid.Range(state, 0, 1, "state").Message("状态只允许0或1") 149 | } 150 | 151 | valid.Min(id, 1, "id").Message("ID必须大于0") 152 | valid.MaxSize(title, 100, "title").Message("标题最长为100字符") 153 | valid.MaxSize(desc, 255, "desc").Message("简述最长为255字符") 154 | valid.MaxSize(content, 65535, "content").Message("内容最长为65535字符") 155 | valid.Required(modifiedBy, "modified_by").Message("修改人不能为空") 156 | valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符") 157 | 158 | code := e.INVALID_PARAMS 159 | if !valid.HasErrors() { 160 | if models.ExistArticleByID(id) { 161 | if models.ExistTagByID(tagId) { 162 | data := make(map[string]interface{}) 163 | if tagId > 0 { 164 | data["tag_id"] = tagId 165 | } 166 | if title != "" { 167 | data["title"] = title 168 | } 169 | if desc != "" { 170 | data["desc"] = desc 171 | } 172 | if content != "" { 173 | data["content"] = content 174 | } 175 | 176 | data["modified_by"] = modifiedBy 177 | 178 | models.EditArticle(id, data) 179 | code = e.SUCCESS 180 | } else { 181 | code = e.ERROR_NOT_EXIST_TAG 182 | } 183 | } else { 184 | code = e.ERROR_NOT_EXIST_ARTICLE 185 | } 186 | } else { 187 | for _, err := range valid.Errors { 188 | log.Printf("err.key: %s, err.message: %s", err.Key, err.Message) 189 | } 190 | } 191 | 192 | c.JSON(http.StatusOK, gin.H{ 193 | "code": code, 194 | "msg": e.GetMsg(code), 195 | "data": make(map[string]string), 196 | }) 197 | } 198 | 199 | //删除文章 200 | func DeleteArticle(c *gin.Context) { 201 | id := com.StrTo(c.Param("id")).MustInt() 202 | 203 | valid := validation.Validation{} 204 | valid.Min(id, 1, "id").Message("ID必须大于0") 205 | 206 | code := e.INVALID_PARAMS 207 | if !valid.HasErrors() { 208 | if models.ExistArticleByID(id) { 209 | models.DeleteArticle(id) 210 | code = e.SUCCESS 211 | } else { 212 | code = e.ERROR_NOT_EXIST_ARTICLE 213 | } 214 | } else { 215 | for _, err := range valid.Errors { 216 | log.Printf("err.key: %s, err.message: %s", err.Key, err.Message) 217 | } 218 | } 219 | 220 | c.JSON(http.StatusOK, gin.H{ 221 | "code": code, 222 | "msg": e.GetMsg(code), 223 | "data": make(map[string]string), 224 | }) 225 | } 226 | -------------------------------------------------------------------------------- /vue-admin/src/views/404.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | 35 | 229 | -------------------------------------------------------------------------------- /docs/sql/mysql.sql: -------------------------------------------------------------------------------- 1 | drop database if exists `blog`; 2 | create database `blog` default character set utf8mb4 collate utf8mb4_unicode_ci; 3 | 4 | use `blog`; 5 | 6 | SET NAMES utf8mb4; 7 | SET FOREIGN_KEY_CHECKS=0; 8 | 9 | -- ---------------------------- 10 | -- Table structure for blog_article 11 | -- ---------------------------- 12 | DROP TABLE IF EXISTS `blog_article`; 13 | CREATE TABLE `blog_article` ( 14 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 15 | `tag_id` int(10) unsigned DEFAULT '0' COMMENT '标签ID', 16 | `title` varchar(100) DEFAULT '' COMMENT '文章标题', 17 | `desc` varchar(255) DEFAULT '' COMMENT '简述', 18 | `content` text COMMENT '内容', 19 | `cover_image_url` varchar(255) DEFAULT '' COMMENT '封面图片地址', 20 | `created_on` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 21 | `created_by` varchar(100) DEFAULT '' COMMENT '创建人', 22 | `modified_on` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', 23 | `modified_by` varchar(255) DEFAULT '' COMMENT '修改人', 24 | `deleted_on` int(10) unsigned DEFAULT '0' COMMENT '删除时间', 25 | `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为草稿、1为已发布、2为删除', 26 | PRIMARY KEY (`id`) 27 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章管理'; 28 | 29 | -- ---------------------------- 30 | -- Table structure for blog_auth 31 | -- ---------------------------- 32 | DROP TABLE IF EXISTS `blog_auth`; 33 | CREATE TABLE `blog_auth` ( 34 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 35 | `username` varchar(50) DEFAULT '' COMMENT '账号', 36 | `password` varchar(50) DEFAULT '' COMMENT '密码', 37 | `avatar` varchar(255) DEFAULT '' COMMENT '头像地址', 38 | PRIMARY KEY (`id`) 39 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 40 | 41 | -- ---------------------------- 42 | -- Table structure for blog_tag 43 | -- ---------------------------- 44 | DROP TABLE IF EXISTS `blog_tag`; 45 | CREATE TABLE `blog_tag` ( 46 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 47 | `name` varchar(100) DEFAULT '' COMMENT '标签名称', 48 | `created_on` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 49 | `created_by` varchar(100) DEFAULT '' COMMENT '创建人', 50 | `modified_on` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', 51 | `modified_by` varchar(100) DEFAULT '' COMMENT '修改人', 52 | `deleted_on` int(10) unsigned DEFAULT '0' COMMENT '删除时间', 53 | `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用、1为启用', 54 | PRIMARY KEY (`id`) 55 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章标签管理'; 56 | 57 | 58 | -- ---------------------------- 59 | -- Table structure for blog_claims 60 | -- ---------------------------- 61 | DROP TABLE IF EXISTS `blog_claims`; 62 | CREATE TABLE `blog_claims` ( 63 | `claim_id` int(10) unsigned NOT NULL AUTO_INCREMENT, 64 | `auth_id` int(10) unsigned NOT NULL COMMENT '用户ID', 65 | `type` varchar(50) DEFAULT '' COMMENT 'claim类型', 66 | `value` varchar(50) DEFAULT '' COMMENT 'claim值', 67 | PRIMARY KEY (`claim_id`) 68 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 69 | 70 | INSERT INTO `blog_auth`(`id`, `username`, `password`, `avatar`) VALUES (1, 'admin', '111111', 'https://zbj-bucket1.oss-cn-shenzhen.aliyuncs.com/avatar.JPG'); 71 | INSERT INTO `blog_auth`(`id`, `username`, `password`, `avatar`) VALUES (2, 'test', '111111', 'https://zbj-bucket1.oss-cn-shenzhen.aliyuncs.com/avatar.JPG'); 72 | 73 | INSERT INTO `blog_tag`(`id`, `name`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (1, '1', '2019-08-18 18:56:01', 'test', NULL, '', 0, 1); 74 | INSERT INTO `blog_tag`(`id`, `name`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (2, '2', '2019-08-16 18:56:06', 'test', NULL, '', 0, 1); 75 | INSERT INTO `blog_tag`(`id`, `name`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (3, '3', '2019-08-18 18:56:09', 'test', NULL, '', 0, 1); 76 | 77 | INSERT INTO `blog_article`(`id`, `tag_id`, `title`, `desc`, `content`, `cover_image_url`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (1, 1, 'test1', 'test-desc', 'test-content', '', '2019-08-19 21:00:39', 'test-created', '2019-08-19 21:00:39', '', 0, 0); 78 | INSERT INTO `blog_article`(`id`, `tag_id`, `title`, `desc`, `content`, `cover_image_url`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (2, 1, 'test2', 'test-desc', 'test-content', '', '2019-08-19 21:00:48', 'test-created', '2019-08-19 21:00:48', '', 0, 2); 79 | INSERT INTO `blog_article`(`id`, `tag_id`, `title`, `desc`, `content`, `cover_image_url`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (3, 1, 'test3', 'test-desc', 'test-content', '', '2019-08-19 21:00:49', 'test-created', '2019-08-19 21:00:49', '', 0, 1); 80 | 81 | INSERT INTO `blog_claims`(`claim_id`, `auth_id`, `type`, `value`) VALUES (1, 1, 'role', 'admin'); 82 | INSERT INTO `blog_claims`(`claim_id`, `auth_id`, `type`, `value`) VALUES (2, 1, 'role', 'test'); 83 | INSERT INTO `blog_claims`(`claim_id`, `auth_id`, `type`, `value`) VALUES (3, 2, 'role', 'test'); 84 | 85 | 86 | -- -- ---------------------------- 87 | -- -- comdb:Table structure for com_system 88 | -- -- ---------------------------- 89 | -- DROP TABLE IF EXISTS `com_system`; 90 | -- CREATE TABLE `com_system` ( 91 | -- `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 92 | -- `sys_name` varchar(50) NOT NULL DEFAULT '' COMMENT '系统名称', 93 | -- `sys_password` varchar(50) NOT NULL DEFAULT '' COMMENT '系统密码', 94 | -- `email` varchar(50) NOT NULL DEFAULT '' COMMENT '邮箱地址', 95 | -- `state` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '状态 0为禁用、1为启用', 96 | -- PRIMARY KEY (`id`) 97 | -- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 98 | 99 | -- -- ---------------------------- 100 | -- -- comdb_sysid:Table structure for sysid_comment 101 | -- -- ---------------------------- 102 | -- DROP TABLE IF EXISTS `sysid_comment`; 103 | -- CREATE TABLE `sysid_comment` ( 104 | -- `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 105 | -- `target_id` varchar(50) NOT NULL DEFAULT '' COMMENT '评论主题的id,可根据需要修改为article_id、course_id等等', 106 | -- `com_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '被评论的评论id,主评论为0', 107 | -- `user_id` varchar(50) NOT NULL DEFAULT '' COMMENT '发表评论的用户id', 108 | -- `user_name` varchar(50) NOT NULL DEFAULT '' COMMENT '发表评论的用户名称(冗余设计)', 109 | -- `avatar_url` varchar(255) NOT NULL DEFAULT '' COMMENT '发表评论的用户头像(冗余设计)', 110 | -- `reply_name` varchar(50) NOT NULL DEFAULT '' COMMENT '回复人的名称', 111 | -- `content` varchar(800) NOT NULL DEFAULT '' COMMENT '评论内容', 112 | -- `created_on` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 113 | -- `support` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '点赞数(出于善良,不设计反对数)', 114 | -- `satisfaction` int(10) NOT NULL DEFAULT '0' COMMENT '满意度(为0时,忽略满意度)', 115 | -- `photo` varchar(800) NOT NULL DEFAULT '' COMMENT '图片地址', 116 | -- PRIMARY KEY (`id`) 117 | -- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 118 | 119 | -- -- ---------------------------- 120 | -- -- comdb_sysid:Table structure for sysid_comment 121 | -- -- ---------------------------- 122 | -- DROP TABLE IF EXISTS `sysid_support`; 123 | -- CREATE TABLE `sysid_support` ( 124 | -- `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 125 | -- `com_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '被点赞的评论id', 126 | -- `user_id` varchar(50) NOT NULL DEFAULT '' COMMENT '点赞的用户ID', 127 | -- PRIMARY KEY (`id`) 128 | -- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 3 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 8 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= 9 | github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= 10 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 11 | github.com/appleboy/gin-jwt v1.0.1 h1:cbLi6ol8KhWUoCBvLFCGpssiBB2iM9eXWwmI9SDG+W0= 12 | github.com/appleboy/gin-jwt v2.5.0+incompatible h1:oLQTP1fiGDoDKoC2UDqXD9iqCP44ABIZMMenfH/xCqw= 13 | github.com/appleboy/gin-jwt v2.5.0+incompatible/go.mod h1:pG7tv32IEe5wEh1NSQzcyD02ZZAqZWp07RdGiIhgaRQ= 14 | github.com/appleboy/gin-jwt/v2 v2.6.4 h1:4YlMh3AjCFnuIRiL27b7TXns7nLx8tU/TiSgh40RRUI= 15 | github.com/appleboy/gin-jwt/v2 v2.6.4/go.mod h1:CZpq1cRw+kqi0+yD2CwVw7VGXrrx4AqBdeZnwxVmoAs= 16 | github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= 17 | github.com/astaxie/beego v1.12.2 h1:CajUexhSX5ONWDiSCpeQBNVfTzOtPb9e9d+3vuU5FuU= 18 | github.com/astaxie/beego v1.12.2/go.mod h1:TMcqhsbhN3UFpN+RCfysaxPAbrhox6QSS3NIAEp/uzE= 19 | github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ= 20 | github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU= 21 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 22 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 23 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 24 | github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= 25 | github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE= 26 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 27 | github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= 28 | github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U= 29 | github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c= 30 | github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= 31 | github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY= 32 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 33 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 34 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 35 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 36 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 37 | github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 38 | github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= 39 | github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= 40 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 41 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 42 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 43 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 44 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 45 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 46 | github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw= 47 | github.com/go-ini/ini v1.57.0 h1:Qwzj3wZQW+Plax5Ntj+GYe07DfGj1OH+aL1nMTMaNow= 48 | github.com/go-ini/ini v1.57.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 49 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 50 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 51 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 52 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 53 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 54 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 55 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 56 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 57 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 58 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 59 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 60 | github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 61 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 62 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 63 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 64 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 65 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 66 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 67 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 68 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 69 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 70 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 71 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 72 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 73 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 74 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 75 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 76 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 77 | github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 78 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 79 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 80 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 81 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 82 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 83 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 84 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 85 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 86 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 87 | github.com/jinzhu/gorm v1.9.15 h1:OdR1qFvtXktlxk73XFYMiYn9ywzTwytqe4QkuMRqc38= 88 | github.com/jinzhu/gorm v1.9.15/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 89 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 90 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 91 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 92 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 93 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 94 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 95 | github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 96 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 97 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 98 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 99 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 100 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 101 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 102 | github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ= 103 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 104 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 105 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 106 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 107 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 108 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 109 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 110 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 111 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 112 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 113 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 114 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 115 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 116 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 117 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 118 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 119 | github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= 120 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 121 | github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 122 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 123 | github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= 124 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 125 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 126 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 127 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 128 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 129 | github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 130 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 131 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 132 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 133 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 134 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 135 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 136 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 137 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 138 | github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo= 139 | github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= 140 | github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= 141 | github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s= 142 | github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= 143 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 144 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 145 | github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 146 | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= 147 | github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE= 148 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 149 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 150 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 151 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 152 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 153 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 154 | github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= 155 | github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= 156 | github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= 157 | github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= 158 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 159 | github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= 160 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 161 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 162 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 163 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 164 | github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= 165 | github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= 166 | github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc= 167 | github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU= 168 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 169 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 170 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 171 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 172 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 173 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 174 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 175 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 176 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 177 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 178 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 179 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 180 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 181 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 182 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 183 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 184 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 185 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 186 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 187 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 188 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 189 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 190 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 191 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 192 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 193 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 194 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 195 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 196 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 197 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= 198 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 199 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 200 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 201 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 202 | golang.org/x/tools v0.0.0-20200117065230-39095c1d176c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 203 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 204 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 205 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 206 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 207 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 208 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 209 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 210 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 211 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 212 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 213 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 214 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 215 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 216 | gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 h1:N46iQqOtHry7Hxzb9PGrP68oovQmj7EhudNoKHvbOvI= 217 | gopkg.in/dgrijalva/jwt-go.v3 v3.2.0/go.mod h1:hdNXC2Z9yC029rvsQ/on2ZNQ44Z2XToVhpXXbR+J05A= 218 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 219 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 220 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 221 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 222 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 223 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 224 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 225 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 226 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 227 | --------------------------------------------------------------------------------