前言
这是基于 Pichubot 开发的QQ机器人,名为皮丘Bot。
文档地址:Pichubot - 皮丘Bot
功能:青年大学习管理系统
这个功能共有三个模块,分别为:
网页面板
在这个模块中,皮丘Bot会提供一个简单的网页面板,用于管理青年大学习系统。
该模块有响应式布局,对手机端适配。
QQ群管理
在这个模块中,皮丘Bot会接收到群消息,并将消息解析处理,这个功能是皮丘Bot的核心功能。
网络API
网页面板采用的是前后端分离的模式,这个模块是用于提供网络API的。
踩过的坑/笔记
前端(Vue.js + Axios + elementui)
前端面板采用的是 vue2 + axios + element-ui 进行开发,在开发过程中遇到了一些问题,比如:
在外部页面需要刷新内部组件的数据
此处我将“成员列表”,写作一个组件,在“成员管理”页面中使用到了这个组件,但是在外部无法直接调用组件内的函数,也就无法做到重新加载列表内的表格。
解决方法
在使用组件的时候给组件加上ref
标签,通过调用this.$refs.内容.函数
即可调用组件内的函数。
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> ... <memberTable ref="memberTable" /> ... </template>
<script> import memberTable from '@/components/membertable.vue' ... export default { ... this.$refs.memberTable.getMemberList() ... } </script>
|
axios 初始化拦截器
由于 API 每次请求都需要 Bearer Token 以及配置请求地址,于是我在 src/ 目录下新建了一个 axios-init.js
文件用于初始化 axios 并输出。
以下为文件源码
src/axios-init.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| import axios from 'axios' import router from '@/router' import cookies from 'vue-cookies' import { Notification } from 'element-ui';
let instance = axios.create(); instance.defaults.baseURL = "http://localhost/api/"; instance.defaults.headers.post['Content-Type'] = 'application/json; charset=utf-8'; axios.defaults.crossDomain = true; axios.defaults.withCredentials = true; instance.defaults.headers.common['Authorization'] = '';
instance.interceptors.request.use( config => { const token = cookies.get('token'); token && (config.headers.Authorization = 'Bearer '+ token); return config; }, error => { return Promise.error(error); } );
instance.interceptors.response.use( response => { if (response.status === 200 && response.data.code !== 200) { switch (response.data.code) { case 1001: cookies.remove('token'); cookies.remove('userId'); cookies.remove('userName'); router.push({path: '/login'}); Notification({ title: '登录超时', message: '请尝试重新登录', type: 'warning' }); break; default: Notification({ title: '提示', message: response.data.message, type: 'error' }); } return Promise.reject(response); } else if (response.status === 200 && response.data.code === 200) { return Promise.resolve(response); } }, error => { return Promise.reject(error); } );
export default instance
|
src/main.js(引入 axios-init 中导出的 instance)
1 2
| import instance from "./axios-init"; Vue.prototype.$http = instance;
|
响应式布局
下图为网页结构
响应式布局仅需设置 el-aside 与 headtable 的 display 属性即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @media screen and (max-width: 768px) { .asidemenu { display: none; } .headmenu { display: visible; } }
@media screen and (min-width: 768px) { .asidemenu { display: visible; } .headmenu { display: none; } }
|
后端(API、数据库操作部分)
检测 Bearer token
新建函数 verifyToken,传入 Header 中的 Authorization。
使用 strings.HasPrefix() 判断是否含有 token,将 token 与已有的 token 进行比对验证,通过后返回 true.
1 2 3 4 5 6 7 8
| func verifyToken(qq string, token string) bool { if strings.HasPrefix(token, "Bearer") { if _, ok := Cookies[qq]; ok && Cookies[qq] == token[7:] { return true } } return false }
|
Golang 实现 16/32 位 MD5 加密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package main
import ( "crypto/md5" "encoding/hex" "fmt" )
func GetMD5Encode(data string) string { h := md5.New() h.Write([]byte(data)) return hex.EncodeToString(h.Sum(nil)) }
func Get16MD5Encode(data string) string{ return GetMD5Encode(data)[8:24] }
func main() { source:="hello" fmt.Println(GetMD5Encode(source)) fmt.Println(Get16MD5Encode(source)) }
|
Golang http 包下 FileServer 的使用
1 2 3 4 5
| http.Handle("/", http.FileServer(http.Dir("./doc")))
http.Handle("/abc/", http.StripPrefix("/abc", http.FileServer(http.Dir("./doc"))))
|
Golang CORS 跨域请求
在 http.ResponseWriter 中允许所有类型的请求,头,域名
1 2 3
| writer.Header().Set("Access-Control-Allow-Origin", "*") writer.Header().Set("Access-Control-Allow-Methods", "*") writer.Header().Set("Access-Control-Allow-Headers", "*")
|
Golang 打包文件夹为 zip 压缩文件
打包函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| func Zip(src_dir string, zip_file_name string) error {
dir, err := ioutil.ReadDir(src_dir) if err != nil { logger.Log.Error("ioutil.ReadDir err:" + err.Error()) return err } if len(dir) == 0 { logger.Log.Info(src_dir + " is empty dir!") return nil } os.RemoveAll(zip_file_name)
zipfile, _ := os.Create(zip_file_name) defer zipfile.Close()
archive := zip.NewWriter(zipfile) defer archive.Close()
filepath.Walk(src_dir, func(path string, info os.FileInfo, _ error) error {
if path == src_dir { return nil }
header, _ := zip.FileInfoHeader(info)
header.Name = strings.TrimPrefix(path, src_dir+`\`)
if info.IsDir() { header.Name += `/` } else { header.Method = zip.Deflate }
writer, _ := archive.CreateHeader(header) if !info.IsDir() { file, _ := os.Open(path) defer file.Close() io.Copy(writer, file) }
return nil }) return nil }
|
需要注意的是:在传入字符串时的路径最好使用 filepath.Join 进行操作,直接使用字符串可能会造成打包错误的问题, zipfile
需要加上扩展名
Golang GORM 根据结构体结构建指定名字的表
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| type user struct { QQ int64 `json:"qq"` Name string `json:"name"` Comp int `json:"comptype"` Url string `json:"url,omitempty"` }
func createTable(groupId string) error { err := database.Table(groupId).Migrator().CreateTable(&user{}) return err }
|
VuePress 文档
部署文档后样式丢失
将 docs/.vuepress/config.js
中的 base
字段由绝对地址 /
改为相对地址 ./
即可
在 VuePress 中使用 element-ui
在 docs/.vuepress/
目录下新建文件 enhanceApp.js
1 2 3 4 5 6 7 8
| import Vue from 'vue'; import VueRouter from 'vue-router'; import Element from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'
Vue.use(VueRouter); Vue.use(Element);
|
使用包管理安装对应的包(vue,element-ui)即可。
让 Vuepress 支持图片放大功能
使用包管理安装插件 @vuepress/plugin-medium-zoom
1 2
| yarn add -D @vuepress/plugin-medium-zoom
|
在 docs/.vuepress/config.js
中添加
1 2 3
| module.exports = { plugins: ["@vuepress/medium-zoom"] }
|